v0.153.0
Minor Changes
-
#1277
1de5e56fThanks @tenphi! - Make the color palette tunable at runtime.setPaletteConfig({ hue: 210, // accent hue — the brand baseHue: 60, // neutral chrome hue; inherits `hue` when unset saturation: 72, themes: { danger: { hue: 12 }, code: { saturation: 60 } }, pastel: false, contrastLevel: "auto", });
Hue is split into two zones:
huedrives the accent zone (theaccent-*family,primary/purple/special, plusfocus, the loading faces and the disabled chip) andbaseHuedrives the base zone (the neutral chrome —surfaceand its ladder, thesurface-text*ramp,border,placeholder).baseHueinheritshue, so the chrome keeps its faint brand tint unless you decouple it. Only thedefaulttheme is affected; a colored theme's tintedsurfacedeliberately follows its own hue, because a danger banner should read as red.Saturation is deliberately not split: it is one seed per theme, and each color's
saturationis a 0–1 factor of it, so moving it rescales the palette while keeping the designed proportions between a subtle surface tint and a saturated accent.Each status theme (
success/danger/warning/note) re-seeds hue and saturation independently.pastelandcontrastLevelare global.setPaletteConfig()replaces, likeuseState— the config you pass is the config, resolved against the shipped defaults. Nothing accumulates, so removing a customization means removing it from the object, re-applying the same config twice does the same thing as once, and<Root palette>can un-set a field by no longer passing it. To change one field of the config already in place, pass an updater; it receives the config as written, sparse, so spreading it preserves what inherits:setPaletteConfig((config) => ({ ...config, hue: 235 }));
New exports:
setPaletteConfig,getPaletteConfig,getPaletteConfigInput,resolvePaletteConfig,resetPaletteConfig,subscribePaletteConfig,invalidatePaletteTokens,usePaletteConfig,usePaletteVersion,getPalette,DEFAULT_PALETTE_CONFIG, and the typesPaletteConfig/ResolvedPaletteConfig/PaletteThemeSeed/PaletteCodeSeed/PaletteThemeName, plusgetCodeTheme.<Root>gains an equivalentpaletteprop, applied during render so the first paint is already correct.Inherited vs pinned. Unset fields inherit, so
baseHuetrackshueandthemes.<status>.saturationtrackssaturationuntil something writes them — they are not linked, they just have no value of their own yet. Writing the field pins it; leaving it out unpins it.getPaletteConfig()resolves everything and so cannot tell you which is which;getPaletteConfigInput()returns the sparse config as set, for settings UIs that need to show an inherited value as inherited — and it is what asetPaletteConfigupdater is handed.Region previews.
renderColorTokens({ …config, scheme, highContrast })resolves the palette for one config and one scheme into flat literal values, ready to apply to a subtree through a tastytokensprop:<Block tokens={renderColorTokens({ hue: 210, scheme: "dark" })} fill="#surface" > …renders in that theme, inside a light page… </Block>
The document palette emits state maps (
@dark/@hc), so a page can only ever show one scheme at a time; collapsing it to a chosen scheme is what lets several themes coexist — a theme picker, or a dark panel in a light page. Config fields layer over the current config — the one place that differs fromsetPaletteConfig— so{ scheme: 'dark' }previews the active theme in dark. A preview means "the theme in use, but in dark", so the fields it does not mention come from the live palette rather than from the defaults.resolvePaletteConfig()layers the same way. Nothing is applied globally. Aliases, shadow tokens and scrollbar colors ride along by reference so they re-resolve against the region rather than freezing to the outer theme.renderPaletteTokensis the same without those, andresolvePaletteConfigresolves a partial without applying it.A mounted
<Root>re-injects the token block automatically when the config changes — no component re-render is involved, since every color compiles to a CSS custom property.This refactor changes no colors. Turning the palette into a function of its seeds is output-neutral: with no config set, every token resolves exactly as it did, and a new snapshot test (156 tokens × 4 scheme variants) enforces it. The surface-ladder and themed-border retune in this release is the only intentional color movement.
Notes:
- The palette is process-global (Glaze's own config is, and the tokens live in a single
bodyrule), so<Root palette>is a convenience wrapper oversetPaletteConfig(), not a per-tree scope. Under SSR, apply it in code that runs on both server and client — per-request palettes are not supported. - The
code-*syntax family is now its own Glaze theme with its own seed, so neither the brand hue nor the palette saturation reaches it. Everycode-*hue is absolute (a re-seeded brand can no longer collide string literals with#code-numberat 156°), and the saturation is fixed at80rather than inheritingsaturation, so muting the palette cannot wash out a code block. Tune it withthemes.code.saturation; the tokens stay adaptive, keeping their['AA','AAA']floor against the real surface in every scheme. Resolved values at the default config are unchanged. - A numeric
contrastLevelremoves the high-contrast tier entirely, so<html data-contrast="high">andprefers-contrast: morestop having an effect while one is set.pastel: truechanges every resolved color by design. Both are documented in the newGetting Started/Themingpage.
- The palette is process-global (Glaze's own config is, and the tokens live in a single
Patch Changes
-
#1277
1de5e56fThanks @tenphi! - Update@tenphi/glaze1.2.0 → 1.3.0. Resolved colors are unchanged: every token was dumped in all four scheme variants ('',@dark,@hc,@dark & @hc) and diffed against 1.2.0 — byte-identical.1.3.0 adds the
contrastLevelconfig field (a manual 0–100 contrast level replacing the two-tier high-contrast model, where levels 0 and 100 reproduce the normal and high-contrast output exactly), theresolveContrastForLevel()export, and thepreferInitialcontrast-solver option. Nothing existing changed behavior.A new snapshot spec (
src/tokens/palette.test.ts) pins the resolved palette — 156 tokens across four scheme variants — so a future Glaze bump or seed retune cannot move colors silently. -
#1277
1de5e56fThanks @tenphi! - Update@tenphi/tasty2.11.0 → 2.11.2, which fixes global-style hook behavior.useGlobalStylesnow keys its injection slots per root instead of in one module-level map, and the SSR / RSC collectors treat anid-keyed entry as replaceable rather than deduplicating it by content.That matters for the runtime-tunable palette:
<Root>injects the token block asuseGlobalStyles('body', …, { id: 'cube-ui-kit-tokens' }), so re-seeding the palette now replaces the previous block correctly on the server and in shadow roots, not just on the client.The bump adds ~400 B to the tree-shaken
Buttonentry, so itssize-limitbudget moved 118 kB → 119 kB. -
#1277
1de5e56fThanks @tenphi! - Add an optionalschemeprop toCubeLogo/CubeFullLogo.The mark is two drawings swapped by the
@darkstate, which is resolved against the document.scheme="light" | "dark"pins one of them for cases where the background is known but the document scheme does not describe it — a fixed-dark panel in a light app, an exported image, or a region themed throughtokens(which overrides token values, and so cannot reach a state). Omitting it keeps today's behaviour: the CSS swap, with no re-render and correct SSR. -
#1277
1de5e56fThanks @tenphi! - Retune the surface ladder, the themed borders and the radio mark.#surface-2,#surface-3and#surface-4gain wider high-contrast tone pairs (['-2','-4'],['-4','-8'],['-6','-12']), so nested panels stay distinguishable when a user asks for more contrast. The tinted<theme>-surfacewidens the same way.#borderdeepens in high contrast too (['-10','-30']).- The tinted
<theme>-borderused by OUTLINE-variant items drops fromsaturation: 0.5to0.3and takes the same wider HC pair, so a themed border reads as a border rather than a second accent. - The checked
Radiomark moves from#primaryto#primary-text, matching the#danger-text/#success-textits own invalid and valid states already used.#primaryis a fixed brand fill that barely moves between schemes;#primary-textis contrast-solved against the surface, so the dot lightens in dark (L 0.54 → 0.76) and in high contrast instead of staying a mid-tone purple. Alertborders now use#<theme>-borderinstead of a 20%-alpha accent fill, which is what makes the themed borders consistent between alerts and outline items. Thespecialalert border moves to#primary-borderfor the same reason.
Scope, measured across all 156 tokens in all four scheme variants: 68 tokens moved — 68 in
@dark & @hcand 62 in@hc. Only six move in the normal and dark schemes, and they are exactly the themed borders (#primary-border,#purple-border,#success-border,#danger-border,#warning-border,#note-border), from the saturation change. Everything else is high-contrast only. No token was added or removed, and thecode-*family does not move at all. -
#1277
1de5e56fThanks @tenphi! - FixonChangeonSwitchandCheckboxnot typechecking in controlled mode.CubeSwitchProps/CubeCheckboxPropswere missingonChange,isSelectedanddefaultSelectedentirely, so every controlled call site needed a@ts-expect-error. Root cause:tsconfig.jsonsetspreserveSymlinks: true, so TypeScript resolvesreact-aria's type re-exports from the symlink path and never finds the@react-aria/*subpackages (they are not direct dependencies);skipLibCheckthen hides the failure and everyAria*Propssilently becomesany. Extending ananybase contributes no members, which is why exactly these props vanished.The selection contract is now declared explicitly as
ToggleSelectionProps(src/shared/form.ts) and mixed into both components, restoring real type checking — a wrong handler signature now fails again. Four@ts-expect-errorsuppressions were removed (DisclosureandTreeinternals plus the theming stories), andCheckboxno longer types its non-DOMonChangeonto the<label>element it spreads props onto.Radiodeliberately keeps noonChange: in React Aria a single radio has none — selection is owned byRadio.Group.Removing
preserveSymlinksis the real fix, but it surfaces ~320 previously-hidden type errors across ~60 files, so it needs its own migration. The cause is documented inAGENTS.mdandtsconfig.jsonso the next person does not re-diagnose it.