Skip to content

Take the colour layer from pmndrs/design-system, distribute Keypoints, compute the palette on the server - #570

Closed
abernier wants to merge 6 commits into
mainfrom
md3-rsc
Closed

Take the colour layer from pmndrs/design-system, distribute Keypoints, compute the palette on the server#570
abernier wants to merge 6 commits into
mainfrom
md3-rsc

Conversation

@abernier

@abernier abernier commented Aug 16, 2026

Copy link
Copy Markdown
Member

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

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

The ~60-line hand-written @theme mapping and the 33-line shadcn remap in globals.css are replaced by one registry item. The item carries both, plus the config they map, so the mapping and the colours it names can no longer drift apart. react-mcu gives way to material-theme-builder, which is what the item installs.

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.

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.

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

  • 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 and others added 3 commits August 16, 2026 12:20
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.
@abernier abernier changed the title refactor: compute the palette on the server, take the baked Tier 1 Compute the palette on the server, take the baked Tier 1 Aug 16, 2026
@abernier
abernier marked this pull request as ready for review August 16, 2026 11:44
@abernier abernier changed the title Compute the palette on the server, take the baked Tier 1 Generate the palette at build, take the baked Tier 1 Aug 16, 2026
@abernier abernier changed the title Generate the palette at build, take the baked Tier 1 Compute the palette on the server, take Tier 1 without its bake Aug 16, 2026
@abernier abernier changed the title Compute the palette on the server, take Tier 1 without its bake Compute the palette on the server, take the shared layer without its bake Aug 16, 2026
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.
@abernier abernier changed the title Compute the palette on the server, take the shared layer without its bake Take the colour layer from pmndrs/design-system, distribute Keypoints, compute the palette on the server Aug 16, 2026
@abernier
abernier changed the base branch from keypoints-block to main August 16, 2026 14:00
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
@abernier

Copy link
Copy Markdown
Member Author

Branch renamed md3-rsc β†’ ds; the rename closed this PR instead of retargeting it. Continues in #571 (same commits, same body).

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