Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/pastel-palette-redesign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@cube-dev/ui-kit': minor
---

Make the pastel palette the default: the app seed moves to saturation 100 with
`pastel: true`, producing a softer, more even spread across hues.

Both are `PaletteConfig` defaults rather than a rewritten recipe, so anything that
already tunes the palette at runtime keeps working and can opt back out with
`{ saturation: 80, pastel: false }`.

The `code-*` syntax family is unaffected. It answers to its own seed, which now
reads a separate `DEFAULT_CODE_SATURATION` (still 80) instead of sharing
`DEFAULT_SATURATION`; sharing it would have pulled syntax colors to 100 as a side
effect of moving the app seed. `pastel` was already held off the code theme.
33 changes: 20 additions & 13 deletions src/stories/Theming.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ value so this stays true.
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| `hue` | **Accent** hue in degrees — the brand. Drives the `accent-*` family, `primary` / `purple` / `special`, and the brand-tinted `focus`, loading faces and disabled chip. | `280.3` |
| `baseHue` | **Base** hue in degrees — the neutral chrome: `surface` and its ladder, the `surface-text*` ramp, `border`, `placeholder`. | inherits `hue` |
| `saturation` | Seed saturation (0–100) for the `default` theme, and the fallback for every theme that sets none. | `80` |
| `saturation` | Seed saturation (0–100) for the `default` theme, and the fallback for every theme that sets none. | `100` |
| `themes.<status>.hue` | Hue for `success` / `danger` / `warning` / `note`. | `156.9` / `23.1` / `84.3` / `302.3` |
| `themes.<status>.saturation` | Saturation for that status theme. | inherits `saturation` |
| `themes.code.saturation` | Saturation for the `code-*` syntax family. Hues are fixed, and this does **not** inherit `saturation`. | `80` |
| `pastel` | Global. Relaxes the sRGB-safe chroma limit. Every theme except `code`. | `false` |
| `themes.code.saturation` | Saturation for the `code-*` syntax family. Hues are fixed, and this does **not** inherit `saturation`, nor track its default. | `80` |
| `pastel` | Global. Relaxes the sRGB-safe chroma limit. Every theme except `code`. | `true` |
| `contrastLevel` | Global. `'auto'`, or a manual `0`–`100` level. | `'auto'` |

### The setter replaces
Expand Down Expand Up @@ -107,9 +107,12 @@ Syntax colors are their own theme with their own seed, so neither the brand hue
the palette saturation reaches them. Hues are absolute literals — a brand re-seeded
toward green would otherwise collide `code-string` with `code-number` (156°) — and
the saturation is fixed at `80` rather than inheriting, so muting the app cannot
wash out a code block. `pastel` skips them for the same reason: it lowers the chroma
ceiling far enough to take `code-keyword` from ~0.19 to ~0.07, collapsing the spread
that keeps the syntax hues apart.
wash out a code block. That `80` is its own constant (`DEFAULT_CODE_SATURATION`),
deliberately not the palette default it once shared a value with: the app seed moved
to `100` with the pastel palette and the syntax colors stayed put, which is the whole
point of them not inheriting. `pastel` skips them for the same reason: it lowers
the chroma ceiling far enough to take `code-keyword` from ~0.19 to ~0.07,
collapsing the spread that keeps the syntax hues apart.

So `themes.code.saturation` is the only thing that moves them. Tune it on its own:

Expand All @@ -123,7 +126,8 @@ and saturation are pinned.

That separate seed is also a hard requirement, not a preference: a Glaze colour can
never exceed its own theme seed, and four of these sit at factor `1.0`. Inside a
default theme seeded below 80 they simply could not hold their chroma.
default theme seeded below `80` they simply could not hold their chroma — which is
also why the pinned seed is a floor to respect rather than a number to tidy up.

### Inherited vs pinned

Expand All @@ -147,7 +151,7 @@ setPaletteConfig(({ baseHue, ...config }) => config);
An explicit `baseHue: undefined` is equivalent to omitting it; neither is a value.

`getPaletteConfig()` resolves everything, which loses that distinction — it cannot
tell you whether `80` was chosen or inherited. `getPaletteConfigInput()` returns the
tell you whether `100` was chosen or inherited. `getPaletteConfigInput()` returns the
sparse config as set, which is what a settings UI needs to render an inherited value
as inherited and offer a way back:

