Skip to content

[DS] - POC Take the colour layer from pmndrs/design-system, distribute Keypoints, compute the palette on the server - #571

Open
abernier wants to merge 7 commits into
mainfrom
ds
Open

[DS] - POC Take the colour layer from pmndrs/design-system, distribute Keypoints, compute the palette on the server#571
abernier wants to merge 7 commits into
mainfrom
ds

Conversation

@abernier

@abernier abernier commented Aug 16, 2026

Copy link
Copy Markdown
Member

PRs 2–4 of pmndrs/design-system#1, collapsed into one, and stacked on #572. This was a three-PR stack β€” #567, #568 and #570 β€” and it is now the same commits reviewable in one place. The two others are closed, not abandoned; their discussion threads stay where they are.

#572 carries the react-mcu β†’ material-theme-builder swap on its own, so what is left here is only the question of where the colour layer lives β€” not which package computes it. Review #572 first.

Three things happen, in order:

  1. docs stops maintaining its own copy of the colour system and installs it from pmndrs/design-system.
  2. Keypoints moves out of src/components/mdx/ and becomes a registry block anyone can install.
  3. The palette stops being computed in every visitor's browser.

1. Take the MD3 colour layer from pmndrs/design-system

#572 already took both CSS layers straight from material-theme-builder β€” @plugin for the Tailwind mapping, @import for the shadcn remap. So what this commit installs from pmndrs/design-system is the seed and its config, and nothing else. globals.css gains not one colour line.

That is worth stating plainly, because it was not the original plan: the md3 registry item does carry a mapping and a remap of its own, and this branch used to vendor them. It no longer needs to. The item's CSS half is inert on this site β€” see Known, not fixed here.

Mostly a rename: --mcu-* becomes --md-sys-color-*. The --color-* names on top are identical either way, so every bg-surface, bg-primary-container and text-on-surface-variant in the codebase keeps working untouched.

The plan said "no component changes". Not quite β€” two components reference the raw variables rather than a Tailwind utility, Code (the prism fixed colour) and Sandpack (its three surface levels). Those four lines are the only component edits.

What the design system owns, and what stays here

The first pass took the five alert colours from the design system too. It shouldn't have β€” note, tip, important, warning and caution are GitHub's palette, and this generator is the only thing rendering markdown alerts. pmndrs/design-system#3 pulled them back out; upstream now ships the seed alone.

So they live in src/lib/mtb.ts, which spreads pmndrsMtb and adds them. Spreading rather than editing keeps src/lib/md3.ts a verbatim copy of the installed item, so updating it stays a clean overwrite. blend: true still harmonizes them against the pmndrs seed, and THEME_NOTE and its four siblings still override them.

Their twenty @theme lines moved out of the shadcn-managed block into one of ours, commented. The config and the mapping have to be read together: a name declared in one and missing from the other makes Tailwind emit no rule at all, with no error β€” so they should not be a block apart with shadcn free to rewrite one of them.

The seed

It moves to the pmndrs seed, the slate #323e48. This is the one visible change β€” everything else is meant to render identically. (Upstream seeded on the mint #5de4c7 when this branch was first written; pmndrs/design-system@f46f2a9 moved it to the slate, and src/lib/md3.ts β€” a verbatim copy of the installed item β€” follows.)

It also stops being restated. The seed lived in layout.tsx, again in build.yml's input defaults, and hard-coded a third time in ci.yml β€” three copies free to disagree, which is exactly what the plan flagged. Now src/lib/md3.ts holds it, installed from the design system, and the workflow inputs default to empty. A site overrides the seed by setting one; a site that leaves it alone inherits the pmndrs default. THEME_PRIMARY, THEME_SCHEME, THEME_CONTRAST and the five alert colours behave as before.

Worth a second look, since build.yml is reusable and other repos call it: the defaults it used to hand out are now the design system's rather than its own.

The preset

apply b5cR4Y50S --only theme. Close to a no-op here, by design β€” radius already matched at 0.625rem, the colour half is overridden by the remap on the next line, and the font half is deliberately left out because docs self-hosts Inter and Inconsolata. What it does land is the identity in components.json and a --sidebar* block in oklch instead of hsl.

Two things to know if you re-run the CLI here. apply writes the palette into the first :root it finds, which here is the layout block holding --rgrid-m and scroll-behavior, not the shadcn one further down β€” which then wins, being later. I moved the values into the shadcn block and restored the layout one. And add matches its :root, .dark block on the exact selector string, which prettier reformats to :root,\n.dark on commit β€” so re-installing the item appends a second, identical copy. Worth a glance at the diff after either command.


2. Extract Keypoints into a registry block

Keypoints becomes the first block of the pmndrs UI distribution:

npx shadcn@latest add pmndrs/docs/keypoints

It is also the first thing anywhere to resolve a dependency across registries. bg-surface-dim is an MD3 surface role shadcn has no equivalent for, so the item declares pmndrs/design-system/md3#v0.3.0 and one add pulls the shared colour layer down with it. That path is what the RFC's whole architecture rests on, and nothing else exercises it β€” which is most of why this component went first, rather than the nav.

Nothing changes for authors. The same <Keypoints> / <KeypointsItem> in the same MDX, rendering the same; only the source moved, from src/components/mdx/Keypoints/ to registry/keypoints/, with registry.json at the root next to it. The app imports it back through a new @/registry/* alias, so what ships to a consumer is the file this site renders rather than a copy that can drift from it.

What being distributable cost

Three things in the component only worked because it lived inside this app.

  • It reached into the MDX component map β€” import { ul as Ul, li as Li } from '../index' β€” which a consumer does not have. Both are trivial wrappers, and no second block needs them yet, so they are inlined. Per the plan's promotion rule that is the n=1 answer; the second block that wants them promotes them to their own registry item.
  • It imported cn through a path components.json does not alias. src/lib/cn.ts and src/lib/utils.ts were byte-equivalent, differing only in export style, and the utils alias β€” the one the CLI rewrites on install β€” pointed at the latter. One commit standardizes all 25 importers on it and deletes the duplicate.
  • It painted its border with border-outline-variant, which is border-border by another name once the MD3 remap is in place. It now uses the stock token, so a consumer who installed the block but skipped the colour layer still gets a border. bg-surface-dim stays β€” that one is the deliberate reach, and the reason for the dependency.

Two smaller corrections while the API was becoming public: className now merges after the defaults rather than before, so a consumer can actually override them, and title is optional in the type as it already was in practice.

The pin is v0.3.0, deliberately

v0.2.0 is the shared layer without its bake: the Tailwind mapping and the shadcn remap, pointing at --md-sys-color-* variables nothing defines unless the consumer mounts <Mtb> themselves. Install the block at that pin and the panel has no background at all β€” and since the remap points the stock variables at MD3 roles, --background and --primary go with it. Silently, as ever. v0.3.0 bakes the palette into the CSS, so shadcn add pmndrs/docs/keypoints is enough on its own.

It stays at v0.3.0 here, which is the mint bake β€” the slate is on the design system's main and has no tag yet. This site is unaffected, since it installs md3-base and computes its own palette; what it means is that a consumer installing the block today gets a mint panel. The pin moves to the slate release once it is tagged.


3. Compute the palette on the server

<Mtb> carries 'use client', so this site shipped material-theme-builder's palette code to every visitor and recomputed identical values on each load. The package's root export holds builder alone and carries no directive, so the root layout calls it and emits toCss() itself. Same rendered result, no palette code on the client.

That leaves ~32 kB of repeated <style> per document β€” but 3.4 kB gzip, 2.1 kB brotli: they're hex declarations, they compress about 15:1. An earlier version added a prebuild script to hoist that into the stylesheet, and it wasn't worth its price β€” a generated file, a fifth npm script wrapping the other four, allowImportingTsExtensions, a changed import style in mtb.ts, and an import-order rule that fails silently whenever the seed happens to match the default. Reverted. The 32 kB figure I first quoted was raw bytes and overstated the real cost by 15Γ—.

globals.css is untouched by it

Upstream splits the item: md3-base is the Tailwind mapping, the shadcn remap and the seed; md3 composes a baked palette on top. Blocks depend on md3 and get colours with nothing to mount. That split is for consumers β€” on this site the first two thirds are redundant, since #572 takes them from the package directly.

This site installs md3-base, because it computes its own palette β€” docsMtb adds the five alert colours, and blend: true derives them from whatever THEME_PRIMARY the deployment set, which an upstream bake cannot know. So globals.css is byte-identical to what part 2 left, rather than carrying 252 generated declarations overridden on the next line, and --md-sys-color-* has exactly one definition instead of two. No cascade-ordering rule to get wrong either, since there is nothing to override. What the site actually takes from md3-base is the seed β€” the mapping and remap it also carries are already in globals.css from #572.

Storybook has been rendering colourless

.storybook/preview.tsx imports the stylesheet and renders no layout, so --md-sys-color-* was undefined there β€” it only ever existed at runtime, from <Mtb> in the app. And since the shadcn remap points the stock variables at MD3 roles, that took --background, --primary and --border down with it. Chromatic has been baselining colourless components this whole time.

Fixed with an <Mtb> decorator. The client component the app deliberately avoids is the right tool here: Storybook is a browser, and there is no build to hook. Expect broad Chromatic changes β€” they are the fix, not a regression.

Net effect on the boundary: this repo keeps material-theme-builder because it is itself a multi-site generator, not because it consumes the design system. A site that just wants pmndrs colours installs md3 and runs none of it.


Verified

Nothing in-repo can catch a missing token: registry validate never parses TSX, Tailwind emits no rule and no error for an unknown utility, and a story runs inside this repo where every token exists by construction. So the scratch-app install is a required step rather than a nicety.

  • The block installs clean from outside any pmndrs repo. A bare create-next-app, shadcn init --preset b5cR4Y50S, then shadcn add pmndrs/docs/keypoints. It resolved the block, followed the dependency to pmndrs/design-system/md3#v0.3.0, installed material-theme-builder and the seed, and rendered a correctly-themed panel with nothing mounted. Every utility the block uses emits a rule in the built CSS β€” checked individually, bg-surface-dim included, which resolves to var(--md-sys-color-surface-dim).
  • No token lost in the migration. All 67 --color-* names the old mapping defined are among the 191 the new one defines.
  • The five alert colours resolve end-to-end after the move: bg-note-container and bg-tip-container emit rules, and --md-sys-color-{note,tip,important,warning,caution}-container are all server-rendered.
  • Full next build, then the running site: alerts render, seed is --md-ref-palette-primary-40: #086b5a, nav accent and Keypoints panel correct in dark mode, 69 sys-colour and 308 palette variables server-rendered. Re-checked on the running site after the seed moved to the slate: --md-ref-palette-primary-40: #266389, the same 69 and 308, and the five alert containers still resolving. No client chunk carries a palette variable, and no stylesheet chunk defines one β€” the <style> is the single source.
  • An earlier build at THEME_PRIMARY=#ff5500 rendered orange throughout, with the alert colours re-harmonized against the new seed.
  • build-storybook, then the built story served statically: the bg-surface-dim panel renders with a real colour.
  • tsc clean, eslint, prettier --check, 95 tests, pnpm check-registry (new script, shadcn registry validate) green. lint reports two react/no-unescaped-entities in src/stories/Page.tsx; both pre-date this branch, verified against main.

Chromatic will show the two Keypoints stories as removed and re-added rather than changed β€” the story ID follows the file path, and the file moved. And expect the broad colour changes described in part 3: those are the fix.

Known, not fixed here

  • pmndrs/design-system/md3 is two-thirds redundant here. Since Move the colour engine to material-theme-builderΒ #572 takes the Tailwind mapping and the shadcn remap from material-theme-builder itself, the only part of the item this site uses is the seed. That is fine for a consumer β€” a third-party block installs one item and gets everything β€” but it means docs is no longer a good test of the item's CSS half. Either the item slims down upstream now the package exposes both, or docs accepts being a direct material-theme-builder consumer and the item targets other sites only. Not decided here.

  • The globals.css diff in this PR is inert. The shadcn preset repaints 18 variables in the :root/.dark blocks; 17 of them are overridden by Move the colour engine to material-theme-builderΒ #572's shadcn.css import, whose selectors are doubled. --radius is the only live one, and its value does not change. It is kept because it is what the CLI writes and hand-editing it would come back on every future run β€” Move the colour engine to material-theme-builderΒ #572 adds a comment above the block saying so.

  • material-theme-builder's shipped dist/tailwind.css contains its own README's example custom colours (--color-myCustomColor1 / 2), so docs now carries 24 dead variables. Harmless, upstream, worth an issue there.

  • shadcn add matches its :root, .dark block on the exact selector string; prettier had reformatted ours to :root,\n.dark, so re-installing appended a second identical copy. Removed by hand, twice over this branch. It will happen on every item update β€” worth an issue upstream too.

  • The plan's CI check for missing tokens β€” extract the --color-* names a block's utilities resolve to, diff against stock shadcn βˆͺ what its registryDependencies emit β€” is still worth doing once there are several blocks. Nothing native does it.

  • vitest.config.ts's exclude: ['node_modules'] is not a glob, so a bare vitest walks into node_modules and tries to run its packages' tests. Noticed in passing, left alone.


πŸ€– Generated with Claude Code

https://claude.ai/code/session_01EDvnsrory1aJsoNALn5aLB

@abernier abernier changed the title Take the colour layer from pmndrs/design-system, distribute Keypoints, compute the palette on the server [DS] - POC Take the colour layer from pmndrs/design-system, distribute Keypoints, compute the palette on the server Aug 18, 2026
@abernier abernier self-assigned this Aug 18, 2026
@abernier
abernier marked this pull request as draft August 18, 2026 16:05
@abernier
abernier changed the base branch from main to mtb August 18, 2026 16:32
@abernier
abernier marked this pull request as ready for review August 18, 2026 16:32
Base automatically changed from mtb to main August 18, 2026 17:26
abernier and others added 6 commits August 18, 2026 19:49
docs stops maintaining its own copy of the colour system. The ~60-line
hand-written @theme mapping and the 33-line shadcn remap in globals.css
are replaced by one registry item, pmndrs/design-system/md3, which
carries both plus the config they map β€” so they can no longer drift from
each other. react-mcu gives way to material-theme-builder, which is what
that item installs.

Mostly a rename: --mcu-* becomes --md-sys-color-*. The --color-* names in
the @theme mapping are identical either way, so every bg-surface,
bg-primary-container and text-on-surface-variant keeps working untouched.
Two components referenced the raw variables rather than a utility β€” Code
and Sandpack β€” and those are the only component edits.

The seed moves to poimandres mint. It also stops being restated: it lived
in the layout, in build.yml's input defaults and hard-coded again in
ci.yml, three copies free to disagree. Now src/lib/md3.ts holds it and
the workflow inputs default to empty, so a site overrides the seed by
setting one and inherits the pmndrs default by leaving it alone.

Also applies the poimandres preset. It is close to a no-op here by
design: radius already matched, the colour half is overridden by the
remap, and the font half is deliberately not applied because docs
self-hosts Inter and Inconsolata. What it does land is the identity in
components.json and a --sidebar* block in oklch rather than hsl.

Verified: no token lost β€” all 67 --color-* names the old mapping defined
are in the 191 the new one does β€” and a full build renders mint, with
bg-surface-dim, bg-note-container and text-primary-fixed all emitting.

Refs pmndrs/design-system#1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDvnsrory1aJsoNALn5aLB
Follows pmndrs/design-system#3. note, tip, important, warning and
caution are GitHub's palette, and this generator is the only thing
rendering markdown alerts β€” they were never the design system's to
carry. Tier 1 now ships the seed alone, at v0.2.0.

src/lib/mtb.ts spreads pmndrsMtb and adds the five, so src/lib/md3.ts
stays a verbatim copy of the installed item and updating it remains a
clean overwrite. blend: true still harmonizes them against the seed, and
THEME_NOTE and its four siblings still override them.

Their twenty @theme lines move out of the shadcn-managed block into one
of our own, commented, next to nothing else β€” the config and the mapping
have to be read together, since a name declared in one and missing from
the other makes Tailwind emit no rule at all, without an error.

Refs pmndrs/design-system#1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDvnsrory1aJsoNALn5aLB
`src/lib/cn.ts` and `src/lib/utils.ts` were byte-equivalent implementations
of the same helper, differing only in export style. `components.json` points
its `utils` alias at the latter, so anything installed by `shadcn add` imports
the named export while everything hand-written imported the default one.

Keep the aliased file, drop the other. Prerequisite for extracting a block:
a distributed component has to import through the alias the CLI rewrites.
First Tier 2 block of the pmndrs UI distribution, and the first thing to
resolve a dependency across registries: `bg-surface-dim` has no shadcn
equivalent, so the item declares `pmndrs/design-system/md3#v0.3.0` and the
install pulls Tier 1 with it. That path is what the whole architecture rests
on, and nothing else exercises it.

The component moves to `registry/keypoints/`, at the root where shadcn looks
for registry items, and this app imports it back through a `@/registry/*`
alias β€” so what ships is the file the site renders, not a copy of it.

Three changes were needed to make it standalone:

- it pulled `ul` and `li` from this app's MDX component map. Both are trivial
  wrappers and no second block needs them yet, so they are inlined; the rule
  is to promote them to their own item at n=2.
- `className` now merges after the defaults rather than before, so a consumer
  can override them, and `title` is optional in the type as it already was in
  practice.
- `border-outline-variant` becomes `border-border`. The two are 1:1, and the
  stock token is the one a consumer who skipped Tier 1 still has.
  `bg-surface-dim` stays: it is the deliberate reach, and the reason for the
  dependency.

`registry.json` is validated by `pnpm check-registry`. That validator never
parses TSX, so it cannot see a missing token β€” an install into a scratch app
outside this repo remains the only check that would.
…thout its bake

`<Mtb>` carries `'use client'`, so this site shipped material-theme-builder's
palette code to every visitor and recomputed identical values on each load. The
package's root export holds `builder` alone and carries no directive, so the
root layout calls it and emits `toCss()` itself. Nothing about the rendered
result changes; the computation moves off the client.

That costs ~2 kB brotli of repeated `<style>` per document β€” the declarations
are highly compressible. A build step could hoist it into the stylesheet and
save that, but not for the price: a generated file, a fifth npm script wrapping
the other four, `allowImportingTsExtensions`, and an import-order rule that
fails silently whenever the seed matches the default. Not worth it at this size.

The design system now splits into `md3-base` β€” the Tailwind mapping, the shadcn remap and
the seed β€” and `md3`, which composes a baked palette on top. Blocks depend on
`md3` and get colours with nothing to mount. This site installs `md3-base`,
because it computes its own palette: `docsMtb` adds the five alert colours, and
`blend: true` derives them from whatever `THEME_PRIMARY` the deployment set,
which an upstream bake cannot know. So `globals.css` is untouched by this
branch, and `--md-sys-color-*` has exactly one definition rather than two.

Storybook gets the palette from an `<Mtb>` decorator. It imports the stylesheet
and renders no layout, so those variables were undefined there β€” and since the
remap points the stock shadcn variables at MD3 roles, that took `--background`,
`--primary` and `--border` down with it. Every story has been rendering
colourless, and Chromatic baselining it. The client component the app avoids is
the right tool here: Storybook is a browser, and there is no build to hook.

`registryDependencies` for `keypoints` stays on `md3`, now at `#v0.3.0`, which
needs the tag on pmndrs/design-system#4.
Upstream moved the pmndrs seed from the mint `#5de4c7` to the slate `#323e48`.
`src/lib/md3.ts` is a verbatim copy of the installed item, so this is that
overwrite, and the changeset's "visible change" line follows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmZpQG8qwLrA7V7RpKySJR
material-theme-builder v5 deletes `tailwind.css` and replaces it with
`@plugin "material-theme-builder/tailwind"`. The plugin emits everything the
stylesheet did β€” the same 115 `--color-*` names, same values β€” and adds the one
thing a static stylesheet could not: it takes the custom colour names and maps
them too.

So the twenty hand-written `@theme inline` lines go. They existed because the
old stylesheet covered standard M3 roles only, and each custom colour needed its
four roles wired by hand; forgetting one emitted no rule and no error, which is
what the comment above them warned about. Naming the five in the `@plugin` block
does the same job in one line, and there is nothing left to forget β€” so the
warning goes with them, in `src/lib/mtb.ts` as well.

The plugin also gives each custom colour its eleven ref-palette shades
(`bg-note-50` … `bg-note-950`), which the stylesheet never offered. Nothing uses
them yet.

Verified in the running dev server rather than on a clean build, since a missing
utility is exactly the failure that builds stay quiet about: the five alerts on
/authoring/gha compute byte-identical backgrounds before and after, in both
themes β€” dark `rgb(27,73,117)` / `rgb(0,82,52)` / `rgb(66,64,120)` /
`rgb(85,69,0)` / `rgb(114,51,63)`, light `rgb(210,228,255)` / `rgb(171,242,201)`
/ `rgb(227,223,255)` / `rgb(251,225,134)` / `rgb(255,217,221)` β€” as do
`bg-surface-dim` and `bg-primary`.

`registryDependencies` moves to `pmndrs/design-system/md3#v0.4.0`, the tag that
carries the `@plugin` item, and `src/lib/md3.ts` is re-synced from it β€” it stays
a verbatim copy of the installed file, and its JSDoc carried the same stale
four-lines-per-colour note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SMiASXrLM2zzaNgMSdbDcu
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant