The single surface for every design token in a site: palette colors, typography presets, built-in styleGuide tokens (radii, spacing scale, form colors, link colors, …), and user-created custom tokens. Replaces the per-row dropdowns previ…
The single surface for every design token in a site: palette colors, typography presets, built-in styleGuide tokens (radii, spacing scale, form colors, link colors, …), and user-created custom tokens. Replaces the per-row dropdowns previously living in StylesTab.
{} braces in any property-row's TypeSelector opens VarPicker anchored to the chip, applies-to-filtered by the current input's tailwindKey / propTag. Picking a row writes var(--name) (or font-(--name) for fontFamily) and closes the panel. Wired by packages/sdk/src/chrome/toolbar/inputs/universal-input/UniversalInputView.tsx.Both entry points render the same <VarPicker> component — only the anchor and propTag / tailwindKey differ.
list ──"+ New Token"──▶ create
list ──row Edit hover──▶ edit
create ──Save──▶ list (token now selectable)
edit ──Save / Delete / Cancel──▶ list
| Stage | File | Role |
|---|---|---|
| Orchestrator | VarPicker/index.tsx | FloatingPanel chrome + stage state machine. |
| List | VarPicker/VarList.tsx | Search field, scrollable rows, hover-revealed Edit pencil (styleGuide tokens only), sticky + New Token footer. |
| Editor | VarPicker/VarEditor.tsx | Type-driven create / edit form, in-use guards, delete button. |
The orchestrator owns the stage state — children don't unmount FloatingPanel on transitions, so panel position + drag state survive list ↔ editor moves.
Custom tokens piggyback on the existing storage that already flows through useDesignVars and designSystemVars.ts:
| Token kind | Storage | CRUD path |
|---|---|---|
| Palette color | theme.palette / theme.darkPalette — {name,color}[] | Existing CreateTokenDialog + DesignSystemPalette. VarPicker delegates — palette rows show in the list but their Edit pencil is hidden (use the existing palette manager). |
| Typography preset | theme.typography[] | Existing TextStyleEditorPanel. Same story — VarPicker shows them but doesn't edit them. |
| Built-in styleGuide token | theme.styleGuide[key] | VarEditor — name + type + appliesTo are LOCKED, only value is editable. |
| Custom styleGuide token | theme.styleGuide[key] + theme.styleGuideMeta[key] | VarEditor — full CRUD, type-driven editor. |
The new sidecar prop:
// On ROOT.props.theme — optional, sparse
styleGuideMeta?: Record<string, {
appliesTo?: ("palette" | "colors" | "spacing" | "typography" | "other")[];
custom?: boolean; // true if user-created
}>;
For built-in keys we already know type + category from the registry, so most tokens never need a styleGuideMeta entry. Custom keys carry custom: true so the UI can show Delete + the list can label them as "(Custom)".
Every styleGuide key surfaced in the picker is listed in styleTokenRegistry.ts:
export const STYLE_TOKEN_REGISTRY: Record<
string,
{
label: string;
category: "spacing" | "colors" | "other" | "typography";
type: "color" | "dimension" | "number" | "text";
quickPicks?: { value: string; label: string }[];
}
>;
category drives the relevance filter (which inputs show this token in the list).type drives the value editor (color → SketchPicker, dimension → UniversalInputControlled, number → number input, text → text input).quickPicks shows click-to-fill chips above the value editor — useful for tokens with a small enumerated value set (shadowStyle, linkUnderline, padding shorthands, clamp() spatial scale).Adding a new built-in styleGuide key: add the registry entry + the default value in DEFAULT_STYLE_GUIDE (packages/sdk/src/utils/defaults.ts). useDesignVars and designSystemVars.ts pick it up automatically.
NON_TOKEN_STYLE_KEYS (in the same file) is the small skip-list — currently just spacingDensity, which is a global multiplier surfaced as a slider in StylesTab rather than a per-token row.
useTokenUsagehooks/useTokenUsage.ts — given a --var-name, returns { count, nodeIds } for nodes whose stringified props contain var(--name).
Used to disable rename + delete when a token is in use:
Match scope: substring search on JSON.stringify(node.data.props). Catches className / mobileClassName / tabletClassName, inline style, attrs, anywhere a string holds var(--brand). Misses indirect alias chains (no aliases in v1).
Cost: walks every node once per call, memoized against the editor's tree-update counter. Sub-ms on typical PageHub trees.
hooks/useDesignVars.ts is the single read entry point for the picker:
theme.palette (or DEFAULT_PALETTE). Emit category palette, source palette.theme.typography. Per font, emit 6 vars (family / size / weight / line-height / letter-spacing / text-transform). Category typography, source typography.DEFAULT_STYLE_GUIDE + theme.styleGuide:
STYLE_TOKEN_REGISTRY → use its label + category, source styleGuide.styleGuideMeta[key].appliesTo[0] ?? value-heuristic (oklch( / # → colors; 1rem → spacing; etc.). Mark custom: true, label as "<HumanizedKey> (Custom)".The DesignVar type carries custom? + key + source so VarPicker can route Edit clicks correctly and lock vs. unlock fields.
VarEditor's dimension-typed value editor reuses the full UniversalInput UX (autocomplete, calc dialog, var picker chaining, type chip) — but UniversalInput is hard-wired to CraftJS's useNode() which throws when there's no <NodeProvider> ancestor. The split:
| File | Role |
|---|---|
| UniversalInputView.tsx | Pure presentational shell. Takes a state bag + handlers + flat config props. No Craft hooks. |
| index.tsx (UniversalInput) | Craft-bound wrapper: useUniversalInputState + useInputHandlers → render View. |
| UniversalInputControlled.tsx | Local-state-only wrapper for VarEditor. Same UX, no node writes. |
| hooks/useControlledInputState.ts | Mirror of useUniversalInputState minus the Craft hooks; sources currentValue from the controlled value prop, dispatches via onChange. |
Both wrappers expose the same prop surface to View, so behavior is identical. Existing callers of UniversalInput are unchanged.
StylesTab.tsx used to host 6 ToolbarSections of hard-coded preset dropdowns (Spacing & Layout, Spatial Scale, Effects & Borders, Sizing, Form Inputs, Links — ~600 lines). All of those rows are now individual built-in tokens editable in VarPicker. The tab keeps:
Manage Tokens → button + read-only summary (X palette · Y spacing · Z custom).Spacing Density slider (multiplies every spatial-scale token; not itself a token).useTokenUsage(varName).count > 0. User must clear references in the canvas first. v1 doesn't auto-rewrite references.STYLE_TOKEN_REGISTRY to prevent shadowing a built-in. Inline error if it would collide.CreateTokenDialog + DesignSystemPalette UI inside the color picker popover.theme.palette / theme.styleGuide / theme.typography shape and CSS-var emission.UnifiedDropdown (multi-column tailwind autocomplete) vs ToolbarDropdown (single-column Listbox). VarPicker is its own thing — neither.