Expand Down Expand Up @@ -351,11 +355,14 @@ and not per request:

## Caveats

**`pastel: true` is a redesign, not a filter.** It changes every resolved color
outside the `code-*` family. The per-color saturation factors in this palette were
tuned for the non-pastel space — pastel equalizes chroma across hues, so a pastel
palette on the shipped seeds resolves more saturated than you may expect. Re-tune
the seeds alongside it.
**`pastel` is a redesign, not a filter — in either direction.** It changes every
resolved color outside the `code-*` family, and the shipped palette is now pastel
(`pastel: true`, seed `100`), so it is `pastel: false` that is the departure. The two
spaces want different seeds: pastel equalizes the chroma ceiling across hues, where
the non-pastel one is per-hue and lets warm hues run further. Flipping the flag alone
therefore re-tunes nothing — a seed picked in one space lands somewhere else in the
other, and the warm statuses move most. Re-tune the seeds alongside it, in whichever
direction you flip.

**Re-seeding costs about 10 ms.** Rebuilding the eight themes and re-solving
~156 tokens across four scheme variants is not free, though it is inside a frame.
Expand Down
26 changes: 22 additions & 4 deletions src/stories/Theming.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,13 @@ const GroupLabel = tasty({
* where it has to (`Forest` sits at moss rather than mid-green to leave room for an
* emerald success; `Ember` at amber rather than orange to leave room for a crimson
* danger).
*
* Every preset except `Cube` also states its `pastel` stance outright rather than
* inheriting it. `Ocean` / `Forest` / `Ember` pin it off because their saturation
* seeds were picked against the per-hue chroma ceiling, and `Slate` pins it on
* because being the soft one is its whole identity — a contrast that only reads
* while its siblings are pinned off. `Cube` stays empty by design: it is the
* shipped palette, so it should follow the default wherever the default goes.
*/
const THEME_PRESETS: { label: string; config: PaletteConfig }[] = [
// Empty on purpose: the setter replaces, so this *is* the shipped palette — the
Expand All @@ -690,6 +697,13 @@ const THEME_PRESETS: { label: string; config: PaletteConfig }[] = [
config: {
hue: 235,
saturation: 70,
// Pinned off. These three were authored against a non-pastel default and
// their saturation seeds are tuned for the per-hue chroma ceiling; now that
// the shipped palette is pastel, inheriting it would quietly restyle them
// and flatten the contrast with `Slate`, whose whole point is being the
// muted one. Stating it also keeps all four presets explicit about a knob
// that visibly changes them.
pastel: false,
themes: {
success: { hue: 165 },
danger: { hue: 25 },
Expand All @@ -705,6 +719,7 @@ const THEME_PRESETS: { label: string; config: PaletteConfig }[] = [
config: {
hue: 128,
saturation: 65,
pastel: false,
themes: {
success: { hue: 172 },
danger: { hue: 25 },
Expand All @@ -718,6 +733,7 @@ const THEME_PRESETS: { label: string; config: PaletteConfig }[] = [
config: {
hue: 48,
saturation: 85,
pastel: false,
themes: {
success: { hue: 155 },
danger: { hue: 6 },
Expand All @@ -730,10 +746,12 @@ const THEME_PRESETS: { label: string; config: PaletteConfig }[] = [
label: 'Slate',
config: {
hue: 250,
// Pastel is what mutes this one: it swaps the per-hue chroma ceiling for a
// flat one, so the saturation seed can stay high and still land soft — 60
// here resolves to the same accent chroma a non-pastel 30 does, but even
// across hues rather than letting the warm statuses run ahead.
// Pastel is what mutes this one relative to the three above, which pin it
// off: it swaps the per-hue chroma ceiling for a flat one, so the saturation
// seed can stay high and still land soft — 60 here resolves to the same
// accent chroma a non-pastel 30 does, but even across hues rather than
// letting the warm statuses run ahead. Kept explicit even though it now
// matches the shipped default, so the four presets read as a set.
saturation: 60,
pastel: true,
themes: {
Expand Down
Loading
Loading