Skip to content

Deterministic theme extraction only speaks shadcn's vocabulary - a Tailwind v4 @theme palette extracts 1 slot of 12 #1340

Description

@YSharara1

Summary

Deterministic theme extraction reads shadcn's token vocabulary and only shadcn's. A Tailwind v4 @theme block named the Tailwind way extracts almost nothing, silently, and the palette falls through to the consent-gated model pass — so a host with no model key gets the neutral blue default from a design-token sheet that declares every color it needs.

A Next 16 app whose entire palette lives in one @theme block produced this:

// .vendo/theme.extracted.json
{ "format": "vendo/theme-extracted@1", "slots": { "border": "#ece9e3" } }

One slot out of twelve — and border only matched by accident.

The obvious diagnosis is wrong

My first conclusion was "the extractor doesn't parse @theme". It does, and stating this saves the next person the same wrong turn:

  • parseCssVars (packages/vendo/src/cli/theme/css-vars.ts) is a brace-depth scanner. It is at-rule agnostic and reads declarations inside @theme exactly as it reads :root.
  • lastLightDecl (extract-theme.ts:282) already tries every allowlisted name in its Tailwind-v4 namespaced spelling: --border → also --color-border.

That second point is precisely why border came through. The @theme block is read correctly. It is the vocabulary that misses.

The actual gap

EXACT_COLOR_TOKENS (extract-theme.ts:259):

Vendo slot Accepted names
background --background / --color-background
text --foreground / --color-foreground
surface --card / --color-card
accent --primary / --color-primary
mutedText --muted-foreground / --color-muted-foreground
border --border / --color-border
danger --destructive / --color-destructive

The app names its tokens the way Tailwind's own docs name them: --color-bg, --color-ink, --color-surface, --color-accent, --color-muted, --color-neg. Nothing matches.

The sharpest edge: Vendo's own slot names are not accepted as token names. A host that writes --color-surface and --color-accent — the two slots Vendo literally calls surface and accent — gets neither, because the allowlist maps those slots to shadcn's --card and --primary. Naming your tokens after the thing you are configuring is the most natural guess available, and it is the one that fails.

examples/demo-bank is the control and shows the same shape:

// examples/demo-bank/.vendo/theme.extracted.json
{ "slots": { "border": "#ecebe8", "fontFamily": "Inter, sans-serif", "headingFamily": "Inter, sans-serif" } }

Same one color slot. Its committed theme.json colors (#FBFBFA, #111111, …) are not in that record — they came from the model pass. The two font slots came from deriveBodyFontStack reading its next/font import, not from CSS.

Why it lands hard

The file's own doc comment sets the expectation correctly: the exact pass is "fully deterministic — no model call, no network, no credential", and everything it leaves unfilled "rides init's consent-gated model step". Both halves work as designed. The problem is that the allowlist is narrow enough that a well-organized Tailwind v4 host lands entirely in the second half — so theme extraction reads as "needs a model key" when for a shadcn app it is free and exact.

And the miss is quiet. defaulted is tracked, but a newcomer seeing a blue theme has no way to reach "my token names aren't on a list I've never seen". The natural conclusion is that Vendo cannot read their CSS.

Affected

packages/vendo/src/cli/theme/extract-theme.ts at main (v0.8.0). Any host using Tailwind v4 @theme without shadcn's variable names.

Suggested fix

  1. Accept Vendo's own slot names as additional spellings: --background, --surface, --text, --muted-text, --accent, --border, --danger, each also in its --color-* form. Purely additive — no scoring, no inference, no new machinery, just more entries in EXACT_COLOR_TOKENS. It turns "name your tokens after the slots" into a documented deterministic path instead of a coincidence.
  2. Report the miss usefully. When brand slots default and the host has a CSS custom-property sheet, name a few of the unmatched properties: "found --color-bg, --color-ink, --color-surface — none are recognized token names; see ". The information is already in hand at that point.
  3. Document the vocabulary. Nothing currently lists which token names extract deterministically. The list is seven names long and is the difference between a free, exact palette and a model call.

Found while building a Next 16 order-ops app on v0.8.0.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions