From d6591e90fb9eb3b6c10b7628db564cd314dc0426 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 9 Sep 2026 16:23:46 -0400 Subject: [PATCH 01/15] feat(ui): add internal gray scale and align Mosaic color tokens with Figma --- .changeset/mosaic-gray-scale.md | 2 + .claude/skills/mosaic/references/stylex.md | 3 + packages/swingset/src/stories/banner.mdx | 2 +- .../mosaic/components/badge/badge.styles.ts | 10 ++-- .../mosaic/components/banner/banner.styles.ts | 12 ++-- .../mosaic/components/button/button.styles.ts | 8 +-- .../src/mosaic/components/item/item.styles.ts | 2 +- packages/ui/src/mosaic/tokens.stylex.ts | 59 ++++++++++++++----- .../user-profile-api-keys-panel.styles.ts | 2 +- ...-profile-billing-history-section.styles.ts | 2 +- .../user-profile-security-panel.styles.ts | 2 +- references/mosaic-architecture.md | 2 + 12 files changed, 68 insertions(+), 38 deletions(-) create mode 100644 .changeset/mosaic-gray-scale.md diff --git a/.changeset/mosaic-gray-scale.md b/.changeset/mosaic-gray-scale.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-gray-scale.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.claude/skills/mosaic/references/stylex.md b/.claude/skills/mosaic/references/stylex.md index 5c6675e468a..765edb4c6d4 100644 --- a/.claude/skills/mosaic/references/stylex.md +++ b/.claude/skills/mosaic/references/stylex.md @@ -78,6 +78,9 @@ export const colorVars = stylex.defineVars(colorDefaults); - **DO** put light + dark in one token value via `light-dark()`. Never ship a second `@media (prefers-color-scheme: dark)` copy of a color. - **DO** reserve `--cl-*`-prefixed keys for the public, overridable contract. +- **DO** point a gray-backed colour token at the internal `gray` scale + (`light-dark(${gray['900']}, ${gray['50']})`) rather than restating the oklch value. + Its keys are unprefixed on purpose, so the names hash and stay un-overridable. - **DO** name internal, non-contract vars with a `--_cl-*` prefix (e.g. a value a parent writes for a child to read). They still emit verbatim but the `_` marks them "not a contract, don't override." diff --git a/packages/swingset/src/stories/banner.mdx b/packages/swingset/src/stories/banner.mdx index cbccb761c8c..7e18b5b4f34 100644 --- a/packages/swingset/src/stories/banner.mdx +++ b/packages/swingset/src/stories/banner.mdx @@ -59,7 +59,7 @@ Each part carries its stable slot class alongside the generated StyleX atoms and | `Banner.Label` | `neutral` \| `warning` \| `negative` | | `Banner.Description` | `neutral` \| `warning` \| `negative` | -The fill is a 4% mix of the color's token rather than its `-faded` surface, so a banner tints whatever it sits on instead of painting over it, and it inverts with the token in dark mode. Retheme a color by overriding the token it reads — `--cl-color-negative`, `--cl-color-warning`, or `--cl-color-neutral` (plus `--cl-color-border`, which draws the neutral hairline) — and the fill, border, icon, and copy all move together. +Warning and negative paint the color's `-subtle` surface inside its `-border` hairline and use the saturated token for the icon and copy, so retheme one by overriding `--cl-color-negative-subtle`, `--cl-color-negative-border`, and `--cl-color-negative` (or the `warning` equivalents). Neutral has no subtle surface of its own: its fill is a 4% mix of `--cl-color-neutral` over whatever it sits on, and `--cl-color-border` draws its hairline. --- diff --git a/packages/ui/src/mosaic/components/badge/badge.styles.ts b/packages/ui/src/mosaic/components/badge/badge.styles.ts index 6e2580d412d..2cad1fb92c1 100644 --- a/packages/ui/src/mosaic/components/badge/badge.styles.ts +++ b/packages/ui/src/mosaic/components/badge/badge.styles.ts @@ -2,10 +2,10 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; -// warning/negative/positive tint a faded fill and use the saturated token as text; +// warning/negative/positive fill with the subtle surface and use the saturated token as text; // primary fills with the solid token and uses its `-foreground` for text. // -// Neutral has no faded surface to tint — `--cl-color-neutral-faded` is a text gray, and its +// Neutral has no subtle surface to fill — `--cl-color-neutral-faded` is a text gray, and its // `-foreground` is a text color rather than an on-fill one, so it is unreadable against the solid // 900. It rides the same black/white scrim the button's neutral fill does, which composites against // any backdrop. Must be a local binding — StyleX inlines it; an imported one fails to compile. @@ -37,15 +37,15 @@ export const colors = stylex.create({ color: colorVars['--cl-color-neutral-foreground'], }, warning: { - backgroundColor: colorVars['--cl-color-warning-faded'], + backgroundColor: colorVars['--cl-color-warning-subtle'], color: colorVars['--cl-color-warning'], }, negative: { - backgroundColor: colorVars['--cl-color-negative-faded'], + backgroundColor: colorVars['--cl-color-negative-subtle'], color: colorVars['--cl-color-negative'], }, positive: { - backgroundColor: colorVars['--cl-color-positive-faded'], + backgroundColor: colorVars['--cl-color-positive-subtle'], color: colorVars['--cl-color-positive'], }, }); diff --git a/packages/ui/src/mosaic/components/banner/banner.styles.ts b/packages/ui/src/mosaic/components/banner/banner.styles.ts index 0e7c1067bec..f59e207f13b 100644 --- a/packages/ui/src/mosaic/components/banner/banner.styles.ts +++ b/packages/ui/src/mosaic/components/banner/banner.styles.ts @@ -3,10 +3,6 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; const neutralFill = `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`; -const warningFill = `color-mix(in oklab, ${colorVars['--cl-color-warning']} 4%, transparent)`; -const negativeFill = `color-mix(in oklab, ${colorVars['--cl-color-negative']} 4%, transparent)`; -const warningBorder = `color-mix(in oklab, ${colorVars['--cl-color-warning']} 20%, transparent)`; -const negativeBorder = `color-mix(in oklab, ${colorVars['--cl-color-negative']} 20%, transparent)`; export const styles = stylex.create({ root: { @@ -47,13 +43,13 @@ export const rootColors = stylex.create({ color: colorVars['--cl-color-neutral-foreground'], }, warning: { - borderColor: warningBorder, - backgroundColor: warningFill, + borderColor: colorVars['--cl-color-warning-border'], + backgroundColor: colorVars['--cl-color-warning-subtle'], color: colorVars['--cl-color-warning'], }, negative: { - borderColor: negativeBorder, - backgroundColor: negativeFill, + borderColor: colorVars['--cl-color-negative-border'], + backgroundColor: colorVars['--cl-color-negative-subtle'], color: colorVars['--cl-color-negative'], }, }); diff --git a/packages/ui/src/mosaic/components/button/button.styles.ts b/packages/ui/src/mosaic/components/button/button.styles.ts index 24e85561a71..807504eb5d8 100644 --- a/packages/ui/src/mosaic/components/button/button.styles.ts +++ b/packages/ui/src/mosaic/components/button/button.styles.ts @@ -45,7 +45,7 @@ const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, // that carry hue fade toward the tint that belongs to them, and the light-on-fill pairs fade toward // their own fill, which is the only backdrop they can ever sit on. const iconFadedNeutral = colorVars['--cl-color-neutral-faded']; -const iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-faded']} 50%)`; +const iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-subtle']} 50%)`; const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-primary-foreground']}, ${colorVars['--cl-color-primary']} 40%)`; const iconFadedOnNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative-foreground']}, ${colorVars['--cl-color-negative']} 40%)`; @@ -361,11 +361,11 @@ export const variants = stylex.create({ }, backgroundColor: { default: 'transparent', - ':not([data-disabled]):not([data-pending]):active': `color-mix(in oklab, ${colorVars['--cl-color-negative-faded']}, ${colorVars['--cl-color-negative']} 8%)`, - ':not([data-disabled])[data-open]': `color-mix(in oklab, ${colorVars['--cl-color-negative-faded']}, ${colorVars['--cl-color-negative']} 8%)`, + ':not([data-disabled]):not([data-pending]):active': `color-mix(in oklab, ${colorVars['--cl-color-negative-subtle']}, ${colorVars['--cl-color-negative']} 8%)`, + ':not([data-disabled])[data-open]': `color-mix(in oklab, ${colorVars['--cl-color-negative-subtle']}, ${colorVars['--cl-color-negative']} 8%)`, '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-negative-faded'], + ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-negative-subtle'], }, }, color: colorVars['--cl-color-negative'], diff --git a/packages/ui/src/mosaic/components/item/item.styles.ts b/packages/ui/src/mosaic/components/item/item.styles.ts index 9efd3a52470..03eb04e70e8 100644 --- a/packages/ui/src/mosaic/components/item/item.styles.ts +++ b/packages/ui/src/mosaic/components/item/item.styles.ts @@ -158,7 +158,7 @@ export const group = stylex.create({ export const separator = stylex.create({ base: { borderStyle: 'none', - backgroundColor: colorVars['--cl-color-border-faded'], + backgroundColor: colorVars['--cl-color-border-subtle'], flexShrink: 0, height: '1px', width: '100%', diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 75dccbfb798..33e06fe35ff 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -19,9 +19,33 @@ import * as stylex from '@stylexjs/stylex'; // `@media (prefers-color-scheme)` duplication. The `--cl-` prefix namespaces // the vars so they never collide with a host app's own custom properties. +// ============================================================================= +// Gray Scale +// ============================================================================= +// Figma's `base/gray/*`, internal only. The keys carry no `--cl-` prefix on purpose: +// StyleX hashes them, so there is no `--cl-gray-*` for a consumer to override and the +// semantic `--cl-color-*` tokens below stay the whole public colour surface. Pure grays, +// so chroma and hue are 0 and only lightness varies. + +export const gray = stylex.defineVars({ + '50': 'oklch(0.9851 0 0)', + '100': 'oklch(0.9702 0 0)', + '200': 'oklch(0.9461 0 0)', + '300': 'oklch(0.8699 0 0)', + '400': 'oklch(0.7155 0 0)', + '500': 'oklch(0.5555 0 0)', + '600': 'oklch(0.4386 0 0)', + '700': 'oklch(0.3715 0 0)', + '800': 'oklch(0.2686 0 0)', + '850': 'oklch(0.2393 0 0)', + '900': 'oklch(0.2046 0 0)', +}); + // ============================================================================= // Color Tokens // ============================================================================= +// Tokens Figma maps to `base/gray/*` read from the scale above. Brand and status +// colours are literal values there too, so they stay literal here. const colorDefaults = { '--cl-color-primary': 'light-dark(oklch(0.205 0 0), oklch(0.922 0 0))', @@ -32,28 +56,31 @@ const colorDefaults = { '--cl-color-neutral-foreground': 'light-dark(oklch(0.24 0 0), oklch(0.96 0 0))', '--cl-color-neutral-faded': 'light-dark(oklch(0.5697 0.0246 279.94), oklch(0.6953 0.0261 285.7))', - '--cl-color-negative': 'light-dark(oklch(0.577 0.245 27.325), oklch(0.637 0.237 25.331))', - '--cl-color-negative-foreground': 'oklch(0.985 0 0)', - '--cl-color-negative-faded': 'light-dark(oklch(0.9757 0.0118 17.36), oklch(0.255 0.0604 22.31))', + '--cl-color-negative': 'light-dark(oklch(0.5903 0.213 26.78), oklch(0.7106 0.1661 22.22))', + '--cl-color-negative-foreground': 'oklch(1 0 0)', + '--cl-color-negative-subtle': 'light-dark(oklch(0.9757 0.0118 17.36), oklch(0.255 0.0604 22.31))', + '--cl-color-negative-border': 'light-dark(oklch(0.8155 0.0983 19.41), oklch(0.3958 0.1331 25.72))', - '--cl-color-positive': 'light-dark(oklch(0.548 0.153 152.535), oklch(0.696 0.17 162.48))', - '--cl-color-positive-foreground': 'oklch(0.985 0 0)', - '--cl-color-positive-faded': 'light-dark(oklch(0.9859 0.0164 156.92), oklch(0.3297 0.052 152.31))', + '--cl-color-positive': 'light-dark(oklch(0.6082 0.1799 145.47), oklch(0.7227 0.192 149.58))', + '--cl-color-positive-foreground': 'oklch(1 0 0)', + '--cl-color-positive-subtle': 'light-dark(oklch(0.9859 0.0164 156.92), oklch(0.3297 0.052 152.31))', + '--cl-color-positive-border': 'light-dark(oklch(0.7922 0.1959 148.18), oklch(0.4479 0.1083 151.33))', - '--cl-color-warning': 'light-dark(oklch(0.646 0.222 41.116), oklch(0.75 0.183 55.934))', - '--cl-color-warning-foreground': 'oklch(0.985 0 0)', - '--cl-color-warning-faded': 'light-dark(oklch(0.9799 0.0147 70.89), oklch(0.2725 0.0547 55.7))', + '--cl-color-warning': 'light-dark(oklch(0.7099 0.1888 44.94), oklch(0.7576 0.159 55.93))', + '--cl-color-warning-foreground': 'oklch(1 0 0)', + '--cl-color-warning-subtle': 'light-dark(oklch(0.9799 0.0147 70.89), oklch(0.2725 0.0547 55.7))', + '--cl-color-warning-border': 'light-dark(oklch(0.8672 0.0902 63.47), oklch(0.4084 0.1165 38.17))', - '--cl-color-background': 'light-dark(oklch(0.985 0 0), oklch(0.2686 0 0))', + '--cl-color-background': `light-dark(${gray['100']}, ${gray['850']})`, - '--cl-color-card': 'light-dark(oklch(1 0 0), oklch(0.205 0 0))', - '--cl-color-card-foreground': 'light-dark(oklch(0.145 0 0), oklch(0.985 0 0))', + '--cl-color-card': `light-dark(oklch(1 0 0), ${gray['900']})`, + '--cl-color-card-foreground': `light-dark(${gray['900']}, ${gray['50']})`, - '--cl-color-input': 'light-dark(oklch(1 0 0), oklch(0.2046 0 0))', - '--cl-color-input-placeholder': '#a3a3a3', + '--cl-color-input': `light-dark(oklch(1 0 0), ${gray['850']})`, + '--cl-color-input-placeholder': gray['400'], - '--cl-color-border': 'light-dark(oklch(0.9219 0 0), oklch(0.3715 0 0))', - '--cl-color-border-faded': 'light-dark(oklch(0.9702 0 0), oklch(0.296 0.0126 285.61))', + '--cl-color-border': `light-dark(${gray['200']}, ${gray['800']})`, + '--cl-color-border-subtle': `light-dark(${gray['100']}, ${gray['850']})`, '--cl-color-ring': 'light-dark(oklch(0.205 0 0), oklch(0.922 0 0))', } as const; diff --git a/packages/ui/src/mosaic/user-profile/user-profile-api-keys-panel.styles.ts b/packages/ui/src/mosaic/user-profile/user-profile-api-keys-panel.styles.ts index 1aa2b4aebd3..40f747c05c7 100644 --- a/packages/ui/src/mosaic/user-profile/user-profile-api-keys-panel.styles.ts +++ b/packages/ui/src/mosaic/user-profile/user-profile-api-keys-panel.styles.ts @@ -35,7 +35,7 @@ export const styles = stylex.create({ textAlign: 'center', }, header: { - backgroundColor: colorVars['--cl-color-border-faded'], + backgroundColor: colorVars['--cl-color-border-subtle'], }, headerCell: { paddingBlock: space['2.5'], diff --git a/packages/ui/src/mosaic/user-profile/user-profile-billing-history-section.styles.ts b/packages/ui/src/mosaic/user-profile/user-profile-billing-history-section.styles.ts index ed30ce9b878..7eec1a2e133 100644 --- a/packages/ui/src/mosaic/user-profile/user-profile-billing-history-section.styles.ts +++ b/packages/ui/src/mosaic/user-profile/user-profile-billing-history-section.styles.ts @@ -24,7 +24,7 @@ export const styles = stylex.create({ textAlign: 'center', }, header: { - backgroundColor: colorVars['--cl-color-border-faded'], + backgroundColor: colorVars['--cl-color-border-subtle'], }, headerCell: { paddingBlock: space['2.5'], diff --git a/packages/ui/src/mosaic/user-profile/user-profile-security-panel.styles.ts b/packages/ui/src/mosaic/user-profile/user-profile-security-panel.styles.ts index 6d4ba165d7d..4ac139e2561 100644 --- a/packages/ui/src/mosaic/user-profile/user-profile-security-panel.styles.ts +++ b/packages/ui/src/mosaic/user-profile/user-profile-security-panel.styles.ts @@ -18,7 +18,7 @@ export const styles = stylex.create({ width: space['4.5'], }, media: { - borderColor: colorVars['--cl-color-border-faded'], + borderColor: colorVars['--cl-color-border-subtle'], borderRadius: radiusVars['--cl-radius-md'], borderStyle: 'solid', borderWidth: '1px', diff --git a/references/mosaic-architecture.md b/references/mosaic-architecture.md index d00d6427612..cb6eff1a4a9 100644 --- a/references/mosaic-architecture.md +++ b/references/mosaic-architecture.md @@ -25,6 +25,8 @@ export const colorVars = stylex.defineVars({ Groups: `colorVars`, `radiusVars`, `targetVars`, `scrollbarVars`, `scrollFadeVars`, `spacingVars`, `space`, `typeScaleVars`, `fontFamilyVars`, `fontWeightVars`, `durationVars`, `easingVars`, `focusVars`. +`gray` is the one group that is not public: its keys carry no `--cl-` prefix, so StyleX hashes the names and there is no `--cl-gray-*` to override. The gray-backed `--cl-color-*` tokens read from it; consumers retheme those, not the scale. + Light and dark come from CSS `light-dark()` on the default values, so there is no theme object and no re-render on theme change — the browser resolves it. `@stylexjs/enforce-extension` requires a `.stylex.ts` file to export nothing but its `defineVars` results, so the derived token-name unions (`ColorVarName`, `RadiusVarName`, …) live in `styles/index.ts` as `keyof typeof colorVars`. From 565d4ab5506a19c5f965ac3137ec298e559ac31c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 9 Sep 2026 16:28:07 -0400 Subject: [PATCH 02/15] feat(ui): restructure Mosaic color tokens into foreground, background, and brand groups --- .claude/skills/mosaic/references/stylex.md | 14 ++--- packages/swingset/src/stories/banner.mdx | 2 +- .../src/stories/icon-frame.stories.tsx | 4 +- packages/swingset/src/stories/item.mdx | 2 +- packages/swingset/src/stories/scroll-area.mdx | 6 +- .../src/stories/scroll-area.stories.tsx | 6 +- .../mosaic/components/avatar/avatar.styles.ts | 6 +- .../mosaic/components/badge/badge.styles.ts | 8 +-- .../mosaic/components/banner/banner.styles.ts | 6 +- .../mosaic/components/button/button.styles.ts | 58 +++++++++---------- .../src/mosaic/components/card/card.styles.ts | 10 ++-- .../mosaic/components/dialog/dialog.styles.ts | 6 +- .../mosaic/components/dialog/dialog.test.tsx | 2 +- .../mosaic/components/field/field.styles.ts | 4 +- .../src/mosaic/components/item/item.styles.ts | 20 +++---- .../src/mosaic/components/menu/menu.styles.ts | 8 +-- .../src/mosaic/components/otp/otp.styles.ts | 4 +- .../components/section/section.styles.ts | 8 +-- packages/ui/src/mosaic/props.ts | 2 +- packages/ui/src/mosaic/tokens.stylex.ts | 43 +++++++------- .../mosaic/user-button/user-button.styles.ts | 2 +- .../user-profile-api-keys-panel.styles.ts | 22 +++---- ...-profile-billing-history-section.styles.ts | 18 +++--- .../user-profile-billing-panel.styles.ts | 2 +- .../user-profile-security-panel.styles.ts | 4 +- .../src/mosaic/utils/focus-outline.styles.ts | 2 +- packages/ui/src/mosaic/utils/input.styles.ts | 2 +- .../ui/src/mosaic/utils/typography.styles.ts | 6 +- references/mosaic-architecture.md | 14 ++--- 29 files changed, 145 insertions(+), 146 deletions(-) diff --git a/.claude/skills/mosaic/references/stylex.md b/.claude/skills/mosaic/references/stylex.md index 765edb4c6d4..d22f4daaf20 100644 --- a/.claude/skills/mosaic/references/stylex.md +++ b/.claude/skills/mosaic/references/stylex.md @@ -70,7 +70,7 @@ it gives consumers stable `--cl-*` vars to override in plain CSS: const colorDefaults = { // one value carries light + dark; resolves against the in-scope `color-scheme`, // so dark mode lives in the token, no `@media (prefers-color-scheme)` copy. - '--cl-color-primary': 'light-dark(oklch(0.205 0 0), oklch(0.922 0 0))', + '--cl-color-brand': 'light-dark(oklch(0.2046 0 0), oklch(0.9851 0 0))', } as const; export const colorVars = stylex.defineVars(colorDefaults); ``` @@ -90,7 +90,7 @@ export const colorVars = stylex.defineVars(colorDefaults); wrapping controls. A role name survives a value change; `--cl-radius-md` doesn't. - **DO** compute tints at the call site with `color-mix()`, not as their own tokens. `color-mix(in oklab, ${primary}, ${fg} 12%)` beats minting - `--cl-color-primary-hover-12`. + `--cl-color-brand-hover-12`. - **DON'T** mint a per-step derivative token for something a `calc()`/`color-mix()` can express from an existing token. - **DON'T** give one value two public names. The focus ring's colour is @@ -105,7 +105,7 @@ export const colorVars = stylex.defineVars(colorDefaults); const step = `color-mix(in oklab, light-dark(oklch(0 0 0), oklch(1 0 0)) 12%, transparent)`; ``` - A gray token like `--cl-color-neutral` is a 900, not black, so the same percentage of + A gray token like `--cl-color-foreground` is a 900, not black, so the same percentage of it lands lighter than the percentage of black — and by an amount that shifts with whatever the control sits on, so the step numbers stop describing what they render. The scrim composites, so one ramp reads consistently on every surface. This applies @@ -131,7 +131,7 @@ imported file"). `defineConsts` is StyleX's shareable inlined-value primitive, s it's the only way to get a scale that is both shared across components and free of per-step vars. -**Reference tokens by bracket string key**, always: `colorVars['--cl-color-primary']`, +**Reference tokens by bracket string key**, always: `colorVars['--cl-color-brand']`, `space['2']`. A computed key (`colorVars[name]`) defeats StyleX static analysis and won't compile. @@ -160,7 +160,7 @@ objects and compose them at the call site. }); // variant map: keyed by the prop value, indexed at the call site const variants = stylex.create({ - primary: { backgroundColor: colorVars['--cl-color-primary'] }, + primary: { backgroundColor: colorVars['--cl-color-brand'] }, secondary: { backgroundColor: colorVars['--cl-color-secondary'] }, }); const sizes = stylex.create({ @@ -235,7 +235,7 @@ instead of 12, with each axis staying independent. **Don't.** inlines it at build, so the duplication leaves the source without emitting a var: ```ts - const primaryHover = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, ${colorVars['--cl-color-primary-foreground']} 12%)`; + const primaryHover = `color-mix(in oklab, ${colorVars['--cl-color-brand']}, ${colorVars['--cl-color-brand-foreground']} 12%)`; ``` Same-file is required — an imported one fails static evaluation ("Atoms" above). @@ -258,7 +258,7 @@ Use StyleX's conditional-value objects (a `default` plus pseudo / at-rule keys). ```ts backgroundColor: { - default: colorVars['--cl-color-primary'], + default: colorVars['--cl-color-brand'], ':active': primaryActive, '@media (hover: hover)': { // the media block contributes only the pseudo; the top-level `default` still diff --git a/packages/swingset/src/stories/banner.mdx b/packages/swingset/src/stories/banner.mdx index 7e18b5b4f34..ec47d060ac0 100644 --- a/packages/swingset/src/stories/banner.mdx +++ b/packages/swingset/src/stories/banner.mdx @@ -59,7 +59,7 @@ Each part carries its stable slot class alongside the generated StyleX atoms and | `Banner.Label` | `neutral` \| `warning` \| `negative` | | `Banner.Description` | `neutral` \| `warning` \| `negative` | -Warning and negative paint the color's `-subtle` surface inside its `-border` hairline and use the saturated token for the icon and copy, so retheme one by overriding `--cl-color-negative-subtle`, `--cl-color-negative-border`, and `--cl-color-negative` (or the `warning` equivalents). Neutral has no subtle surface of its own: its fill is a 4% mix of `--cl-color-neutral` over whatever it sits on, and `--cl-color-border` draws its hairline. +Warning and negative paint the color's `-subtle` surface inside its `-border` hairline and use the saturated token for the icon and copy, so retheme one by overriding `--cl-color-negative-subtle`, `--cl-color-negative-border`, and `--cl-color-negative` (or the `warning` equivalents). Neutral has no subtle surface of its own: its fill is a 4% mix of `--cl-color-foreground` over whatever it sits on, and `--cl-color-border` draws its hairline. --- diff --git a/packages/swingset/src/stories/icon-frame.stories.tsx b/packages/swingset/src/stories/icon-frame.stories.tsx index 376070f31e7..39246e6bf79 100644 --- a/packages/swingset/src/stories/icon-frame.stories.tsx +++ b/packages/swingset/src/stories/icon-frame.stories.tsx @@ -113,8 +113,8 @@ export function CustomSurface() { return ( `, so they drive position as readily as opac slides out from behind its own edge as it fades in. Clip the root — `overflow: hidden` — so the half that is still offscreen stays there. -Mix the scrim from a theme colour rather than hardcoding black: `--cl-color-card-foreground` inverts +Mix the scrim from a theme colour rather than hardcoding black: `--cl-color-foreground` inverts with the theme, so one declaration reads as a shadow on light and a soft glow on dark, where black would vanish. diff --git a/packages/swingset/src/stories/scroll-area.stories.tsx b/packages/swingset/src/stories/scroll-area.stories.tsx index 8226bdaf329..80be1eac572 100644 --- a/packages/swingset/src/stories/scroll-area.stories.tsx +++ b/packages/swingset/src/stories/scroll-area.stories.tsx @@ -274,7 +274,7 @@ export function ThemedScrollbar() { /** * The mask retired for overlay scrims, each reading the progress var for its edge. The scrim mixes - * from `--cl-color-card-foreground`, so it reads as a shadow on light and a glow on dark; + * from `--cl-color-foreground`, so it reads as a shadow on light and a glow on dark; * hardcoded black would vanish on a dark surface. `overflow: hidden` on the root keeps the scrims * inside its rounded corners. * @@ -302,13 +302,13 @@ export function ShadowIndicators() { } .demo-scroll-shadows .cl-item-group::before { top: 0; - background: linear-gradient(to bottom, color-mix(in oklab, var(--cl-color-card-foreground) 22%, transparent), transparent); + background: linear-gradient(to bottom, color-mix(in oklab, var(--cl-color-foreground) 22%, transparent), transparent); opacity: var(--cl-scroll-area-progress-start); transform: translateY(calc((var(--cl-scroll-area-progress-start) - 1) * var(--cl-scroll-fade-size))); } .demo-scroll-shadows .cl-item-group::after { bottom: 0; - background: linear-gradient(to top, color-mix(in oklab, var(--cl-color-card-foreground) 22%, transparent), transparent); + background: linear-gradient(to top, color-mix(in oklab, var(--cl-color-foreground) 22%, transparent), transparent); opacity: var(--cl-scroll-area-progress-end); transform: translateY(calc((1 - var(--cl-scroll-area-progress-end)) * var(--cl-scroll-fade-size))); } diff --git a/packages/ui/src/mosaic/components/avatar/avatar.styles.ts b/packages/ui/src/mosaic/components/avatar/avatar.styles.ts index e68a547bf1a..2e59ad80521 100644 --- a/packages/ui/src/mosaic/components/avatar/avatar.styles.ts +++ b/packages/ui/src/mosaic/components/avatar/avatar.styles.ts @@ -50,8 +50,8 @@ export const styles = stylex.create({ fallback: { borderRadius: 'inherit', alignItems: 'center', - backgroundColor: `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 8%, transparent)`, - color: colorVars['--cl-color-neutral'], + backgroundColor: `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 8%, transparent)`, + color: colorVars['--cl-color-foreground'], display: 'flex', justifyContent: 'center', height: '100%', @@ -81,7 +81,7 @@ export const styles = stylex.create({ borderWidth: '1px', overflow: 'hidden', alignItems: 'center', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: colorVars['--cl-color-background'], boxSizing: 'border-box', display: 'flex', insetBlockEnd: `calc(${space['2']} * -1)`, diff --git a/packages/ui/src/mosaic/components/badge/badge.styles.ts b/packages/ui/src/mosaic/components/badge/badge.styles.ts index 2cad1fb92c1..e7517da931f 100644 --- a/packages/ui/src/mosaic/components/badge/badge.styles.ts +++ b/packages/ui/src/mosaic/components/badge/badge.styles.ts @@ -5,7 +5,7 @@ import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScale // warning/negative/positive fill with the subtle surface and use the saturated token as text; // primary fills with the solid token and uses its `-foreground` for text. // -// Neutral has no subtle surface to fill — `--cl-color-neutral-faded` is a text gray, and its +// Neutral has no subtle surface to fill — `--cl-color-foreground-secondary` is a text gray, and its // `-foreground` is a text color rather than an on-fill one, so it is unreadable against the solid // 900. It rides the same black/white scrim the button's neutral fill does, which composites against // any backdrop. Must be a local binding — StyleX inlines it; an imported one fails to compile. @@ -29,12 +29,12 @@ export const styles = stylex.create({ export const colors = stylex.create({ primary: { - backgroundColor: colorVars['--cl-color-primary'], - color: colorVars['--cl-color-primary-foreground'], + backgroundColor: colorVars['--cl-color-brand'], + color: colorVars['--cl-color-brand-foreground'], }, neutral: { backgroundColor: neutralScrim, - color: colorVars['--cl-color-neutral-foreground'], + color: colorVars['--cl-color-foreground'], }, warning: { backgroundColor: colorVars['--cl-color-warning-subtle'], diff --git a/packages/ui/src/mosaic/components/banner/banner.styles.ts b/packages/ui/src/mosaic/components/banner/banner.styles.ts index f59e207f13b..7a7232ba031 100644 --- a/packages/ui/src/mosaic/components/banner/banner.styles.ts +++ b/packages/ui/src/mosaic/components/banner/banner.styles.ts @@ -2,7 +2,7 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; -const neutralFill = `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`; +const neutralFill = `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent)`; export const styles = stylex.create({ root: { @@ -40,7 +40,7 @@ export const rootColors = stylex.create({ neutral: { borderColor: colorVars['--cl-color-border'], backgroundColor: neutralFill, - color: colorVars['--cl-color-neutral-foreground'], + color: colorVars['--cl-color-foreground'], }, warning: { borderColor: colorVars['--cl-color-warning-border'], @@ -55,7 +55,7 @@ export const rootColors = stylex.create({ }); export const descriptionColors = stylex.create({ - neutral: { color: colorVars['--cl-color-neutral-faded'] }, + neutral: { color: colorVars['--cl-color-foreground-secondary'] }, warning: { color: colorVars['--cl-color-warning'] }, negative: { color: colorVars['--cl-color-negative'] }, }); diff --git a/packages/ui/src/mosaic/components/button/button.styles.ts b/packages/ui/src/mosaic/components/button/button.styles.ts index 807504eb5d8..d4e65850af2 100644 --- a/packages/ui/src/mosaic/components/button/button.styles.ts +++ b/packages/ui/src/mosaic/components/button/button.styles.ts @@ -18,11 +18,11 @@ import { iconScope } from '../icon/icon.markers.stylex'; // step 2 — `filled-neutral` pressed // // Scrim opacities over `transparent`, so they composite against any backdrop. Not -// `--cl-color-neutral`: it's a 900, so a percentage of it lands lighter than the same +// `--cl-color-foreground`: it's a 900, so a percentage of it lands lighter than the same // percentage of black by an amount that shifts with the backdrop. // // TODO: codify this %-mix scale as shared tokens once more components adopt these -// shades — `item` and `avatar` still mix from `--cl-color-neutral`, and unifying them +// shades — `item` and `avatar` still mix from `--cl-color-foreground`, and unifying them // is a palette-wide change. // // Must be local bindings — StyleX inlines them; an imported one fails to compile. @@ -32,8 +32,8 @@ const neutralStep2 = `color-mix(in oklab, light-dark(oklch(0 0 0), oklch(1 0 0)) // Opaque fills blend toward their own on-fill at the same 12%/18%. Neutral has no fill // to blend from, so it rides the opacity ramp above. -const primaryHover = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, ${colorVars['--cl-color-primary-foreground']} 12%)`; -const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, ${colorVars['--cl-color-primary-foreground']} 18%)`; +const primaryHover = `color-mix(in oklab, ${colorVars['--cl-color-brand']}, ${colorVars['--cl-color-brand-foreground']} 12%)`; +const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-brand']}, ${colorVars['--cl-color-brand-foreground']} 18%)`; const negativeHover = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 12%)`; const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`; @@ -44,9 +44,9 @@ const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, // The achromatic foregrounds (`primary`, `neutral-foreground`) share the house faded gray. The two // that carry hue fade toward the tint that belongs to them, and the light-on-fill pairs fade toward // their own fill, which is the only backdrop they can ever sit on. -const iconFadedNeutral = colorVars['--cl-color-neutral-faded']; +const iconFadedNeutral = colorVars['--cl-color-foreground-secondary']; const iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-subtle']} 50%)`; -const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-primary-foreground']}, ${colorVars['--cl-color-primary']} 40%)`; +const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-brand-foreground']}, ${colorVars['--cl-color-brand']} 40%)`; const iconFadedOnNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative-foreground']}, ${colorVars['--cl-color-negative']} 40%)`; // Interactive states are gated on `:not([data-disabled])`: the button stays hit-testable while @@ -183,14 +183,14 @@ export const variants = stylex.create({ 'filled-primary': { '--_cl-icon-color': { default: iconFadedOnPrimary, - ':not([data-disabled])[data-open]': colorVars['--cl-color-primary-foreground'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-brand-foreground'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover': colorVars['--cl-color-primary-foreground'], + ':not([data-disabled]):hover': colorVars['--cl-color-brand-foreground'], }, }, backgroundColor: { - default: colorVars['--cl-color-primary'], + default: colorVars['--cl-color-brand'], ':not([data-disabled]):not([data-pending]):active': primaryActive, ':not([data-disabled])[data-open]': primaryActive, '@media (hover: hover)': { @@ -198,15 +198,15 @@ export const variants = stylex.create({ ':not([data-disabled]):hover:not(:active):not([data-open])': primaryHover, }, }, - color: colorVars['--cl-color-primary-foreground'], + color: colorVars['--cl-color-brand-foreground'], }, 'filled-neutral': { '--_cl-icon-color': { default: iconFadedNeutral, - ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-foreground'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-foreground'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover': colorVars['--cl-color-neutral-foreground'], + ':not([data-disabled]):hover': colorVars['--cl-color-foreground'], }, }, backgroundColor: { @@ -218,7 +218,7 @@ export const variants = stylex.create({ ':not([data-disabled]):hover:not(:active):not([data-open])': neutralStep1, }, }, - color: colorVars['--cl-color-neutral-foreground'], + color: colorVars['--cl-color-foreground'], }, 'filled-negative': { '--_cl-icon-color': { @@ -247,10 +247,10 @@ export const variants = stylex.create({ 'outline-primary': { '--_cl-icon-color': { default: iconFadedNeutral, - ':not([data-disabled])[data-open]': colorVars['--cl-color-primary'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-brand'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover': colorVars['--cl-color-primary'], + ':not([data-disabled]):hover': colorVars['--cl-color-brand'], }, }, borderColor: colorVars['--cl-color-border'], @@ -263,15 +263,15 @@ export const variants = stylex.create({ ':not([data-disabled]):hover:not(:active):not([data-open])': neutralStep0, }, }, - color: colorVars['--cl-color-primary'], + color: colorVars['--cl-color-brand'], }, 'outline-neutral': { '--_cl-icon-color': { default: iconFadedNeutral, - ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-foreground'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-foreground'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover': colorVars['--cl-color-neutral-foreground'], + ':not([data-disabled]):hover': colorVars['--cl-color-foreground'], }, }, borderColor: colorVars['--cl-color-border'], @@ -284,7 +284,7 @@ export const variants = stylex.create({ ':not([data-disabled]):hover:not(:active):not([data-open])': neutralStep0, }, }, - color: colorVars['--cl-color-neutral-foreground'], + color: colorVars['--cl-color-foreground'], }, 'outline-negative': { '--_cl-icon-color': { @@ -311,10 +311,10 @@ export const variants = stylex.create({ 'ghost-primary': { '--_cl-icon-color': { default: iconFadedNeutral, - ':not([data-disabled])[data-open]': colorVars['--cl-color-primary'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-brand'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover': colorVars['--cl-color-primary'], + ':not([data-disabled]):hover': colorVars['--cl-color-brand'], }, }, backgroundColor: { @@ -326,15 +326,15 @@ export const variants = stylex.create({ ':not([data-disabled]):hover:not(:active):not([data-open])': neutralStep0, }, }, - color: colorVars['--cl-color-primary'], + color: colorVars['--cl-color-brand'], }, 'ghost-neutral': { '--_cl-icon-color': { default: iconFadedNeutral, - ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-foreground'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-foreground'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover': colorVars['--cl-color-neutral-foreground'], + ':not([data-disabled]):hover': colorVars['--cl-color-foreground'], }, }, backgroundColor: { @@ -346,7 +346,7 @@ export const variants = stylex.create({ ':not([data-disabled]):hover:not(:active):not([data-open])': neutralStep0, }, }, - color: colorVars['--cl-color-neutral-foreground'], + color: colorVars['--cl-color-foreground'], }, // The one ghost that tints instead of graying, so its pressed step walks its own faded // fill toward the negative it carries rather than joining the gray ramp. @@ -382,11 +382,11 @@ export const variants = stylex.create({ default: iconFadedNeutral, '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover': colorVars['--cl-color-primary'], + ':not([data-disabled]):hover': colorVars['--cl-color-brand'], }, }, backgroundColor: 'transparent', - color: colorVars['--cl-color-primary'], + color: colorVars['--cl-color-brand'], paddingInlineEnd: 0, paddingInlineStart: 0, textDecorationColor: { default: 'transparent', ':not([data-disabled]):hover': 'currentColor' }, @@ -399,11 +399,11 @@ export const variants = stylex.create({ default: iconFadedNeutral, '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover': colorVars['--cl-color-neutral-foreground'], + ':not([data-disabled]):hover': colorVars['--cl-color-foreground'], }, }, backgroundColor: 'transparent', - color: colorVars['--cl-color-neutral-foreground'], + color: colorVars['--cl-color-foreground'], paddingInlineEnd: 0, paddingInlineStart: 0, textDecorationColor: { default: 'transparent', ':not([data-disabled]):hover': 'currentColor' }, diff --git a/packages/ui/src/mosaic/components/card/card.styles.ts b/packages/ui/src/mosaic/components/card/card.styles.ts index 446abee637b..3725313ef21 100644 --- a/packages/ui/src/mosaic/components/card/card.styles.ts +++ b/packages/ui/src/mosaic/components/card/card.styles.ts @@ -7,7 +7,7 @@ const compactCard = '@container card (max-width: 20rem)' as const; export const root = stylex.create({ base: { - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], containerName: 'card', containerType: 'inline-size', display: 'flex', @@ -18,7 +18,7 @@ export const root = stylex.create({ card: { borderRadius: radiusVars['--cl-radius-xl'], overflow: 'hidden', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: colorVars['--cl-color-background'], boxShadow: shadowVars['--cl-shadow-card'], }, flush: { @@ -30,7 +30,7 @@ export const root = stylex.create({ overlay: { borderRadius: radiusVars['--cl-radius-xl'], overflow: 'hidden', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: colorVars['--cl-color-background'], boxShadow: shadowVars['--cl-shadow-card'], }, }); @@ -52,14 +52,14 @@ export const header = stylex.create({ rowGap: space['1'], }, title: { - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], fontSize: typeScaleVars['--cl-text-base-size'], fontWeight: fontWeightVars['--cl-font-semibold'], lineHeight: typeScaleVars['--cl-text-base-leading'], textWrap: 'balance', }, description: { - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], fontSize: typeScaleVars['--cl-text-sm-size'], lineHeight: typeScaleVars['--cl-text-sm-leading'], textWrap: 'pretty', diff --git a/packages/ui/src/mosaic/components/dialog/dialog.styles.ts b/packages/ui/src/mosaic/components/dialog/dialog.styles.ts index 6ea8473ff4d..4ba36f521c0 100644 --- a/packages/ui/src/mosaic/components/dialog/dialog.styles.ts +++ b/packages/ui/src/mosaic/components/dialog/dialog.styles.ts @@ -209,7 +209,7 @@ export const styles = stylex.create({ // Cleared because `FloatingFocusManager` focuses the popup itself when it holds no // tabbable content, which would otherwise draw a ring around the whole surface. outline: 'none', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: colorVars['--cl-color-background'], // Matches `Menu`. The two schemes are different treatments, not one at two strengths: light // gets the two drop layers and a dark hairline, dark drops them to `transparent` and separates // with a light hairline instead — a shadow reads as depth against a light page and as nothing @@ -224,7 +224,7 @@ export const styles = stylex.create({ boxShadow: `0 12px 12px -7px light-dark(oklch(0.2046 0 0 / 12%), transparent), 0 24px 24px -10px light-dark(oklch(0.2046 0 0 / 4%), transparent), 0 0 0 1px light-dark(oklch(0.2046 0 0 / 4%), oklch(1 0 0 / 10%))`, - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], display: 'flex', flexDirection: 'column', // A dialog holds prose it did not author — an email address, an org slug, an API key — and a @@ -238,7 +238,7 @@ export const styles = stylex.create({ inset: 0, // Follows the popup's own radius. borderRadius: 'inherit', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: colorVars['--cl-color-background'], content: '""', opacity: 'var(--_cl-stack-veil, 0)', pointerEvents: 'none', diff --git a/packages/ui/src/mosaic/components/dialog/dialog.test.tsx b/packages/ui/src/mosaic/components/dialog/dialog.test.tsx index f64848c53b0..b8aa490a4f2 100644 --- a/packages/ui/src/mosaic/components/dialog/dialog.test.tsx +++ b/packages/ui/src/mosaic/components/dialog/dialog.test.tsx @@ -520,7 +520,7 @@ describe('popup surface', () => { // an atom from its property and value, so a probe with the popup's own values yields the very // atoms `styles.popup` declares. const probe = stylex.create({ - background: { backgroundColor: colorVars['--cl-color-card'] }, + background: { backgroundColor: colorVars['--cl-color-background'] }, radius: { borderRadius: radiusVars['--cl-radius-xl'] }, }); diff --git a/packages/ui/src/mosaic/components/field/field.styles.ts b/packages/ui/src/mosaic/components/field/field.styles.ts index ce762ce4eb3..ac169df222f 100644 --- a/packages/ui/src/mosaic/components/field/field.styles.ts +++ b/packages/ui/src/mosaic/components/field/field.styles.ts @@ -9,14 +9,14 @@ export const styles = stylex.create({ flexDirection: 'column', }, label: { - color: colorVars['--cl-color-primary'], + color: colorVars['--cl-color-brand'], fontWeight: fontWeightVars['--cl-font-medium'], }, message: { margin: 0, }, description: { - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], }, error: { gap: space['1'], diff --git a/packages/ui/src/mosaic/components/item/item.styles.ts b/packages/ui/src/mosaic/components/item/item.styles.ts index 03eb04e70e8..f01ddedbc80 100644 --- a/packages/ui/src/mosaic/components/item/item.styles.ts +++ b/packages/ui/src/mosaic/components/item/item.styles.ts @@ -9,11 +9,11 @@ export const item = stylex.create({ // rule, so the value crosses the element boundary as a custom property. It is restated in // `interactive` rather than gaining a hover branch here: StyleX resolves a property to the // last style that declares it, so the two can't merge. - '--_cl-icon-color': colorVars['--cl-color-neutral-faded'], + '--_cl-icon-color': colorVars['--cl-color-foreground-secondary'], borderRadius: radiusVars['--cl-radius-lg'], paddingInline: space['1.5'], alignItems: 'center', - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], display: 'flex', fontFamily: fontFamilyVars['--cl-font-family-sans'], fontSize: typeScaleVars['--cl-text-sm-size'], @@ -31,24 +31,24 @@ export const item = stylex.create({ // instead of the native attribute. interactive: { '--_cl-icon-color': { - default: colorVars['--cl-color-neutral-faded'], + default: colorVars['--cl-color-foreground-secondary'], '@media (hover: hover)': { default: null, - ':hover:not(:disabled, [aria-disabled="true"])': colorVars['--cl-color-neutral'], + ':hover:not(:disabled, [aria-disabled="true"])': colorVars['--cl-color-foreground'], }, }, backgroundColor: { default: null, - ':active:not(:disabled, [aria-disabled="true"])': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 8%, transparent)`, + ':active:not(:disabled, [aria-disabled="true"])': `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 8%, transparent)`, '@media (hover: hover)': { - ':hover:not(:disabled, [aria-disabled="true"])': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`, + ':hover:not(:disabled, [aria-disabled="true"])': `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent)`, }, }, color: { - default: colorVars['--cl-color-neutral-faded'], + default: colorVars['--cl-color-foreground-secondary'], '@media (hover: hover)': { default: null, - ':hover:not(:disabled, [aria-disabled="true"])': colorVars['--cl-color-neutral'], + ':hover:not(:disabled, [aria-disabled="true"])': colorVars['--cl-color-foreground'], }, }, cursor: { @@ -110,7 +110,7 @@ export const label = stylex.create({ }, default: { - color: colorVars['--cl-color-neutral'], + color: colorVars['--cl-color-foreground'], }, // Declares no color, so `reset`'s `inherit` stands and the row's own color reaches it. That is // what carries it through the hover promotion on an interactive row, which a fixed color would @@ -121,7 +121,7 @@ export const label = stylex.create({ export const description = stylex.create({ base: { - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], fontSize: typeScaleVars['--cl-text-xs-size'], fontWeight: fontWeightVars['--cl-font-normal'], lineHeight: typeScaleVars['--cl-text-xs-leading'], diff --git a/packages/ui/src/mosaic/components/menu/menu.styles.ts b/packages/ui/src/mosaic/components/menu/menu.styles.ts index a81e051855d..a4f607a751c 100644 --- a/packages/ui/src/mosaic/components/menu/menu.styles.ts +++ b/packages/ui/src/mosaic/components/menu/menu.styles.ts @@ -15,11 +15,11 @@ export const popup = stylex.create({ base: { borderRadius: radiusVars['--cl-radius-lg'], outline: 'none', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: colorVars['--cl-color-background'], boxShadow: `0 12px 12px -7px light-dark(oklch(0.2046 0 0 / 12%), transparent), 0 24px 24px -10px light-dark(oklch(0.2046 0 0 / 4%), transparent), 0 0 0 1px light-dark(oklch(0.2046 0 0 / 4%), oklch(1 0 0 / 10%))`, - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], opacity: { default: 1, ':is([data-ending-style])': 0, @@ -73,9 +73,9 @@ export const item = stylex.create({ alignItems: 'center', backgroundColor: { default: 'transparent', - ':is([data-active])': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`, + ':is([data-active])': `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent)`, '@media (hover: hover)': { - ':hover': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`, + ':hover': `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent)`, }, }, cursor: { default: 'pointer', ':is([data-disabled])': 'not-allowed' }, diff --git a/packages/ui/src/mosaic/components/otp/otp.styles.ts b/packages/ui/src/mosaic/components/otp/otp.styles.ts index 7996578d182..27cd41b2b94 100644 --- a/packages/ui/src/mosaic/components/otp/otp.styles.ts +++ b/packages/ui/src/mosaic/components/otp/otp.styles.ts @@ -24,8 +24,8 @@ export const styles = stylex.create({ }, slot: { borderRadius: radiusVars['--cl-radius-lg'], - caretColor: colorVars['--cl-color-primary'], - color: colorVars['--cl-color-card-foreground'], + caretColor: colorVars['--cl-color-brand'], + color: colorVars['--cl-color-foreground'], fontFamily: fontFamilyVars['--cl-font-family-sans'], fontSize: typeScaleVars['--cl-text-lg-size'], fontVariantNumeric: 'tabular-nums', diff --git a/packages/ui/src/mosaic/components/section/section.styles.ts b/packages/ui/src/mosaic/components/section/section.styles.ts index 57991bbc1c5..f3cf262d762 100644 --- a/packages/ui/src/mosaic/components/section/section.styles.ts +++ b/packages/ui/src/mosaic/components/section/section.styles.ts @@ -13,7 +13,7 @@ export const styles = stylex.create({ width: '100%', }, title: { - color: colorVars['--cl-color-neutral'], + color: colorVars['--cl-color-foreground'], fontWeight: fontWeightVars['--cl-font-medium'], }, group: { @@ -22,7 +22,7 @@ export const styles = stylex.create({ borderStyle: 'solid', borderWidth: '1px', overflow: 'hidden', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: colorVars['--cl-color-background'], width: '100%', }, row: { @@ -110,13 +110,13 @@ export const styles = stylex.create({ minWidth: 0, }, label: { - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], fontSize: typeScaleVars['--cl-text-sm-size'], fontWeight: fontWeightVars['--cl-font-medium'], lineHeight: typeScaleVars['--cl-text-sm-leading'], }, description: { - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], fontSize: typeScaleVars['--cl-text-sm-size'], fontWeight: fontWeightVars['--cl-font-normal'], lineHeight: typeScaleVars['--cl-text-sm-leading'], diff --git a/packages/ui/src/mosaic/props.ts b/packages/ui/src/mosaic/props.ts index 8289cfea83b..097b6273499 100644 --- a/packages/ui/src/mosaic/props.ts +++ b/packages/ui/src/mosaic/props.ts @@ -22,7 +22,7 @@ export type MosaicElementProps = export type MosaicComponentProps = ComponentProps; // The public styling contract, emitted onto a component's root element: -// 1. `--cl-*` vars — from `tokens.stylex.ts` (`:root { --cl-color-primary: … }`) +// 1. `--cl-*` vars — from `tokens.stylex.ts` (`:root { --cl-color-brand: … }`) // 2. `.cl-` class — from `themeProps` (`.cl-button { … }`) // 3. `data-` attrs — from `themeProps` (`.cl-button[data-variant='outline']`) // diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 33e06fe35ff..0b7ff24b637 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -7,9 +7,9 @@ import * as stylex from '@stylexjs/stylex'; // these become real, stable custom properties a consumer can override in plain // CSS without touching StyleX: // -// :root { --cl-color-primary: rebeccapurple; } +// :root { --cl-color-brand: rebeccapurple; } // -// StyleX still emits `:root { --cl-color-primary: … }` as the default. Following +// StyleX still emits `:root { --cl-color-brand: … }` as the default. Following // astryx's structure: each group is a plain `*Defaults` object wrapped by // `defineVars`. The `*VarName` unions are derived from the exported `*Vars` in // `styles/index.ts` — the `@stylexjs/enforce-extension` rule requires a @@ -48,13 +48,20 @@ export const gray = stylex.defineVars({ // colours are literal values there too, so they stay literal here. const colorDefaults = { - '--cl-color-primary': 'light-dark(oklch(0.205 0 0), oklch(0.922 0 0))', - '--cl-color-primary-foreground': 'light-dark(oklch(0.985 0 0), oklch(0.205 0 0))', - '--cl-color-primary-faded': 'light-dark(oklch(0.9583 0.0214 291.74), oklch(0.3097 0.1008 285.05))', + '--cl-color-foreground': `light-dark(${gray['900']}, ${gray['50']})`, + '--cl-color-foreground-secondary': `light-dark(${gray['600']}, ${gray['400']})`, + '--cl-color-foreground-disabled': `light-dark(${gray['400']}, ${gray['500']})`, - '--cl-color-neutral': 'light-dark(oklch(0.2928 0.0163 285.35), oklch(0.9854 0.0013 286.38))', - '--cl-color-neutral-foreground': 'light-dark(oklch(0.24 0 0), oklch(0.96 0 0))', - '--cl-color-neutral-faded': 'light-dark(oklch(0.5697 0.0246 279.94), oklch(0.6953 0.0261 285.7))', + '--cl-color-background': `light-dark(oklch(1 0 0), ${gray['900']})`, + '--cl-color-background-subtle': `light-dark(${gray['100']}, ${gray['850']})`, + + '--cl-color-border': `light-dark(${gray['200']}, ${gray['800']})`, + '--cl-color-border-subtle': `light-dark(${gray['100']}, ${gray['850']})`, + + '--cl-color-brand': 'light-dark(oklch(0.2046 0 0), oklch(0.9851 0 0))', + '--cl-color-brand-secondary': 'light-dark(oklch(0.5555 0 0), oklch(0.7155 0 0))', + '--cl-color-brand-border': 'light-dark(oklch(0.9219 0 0), oklch(0.3715 0 0))', + '--cl-color-brand-foreground': 'light-dark(oklch(1 0 0), oklch(0.2046 0 0))', '--cl-color-negative': 'light-dark(oklch(0.5903 0.213 26.78), oklch(0.7106 0.1661 22.22))', '--cl-color-negative-foreground': 'oklch(1 0 0)', @@ -71,17 +78,9 @@ const colorDefaults = { '--cl-color-warning-subtle': 'light-dark(oklch(0.9799 0.0147 70.89), oklch(0.2725 0.0547 55.7))', '--cl-color-warning-border': 'light-dark(oklch(0.8672 0.0902 63.47), oklch(0.4084 0.1165 38.17))', - '--cl-color-background': `light-dark(${gray['100']}, ${gray['850']})`, - - '--cl-color-card': `light-dark(oklch(1 0 0), ${gray['900']})`, - '--cl-color-card-foreground': `light-dark(${gray['900']}, ${gray['50']})`, - '--cl-color-input': `light-dark(oklch(1 0 0), ${gray['850']})`, '--cl-color-input-placeholder': gray['400'], - '--cl-color-border': `light-dark(${gray['200']}, ${gray['800']})`, - '--cl-color-border-subtle': `light-dark(${gray['100']}, ${gray['850']})`, - '--cl-color-ring': 'light-dark(oklch(0.205 0 0), oklch(0.922 0 0))', } as const; @@ -150,9 +149,9 @@ export const targetVars = stylex.defineVars(targetDefaults); // pointer is elsewhere, the base once it reaches the region, then `hover` and `active` for the // thumb's own two. Each derives from `--cl-scrollbar-thumb` rather than baking its value in, so // they resolve at use time — overriding the base re-derives all three, while any one stays -// individually overridable. The base is itself mixed most of the way toward `--cl-color-card`, +// individually overridable. The base is itself mixed most of the way toward `--cl-color-background`, // which keeps a 4px bar reading as a hairline rather than a hard rule; `idle` carries on in that -// direction, and the other two step back toward `--cl-color-card-foreground`, deepening in light +// direction, and the other two step back toward `--cl-color-foreground`, deepening in light // mode and lightening in dark, since that token already carries both. // // Only the idle → base step can animate. It is set on the scroller, which owns the transition; @@ -173,10 +172,10 @@ const scrollbarThumb = 'var(--cl-scrollbar-thumb)'; const scrollbarDefaults = { '--cl-scrollbar-width': '8px', '--cl-scrollbar-thumb-inset': '2px', - '--cl-scrollbar-thumb': `color-mix(in oklab, ${colorVars['--cl-color-neutral-faded']}, ${colorVars['--cl-color-card']} 55%)`, - '--cl-scrollbar-thumb-idle': `color-mix(in oklab, ${scrollbarThumb}, ${colorVars['--cl-color-card']} 45%)`, - '--cl-scrollbar-thumb-hover': `color-mix(in oklab, ${scrollbarThumb}, ${colorVars['--cl-color-card-foreground']} 15%)`, - '--cl-scrollbar-thumb-active': `color-mix(in oklab, ${scrollbarThumb}, ${colorVars['--cl-color-card-foreground']} 30%)`, + '--cl-scrollbar-thumb': `color-mix(in oklab, ${colorVars['--cl-color-foreground-secondary']}, ${colorVars['--cl-color-background']} 55%)`, + '--cl-scrollbar-thumb-idle': `color-mix(in oklab, ${scrollbarThumb}, ${colorVars['--cl-color-background']} 45%)`, + '--cl-scrollbar-thumb-hover': `color-mix(in oklab, ${scrollbarThumb}, ${colorVars['--cl-color-foreground']} 15%)`, + '--cl-scrollbar-thumb-active': `color-mix(in oklab, ${scrollbarThumb}, ${colorVars['--cl-color-foreground']} 30%)`, } as const; export const scrollbarVars = stylex.defineVars(scrollbarDefaults); diff --git a/packages/ui/src/mosaic/user-button/user-button.styles.ts b/packages/ui/src/mosaic/user-button/user-button.styles.ts index 779142151f7..1c44db2dc91 100644 --- a/packages/ui/src/mosaic/user-button/user-button.styles.ts +++ b/packages/ui/src/mosaic/user-button/user-button.styles.ts @@ -22,7 +22,7 @@ export const styles = stylex.create({ // Matches `Item.Label`, so the trigger names a workspace the same way its row does. Capped, // because the trigger sits in a host app's chrome and a long workspace name would push it apart. triggerName: { - color: colorVars['--cl-color-neutral'], + color: colorVars['--cl-color-foreground'], fontSize: typeScaleVars['--cl-text-sm-size'], fontWeight: fontWeightVars['--cl-font-medium'], lineHeight: typeScaleVars['--cl-text-sm-leading'], diff --git a/packages/ui/src/mosaic/user-profile/user-profile-api-keys-panel.styles.ts b/packages/ui/src/mosaic/user-profile/user-profile-api-keys-panel.styles.ts index 40f747c05c7..44eaad2024a 100644 --- a/packages/ui/src/mosaic/user-profile/user-profile-api-keys-panel.styles.ts +++ b/packages/ui/src/mosaic/user-profile/user-profile-api-keys-panel.styles.ts @@ -10,13 +10,13 @@ export const styles = stylex.create({ cell: { paddingBlock: space['3'], paddingInline: space['4'], - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], fontSize: typeScaleVars['--cl-text-sm-size'], lineHeight: typeScaleVars['--cl-text-sm-leading'], verticalAlign: 'middle', }, checkbox: { - accentColor: colorVars['--cl-color-primary'], + accentColor: colorVars['--cl-color-brand'], cursor: 'pointer', height: space['4'], width: space['4'], @@ -29,7 +29,7 @@ export const styles = stylex.create({ }, emptyCell: { paddingBlock: space['8'], - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], fontSize: typeScaleVars['--cl-text-sm-size'], lineHeight: typeScaleVars['--cl-text-sm-leading'], textAlign: 'center', @@ -40,14 +40,14 @@ export const styles = stylex.create({ headerCell: { paddingBlock: space['2.5'], paddingInline: space['4'], - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], fontSize: typeScaleVars['--cl-text-xs-size'], fontWeight: fontWeightVars['--cl-font-medium'], lineHeight: typeScaleVars['--cl-text-xs-leading'], textAlign: 'start', }, keyDescription: { - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], fontSize: typeScaleVars['--cl-text-xs-size'], lineHeight: typeScaleVars['--cl-text-xs-leading'], marginBlockStart: space['0.5'], @@ -55,7 +55,7 @@ export const styles = stylex.create({ keyName: { gap: space['2'], alignItems: 'center', - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], display: 'flex', fontSize: typeScaleVars['--cl-text-sm-size'], fontWeight: fontWeightVars['--cl-font-medium'], @@ -67,7 +67,7 @@ export const styles = stylex.create({ pageSizeLabel: { gap: space['2'], alignItems: 'center', - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], display: 'flex', fontSize: typeScaleVars['--cl-text-xs-size'], lineHeight: typeScaleVars['--cl-text-xs-leading'], @@ -79,8 +79,8 @@ export const styles = stylex.create({ borderWidth: '1px', paddingBlock: space['1'], paddingInline: space['2'], - backgroundColor: colorVars['--cl-color-card'], - color: colorVars['--cl-color-card-foreground'], + backgroundColor: colorVars['--cl-color-background'], + color: colorVars['--cl-color-foreground'], fontSize: typeScaleVars['--cl-text-xs-size'], lineHeight: typeScaleVars['--cl-text-xs-leading'], }, @@ -111,7 +111,7 @@ export const styles = stylex.create({ paddingInlineStart: space['8'], }, searchIcon: { - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], insetInlineStart: space['3'], pointerEvents: 'none', position: 'absolute', @@ -137,7 +137,7 @@ export const styles = stylex.create({ borderStyle: 'solid', borderWidth: '1px', overflow: 'hidden', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: colorVars['--cl-color-background'], width: '100%', }, toolbar: { diff --git a/packages/ui/src/mosaic/user-profile/user-profile-billing-history-section.styles.ts b/packages/ui/src/mosaic/user-profile/user-profile-billing-history-section.styles.ts index 7eec1a2e133..b81b2c5a9ef 100644 --- a/packages/ui/src/mosaic/user-profile/user-profile-billing-history-section.styles.ts +++ b/packages/ui/src/mosaic/user-profile/user-profile-billing-history-section.styles.ts @@ -7,7 +7,7 @@ export const styles = stylex.create({ textAlign: 'end', }, amountCell: { - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], fontSize: typeScaleVars['--cl-text-sm-size'], lineHeight: typeScaleVars['--cl-text-sm-leading'], }, @@ -18,7 +18,7 @@ export const styles = stylex.create({ }, emptyCell: { paddingBlock: space['6'], - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], fontSize: typeScaleVars['--cl-text-sm-size'], lineHeight: typeScaleVars['--cl-text-sm-leading'], textAlign: 'center', @@ -29,7 +29,7 @@ export const styles = stylex.create({ headerCell: { paddingBlock: space['2.5'], paddingInline: space['4'], - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], fontSize: typeScaleVars['--cl-text-xs-size'], fontWeight: fontWeightVars['--cl-font-medium'], lineHeight: typeScaleVars['--cl-text-xs-leading'], @@ -40,7 +40,7 @@ export const styles = stylex.create({ }, invoiceId: { overflow: 'hidden', - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], fontSize: typeScaleVars['--cl-text-xs-size'], lineHeight: typeScaleVars['--cl-text-xs-leading'], marginBlockStart: space['0.5'], @@ -48,7 +48,7 @@ export const styles = stylex.create({ whiteSpace: 'nowrap', }, invoiceLabel: { - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], fontSize: typeScaleVars['--cl-text-sm-size'], fontWeight: fontWeightVars['--cl-font-medium'], lineHeight: typeScaleVars['--cl-text-sm-leading'], @@ -56,7 +56,7 @@ export const styles = stylex.create({ pageSizeLabel: { gap: space['2'], alignItems: 'center', - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], display: 'flex', fontSize: typeScaleVars['--cl-text-xs-size'], lineHeight: typeScaleVars['--cl-text-xs-leading'], @@ -68,8 +68,8 @@ export const styles = stylex.create({ borderWidth: '1px', paddingBlock: space['1'], paddingInline: space['2'], - backgroundColor: colorVars['--cl-color-card'], - color: colorVars['--cl-color-card-foreground'], + backgroundColor: colorVars['--cl-color-background'], + color: colorVars['--cl-color-foreground'], fontSize: typeScaleVars['--cl-text-xs-size'], lineHeight: typeScaleVars['--cl-text-xs-leading'], }, @@ -100,7 +100,7 @@ export const styles = stylex.create({ borderStyle: 'solid', borderWidth: '1px', overflow: 'hidden', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: colorVars['--cl-color-background'], width: '100%', }, statusColumn: { diff --git a/packages/ui/src/mosaic/user-profile/user-profile-billing-panel.styles.ts b/packages/ui/src/mosaic/user-profile/user-profile-billing-panel.styles.ts index 13c1602d4aa..5fd34335bf2 100644 --- a/packages/ui/src/mosaic/user-profile/user-profile-billing-panel.styles.ts +++ b/packages/ui/src/mosaic/user-profile/user-profile-billing-panel.styles.ts @@ -4,7 +4,7 @@ import { colorVars, fontWeightVars, space, typeScaleVars } from '../tokens.style export const styles = stylex.create({ amount: { - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], fontSize: typeScaleVars['--cl-text-base-size'], fontWeight: fontWeightVars['--cl-font-semibold'], lineHeight: typeScaleVars['--cl-text-base-leading'], diff --git a/packages/ui/src/mosaic/user-profile/user-profile-security-panel.styles.ts b/packages/ui/src/mosaic/user-profile/user-profile-security-panel.styles.ts index 4ac139e2561..8f153c551e9 100644 --- a/packages/ui/src/mosaic/user-profile/user-profile-security-panel.styles.ts +++ b/packages/ui/src/mosaic/user-profile/user-profile-security-panel.styles.ts @@ -12,7 +12,7 @@ export const styles = stylex.create({ flexWrap: 'wrap', }, icon: { - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], display: 'block', height: space['4.5'], width: space['4.5'], @@ -22,7 +22,7 @@ export const styles = stylex.create({ borderRadius: radiusVars['--cl-radius-md'], borderStyle: 'solid', borderWidth: '1px', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-background-subtle'], }, root: { gap: space['4'], diff --git a/packages/ui/src/mosaic/utils/focus-outline.styles.ts b/packages/ui/src/mosaic/utils/focus-outline.styles.ts index ffee40d29f0..ededf6b4ecb 100644 --- a/packages/ui/src/mosaic/utils/focus-outline.styles.ts +++ b/packages/ui/src/mosaic/utils/focus-outline.styles.ts @@ -3,7 +3,7 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, focusVars } from '../tokens.stylex'; // The keyboard focus ring, in one place. Every focusable Mosaic surface drew the same -// `2px solid --cl-color-primary` inline, so the ring was a dozen copies of one decision +// `2px solid --cl-color-brand` inline, so the ring was a dozen copies of one decision // with nothing keeping them in step — the offsets had already drifted. // // Values come from tokens (`--cl-focus-outline-*` and `--cl-color-ring`), which is how a diff --git a/packages/ui/src/mosaic/utils/input.styles.ts b/packages/ui/src/mosaic/utils/input.styles.ts index 852998c34ff..57419da3d80 100644 --- a/packages/ui/src/mosaic/utils/input.styles.ts +++ b/packages/ui/src/mosaic/utils/input.styles.ts @@ -10,7 +10,7 @@ const invalidFocusShadow = `0 0 0 3px light-dark( color-mix(in oklab, ${colorVars['--cl-color-negative']} 12%, transparent), color-mix(in oklab, ${colorVars['--cl-color-negative']} 15%, transparent) )`; -const disabledBackgroundColor = `color-mix(in oklab, ${colorVars['--cl-color-primary']} 5%, transparent)`; +const disabledBackgroundColor = `color-mix(in oklab, ${colorVars['--cl-color-brand']} 5%, transparent)`; export const inputStyles = stylex.create({ base: { diff --git a/packages/ui/src/mosaic/utils/typography.styles.ts b/packages/ui/src/mosaic/utils/typography.styles.ts index e4731eb19d7..4077e7bb1ac 100644 --- a/packages/ui/src/mosaic/utils/typography.styles.ts +++ b/packages/ui/src/mosaic/utils/typography.styles.ts @@ -41,10 +41,10 @@ export const sizes = stylex.create({ // Text has no fill to sit on, so each color resolves to the readable token of its // pair: the saturated one for warning/negative/positive, `-foreground` for neutral -// (`--cl-color-neutral` is a surface fill). +// (`--cl-color-foreground` is a surface fill). export const colors = stylex.create({ - primary: { color: colorVars['--cl-color-primary'] }, - neutral: { color: colorVars['--cl-color-neutral-foreground'] }, + primary: { color: colorVars['--cl-color-brand'] }, + neutral: { color: colorVars['--cl-color-foreground'] }, warning: { color: colorVars['--cl-color-warning'] }, negative: { color: colorVars['--cl-color-negative'] }, positive: { color: colorVars['--cl-color-positive'] }, diff --git a/references/mosaic-architecture.md b/references/mosaic-architecture.md index cb6eff1a4a9..e2b0219bba1 100644 --- a/references/mosaic-architecture.md +++ b/references/mosaic-architecture.md @@ -12,13 +12,13 @@ Once migration is complete, the old system is removed and Mosaic becomes the sol ## Token architecture -Tokens are CSS custom properties declared through `stylex.defineVars` in `tokens.stylex.ts`. Each var is named explicitly (`'--cl-color-primary'`, not a generated hash) so it is a stable, documented handle a consumer can override: +Tokens are CSS custom properties declared through `stylex.defineVars` in `tokens.stylex.ts`. Each var is named explicitly (`'--cl-color-brand'`, not a generated hash) so it is a stable, documented handle a consumer can override: ```ts // packages/ui/src/mosaic/tokens.stylex.ts export const colorVars = stylex.defineVars({ - '--cl-color-primary': 'light-dark(oklch(0.205 0 0), oklch(0.922 0 0))', - '--cl-color-primary-foreground': 'light-dark(oklch(0.985 0 0), oklch(0.205 0 0))', + '--cl-color-brand': 'light-dark(oklch(0.2046 0 0), oklch(0.9851 0 0))', + '--cl-color-brand-foreground': 'light-dark(oklch(1 0 0), oklch(0.2046 0 0))', // … }); ``` @@ -52,7 +52,7 @@ Two ways to style a part — both hit the same class + attributes: border-radius: 4px; } .cl-item[data-interactive] { - background-color: var(--cl-color-card); + background-color: var(--cl-color-background); } ``` @@ -92,9 +92,9 @@ const styles = stylex.create({ // `color` and `variant` are independent props, but every pair resolves to one style, so they are // keyed compositely rather than merged at render time. const variants = stylex.create({ - 'filled-primary': { backgroundColor: colorVars['--cl-color-primary'], borderColor: 'transparent' }, + 'filled-primary': { backgroundColor: colorVars['--cl-color-brand'], borderColor: 'transparent' }, 'filled-negative': { backgroundColor: colorVars['--cl-color-negative'], borderColor: 'transparent' }, - 'outline-primary': { backgroundColor: 'transparent', borderColor: colorVars['--cl-color-primary'] }, + 'outline-primary': { backgroundColor: 'transparent', borderColor: colorVars['--cl-color-brand'] }, 'outline-negative': { backgroundColor: 'transparent', borderColor: colorVars['--cl-color-negative'] }, }); @@ -437,7 +437,7 @@ To migrate a component from the old system to Mosaic: cursor: { default: 'pointer', ':is([data-disabled])': 'not-allowed' }, ``` -4. Update token references — e.g. `theme.colors.$primary500` → `colorVars['--cl-color-primary']`. +4. Update token references — e.g. `theme.colors.$primary500` → `colorVars['--cl-color-brand']`. 5. Export the component from `styles/index.ts` and run `pnpm build:mosaic`. The steps above cover the **styling** migration. For **flow** components — where the legacy component also fuses data-fetching and interaction state into the same file — splitting that into the model/controller/view layers and verifying no implicit behavior is dropped is its own end-to-end workflow. See the `mosaic` Claude Code skill (`.claude/skills/mosaic/`), in particular its `references/migration.md`. From 4b3b744148fa3855542d2a0b0fcffa0db19397ce Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 9 Sep 2026 21:43:00 -0400 Subject: [PATCH 03/15] feat(ui): add neutral alpha scale for mosaic scrims --- .claude/skills/mosaic/references/stylex.md | 13 ++-- .../mosaic/components/avatar/avatar.styles.ts | 4 +- .../mosaic/components/badge/badge.styles.ts | 16 +++-- .../mosaic/components/banner/banner.styles.ts | 14 +++-- .../mosaic/components/button/button.styles.ts | 60 ++++++++----------- .../src/mosaic/components/item/item.styles.ts | 14 ++++- .../src/mosaic/components/menu/menu.styles.ts | 14 ++++- packages/ui/src/mosaic/tokens.stylex.ts | 40 +++++-------- references/mosaic-architecture.md | 2 +- 9 files changed, 91 insertions(+), 86 deletions(-) diff --git a/.claude/skills/mosaic/references/stylex.md b/.claude/skills/mosaic/references/stylex.md index d22f4daaf20..024c541acdd 100644 --- a/.claude/skills/mosaic/references/stylex.md +++ b/.claude/skills/mosaic/references/stylex.md @@ -98,11 +98,12 @@ export const colorVars = stylex.defineVars(colorDefaults); would let a consumer override one and not the other, and the ring's appearance would then depend on which they picked. - **DO** build a fill that sits _on top of_ an unknown backdrop — a hover or pressed - wash on a transparent `outline`/`ghost` control — as a **scrim**: an opacity of - black-on-light / white-on-dark over `transparent`, not a percentage of a gray token. + wash on a transparent `outline`/`ghost` control — from the internal `neutralAlpha` + scale: an opacity of black-on-light / white-on-dark over `transparent`, keyed by + that opacity, not a percentage of a gray token. ```ts - const step = `color-mix(in oklab, light-dark(oklch(0 0 0), oklch(1 0 0)) 12%, transparent)`; + backgroundColor: neutralAlpha['12'], ``` A gray token like `--cl-color-foreground` is a 900, not black, so the same percentage of @@ -315,11 +316,11 @@ device, while touch devices look correct. ```ts backgroundColor: { default: 'transparent', - ':enabled:active': neutralStep1, - ':enabled[data-open]': neutralStep1, + ':enabled:active': neutralAlpha['12'], + ':enabled[data-open]': neutralAlpha['12'], '@media (hover: hover)': { default: null, - ':enabled:hover:not(:active):not([data-open])': neutralStep0, + ':enabled:hover:not(:active):not([data-open])': neutralAlpha['6'], }, }, ``` diff --git a/packages/ui/src/mosaic/components/avatar/avatar.styles.ts b/packages/ui/src/mosaic/components/avatar/avatar.styles.ts index 2e59ad80521..f6f35d562fd 100644 --- a/packages/ui/src/mosaic/components/avatar/avatar.styles.ts +++ b/packages/ui/src/mosaic/components/avatar/avatar.styles.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space } from '../../tokens.stylex'; +import { colorVars, fontFamilyVars, fontWeightVars, neutralAlpha, radiusVars, space } from '../../tokens.stylex'; // Timed to match `skeleton.tsx`'s pulse, so the two generations of placeholder read as one thing. const pulse = stylex.keyframes({ @@ -50,7 +50,7 @@ export const styles = stylex.create({ fallback: { borderRadius: 'inherit', alignItems: 'center', - backgroundColor: `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 8%, transparent)`, + backgroundColor: neutralAlpha['8'], color: colorVars['--cl-color-foreground'], display: 'flex', justifyContent: 'center', diff --git a/packages/ui/src/mosaic/components/badge/badge.styles.ts b/packages/ui/src/mosaic/components/badge/badge.styles.ts index e7517da931f..118f3456646 100644 --- a/packages/ui/src/mosaic/components/badge/badge.styles.ts +++ b/packages/ui/src/mosaic/components/badge/badge.styles.ts @@ -1,15 +1,21 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; +import { + colorVars, + fontFamilyVars, + fontWeightVars, + neutralAlpha, + radiusVars, + space, + typeScaleVars, +} from '../../tokens.stylex'; // warning/negative/positive fill with the subtle surface and use the saturated token as text; // primary fills with the solid token and uses its `-foreground` for text. // // Neutral has no subtle surface to fill — `--cl-color-foreground-secondary` is a text gray, and its // `-foreground` is a text color rather than an on-fill one, so it is unreadable against the solid -// 900. It rides the same black/white scrim the button's neutral fill does, which composites against -// any backdrop. Must be a local binding — StyleX inlines it; an imported one fails to compile. -const neutralScrim = `color-mix(in oklab, light-dark(oklch(0 0 0), oklch(1 0 0)) 6%, transparent)`; +// 900. It rides the same neutral scrim the button's neutral fill does. export const styles = stylex.create({ base: { borderRadius: radiusVars['--cl-radius-full'], @@ -33,7 +39,7 @@ export const colors = stylex.create({ color: colorVars['--cl-color-brand-foreground'], }, neutral: { - backgroundColor: neutralScrim, + backgroundColor: neutralAlpha['6'], color: colorVars['--cl-color-foreground'], }, warning: { diff --git a/packages/ui/src/mosaic/components/banner/banner.styles.ts b/packages/ui/src/mosaic/components/banner/banner.styles.ts index 7a7232ba031..9785fa69b10 100644 --- a/packages/ui/src/mosaic/components/banner/banner.styles.ts +++ b/packages/ui/src/mosaic/components/banner/banner.styles.ts @@ -1,8 +1,14 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; - -const neutralFill = `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent)`; +import { + colorVars, + fontFamilyVars, + fontWeightVars, + neutralAlpha, + radiusVars, + space, + typeScaleVars, +} from '../../tokens.stylex'; export const styles = stylex.create({ root: { @@ -39,7 +45,7 @@ export const styles = stylex.create({ export const rootColors = stylex.create({ neutral: { borderColor: colorVars['--cl-color-border'], - backgroundColor: neutralFill, + backgroundColor: neutralAlpha['4'], color: colorVars['--cl-color-foreground'], }, warning: { diff --git a/packages/ui/src/mosaic/components/button/button.styles.ts b/packages/ui/src/mosaic/components/button/button.styles.ts index d4e65850af2..78febd2e0da 100644 --- a/packages/ui/src/mosaic/components/button/button.styles.ts +++ b/packages/ui/src/mosaic/components/button/button.styles.ts @@ -5,6 +5,7 @@ import { durationVars, fontFamilyVars, fontWeightVars, + neutralAlpha, radiusVars, space, targetVars, @@ -12,23 +13,10 @@ import { } from '../../tokens.stylex'; import { iconScope } from '../icon/icon.markers.stylex'; -// Neutral fills are three steps up one gray ramp, shared across variants: -// step 0 — `filled-neutral` rest, `outline-*`/`ghost-*` hover -// step 1 — `filled-neutral` hover, `outline-*`/`ghost-*` pressed -// step 2 — `filled-neutral` pressed -// -// Scrim opacities over `transparent`, so they composite against any backdrop. Not -// `--cl-color-foreground`: it's a 900, so a percentage of it lands lighter than the same -// percentage of black by an amount that shifts with the backdrop. -// -// TODO: codify this %-mix scale as shared tokens once more components adopt these -// shades — `item` and `avatar` still mix from `--cl-color-foreground`, and unifying them -// is a palette-wide change. -// -// Must be local bindings — StyleX inlines them; an imported one fails to compile. -const neutralStep0 = `color-mix(in oklab, light-dark(oklch(0 0 0), oklch(1 0 0)) 6%, transparent)`; -const neutralStep1 = `color-mix(in oklab, light-dark(oklch(0 0 0), oklch(1 0 0)) 12%, transparent)`; -const neutralStep2 = `color-mix(in oklab, light-dark(oklch(0 0 0), oklch(1 0 0)) 18%, transparent)`; +// Neutral fills are three steps up the `neutralAlpha` scale, shared across variants: +// 6 — `filled-neutral` rest, `outline-*`/`ghost-*` hover +// 12 — `filled-neutral` hover, `outline-*`/`ghost-*` pressed +// 18 — `filled-neutral` pressed // Opaque fills blend toward their own on-fill at the same 12%/18%. Neutral has no fill // to blend from, so it rides the opacity ramp above. @@ -210,12 +198,12 @@ export const variants = stylex.create({ }, }, backgroundColor: { - default: neutralStep0, - ':not([data-disabled]):not([data-pending]):active': neutralStep2, - ':not([data-disabled])[data-open]': neutralStep2, + default: neutralAlpha['6'], + ':not([data-disabled]):not([data-pending]):active': neutralAlpha['18'], + ':not([data-disabled])[data-open]': neutralAlpha['18'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralStep1, + ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['12'], }, }, color: colorVars['--cl-color-foreground'], @@ -256,11 +244,11 @@ export const variants = stylex.create({ borderColor: colorVars['--cl-color-border'], backgroundColor: { default: 'transparent', - ':not([data-disabled]):not([data-pending]):active': neutralStep1, - ':not([data-disabled])[data-open]': neutralStep1, + ':not([data-disabled]):not([data-pending]):active': neutralAlpha['12'], + ':not([data-disabled])[data-open]': neutralAlpha['12'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralStep0, + ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['6'], }, }, color: colorVars['--cl-color-brand'], @@ -277,11 +265,11 @@ export const variants = stylex.create({ borderColor: colorVars['--cl-color-border'], backgroundColor: { default: 'transparent', - ':not([data-disabled]):not([data-pending]):active': neutralStep1, - ':not([data-disabled])[data-open]': neutralStep1, + ':not([data-disabled]):not([data-pending]):active': neutralAlpha['12'], + ':not([data-disabled])[data-open]': neutralAlpha['12'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralStep0, + ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['6'], }, }, color: colorVars['--cl-color-foreground'], @@ -298,11 +286,11 @@ export const variants = stylex.create({ borderColor: colorVars['--cl-color-border'], backgroundColor: { default: 'transparent', - ':not([data-disabled]):not([data-pending]):active': neutralStep1, - ':not([data-disabled])[data-open]': neutralStep1, + ':not([data-disabled]):not([data-pending]):active': neutralAlpha['12'], + ':not([data-disabled])[data-open]': neutralAlpha['12'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralStep0, + ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['6'], }, }, color: colorVars['--cl-color-negative'], @@ -319,11 +307,11 @@ export const variants = stylex.create({ }, backgroundColor: { default: 'transparent', - ':not([data-disabled]):not([data-pending]):active': neutralStep1, - ':not([data-disabled])[data-open]': neutralStep1, + ':not([data-disabled]):not([data-pending]):active': neutralAlpha['12'], + ':not([data-disabled])[data-open]': neutralAlpha['12'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralStep0, + ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['6'], }, }, color: colorVars['--cl-color-brand'], @@ -339,11 +327,11 @@ export const variants = stylex.create({ }, backgroundColor: { default: 'transparent', - ':not([data-disabled]):not([data-pending]):active': neutralStep1, - ':not([data-disabled])[data-open]': neutralStep1, + ':not([data-disabled]):not([data-pending]):active': neutralAlpha['12'], + ':not([data-disabled])[data-open]': neutralAlpha['12'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralStep0, + ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['6'], }, }, color: colorVars['--cl-color-foreground'], diff --git a/packages/ui/src/mosaic/components/item/item.styles.ts b/packages/ui/src/mosaic/components/item/item.styles.ts index f01ddedbc80..c6b3e2c41a6 100644 --- a/packages/ui/src/mosaic/components/item/item.styles.ts +++ b/packages/ui/src/mosaic/components/item/item.styles.ts @@ -1,6 +1,14 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; +import { + colorVars, + fontFamilyVars, + fontWeightVars, + neutralAlpha, + radiusVars, + space, + typeScaleVars, +} from '../../tokens.stylex'; export const item = stylex.create({ base: { @@ -39,9 +47,9 @@ export const item = stylex.create({ }, backgroundColor: { default: null, - ':active:not(:disabled, [aria-disabled="true"])': `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 8%, transparent)`, + ':active:not(:disabled, [aria-disabled="true"])': neutralAlpha['8'], '@media (hover: hover)': { - ':hover:not(:disabled, [aria-disabled="true"])': `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent)`, + ':hover:not(:disabled, [aria-disabled="true"])': neutralAlpha['4'], }, }, color: { diff --git a/packages/ui/src/mosaic/components/menu/menu.styles.ts b/packages/ui/src/mosaic/components/menu/menu.styles.ts index a4f607a751c..4317e05e944 100644 --- a/packages/ui/src/mosaic/components/menu/menu.styles.ts +++ b/packages/ui/src/mosaic/components/menu/menu.styles.ts @@ -1,6 +1,14 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; +import { + colorVars, + fontFamilyVars, + fontWeightVars, + neutralAlpha, + radiusVars, + space, + typeScaleVars, +} from '../../tokens.stylex'; // Positioning is applied inline by the headless positioner; this only clears the // focus outline it receives. No z-index: the portalled, fixed positioner already @@ -73,9 +81,9 @@ export const item = stylex.create({ alignItems: 'center', backgroundColor: { default: 'transparent', - ':is([data-active])': `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent)`, + ':is([data-active])': neutralAlpha['4'], '@media (hover: hover)': { - ':hover': `color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent)`, + ':hover': neutralAlpha['4'], }, }, cursor: { default: 'pointer', ':is([data-disabled])': 'not-allowed' }, diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 0b7ff24b637..5524d06002c 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -22,10 +22,6 @@ import * as stylex from '@stylexjs/stylex'; // ============================================================================= // Gray Scale // ============================================================================= -// Figma's `base/gray/*`, internal only. The keys carry no `--cl-` prefix on purpose: -// StyleX hashes them, so there is no `--cl-gray-*` for a consumer to override and the -// semantic `--cl-color-*` tokens below stay the whole public colour surface. Pure grays, -// so chroma and hue are 0 and only lightness varies. export const gray = stylex.defineVars({ '50': 'oklch(0.9851 0 0)', @@ -44,10 +40,10 @@ export const gray = stylex.defineVars({ // ============================================================================= // Color Tokens // ============================================================================= -// Tokens Figma maps to `base/gray/*` read from the scale above. Brand and status -// colours are literal values there too, so they stay literal here. const colorDefaults = { + '--cl-color-neutral': 'light-dark(oklch(0 0 0), oklch(1 0 0))', + '--cl-color-foreground': `light-dark(${gray['900']}, ${gray['50']})`, '--cl-color-foreground-secondary': `light-dark(${gray['600']}, ${gray['400']})`, '--cl-color-foreground-disabled': `light-dark(${gray['400']}, ${gray['500']})`, @@ -86,11 +82,21 @@ const colorDefaults = { export const colorVars = stylex.defineVars(colorDefaults); +// ============================================================================= +// Neutral Alpha Scale +// ============================================================================= + +export const neutralAlpha = stylex.defineVars({ + '4': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`, + '6': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 6%, transparent)`, + '8': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 8%, transparent)`, + '12': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 12%, transparent)`, + '18': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 18%, transparent)`, +}); + // ============================================================================= // Radius Tokens // ============================================================================= -// `md` is 6px rather than a 4/8/12 step: it is the control radius (button, avatar -// square), and neither neighbour sits right on a control. const radiusDefaults = { '--cl-radius-none': '0rem', @@ -205,8 +211,6 @@ export const scrollFadeVars = stylex.defineVars(scrollFadeDefaults); // ============================================================================= // Spacing Tokens // ============================================================================= -// `--cl-spacing` is the ONLY exposed custom property (the base unit, Tailwind's -// model). Overriding it rescales every gap, pad, and control height at once. const spacingDefaults = { '--cl-spacing': '0.25rem', @@ -214,12 +218,6 @@ const spacingDefaults = { export const spacingVars = stylex.defineVars(spacingDefaults); -// The scale is `defineVars` (like astryx's `spacingVars`): each step is a StyleX -// var whose default is `calc(var(--cl-spacing) * n)`, so overriding `--cl-spacing` -// still rescales the whole scale. `defineConsts` was tried here but emits no CSS -// across module boundaries — consumers got dangling `var(--hash)` refs. StyleX -// hashes these var names (they aren't `--cl-*`), so only `--cl-spacing` stays a -// stable, targetable custom property. `space['2']` reads like Tailwind's `space-2`. const step = (multiple: number): string => `calc(var(--cl-spacing) * ${multiple})`; export const space = stylex.defineVars({ @@ -309,8 +307,6 @@ export const space = stylex.defineVars({ // ============================================================================= // Typography Tokens — type scale // ============================================================================= -// One named step scale every text-bearing component sizes against. `defineVars` -// values are single CSS values, so size and leading are each their own var. const typeScaleDefaults = { '--cl-text-xs-size': '0.75rem', @@ -342,7 +338,6 @@ export const fontFamilyVars = stylex.defineVars(fontFamilyDefaults); // ============================================================================= // Typography Tokens — font weight // ============================================================================= -// Kept separate from the step scale: weight and size vary independently. const fontWeightDefaults = { '--cl-font-normal': '400', @@ -424,13 +419,6 @@ export const easingVars = stylex.defineVars(easingDefaults); // ============================================================================= // Focus Tokens // ============================================================================= -// The keyboard focus ring, as one definition for the whole system. Components read -// these through `utils/focus-outline.styles.ts`; the `:focus-visible` condition stays -// there, so a theme can restyle the ring but cannot show it to pointer users. -// -// The colour is NOT restated here: it is `--cl-color-ring`, which lives with the other -// colours. A `--cl-focus-outline-color` alias would be a second public name for the same -// value, and the two would drift the moment a consumer overrode one of them. const focusDefaults = { '--cl-focus-outline-width': '2px', diff --git a/references/mosaic-architecture.md b/references/mosaic-architecture.md index e2b0219bba1..bd90380c2c2 100644 --- a/references/mosaic-architecture.md +++ b/references/mosaic-architecture.md @@ -25,7 +25,7 @@ export const colorVars = stylex.defineVars({ Groups: `colorVars`, `radiusVars`, `targetVars`, `scrollbarVars`, `scrollFadeVars`, `spacingVars`, `space`, `typeScaleVars`, `fontFamilyVars`, `fontWeightVars`, `durationVars`, `easingVars`, `focusVars`. -`gray` is the one group that is not public: its keys carry no `--cl-` prefix, so StyleX hashes the names and there is no `--cl-gray-*` to override. The gray-backed `--cl-color-*` tokens read from it; consumers retheme those, not the scale. +`gray` and `neutralAlpha` are the two groups that are not public: their keys carry no `--cl-` prefix, so StyleX hashes the names and there is nothing to override. The gray-backed `--cl-color-*` tokens read from `gray`, and hover, pressed, and resting washes over an unknown backdrop read from `neutralAlpha` (`--cl-color-neutral`, black on light and white on dark, at a fixed opacity over `transparent`); consumers retheme the `--cl-color-*` tokens, not the scales. Light and dark come from CSS `light-dark()` on the default values, so there is no theme object and no re-render on theme change — the browser resolves it. From 308990bce762868ac0a11c133c2991957e91e924 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 10 Sep 2026 09:24:57 -0400 Subject: [PATCH 04/15] feat(ui): align menu, item, and icon frame fills with mosaic tokens --- packages/swingset/src/stories/icon-frame.mdx | 2 +- packages/ui/src/mosaic/components/icon/icon-frame.styles.ts | 5 +---- packages/ui/src/mosaic/components/item/item.styles.ts | 2 +- packages/ui/src/mosaic/components/menu/menu.styles.ts | 4 ++-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/swingset/src/stories/icon-frame.mdx b/packages/swingset/src/stories/icon-frame.mdx index ec98733d3b8..6eb7c679ada 100644 --- a/packages/swingset/src/stories/icon-frame.mdx +++ b/packages/swingset/src/stories/icon-frame.mdx @@ -2,7 +2,7 @@ import * as IconFrameStories from './icon-frame.stories'; # IconFrame -IconFrame centers an icon or other visual in a square Mosaic surface. `bordered` and `filled` are independent treatments, while `size` controls the frame dimensions. The default is a bordered 40px frame; the filled treatment uses `oklch(0.9702 0 0)` in light mode and `oklch(0.2393 0 0)` in dark mode. +IconFrame centers an icon or other visual in a square Mosaic surface. `bordered` and `filled` are independent treatments, while `size` controls the frame dimensions. The default is a bordered 40px frame; the filled treatment uses `--cl-color-background-subtle`. ## Playground diff --git a/packages/ui/src/mosaic/components/icon/icon-frame.styles.ts b/packages/ui/src/mosaic/components/icon/icon-frame.styles.ts index 8a1b94eb7c5..30dd9ac5094 100644 --- a/packages/ui/src/mosaic/components/icon/icon-frame.styles.ts +++ b/packages/ui/src/mosaic/components/icon/icon-frame.styles.ts @@ -2,9 +2,6 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, radiusVars, space } from '../../tokens.stylex'; -// Figma uses the 'page faded' token -const filledBackground = 'light-dark(oklch(0.9702 0 0), oklch(0.2393 0 0))'; - export const styles = stylex.create({ base: { borderColor: 'transparent', @@ -23,7 +20,7 @@ export const styles = stylex.create({ borderColor: colorVars['--cl-color-border'], }, filled: { - backgroundColor: filledBackground, + backgroundColor: colorVars['--cl-color-background-subtle'], }, }); diff --git a/packages/ui/src/mosaic/components/item/item.styles.ts b/packages/ui/src/mosaic/components/item/item.styles.ts index c6b3e2c41a6..e02e72a3080 100644 --- a/packages/ui/src/mosaic/components/item/item.styles.ts +++ b/packages/ui/src/mosaic/components/item/item.styles.ts @@ -49,7 +49,7 @@ export const item = stylex.create({ default: null, ':active:not(:disabled, [aria-disabled="true"])': neutralAlpha['8'], '@media (hover: hover)': { - ':hover:not(:disabled, [aria-disabled="true"])': neutralAlpha['4'], + ':hover:not(:disabled, [aria-disabled="true"])': neutralAlpha['6'], }, }, color: { diff --git a/packages/ui/src/mosaic/components/menu/menu.styles.ts b/packages/ui/src/mosaic/components/menu/menu.styles.ts index 4317e05e944..cfaf8b7b3c1 100644 --- a/packages/ui/src/mosaic/components/menu/menu.styles.ts +++ b/packages/ui/src/mosaic/components/menu/menu.styles.ts @@ -81,9 +81,9 @@ export const item = stylex.create({ alignItems: 'center', backgroundColor: { default: 'transparent', - ':is([data-active])': neutralAlpha['4'], + ':is([data-active])': neutralAlpha['6'], '@media (hover: hover)': { - ':hover': neutralAlpha['4'], + ':hover': neutralAlpha['6'], }, }, cursor: { default: 'pointer', ':is([data-disabled])': 'not-allowed' }, From a16e940bec84bb3788959611b86169d76ce7d77e Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 10 Sep 2026 12:03:08 -0400 Subject: [PATCH 05/15] feat(ui): rename mosaic color tokens in drawer, profile, branding, and input --- .../components/branding/branding.styles.ts | 2 +- .../mosaic/components/drawer/drawer.styles.ts | 6 +++--- .../src/mosaic/components/input/input.styles.ts | 2 +- .../mosaic/components/profile/profile.styles.ts | 16 ++++++++-------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/ui/src/mosaic/components/branding/branding.styles.ts b/packages/ui/src/mosaic/components/branding/branding.styles.ts index 41e2afd7b7e..3a107de0a5b 100644 --- a/packages/ui/src/mosaic/components/branding/branding.styles.ts +++ b/packages/ui/src/mosaic/components/branding/branding.styles.ts @@ -5,7 +5,7 @@ import { colorVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex export const styles = stylex.create({ // The mark only: text and link. Where it sits (a card's foot, a sidebar's) is the host's call. base: { - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], display: 'inline-block', fontSize: typeScaleVars['--cl-text-xs-size'], lineHeight: typeScaleVars['--cl-text-xs-leading'], diff --git a/packages/ui/src/mosaic/components/drawer/drawer.styles.ts b/packages/ui/src/mosaic/components/drawer/drawer.styles.ts index 5ad97e9a3b3..d750a122ab8 100644 --- a/packages/ui/src/mosaic/components/drawer/drawer.styles.ts +++ b/packages/ui/src/mosaic/components/drawer/drawer.styles.ts @@ -77,14 +77,14 @@ export const styles = stylex.create({ borderWidth: { default: null, '@media (forced-colors: active)': '1px' }, outline: 'none', overscrollBehavior: 'contain', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: colorVars['--cl-color-background'], borderStartEndRadius: radiusVars['--cl-radius-2xl'], borderStartStartRadius: radiusVars['--cl-radius-2xl'], // No drop shadow — the sheet sits on the screen edge, so there is nothing for it to float over. // The hairline ring stays: a faint dark edge in light, and the light edge that separates a dark // sheet from a dark page. boxShadow: `0 0 0 1px light-dark(oklch(0.2046 0 0 / 4%), oklch(1 0 0 / 10%))`, - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], display: 'flex', flexDirection: 'column', marginBlockEnd: `calc(-1 * ${BLEED})`, @@ -135,7 +135,7 @@ export const styles = stylex.create({ // is not a press on the handle, and leaves it alone. '--_cl-grip-color': { default: colorVars['--cl-color-border'], - ':active': `color-mix(in oklab, ${colorVars['--cl-color-border']}, ${colorVars['--cl-color-neutral-faded']} 35%)`, + ':active': `color-mix(in oklab, ${colorVars['--cl-color-border']}, ${colorVars['--cl-color-foreground-secondary']} 35%)`, }, placeItems: 'center', display: 'grid', diff --git a/packages/ui/src/mosaic/components/input/input.styles.ts b/packages/ui/src/mosaic/components/input/input.styles.ts index df7776bc2f9..3feda365e7e 100644 --- a/packages/ui/src/mosaic/components/input/input.styles.ts +++ b/packages/ui/src/mosaic/components/input/input.styles.ts @@ -4,7 +4,7 @@ import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScale export const styles = stylex.create({ base: { - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], display: 'block', fontFamily: fontFamilyVars['--cl-font-family-sans'], minWidth: 0, diff --git a/packages/ui/src/mosaic/components/profile/profile.styles.ts b/packages/ui/src/mosaic/components/profile/profile.styles.ts index bd2543ec370..a5b8646c8b3 100644 --- a/packages/ui/src/mosaic/components/profile/profile.styles.ts +++ b/packages/ui/src/mosaic/components/profile/profile.styles.ts @@ -114,7 +114,7 @@ export const styles = stylex.create({ // `clip` rather than `hidden`: the surface must never become a scroll container itself, or // focusing something in the content column would scroll the whole surface instead of the column. overflow: 'clip', - backgroundColor: colorVars['--cl-color-card'], + backgroundColor: colorVars['--cl-color-background'], blockSize: { [compact]: '100dvh', default: '45rem', @@ -122,7 +122,7 @@ export const styles = stylex.create({ // In a page — standalone or inline — the frame is its border alone, the way a card sits flat in // content. The card's elevation belongs to the overlay; see `layoutInDialog`. boxShadow: 'none', - color: colorVars['--cl-color-card-foreground'], + color: colorVars['--cl-color-foreground'], display: 'grid', gridTemplateColumns: { [compact]: 'minmax(0, 1fr)', @@ -214,16 +214,16 @@ export const styles = stylex.create({ alignItems: 'center', backgroundColor: { default: 'transparent', - ':where([data-selected])': colorVars['--cl-color-border-faded'], - ':active': colorVars['--cl-color-border-faded'], + ':where([data-selected])': colorVars['--cl-color-border-subtle'], + ':active': colorVars['--cl-color-border-subtle'], '@media (hover: hover)': { default: null, - ':hover:not(:active):not([data-selected])': colorVars['--cl-color-border-faded'], + ':hover:not(:active):not([data-selected])': colorVars['--cl-color-border-subtle'], }, }, color: { - default: colorVars['--cl-color-neutral-faded'], - ':where([data-selected])': colorVars['--cl-color-card-foreground'], + default: colorVars['--cl-color-foreground-secondary'], + ':where([data-selected])': colorVars['--cl-color-foreground'], }, cursor: 'pointer', display: 'flex', @@ -342,7 +342,7 @@ export const styles = stylex.create({ * own variable rather than `color`, which the icon sets itself. */ caret: { - '--_cl-icon-color': colorVars['--cl-color-neutral-faded'], + '--_cl-icon-color': colorVars['--cl-color-foreground-secondary'], fontSize: '0.6em', marginInlineStart: '0.25em', verticalAlign: 'middle', From 7b717e031de83637de8d9b685d83fb5f6c6e379f Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 10 Sep 2026 12:03:09 -0400 Subject: [PATCH 06/15] feat(ui): add white and black to the mosaic gray scale --- packages/ui/src/mosaic/tokens.stylex.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 5524d06002c..bf10648a18c 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -24,6 +24,7 @@ import * as stylex from '@stylexjs/stylex'; // ============================================================================= export const gray = stylex.defineVars({ + '0': 'oklch(1 0 0)', '50': 'oklch(0.9851 0 0)', '100': 'oklch(0.9702 0 0)', '200': 'oklch(0.9461 0 0)', @@ -35,6 +36,7 @@ export const gray = stylex.defineVars({ '800': 'oklch(0.2686 0 0)', '850': 'oklch(0.2393 0 0)', '900': 'oklch(0.2046 0 0)', + '1000': 'oklch(0 0 0)', }); // ============================================================================= @@ -42,13 +44,13 @@ export const gray = stylex.defineVars({ // ============================================================================= const colorDefaults = { - '--cl-color-neutral': 'light-dark(oklch(0 0 0), oklch(1 0 0))', + '--cl-color-neutral': `light-dark(${gray['1000']}, ${gray['0']})`, '--cl-color-foreground': `light-dark(${gray['900']}, ${gray['50']})`, '--cl-color-foreground-secondary': `light-dark(${gray['600']}, ${gray['400']})`, '--cl-color-foreground-disabled': `light-dark(${gray['400']}, ${gray['500']})`, - '--cl-color-background': `light-dark(oklch(1 0 0), ${gray['900']})`, + '--cl-color-background': `light-dark(${gray['0']}, ${gray['900']})`, '--cl-color-background-subtle': `light-dark(${gray['100']}, ${gray['850']})`, '--cl-color-border': `light-dark(${gray['200']}, ${gray['800']})`, @@ -60,21 +62,21 @@ const colorDefaults = { '--cl-color-brand-foreground': 'light-dark(oklch(1 0 0), oklch(0.2046 0 0))', '--cl-color-negative': 'light-dark(oklch(0.5903 0.213 26.78), oklch(0.7106 0.1661 22.22))', - '--cl-color-negative-foreground': 'oklch(1 0 0)', + '--cl-color-negative-foreground': gray['0'], '--cl-color-negative-subtle': 'light-dark(oklch(0.9757 0.0118 17.36), oklch(0.255 0.0604 22.31))', '--cl-color-negative-border': 'light-dark(oklch(0.8155 0.0983 19.41), oklch(0.3958 0.1331 25.72))', '--cl-color-positive': 'light-dark(oklch(0.6082 0.1799 145.47), oklch(0.7227 0.192 149.58))', - '--cl-color-positive-foreground': 'oklch(1 0 0)', + '--cl-color-positive-foreground': gray['0'], '--cl-color-positive-subtle': 'light-dark(oklch(0.9859 0.0164 156.92), oklch(0.3297 0.052 152.31))', '--cl-color-positive-border': 'light-dark(oklch(0.7922 0.1959 148.18), oklch(0.4479 0.1083 151.33))', '--cl-color-warning': 'light-dark(oklch(0.7099 0.1888 44.94), oklch(0.7576 0.159 55.93))', - '--cl-color-warning-foreground': 'oklch(1 0 0)', + '--cl-color-warning-foreground': gray['0'], '--cl-color-warning-subtle': 'light-dark(oklch(0.9799 0.0147 70.89), oklch(0.2725 0.0547 55.7))', '--cl-color-warning-border': 'light-dark(oklch(0.8672 0.0902 63.47), oklch(0.4084 0.1165 38.17))', - '--cl-color-input': `light-dark(oklch(1 0 0), ${gray['850']})`, + '--cl-color-input': `light-dark(${gray['0']}, ${gray['850']})`, '--cl-color-input-placeholder': gray['400'], '--cl-color-ring': 'light-dark(oklch(0.205 0 0), oklch(0.922 0 0))', @@ -433,9 +435,9 @@ export const focusVars = stylex.defineVars(focusDefaults); // dark. Branched per colour via `light-dark()` since a shadow's geometry cannot branch — see the // note on `Dialog`'s popup for why `@media (prefers-color-scheme)` is not the escape hatch. const shadowDefaults = { - '--cl-shadow-card': `0 12px 12px -7px light-dark(oklch(0.2046 0 0 / 12%), transparent), - 0 24px 24px -10px light-dark(oklch(0.2046 0 0 / 4%), transparent), - 0 0 0 1px light-dark(oklch(0.2046 0 0 / 4%), oklch(1 0 0 / 10%))`, + '--cl-shadow-card': `0 12px 12px -7px light-dark(color-mix(in oklab, ${gray['900']} 12%, transparent), transparent), + 0 24px 24px -10px light-dark(color-mix(in oklab, ${gray['900']} 4%, transparent), transparent), + 0 0 0 1px light-dark(color-mix(in oklab, ${gray['900']} 4%, transparent), color-mix(in oklab, ${gray['0']} 10%, transparent))`, }; export const shadowVars = stylex.defineVars(shadowDefaults); From 4127ab75c8a43dd2d48d035dfccd013b09e088f6 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 10 Sep 2026 14:15:34 -0400 Subject: [PATCH 07/15] feat(ui): add public neutral and status alpha tokens --- .claude/skills/mosaic/references/stylex.md | 15 +++--- .../mosaic/components/avatar/avatar.styles.ts | 4 +- .../mosaic/components/badge/badge.styles.ts | 12 +---- .../mosaic/components/banner/banner.styles.ts | 12 +---- .../mosaic/components/button/button.styles.ts | 47 +++++++++---------- .../src/mosaic/components/item/item.styles.ts | 14 ++---- .../src/mosaic/components/menu/menu.styles.ts | 18 ++----- .../src/mosaic/components/otp/otp.styles.ts | 5 +- packages/ui/src/mosaic/tokens.stylex.ts | 24 +++++----- packages/ui/src/mosaic/utils/input.styles.ts | 5 +- references/mosaic-architecture.md | 2 +- 11 files changed, 60 insertions(+), 98 deletions(-) diff --git a/.claude/skills/mosaic/references/stylex.md b/.claude/skills/mosaic/references/stylex.md index 024c541acdd..908f4fe0ebf 100644 --- a/.claude/skills/mosaic/references/stylex.md +++ b/.claude/skills/mosaic/references/stylex.md @@ -98,12 +98,13 @@ export const colorVars = stylex.defineVars(colorDefaults); would let a consumer override one and not the other, and the ring's appearance would then depend on which they picked. - **DO** build a fill that sits _on top of_ an unknown backdrop — a hover or pressed - wash on a transparent `outline`/`ghost` control — from the internal `neutralAlpha` - scale: an opacity of black-on-light / white-on-dark over `transparent`, keyed by - that opacity, not a percentage of a gray token. + wash on a transparent `outline`/`ghost` control — from the `--cl-color-neutral-alpha-*` + tokens: `--cl-color-neutral` (black on light, white on dark) at a fixed opacity over + `transparent`, not a percentage of a gray token. Status colors have the same three + steps (`--cl-color-negative-alpha-100` …). ```ts - backgroundColor: neutralAlpha['12'], + backgroundColor: colorVars['--cl-color-neutral-alpha-300'], ``` A gray token like `--cl-color-foreground` is a 900, not black, so the same percentage of @@ -316,11 +317,11 @@ device, while touch devices look correct. ```ts backgroundColor: { default: 'transparent', - ':enabled:active': neutralAlpha['12'], - ':enabled[data-open]': neutralAlpha['12'], + ':enabled:active': colorVars['--cl-color-neutral-alpha-200'], + ':enabled[data-open]': colorVars['--cl-color-neutral-alpha-200'], '@media (hover: hover)': { default: null, - ':enabled:hover:not(:active):not([data-open])': neutralAlpha['6'], + ':enabled:hover:not(:active):not([data-open])': colorVars['--cl-color-neutral-alpha-100'], }, }, ``` diff --git a/packages/ui/src/mosaic/components/avatar/avatar.styles.ts b/packages/ui/src/mosaic/components/avatar/avatar.styles.ts index f6f35d562fd..096887c9db7 100644 --- a/packages/ui/src/mosaic/components/avatar/avatar.styles.ts +++ b/packages/ui/src/mosaic/components/avatar/avatar.styles.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, fontFamilyVars, fontWeightVars, neutralAlpha, radiusVars, space } from '../../tokens.stylex'; +import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space } from '../../tokens.stylex'; // Timed to match `skeleton.tsx`'s pulse, so the two generations of placeholder read as one thing. const pulse = stylex.keyframes({ @@ -50,7 +50,7 @@ export const styles = stylex.create({ fallback: { borderRadius: 'inherit', alignItems: 'center', - backgroundColor: neutralAlpha['8'], + backgroundColor: colorVars['--cl-color-neutral-alpha-200'], color: colorVars['--cl-color-foreground'], display: 'flex', justifyContent: 'center', diff --git a/packages/ui/src/mosaic/components/badge/badge.styles.ts b/packages/ui/src/mosaic/components/badge/badge.styles.ts index 118f3456646..79c83f46dd2 100644 --- a/packages/ui/src/mosaic/components/badge/badge.styles.ts +++ b/packages/ui/src/mosaic/components/badge/badge.styles.ts @@ -1,14 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { - colorVars, - fontFamilyVars, - fontWeightVars, - neutralAlpha, - radiusVars, - space, - typeScaleVars, -} from '../../tokens.stylex'; +import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; // warning/negative/positive fill with the subtle surface and use the saturated token as text; // primary fills with the solid token and uses its `-foreground` for text. @@ -39,7 +31,7 @@ export const colors = stylex.create({ color: colorVars['--cl-color-brand-foreground'], }, neutral: { - backgroundColor: neutralAlpha['6'], + backgroundColor: colorVars['--cl-color-neutral-alpha-100'], color: colorVars['--cl-color-foreground'], }, warning: { diff --git a/packages/ui/src/mosaic/components/banner/banner.styles.ts b/packages/ui/src/mosaic/components/banner/banner.styles.ts index 9785fa69b10..9f7c15cc42c 100644 --- a/packages/ui/src/mosaic/components/banner/banner.styles.ts +++ b/packages/ui/src/mosaic/components/banner/banner.styles.ts @@ -1,14 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { - colorVars, - fontFamilyVars, - fontWeightVars, - neutralAlpha, - radiusVars, - space, - typeScaleVars, -} from '../../tokens.stylex'; +import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; export const styles = stylex.create({ root: { @@ -45,7 +37,7 @@ export const styles = stylex.create({ export const rootColors = stylex.create({ neutral: { borderColor: colorVars['--cl-color-border'], - backgroundColor: neutralAlpha['4'], + backgroundColor: colorVars['--cl-color-neutral-alpha-100'], color: colorVars['--cl-color-foreground'], }, warning: { diff --git a/packages/ui/src/mosaic/components/button/button.styles.ts b/packages/ui/src/mosaic/components/button/button.styles.ts index 78febd2e0da..02a76ba58b6 100644 --- a/packages/ui/src/mosaic/components/button/button.styles.ts +++ b/packages/ui/src/mosaic/components/button/button.styles.ts @@ -5,7 +5,6 @@ import { durationVars, fontFamilyVars, fontWeightVars, - neutralAlpha, radiusVars, space, targetVars, @@ -13,10 +12,10 @@ import { } from '../../tokens.stylex'; import { iconScope } from '../icon/icon.markers.stylex'; -// Neutral fills are three steps up the `neutralAlpha` scale, shared across variants: -// 6 — `filled-neutral` rest, `outline-*`/`ghost-*` hover -// 12 — `filled-neutral` hover, `outline-*`/`ghost-*` pressed -// 18 — `filled-neutral` pressed +// Neutral fills are the three neutral alpha steps, shared across variants: +// 100 — `filled-neutral` rest, `outline-*`/`ghost-*` hover +// 200 — `filled-neutral` hover, `outline-*`/`ghost-*` pressed +// 300 — `filled-neutral` pressed // Opaque fills blend toward their own on-fill at the same 12%/18%. Neutral has no fill // to blend from, so it rides the opacity ramp above. @@ -198,12 +197,12 @@ export const variants = stylex.create({ }, }, backgroundColor: { - default: neutralAlpha['6'], - ':not([data-disabled]):not([data-pending]):active': neutralAlpha['18'], - ':not([data-disabled])[data-open]': neutralAlpha['18'], + default: colorVars['--cl-color-neutral-alpha-100'], + ':not([data-disabled]):not([data-pending]):active': colorVars['--cl-color-neutral-alpha-300'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-alpha-300'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['12'], + ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-neutral-alpha-200'], }, }, color: colorVars['--cl-color-foreground'], @@ -244,11 +243,11 @@ export const variants = stylex.create({ borderColor: colorVars['--cl-color-border'], backgroundColor: { default: 'transparent', - ':not([data-disabled]):not([data-pending]):active': neutralAlpha['12'], - ':not([data-disabled])[data-open]': neutralAlpha['12'], + ':not([data-disabled]):not([data-pending]):active': colorVars['--cl-color-neutral-alpha-200'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-alpha-200'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['6'], + ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-neutral-alpha-100'], }, }, color: colorVars['--cl-color-brand'], @@ -265,11 +264,11 @@ export const variants = stylex.create({ borderColor: colorVars['--cl-color-border'], backgroundColor: { default: 'transparent', - ':not([data-disabled]):not([data-pending]):active': neutralAlpha['12'], - ':not([data-disabled])[data-open]': neutralAlpha['12'], + ':not([data-disabled]):not([data-pending]):active': colorVars['--cl-color-neutral-alpha-200'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-alpha-200'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['6'], + ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-neutral-alpha-100'], }, }, color: colorVars['--cl-color-foreground'], @@ -286,11 +285,11 @@ export const variants = stylex.create({ borderColor: colorVars['--cl-color-border'], backgroundColor: { default: 'transparent', - ':not([data-disabled]):not([data-pending]):active': neutralAlpha['12'], - ':not([data-disabled])[data-open]': neutralAlpha['12'], + ':not([data-disabled]):not([data-pending]):active': colorVars['--cl-color-neutral-alpha-200'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-alpha-200'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['6'], + ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-neutral-alpha-100'], }, }, color: colorVars['--cl-color-negative'], @@ -307,11 +306,11 @@ export const variants = stylex.create({ }, backgroundColor: { default: 'transparent', - ':not([data-disabled]):not([data-pending]):active': neutralAlpha['12'], - ':not([data-disabled])[data-open]': neutralAlpha['12'], + ':not([data-disabled]):not([data-pending]):active': colorVars['--cl-color-neutral-alpha-200'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-alpha-200'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['6'], + ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-neutral-alpha-100'], }, }, color: colorVars['--cl-color-brand'], @@ -327,11 +326,11 @@ export const variants = stylex.create({ }, backgroundColor: { default: 'transparent', - ':not([data-disabled]):not([data-pending]):active': neutralAlpha['12'], - ':not([data-disabled])[data-open]': neutralAlpha['12'], + ':not([data-disabled]):not([data-pending]):active': colorVars['--cl-color-neutral-alpha-200'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-alpha-200'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralAlpha['6'], + ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-neutral-alpha-100'], }, }, color: colorVars['--cl-color-foreground'], diff --git a/packages/ui/src/mosaic/components/item/item.styles.ts b/packages/ui/src/mosaic/components/item/item.styles.ts index e02e72a3080..46aaf947b39 100644 --- a/packages/ui/src/mosaic/components/item/item.styles.ts +++ b/packages/ui/src/mosaic/components/item/item.styles.ts @@ -1,14 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { - colorVars, - fontFamilyVars, - fontWeightVars, - neutralAlpha, - radiusVars, - space, - typeScaleVars, -} from '../../tokens.stylex'; +import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; export const item = stylex.create({ base: { @@ -47,9 +39,9 @@ export const item = stylex.create({ }, backgroundColor: { default: null, - ':active:not(:disabled, [aria-disabled="true"])': neutralAlpha['8'], + ':active:not(:disabled, [aria-disabled="true"])': colorVars['--cl-color-neutral-alpha-200'], '@media (hover: hover)': { - ':hover:not(:disabled, [aria-disabled="true"])': neutralAlpha['6'], + ':hover:not(:disabled, [aria-disabled="true"])': colorVars['--cl-color-neutral-alpha-100'], }, }, color: { diff --git a/packages/ui/src/mosaic/components/menu/menu.styles.ts b/packages/ui/src/mosaic/components/menu/menu.styles.ts index cfaf8b7b3c1..952dc6a34c5 100644 --- a/packages/ui/src/mosaic/components/menu/menu.styles.ts +++ b/packages/ui/src/mosaic/components/menu/menu.styles.ts @@ -1,14 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { - colorVars, - fontFamilyVars, - fontWeightVars, - neutralAlpha, - radiusVars, - space, - typeScaleVars, -} from '../../tokens.stylex'; +import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; // Positioning is applied inline by the headless positioner; this only clears the // focus outline it receives. No z-index: the portalled, fixed positioner already @@ -81,9 +73,9 @@ export const item = stylex.create({ alignItems: 'center', backgroundColor: { default: 'transparent', - ':is([data-active])': neutralAlpha['6'], + ':is([data-active])': colorVars['--cl-color-neutral-alpha-100'], '@media (hover: hover)': { - ':hover': neutralAlpha['6'], + ':hover': colorVars['--cl-color-neutral-alpha-100'], }, }, cursor: { default: 'pointer', ':is([data-disabled])': 'not-allowed' }, @@ -117,9 +109,9 @@ export const item = stylex.create({ negative: { backgroundColor: { default: 'transparent', - ':is([data-active])': `color-mix(in oklab, ${colorVars['--cl-color-negative']} 8%, transparent)`, + ':is([data-active])': colorVars['--cl-color-negative-alpha-200'], '@media (hover: hover)': { - ':hover': `color-mix(in oklab, ${colorVars['--cl-color-negative']} 8%, transparent)`, + ':hover': colorVars['--cl-color-negative-alpha-200'], }, }, color: colorVars['--cl-color-negative'], diff --git a/packages/ui/src/mosaic/components/otp/otp.styles.ts b/packages/ui/src/mosaic/components/otp/otp.styles.ts index 27cd41b2b94..42887c5a8e3 100644 --- a/packages/ui/src/mosaic/components/otp/otp.styles.ts +++ b/packages/ui/src/mosaic/components/otp/otp.styles.ts @@ -10,10 +10,7 @@ import { typeScaleVars, } from '../../tokens.stylex'; -const positiveFocusShadow = `0 0 0 3px light-dark( - color-mix(in oklab, ${colorVars['--cl-color-positive']} 12%, transparent), - color-mix(in oklab, ${colorVars['--cl-color-positive']} 15%, transparent) -)`; +const positiveFocusShadow = `0 0 0 3px ${colorVars['--cl-color-positive-alpha-300']}`; export const styles = stylex.create({ root: { diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index bf10648a18c..46a20a73d6b 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -45,6 +45,9 @@ export const gray = stylex.defineVars({ const colorDefaults = { '--cl-color-neutral': `light-dark(${gray['1000']}, ${gray['0']})`, + '--cl-color-neutral-alpha-100': 'color-mix(in oklab, var(--cl-color-neutral) 6%, transparent)', + '--cl-color-neutral-alpha-200': 'color-mix(in oklab, var(--cl-color-neutral) 8%, transparent)', + '--cl-color-neutral-alpha-300': 'color-mix(in oklab, var(--cl-color-neutral) 12%, transparent)', '--cl-color-foreground': `light-dark(${gray['900']}, ${gray['50']})`, '--cl-color-foreground-secondary': `light-dark(${gray['600']}, ${gray['400']})`, @@ -65,16 +68,25 @@ const colorDefaults = { '--cl-color-negative-foreground': gray['0'], '--cl-color-negative-subtle': 'light-dark(oklch(0.9757 0.0118 17.36), oklch(0.255 0.0604 22.31))', '--cl-color-negative-border': 'light-dark(oklch(0.8155 0.0983 19.41), oklch(0.3958 0.1331 25.72))', + '--cl-color-negative-alpha-100': 'color-mix(in oklab, var(--cl-color-negative) 6%, transparent)', + '--cl-color-negative-alpha-200': 'color-mix(in oklab, var(--cl-color-negative) 8%, transparent)', + '--cl-color-negative-alpha-300': 'color-mix(in oklab, var(--cl-color-negative) 12%, transparent)', '--cl-color-positive': 'light-dark(oklch(0.6082 0.1799 145.47), oklch(0.7227 0.192 149.58))', '--cl-color-positive-foreground': gray['0'], '--cl-color-positive-subtle': 'light-dark(oklch(0.9859 0.0164 156.92), oklch(0.3297 0.052 152.31))', '--cl-color-positive-border': 'light-dark(oklch(0.7922 0.1959 148.18), oklch(0.4479 0.1083 151.33))', + '--cl-color-positive-alpha-100': 'color-mix(in oklab, var(--cl-color-positive) 6%, transparent)', + '--cl-color-positive-alpha-200': 'color-mix(in oklab, var(--cl-color-positive) 8%, transparent)', + '--cl-color-positive-alpha-300': 'color-mix(in oklab, var(--cl-color-positive) 12%, transparent)', '--cl-color-warning': 'light-dark(oklch(0.7099 0.1888 44.94), oklch(0.7576 0.159 55.93))', '--cl-color-warning-foreground': gray['0'], '--cl-color-warning-subtle': 'light-dark(oklch(0.9799 0.0147 70.89), oklch(0.2725 0.0547 55.7))', '--cl-color-warning-border': 'light-dark(oklch(0.8672 0.0902 63.47), oklch(0.4084 0.1165 38.17))', + '--cl-color-warning-alpha-100': 'color-mix(in oklab, var(--cl-color-warning) 6%, transparent)', + '--cl-color-warning-alpha-200': 'color-mix(in oklab, var(--cl-color-warning) 8%, transparent)', + '--cl-color-warning-alpha-300': 'color-mix(in oklab, var(--cl-color-warning) 12%, transparent)', '--cl-color-input': `light-dark(${gray['0']}, ${gray['850']})`, '--cl-color-input-placeholder': gray['400'], @@ -84,18 +96,6 @@ const colorDefaults = { export const colorVars = stylex.defineVars(colorDefaults); -// ============================================================================= -// Neutral Alpha Scale -// ============================================================================= - -export const neutralAlpha = stylex.defineVars({ - '4': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`, - '6': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 6%, transparent)`, - '8': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 8%, transparent)`, - '12': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 12%, transparent)`, - '18': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 18%, transparent)`, -}); - // ============================================================================= // Radius Tokens // ============================================================================= diff --git a/packages/ui/src/mosaic/utils/input.styles.ts b/packages/ui/src/mosaic/utils/input.styles.ts index 57419da3d80..39d27821218 100644 --- a/packages/ui/src/mosaic/utils/input.styles.ts +++ b/packages/ui/src/mosaic/utils/input.styles.ts @@ -6,10 +6,7 @@ import { colorVars, durationVars, easingVars } from '../tokens.stylex'; // system focus ring, so a text-entry field reads as a field rather than a control. const hoverBorderColor = 'light-dark(#bebebe, #525252)'; const focusShadow = '0 0 0 3px light-dark(rgb(23 23 23 / 8%), rgb(255 255 255 / 8%))'; -const invalidFocusShadow = `0 0 0 3px light-dark( - color-mix(in oklab, ${colorVars['--cl-color-negative']} 12%, transparent), - color-mix(in oklab, ${colorVars['--cl-color-negative']} 15%, transparent) -)`; +const invalidFocusShadow = `0 0 0 3px ${colorVars['--cl-color-negative-alpha-300']}`; const disabledBackgroundColor = `color-mix(in oklab, ${colorVars['--cl-color-brand']} 5%, transparent)`; export const inputStyles = stylex.create({ diff --git a/references/mosaic-architecture.md b/references/mosaic-architecture.md index bd90380c2c2..604c9f12eca 100644 --- a/references/mosaic-architecture.md +++ b/references/mosaic-architecture.md @@ -25,7 +25,7 @@ export const colorVars = stylex.defineVars({ Groups: `colorVars`, `radiusVars`, `targetVars`, `scrollbarVars`, `scrollFadeVars`, `spacingVars`, `space`, `typeScaleVars`, `fontFamilyVars`, `fontWeightVars`, `durationVars`, `easingVars`, `focusVars`. -`gray` and `neutralAlpha` are the two groups that are not public: their keys carry no `--cl-` prefix, so StyleX hashes the names and there is nothing to override. The gray-backed `--cl-color-*` tokens read from `gray`, and hover, pressed, and resting washes over an unknown backdrop read from `neutralAlpha` (`--cl-color-neutral`, black on light and white on dark, at a fixed opacity over `transparent`); consumers retheme the `--cl-color-*` tokens, not the scales. +`gray` is the one group that is not public: its keys carry no `--cl-` prefix, so StyleX hashes the names and there is no `--cl-gray-*` to override. The gray-backed `--cl-color-*` tokens read from it; consumers retheme those, not the scale. The `--cl--alpha-100|200|300` tokens in `colorVars` are washes (6, 8, and 12 percent of `--cl-color-neutral`, `-negative`, `-positive`, and `-warning` over `transparent`); overriding the solid color retints its washes. Light and dark come from CSS `light-dark()` on the default values, so there is no theme object and no re-render on theme change — the browser resolves it. From d896acbe061a3624385aa4607822a99375f45755 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 13:24:51 -0400 Subject: [PATCH 08/15] feat(ui): derive gray tokens from neutral and background (#9710) --- .claude/skills/mosaic/references/stylex.md | 6 +-- packages/ui/src/mosaic/tokens.stylex.ts | 62 +++++++++------------- references/mosaic-architecture.md | 2 +- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/.claude/skills/mosaic/references/stylex.md b/.claude/skills/mosaic/references/stylex.md index 908f4fe0ebf..e0a5d5013fa 100644 --- a/.claude/skills/mosaic/references/stylex.md +++ b/.claude/skills/mosaic/references/stylex.md @@ -78,9 +78,9 @@ export const colorVars = stylex.defineVars(colorDefaults); - **DO** put light + dark in one token value via `light-dark()`. Never ship a second `@media (prefers-color-scheme: dark)` copy of a color. - **DO** reserve `--cl-*`-prefixed keys for the public, overridable contract. -- **DO** point a gray-backed colour token at the internal `gray` scale - (`light-dark(${gray['900']}, ${gray['50']})`) rather than restating the oklch value. - Its keys are unprefixed on purpose, so the names hash and stay un-overridable. +- **DO** derive a gray-backed colour token from `--cl-color-neutral` mixed into + `--cl-color-background` (`neutralMix(80, 98)` in `tokens.stylex.ts`) rather than + restating an oklch value. There is no gray scale: retinting neutral retints the ramp. - **DO** name internal, non-contract vars with a `--_cl-*` prefix (e.g. a value a parent writes for a child to read). They still emit verbatim but the `_` marks them "not a contract, don't override." diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 46a20a73d6b..b3984833170 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -19,45 +19,32 @@ import * as stylex from '@stylexjs/stylex'; // `@media (prefers-color-scheme)` duplication. The `--cl-` prefix namespaces // the vars so they never collide with a host app's own custom properties. -// ============================================================================= -// Gray Scale -// ============================================================================= - -export const gray = stylex.defineVars({ - '0': 'oklch(1 0 0)', - '50': 'oklch(0.9851 0 0)', - '100': 'oklch(0.9702 0 0)', - '200': 'oklch(0.9461 0 0)', - '300': 'oklch(0.8699 0 0)', - '400': 'oklch(0.7155 0 0)', - '500': 'oklch(0.5555 0 0)', - '600': 'oklch(0.4386 0 0)', - '700': 'oklch(0.3715 0 0)', - '800': 'oklch(0.2686 0 0)', - '850': 'oklch(0.2393 0 0)', - '900': 'oklch(0.2046 0 0)', - '1000': 'oklch(0 0 0)', -}); - // ============================================================================= // Color Tokens // ============================================================================= +// Every achromatic token is `--cl-color-neutral` mixed into `--cl-color-background`, so +// retinting neutral (a warm gray, say) retints the whole ramp. The percentages reproduce +// the Figma gray scale: grays have zero chroma, so an oklab mix is linear in lightness. +// Dark mode needs its own percentage because its background is a 900, not black. +const neutralMix = (light: number, dark: number) => + `light-dark(color-mix(in oklab, var(--cl-color-neutral) ${light}%, var(--cl-color-background)), color-mix(in oklab, var(--cl-color-neutral) ${dark}%, var(--cl-color-background)))`; + const colorDefaults = { - '--cl-color-neutral': `light-dark(${gray['1000']}, ${gray['0']})`, + '--cl-color-neutral': 'light-dark(oklch(0 0 0), oklch(1 0 0))', '--cl-color-neutral-alpha-100': 'color-mix(in oklab, var(--cl-color-neutral) 6%, transparent)', '--cl-color-neutral-alpha-200': 'color-mix(in oklab, var(--cl-color-neutral) 8%, transparent)', '--cl-color-neutral-alpha-300': 'color-mix(in oklab, var(--cl-color-neutral) 12%, transparent)', - '--cl-color-foreground': `light-dark(${gray['900']}, ${gray['50']})`, - '--cl-color-foreground-secondary': `light-dark(${gray['600']}, ${gray['400']})`, - '--cl-color-foreground-disabled': `light-dark(${gray['400']}, ${gray['500']})`, + '--cl-color-background': 'light-dark(oklch(1 0 0), oklch(0.2046 0 0))', + '--cl-color-background-subtle': neutralMix(3, 4), - '--cl-color-background': `light-dark(${gray['0']}, ${gray['900']})`, - '--cl-color-background-subtle': `light-dark(${gray['100']}, ${gray['850']})`, + '--cl-color-foreground': neutralMix(79.5, 98), + '--cl-color-foreground-secondary': neutralMix(56, 64.25), + '--cl-color-foreground-disabled': neutralMix(28.5, 44), - '--cl-color-border': `light-dark(${gray['200']}, ${gray['800']})`, - '--cl-color-border-subtle': `light-dark(${gray['100']}, ${gray['850']})`, + '--cl-color-border': neutralMix(5.5, 8), + '--cl-color-border-subtle': neutralMix(3, 4.25), '--cl-color-brand': 'light-dark(oklch(0.2046 0 0), oklch(0.9851 0 0))', '--cl-color-brand-secondary': 'light-dark(oklch(0.5555 0 0), oklch(0.7155 0 0))', @@ -65,7 +52,7 @@ const colorDefaults = { '--cl-color-brand-foreground': 'light-dark(oklch(1 0 0), oklch(0.2046 0 0))', '--cl-color-negative': 'light-dark(oklch(0.5903 0.213 26.78), oklch(0.7106 0.1661 22.22))', - '--cl-color-negative-foreground': gray['0'], + '--cl-color-negative-foreground': 'oklch(1 0 0)', '--cl-color-negative-subtle': 'light-dark(oklch(0.9757 0.0118 17.36), oklch(0.255 0.0604 22.31))', '--cl-color-negative-border': 'light-dark(oklch(0.8155 0.0983 19.41), oklch(0.3958 0.1331 25.72))', '--cl-color-negative-alpha-100': 'color-mix(in oklab, var(--cl-color-negative) 6%, transparent)', @@ -73,7 +60,7 @@ const colorDefaults = { '--cl-color-negative-alpha-300': 'color-mix(in oklab, var(--cl-color-negative) 12%, transparent)', '--cl-color-positive': 'light-dark(oklch(0.6082 0.1799 145.47), oklch(0.7227 0.192 149.58))', - '--cl-color-positive-foreground': gray['0'], + '--cl-color-positive-foreground': 'oklch(1 0 0)', '--cl-color-positive-subtle': 'light-dark(oklch(0.9859 0.0164 156.92), oklch(0.3297 0.052 152.31))', '--cl-color-positive-border': 'light-dark(oklch(0.7922 0.1959 148.18), oklch(0.4479 0.1083 151.33))', '--cl-color-positive-alpha-100': 'color-mix(in oklab, var(--cl-color-positive) 6%, transparent)', @@ -81,17 +68,18 @@ const colorDefaults = { '--cl-color-positive-alpha-300': 'color-mix(in oklab, var(--cl-color-positive) 12%, transparent)', '--cl-color-warning': 'light-dark(oklch(0.7099 0.1888 44.94), oklch(0.7576 0.159 55.93))', - '--cl-color-warning-foreground': gray['0'], + '--cl-color-warning-foreground': 'oklch(1 0 0)', '--cl-color-warning-subtle': 'light-dark(oklch(0.9799 0.0147 70.89), oklch(0.2725 0.0547 55.7))', '--cl-color-warning-border': 'light-dark(oklch(0.8672 0.0902 63.47), oklch(0.4084 0.1165 38.17))', '--cl-color-warning-alpha-100': 'color-mix(in oklab, var(--cl-color-warning) 6%, transparent)', '--cl-color-warning-alpha-200': 'color-mix(in oklab, var(--cl-color-warning) 8%, transparent)', '--cl-color-warning-alpha-300': 'color-mix(in oklab, var(--cl-color-warning) 12%, transparent)', - '--cl-color-input': `light-dark(${gray['0']}, ${gray['850']})`, - '--cl-color-input-placeholder': gray['400'], + '--cl-color-input': + 'light-dark(var(--cl-color-background), color-mix(in oklab, var(--cl-color-neutral) 4%, var(--cl-color-background)))', + '--cl-color-input-placeholder': neutralMix(28, 64), - '--cl-color-ring': 'light-dark(oklch(0.205 0 0), oklch(0.922 0 0))', + '--cl-color-ring': neutralMix(80, 90), } as const; export const colorVars = stylex.defineVars(colorDefaults); @@ -435,9 +423,9 @@ export const focusVars = stylex.defineVars(focusDefaults); // dark. Branched per colour via `light-dark()` since a shadow's geometry cannot branch — see the // note on `Dialog`'s popup for why `@media (prefers-color-scheme)` is not the escape hatch. const shadowDefaults = { - '--cl-shadow-card': `0 12px 12px -7px light-dark(color-mix(in oklab, ${gray['900']} 12%, transparent), transparent), - 0 24px 24px -10px light-dark(color-mix(in oklab, ${gray['900']} 4%, transparent), transparent), - 0 0 0 1px light-dark(color-mix(in oklab, ${gray['900']} 4%, transparent), color-mix(in oklab, ${gray['0']} 10%, transparent))`, + '--cl-shadow-card': `0 12px 12px -7px light-dark(color-mix(in oklab, ${colorVars['--cl-color-foreground']} 12%, transparent), transparent), + 0 24px 24px -10px light-dark(color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent), transparent), + 0 0 0 1px light-dark(color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`, }; export const shadowVars = stylex.defineVars(shadowDefaults); diff --git a/references/mosaic-architecture.md b/references/mosaic-architecture.md index 604c9f12eca..21b655e74d1 100644 --- a/references/mosaic-architecture.md +++ b/references/mosaic-architecture.md @@ -25,7 +25,7 @@ export const colorVars = stylex.defineVars({ Groups: `colorVars`, `radiusVars`, `targetVars`, `scrollbarVars`, `scrollFadeVars`, `spacingVars`, `space`, `typeScaleVars`, `fontFamilyVars`, `fontWeightVars`, `durationVars`, `easingVars`, `focusVars`. -`gray` is the one group that is not public: its keys carry no `--cl-` prefix, so StyleX hashes the names and there is no `--cl-gray-*` to override. The gray-backed `--cl-color-*` tokens read from it; consumers retheme those, not the scale. The `--cl--alpha-100|200|300` tokens in `colorVars` are washes (6, 8, and 12 percent of `--cl-color-neutral`, `-negative`, `-positive`, and `-warning` over `transparent`); overriding the solid color retints its washes. +There is no gray scale. Every achromatic token (`--cl-color-foreground`, `-border`, `-background-subtle`, …) is `--cl-color-neutral` (black on light, white on dark) mixed into `--cl-color-background` at a fixed percentage per mode, so overriding neutral to a warm gray retints the whole ramp; in dark mode override `--cl-color-background` too, since the ramp mixes toward it. The `--cl--alpha-100|200|300` tokens in `colorVars` are washes (6, 8, and 12 percent of `--cl-color-neutral`, `-negative`, `-positive`, and `-warning` over `transparent`); overriding the solid color retints its washes. Light and dark come from CSS `light-dark()` on the default values, so there is no theme object and no re-render on theme change — the browser resolves it. From b9e5cd5117623cbcce49b3a33fec79ebe8c00f74 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 13:35:46 -0400 Subject: [PATCH 09/15] feat(ui): derive mosaic shadows from neutral and share the card shadow token --- .../src/mosaic/components/dialog/dialog.styles.ts | 14 ++++++-------- .../src/mosaic/components/drawer/drawer.styles.ts | 2 +- .../ui/src/mosaic/components/menu/menu.styles.ts | 6 ++---- packages/ui/src/mosaic/tokens.stylex.ts | 12 +++++++----- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/ui/src/mosaic/components/dialog/dialog.styles.ts b/packages/ui/src/mosaic/components/dialog/dialog.styles.ts index 4ba36f521c0..808adcb566a 100644 --- a/packages/ui/src/mosaic/components/dialog/dialog.styles.ts +++ b/packages/ui/src/mosaic/components/dialog/dialog.styles.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, durationVars, easingVars, radiusVars, space } from '../../tokens.stylex'; +import { colorVars, durationVars, easingVars, radiusVars, shadowVars, space } from '../../tokens.stylex'; // How far the contents of a surface beneath a stacked prompt are veiled toward its own background. // Declared up here rather than beside `STACK_SCALE` further down because `sizes` reads it, and @@ -210,10 +210,10 @@ export const styles = stylex.create({ // tabbable content, which would otherwise draw a ring around the whole surface. outline: 'none', backgroundColor: colorVars['--cl-color-background'], - // Matches `Menu`. The two schemes are different treatments, not one at two strengths: light - // gets the two drop layers and a dark hairline, dark drops them to `transparent` and separates - // with a light hairline instead — a shadow reads as depth against a light page and as nothing - // against a dark one. + // The card shadow, shared with `Card` and `Menu`. The two schemes are different treatments, + // not one at two strengths: light gets the two drop layers and a dark hairline, dark drops them + // to `transparent` and separates with a light hairline instead — a shadow reads as depth + // against a light page and as nothing against a dark one. // // Branched per COLOUR via `light-dark()`, which is the only shape available: `light-dark()` // resolves to a colour and cannot carry an offset or a blur, so the geometry has to be shared. @@ -221,9 +221,7 @@ export const styles = stylex.create({ // OS preference while `light-dark()` tracks the `color-scheme` in scope, so an app forcing a // scheme (swingset's own toggle does, via next-themes) would take its colours from one and its // geometry from the other. - boxShadow: `0 12px 12px -7px light-dark(oklch(0.2046 0 0 / 12%), transparent), - 0 24px 24px -10px light-dark(oklch(0.2046 0 0 / 4%), transparent), - 0 0 0 1px light-dark(oklch(0.2046 0 0 / 4%), oklch(1 0 0 / 10%))`, + boxShadow: shadowVars['--cl-shadow-card'], color: colorVars['--cl-color-foreground'], display: 'flex', flexDirection: 'column', diff --git a/packages/ui/src/mosaic/components/drawer/drawer.styles.ts b/packages/ui/src/mosaic/components/drawer/drawer.styles.ts index d750a122ab8..b7b02b7c496 100644 --- a/packages/ui/src/mosaic/components/drawer/drawer.styles.ts +++ b/packages/ui/src/mosaic/components/drawer/drawer.styles.ts @@ -83,7 +83,7 @@ export const styles = stylex.create({ // No drop shadow — the sheet sits on the screen edge, so there is nothing for it to float over. // The hairline ring stays: a faint dark edge in light, and the light edge that separates a dark // sheet from a dark page. - boxShadow: `0 0 0 1px light-dark(oklch(0.2046 0 0 / 4%), oklch(1 0 0 / 10%))`, + boxShadow: `0 0 0 1px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`, color: colorVars['--cl-color-foreground'], display: 'flex', flexDirection: 'column', diff --git a/packages/ui/src/mosaic/components/menu/menu.styles.ts b/packages/ui/src/mosaic/components/menu/menu.styles.ts index 952dc6a34c5..1f94e2e9acd 100644 --- a/packages/ui/src/mosaic/components/menu/menu.styles.ts +++ b/packages/ui/src/mosaic/components/menu/menu.styles.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; +import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, shadowVars, space, typeScaleVars } from '../../tokens.stylex'; // Positioning is applied inline by the headless positioner; this only clears the // focus outline it receives. No z-index: the portalled, fixed positioner already @@ -16,9 +16,7 @@ export const popup = stylex.create({ borderRadius: radiusVars['--cl-radius-lg'], outline: 'none', backgroundColor: colorVars['--cl-color-background'], - boxShadow: `0 12px 12px -7px light-dark(oklch(0.2046 0 0 / 12%), transparent), - 0 24px 24px -10px light-dark(oklch(0.2046 0 0 / 4%), transparent), - 0 0 0 1px light-dark(oklch(0.2046 0 0 / 4%), oklch(1 0 0 / 10%))`, + boxShadow: shadowVars['--cl-shadow-card'], color: colorVars['--cl-color-foreground'], opacity: { default: 1, diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index b3984833170..e3b036ad8c4 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -420,12 +420,14 @@ export const focusVars = stylex.defineVars(focusDefaults); // Elevation. The one card shadow, as a token so every surface at that elevation reads the same: // two drop layers that fall away in dark, and a hairline ring that is dark on light and light on -// dark. Branched per colour via `light-dark()` since a shadow's geometry cannot branch — see the -// note on `Dialog`'s popup for why `@media (prefers-color-scheme)` is not the escape hatch. +// dark. Every layer is a wash of `--cl-color-neutral` (black on light, white on dark), matching +// Figma, where shadows are #000 rather than the foreground. Branched per colour via `light-dark()` +// since a shadow's geometry cannot branch — see the note on `Dialog`'s popup for why +// `@media (prefers-color-scheme)` is not the escape hatch. const shadowDefaults = { - '--cl-shadow-card': `0 12px 12px -7px light-dark(color-mix(in oklab, ${colorVars['--cl-color-foreground']} 12%, transparent), transparent), - 0 24px 24px -10px light-dark(color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent), transparent), - 0 0 0 1px light-dark(color-mix(in oklab, ${colorVars['--cl-color-foreground']} 4%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`, + '--cl-shadow-card': `0 12px 12px -7px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 12%, transparent), transparent), + 0 24px 24px -10px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), transparent), + 0 0 0 1px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`, }; export const shadowVars = stylex.defineVars(shadowDefaults); From e2cfded59ab110f0549ccb8a86030ffd88f85bcd Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 13:43:27 -0400 Subject: [PATCH 10/15] chore(ui): format menu styles import --- packages/ui/src/mosaic/components/menu/menu.styles.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/mosaic/components/menu/menu.styles.ts b/packages/ui/src/mosaic/components/menu/menu.styles.ts index 1f94e2e9acd..0f471963a3c 100644 --- a/packages/ui/src/mosaic/components/menu/menu.styles.ts +++ b/packages/ui/src/mosaic/components/menu/menu.styles.ts @@ -1,6 +1,14 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, shadowVars, space, typeScaleVars } from '../../tokens.stylex'; +import { + colorVars, + fontFamilyVars, + fontWeightVars, + radiusVars, + shadowVars, + space, + typeScaleVars, +} from '../../tokens.stylex'; // Positioning is applied inline by the headless positioner; this only clears the // focus outline it receives. No z-index: the portalled, fixed positioner already From 842f6147c8c65a59a40b050d36e2be2b7a4470e1 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 13:59:51 -0400 Subject: [PATCH 11/15] fix(ui): share the card shadow with select, combobox, and phone input popups --- packages/swingset/src/stories/banner.mdx | 2 +- .../components/combobox/combobox.styles.ts | 14 ++++++++++---- .../components/phone-input/phone-input.styles.ts | 6 ++---- .../mosaic/components/select/select.styles.ts | 6 +++--- packages/ui/src/mosaic/tokens.stylex.ts | 16 ++++++++++------ 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/swingset/src/stories/banner.mdx b/packages/swingset/src/stories/banner.mdx index baf1bb502df..09bf18b40f4 100644 --- a/packages/swingset/src/stories/banner.mdx +++ b/packages/swingset/src/stories/banner.mdx @@ -58,7 +58,7 @@ Each part carries its stable slot class alongside the generated StyleX atoms and | `Banner.Label` | `neutral` \| `warning` \| `negative` | | `Banner.Description` | `neutral` \| `warning` \| `negative` | -Warning and negative paint the color's `-subtle` surface inside its `-border` hairline and use the saturated token for the icon and copy, so retheme one by overriding `--cl-color-negative-subtle`, `--cl-color-negative-border`, and `--cl-color-negative` (or the `warning` equivalents). Neutral has no subtle surface of its own: its fill is a 4% mix of `--cl-color-foreground` over whatever it sits on, and `--cl-color-border` draws its hairline. +Warning and negative paint the color's `-subtle` surface inside its `-border` hairline and use the saturated token for the icon and copy, so retheme one by overriding `--cl-color-negative-subtle`, `--cl-color-negative-border`, and `--cl-color-negative` (or the `warning` equivalents). Neutral has no subtle surface of its own: its fill is `--cl-color-neutral-alpha-100`, a 6% wash of `--cl-color-neutral` over whatever it sits on, and `--cl-color-border` draws its hairline. --- diff --git a/packages/ui/src/mosaic/components/combobox/combobox.styles.ts b/packages/ui/src/mosaic/components/combobox/combobox.styles.ts index bceab23e1e0..4ab169af707 100644 --- a/packages/ui/src/mosaic/components/combobox/combobox.styles.ts +++ b/packages/ui/src/mosaic/components/combobox/combobox.styles.ts @@ -1,6 +1,14 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; +import { + colorVars, + fontFamilyVars, + fontWeightVars, + radiusVars, + shadowVars, + space, + typeScaleVars, +} from '../../tokens.stylex'; export const styles = stylex.create({ positioner: { @@ -11,9 +19,7 @@ export const styles = stylex.create({ outline: 'none', overflow: 'hidden', backgroundColor: colorVars['--cl-color-background'], - boxShadow: `0 12px 12px -7px light-dark(oklch(0.2046 0 0 / 12%), transparent), - 0 24px 24px -10px light-dark(oklch(0.2046 0 0 / 4%), transparent), - 0 0 0 1px light-dark(oklch(0.2046 0 0 / 4%), oklch(1 0 0 / 10%))`, + boxShadow: shadowVars['--cl-shadow-card'], color: colorVars['--cl-color-foreground'], opacity: { default: 1, diff --git a/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts b/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts index d4752a2aac2..ae8dea02bc0 100644 --- a/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts +++ b/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; +import { colorVars, radiusVars, shadowVars, space, typeScaleVars } from '../../tokens.stylex'; export const styles = stylex.create({ triggerContent: { @@ -42,9 +42,7 @@ export const styles = stylex.create({ borderRadius: radiusVars['--cl-radius-lg'], overflow: 'hidden', backgroundColor: colorVars['--cl-color-background'], - boxShadow: `0 12px 12px -7px light-dark(oklch(0.2046 0 0 / 12%), transparent), - 0 24px 24px -10px light-dark(oklch(0.2046 0 0 / 4%), transparent), - 0 0 0 1px light-dark(oklch(0.2046 0 0 / 4%), oklch(1 0 0 / 10%))`, + boxShadow: shadowVars['--cl-shadow-card'], color: colorVars['--cl-color-foreground'], }, countrySearchContainer: { diff --git a/packages/ui/src/mosaic/components/select/select.styles.ts b/packages/ui/src/mosaic/components/select/select.styles.ts index 081cca44efe..e2aa82ecf09 100644 --- a/packages/ui/src/mosaic/components/select/select.styles.ts +++ b/packages/ui/src/mosaic/components/select/select.styles.ts @@ -7,6 +7,7 @@ import { fontFamilyVars, fontWeightVars, radiusVars, + shadowVars, space, typeScaleVars, } from '../../tokens.stylex'; @@ -34,14 +35,13 @@ export const positioner = stylex.create({ export const popup = stylex.create({ base: { // A real border, not a shadow ring, so it occupies the 1px of layout the alignment counts on. - borderColor: 'light-dark(oklch(0.2046 0 0 / 4%), oklch(1 0 0 / 10%))', + borderColor: `light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`, borderRadius: radiusVars['--cl-radius-lg'], borderStyle: 'solid', borderWidth: '1px', outline: 'none', backgroundColor: colorVars['--cl-color-background'], - boxShadow: `0 12px 12px -7px light-dark(oklch(0.2046 0 0 / 12%), transparent), - 0 24px 24px -10px light-dark(oklch(0.2046 0 0 / 4%), transparent)`, + boxShadow: shadowVars['--cl-shadow-card-drop'], color: colorVars['--cl-color-foreground'], opacity: { default: 1, diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 64c8a35fd81..31ddf2d60c6 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -422,14 +422,18 @@ export const focusVars = stylex.defineVars(focusDefaults); // Elevation. The one card shadow, as a token so every surface at that elevation reads the same: // two drop layers that fall away in dark, and a hairline ring that is dark on light and light on -// dark. Every layer is a wash of `--cl-color-neutral` (black on light, white on dark), matching -// Figma, where shadows are #000 rather than the foreground. Branched per colour via `light-dark()` -// since a shadow's geometry cannot branch — see the note on `Dialog`'s popup for why +// dark. `-drop` is the drop layers alone, for a popup that draws a real border instead of the ring. +// Every layer is a wash of `--cl-color-neutral` (black on light, white on dark), matching Figma, +// where shadows are #000 rather than the foreground. Branched per colour via `light-dark()` since +// a shadow's geometry cannot branch — see the note on `Dialog`'s popup for why // `@media (prefers-color-scheme)` is not the escape hatch. +const shadowDrop = `0 12px 12px -7px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 12%, transparent), transparent), + 0 24px 24px -10px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), transparent)`; +const shadowRing = `0 0 0 1px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`; + const shadowDefaults = { - '--cl-shadow-card': `0 12px 12px -7px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 12%, transparent), transparent), - 0 24px 24px -10px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), transparent), - 0 0 0 1px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`, + '--cl-shadow-card': `${shadowDrop}, ${shadowRing}`, + '--cl-shadow-card-drop': shadowDrop, }; export const shadowVars = stylex.defineVars(shadowDefaults); From 6b2c5a566ab2f8698d01db4ba11642769c399891 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 14:02:29 -0400 Subject: [PATCH 12/15] refactor(ui): split the card shadow into raised and ring tokens --- .../src/mosaic/components/card/card.styles.ts | 4 ++-- .../components/combobox/combobox.styles.ts | 2 +- .../mosaic/components/dialog/dialog.styles.ts | 10 ++++----- .../mosaic/components/drawer/drawer.styles.ts | 4 ++-- .../src/mosaic/components/menu/menu.styles.ts | 2 +- .../phone-input/phone-input.styles.ts | 2 +- .../components/profile/profile.styles.ts | 2 +- .../mosaic/components/select/select.styles.ts | 2 +- packages/ui/src/mosaic/tokens.stylex.ts | 21 ++++++++----------- 9 files changed, 23 insertions(+), 26 deletions(-) diff --git a/packages/ui/src/mosaic/components/card/card.styles.ts b/packages/ui/src/mosaic/components/card/card.styles.ts index e7127eb6eae..e491e2700db 100644 --- a/packages/ui/src/mosaic/components/card/card.styles.ts +++ b/packages/ui/src/mosaic/components/card/card.styles.ts @@ -19,7 +19,7 @@ export const root = stylex.create({ borderRadius: radiusVars['--cl-radius-xl'], overflow: 'hidden', backgroundColor: colorVars['--cl-color-background'], - boxShadow: shadowVars['--cl-shadow-card'], + boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, }, flush: { borderRadius: radiusVars['--cl-radius-xl'], @@ -31,7 +31,7 @@ export const root = stylex.create({ borderRadius: radiusVars['--cl-radius-xl'], overflow: 'hidden', backgroundColor: colorVars['--cl-color-background'], - boxShadow: shadowVars['--cl-shadow-card'], + boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, }, }); diff --git a/packages/ui/src/mosaic/components/combobox/combobox.styles.ts b/packages/ui/src/mosaic/components/combobox/combobox.styles.ts index 4ab169af707..6fb2fd2e786 100644 --- a/packages/ui/src/mosaic/components/combobox/combobox.styles.ts +++ b/packages/ui/src/mosaic/components/combobox/combobox.styles.ts @@ -19,7 +19,7 @@ export const styles = stylex.create({ outline: 'none', overflow: 'hidden', backgroundColor: colorVars['--cl-color-background'], - boxShadow: shadowVars['--cl-shadow-card'], + boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, color: colorVars['--cl-color-foreground'], opacity: { default: 1, diff --git a/packages/ui/src/mosaic/components/dialog/dialog.styles.ts b/packages/ui/src/mosaic/components/dialog/dialog.styles.ts index 808adcb566a..77182feedd4 100644 --- a/packages/ui/src/mosaic/components/dialog/dialog.styles.ts +++ b/packages/ui/src/mosaic/components/dialog/dialog.styles.ts @@ -210,10 +210,10 @@ export const styles = stylex.create({ // tabbable content, which would otherwise draw a ring around the whole surface. outline: 'none', backgroundColor: colorVars['--cl-color-background'], - // The card shadow, shared with `Card` and `Menu`. The two schemes are different treatments, - // not one at two strengths: light gets the two drop layers and a dark hairline, dark drops them - // to `transparent` and separates with a light hairline instead — a shadow reads as depth - // against a light page and as nothing against a dark one. + // The raised shadow and ring, shared with `Card` and `Menu`. The two schemes are different + // treatments, not one at two strengths: light gets the two drop layers and a dark hairline, + // dark drops them to `transparent` and separates with a light hairline instead — a shadow + // reads as depth against a light page and as nothing against a dark one. // // Branched per COLOUR via `light-dark()`, which is the only shape available: `light-dark()` // resolves to a colour and cannot carry an offset or a blur, so the geometry has to be shared. @@ -221,7 +221,7 @@ export const styles = stylex.create({ // OS preference while `light-dark()` tracks the `color-scheme` in scope, so an app forcing a // scheme (swingset's own toggle does, via next-themes) would take its colours from one and its // geometry from the other. - boxShadow: shadowVars['--cl-shadow-card'], + boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, color: colorVars['--cl-color-foreground'], display: 'flex', flexDirection: 'column', diff --git a/packages/ui/src/mosaic/components/drawer/drawer.styles.ts b/packages/ui/src/mosaic/components/drawer/drawer.styles.ts index b7b02b7c496..3a0e244fc4a 100644 --- a/packages/ui/src/mosaic/components/drawer/drawer.styles.ts +++ b/packages/ui/src/mosaic/components/drawer/drawer.styles.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, durationVars, easingVars, radiusVars, space } from '../../tokens.stylex'; +import { colorVars, durationVars, easingVars, radiusVars, shadowVars, space } from '../../tokens.stylex'; // The dialog's scrim, and the nested value a dialog paints over a `profile` or a `card` — see // `dialog.styles.ts` for the composite arithmetic. A sheet opening from inside a profile dialog is @@ -83,7 +83,7 @@ export const styles = stylex.create({ // No drop shadow — the sheet sits on the screen edge, so there is nothing for it to float over. // The hairline ring stays: a faint dark edge in light, and the light edge that separates a dark // sheet from a dark page. - boxShadow: `0 0 0 1px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`, + boxShadow: shadowVars['--cl-shadow-ring'], color: colorVars['--cl-color-foreground'], display: 'flex', flexDirection: 'column', diff --git a/packages/ui/src/mosaic/components/menu/menu.styles.ts b/packages/ui/src/mosaic/components/menu/menu.styles.ts index 0f471963a3c..3e2a6e31bbe 100644 --- a/packages/ui/src/mosaic/components/menu/menu.styles.ts +++ b/packages/ui/src/mosaic/components/menu/menu.styles.ts @@ -24,7 +24,7 @@ export const popup = stylex.create({ borderRadius: radiusVars['--cl-radius-lg'], outline: 'none', backgroundColor: colorVars['--cl-color-background'], - boxShadow: shadowVars['--cl-shadow-card'], + boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, color: colorVars['--cl-color-foreground'], opacity: { default: 1, diff --git a/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts b/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts index ae8dea02bc0..e5784cad1b4 100644 --- a/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts +++ b/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts @@ -42,7 +42,7 @@ export const styles = stylex.create({ borderRadius: radiusVars['--cl-radius-lg'], overflow: 'hidden', backgroundColor: colorVars['--cl-color-background'], - boxShadow: shadowVars['--cl-shadow-card'], + boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, color: colorVars['--cl-color-foreground'], }, countrySearchContainer: { diff --git a/packages/ui/src/mosaic/components/profile/profile.styles.ts b/packages/ui/src/mosaic/components/profile/profile.styles.ts index a5b8646c8b3..736bfdd9bb7 100644 --- a/packages/ui/src/mosaic/components/profile/profile.styles.ts +++ b/packages/ui/src/mosaic/components/profile/profile.styles.ts @@ -159,7 +159,7 @@ export const styles = stylex.create({ // popup is the screen and there is nothing to lift off. boxShadow: { [compact]: 'none', - default: shadowVars['--cl-shadow-card'], + default: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, }, flexGrow: 1, minHeight: 0, diff --git a/packages/ui/src/mosaic/components/select/select.styles.ts b/packages/ui/src/mosaic/components/select/select.styles.ts index e2aa82ecf09..7e813a2556b 100644 --- a/packages/ui/src/mosaic/components/select/select.styles.ts +++ b/packages/ui/src/mosaic/components/select/select.styles.ts @@ -41,7 +41,7 @@ export const popup = stylex.create({ borderWidth: '1px', outline: 'none', backgroundColor: colorVars['--cl-color-background'], - boxShadow: shadowVars['--cl-shadow-card-drop'], + boxShadow: shadowVars['--cl-shadow-raised'], color: colorVars['--cl-color-foreground'], opacity: { default: 1, diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 31ddf2d60c6..ccefa49f64c 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -420,20 +420,17 @@ const focusDefaults = { export const focusVars = stylex.defineVars(focusDefaults); -// Elevation. The one card shadow, as a token so every surface at that elevation reads the same: -// two drop layers that fall away in dark, and a hairline ring that is dark on light and light on -// dark. `-drop` is the drop layers alone, for a popup that draws a real border instead of the ring. -// Every layer is a wash of `--cl-color-neutral` (black on light, white on dark), matching Figma, -// where shadows are #000 rather than the foreground. Branched per colour via `light-dark()` since -// a shadow's geometry cannot branch — see the note on `Dialog`'s popup for why +// Elevation, in two composable pieces: `raised` is two drop layers that fall away in dark, and +// `ring` is a hairline that is dark on light and light on dark. A lifted surface takes both, a +// popup with a real border takes `raised` alone, and a sheet on the screen edge takes `ring` +// alone. Every layer is a wash of `--cl-color-neutral` (black on light, white on dark), matching +// Figma, where shadows are #000 rather than the foreground. Branched per colour via `light-dark()` +// since a shadow's geometry cannot branch — see the note on `Dialog`'s popup for why // `@media (prefers-color-scheme)` is not the escape hatch. -const shadowDrop = `0 12px 12px -7px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 12%, transparent), transparent), - 0 24px 24px -10px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), transparent)`; -const shadowRing = `0 0 0 1px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`; - const shadowDefaults = { - '--cl-shadow-card': `${shadowDrop}, ${shadowRing}`, - '--cl-shadow-card-drop': shadowDrop, + '--cl-shadow-raised': `0 12px 12px -7px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 12%, transparent), transparent), + 0 24px 24px -10px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), transparent)`, + '--cl-shadow-ring': `0 0 0 1px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`, }; export const shadowVars = stylex.defineVars(shadowDefaults); From 81ea244cf406e2d6ee19d9f7072e11636d126c38 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 14:14:51 -0400 Subject: [PATCH 13/15] feat(ui): add the mosaic shadow scale from figma --- .../src/mosaic/components/card/card.styles.ts | 4 ++-- .../components/combobox/combobox.styles.ts | 2 +- .../mosaic/components/dialog/dialog.styles.ts | 4 ++-- .../mosaic/components/drawer/drawer.styles.ts | 2 +- .../src/mosaic/components/menu/menu.styles.ts | 2 +- .../phone-input/phone-input.styles.ts | 2 +- .../components/profile/profile.styles.ts | 2 +- .../mosaic/components/select/select.styles.ts | 2 +- packages/ui/src/mosaic/tokens.stylex.ts | 20 +++++++++++-------- 9 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/ui/src/mosaic/components/card/card.styles.ts b/packages/ui/src/mosaic/components/card/card.styles.ts index e491e2700db..11a696bd8c3 100644 --- a/packages/ui/src/mosaic/components/card/card.styles.ts +++ b/packages/ui/src/mosaic/components/card/card.styles.ts @@ -19,7 +19,7 @@ export const root = stylex.create({ borderRadius: radiusVars['--cl-radius-xl'], overflow: 'hidden', backgroundColor: colorVars['--cl-color-background'], - boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, + boxShadow: shadowVars['--cl-shadow-lg'], }, flush: { borderRadius: radiusVars['--cl-radius-xl'], @@ -31,7 +31,7 @@ export const root = stylex.create({ borderRadius: radiusVars['--cl-radius-xl'], overflow: 'hidden', backgroundColor: colorVars['--cl-color-background'], - boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, + boxShadow: shadowVars['--cl-shadow-lg'], }, }); diff --git a/packages/ui/src/mosaic/components/combobox/combobox.styles.ts b/packages/ui/src/mosaic/components/combobox/combobox.styles.ts index 6fb2fd2e786..64236e7bcbc 100644 --- a/packages/ui/src/mosaic/components/combobox/combobox.styles.ts +++ b/packages/ui/src/mosaic/components/combobox/combobox.styles.ts @@ -19,7 +19,7 @@ export const styles = stylex.create({ outline: 'none', overflow: 'hidden', backgroundColor: colorVars['--cl-color-background'], - boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, + boxShadow: shadowVars['--cl-shadow-md'], color: colorVars['--cl-color-foreground'], opacity: { default: 1, diff --git a/packages/ui/src/mosaic/components/dialog/dialog.styles.ts b/packages/ui/src/mosaic/components/dialog/dialog.styles.ts index 77182feedd4..10d9ab3d461 100644 --- a/packages/ui/src/mosaic/components/dialog/dialog.styles.ts +++ b/packages/ui/src/mosaic/components/dialog/dialog.styles.ts @@ -210,7 +210,7 @@ export const styles = stylex.create({ // tabbable content, which would otherwise draw a ring around the whole surface. outline: 'none', backgroundColor: colorVars['--cl-color-background'], - // The raised shadow and ring, shared with `Card` and `Menu`. The two schemes are different + // The `lg` shadow, shared with `Card`. The two schemes are different // treatments, not one at two strengths: light gets the two drop layers and a dark hairline, // dark drops them to `transparent` and separates with a light hairline instead — a shadow // reads as depth against a light page and as nothing against a dark one. @@ -221,7 +221,7 @@ export const styles = stylex.create({ // OS preference while `light-dark()` tracks the `color-scheme` in scope, so an app forcing a // scheme (swingset's own toggle does, via next-themes) would take its colours from one and its // geometry from the other. - boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, + boxShadow: shadowVars['--cl-shadow-lg'], color: colorVars['--cl-color-foreground'], display: 'flex', flexDirection: 'column', diff --git a/packages/ui/src/mosaic/components/drawer/drawer.styles.ts b/packages/ui/src/mosaic/components/drawer/drawer.styles.ts index 3a0e244fc4a..2e06abd4dc9 100644 --- a/packages/ui/src/mosaic/components/drawer/drawer.styles.ts +++ b/packages/ui/src/mosaic/components/drawer/drawer.styles.ts @@ -83,7 +83,7 @@ export const styles = stylex.create({ // No drop shadow — the sheet sits on the screen edge, so there is nothing for it to float over. // The hairline ring stays: a faint dark edge in light, and the light edge that separates a dark // sheet from a dark page. - boxShadow: shadowVars['--cl-shadow-ring'], + boxShadow: shadowVars['--cl-shadow-sm'], color: colorVars['--cl-color-foreground'], display: 'flex', flexDirection: 'column', diff --git a/packages/ui/src/mosaic/components/menu/menu.styles.ts b/packages/ui/src/mosaic/components/menu/menu.styles.ts index 3e2a6e31bbe..55a15e1ef15 100644 --- a/packages/ui/src/mosaic/components/menu/menu.styles.ts +++ b/packages/ui/src/mosaic/components/menu/menu.styles.ts @@ -24,7 +24,7 @@ export const popup = stylex.create({ borderRadius: radiusVars['--cl-radius-lg'], outline: 'none', backgroundColor: colorVars['--cl-color-background'], - boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, + boxShadow: shadowVars['--cl-shadow-md'], color: colorVars['--cl-color-foreground'], opacity: { default: 1, diff --git a/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts b/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts index e5784cad1b4..23a6f199762 100644 --- a/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts +++ b/packages/ui/src/mosaic/components/phone-input/phone-input.styles.ts @@ -42,7 +42,7 @@ export const styles = stylex.create({ borderRadius: radiusVars['--cl-radius-lg'], overflow: 'hidden', backgroundColor: colorVars['--cl-color-background'], - boxShadow: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, + boxShadow: shadowVars['--cl-shadow-md'], color: colorVars['--cl-color-foreground'], }, countrySearchContainer: { diff --git a/packages/ui/src/mosaic/components/profile/profile.styles.ts b/packages/ui/src/mosaic/components/profile/profile.styles.ts index 736bfdd9bb7..2b8f0e2a71c 100644 --- a/packages/ui/src/mosaic/components/profile/profile.styles.ts +++ b/packages/ui/src/mosaic/components/profile/profile.styles.ts @@ -159,7 +159,7 @@ export const styles = stylex.create({ // popup is the screen and there is nothing to lift off. boxShadow: { [compact]: 'none', - default: `${shadowVars['--cl-shadow-raised']}, ${shadowVars['--cl-shadow-ring']}`, + default: shadowVars['--cl-shadow-lg'], }, flexGrow: 1, minHeight: 0, diff --git a/packages/ui/src/mosaic/components/select/select.styles.ts b/packages/ui/src/mosaic/components/select/select.styles.ts index 7e813a2556b..6207989a7a4 100644 --- a/packages/ui/src/mosaic/components/select/select.styles.ts +++ b/packages/ui/src/mosaic/components/select/select.styles.ts @@ -41,7 +41,7 @@ export const popup = stylex.create({ borderWidth: '1px', outline: 'none', backgroundColor: colorVars['--cl-color-background'], - boxShadow: shadowVars['--cl-shadow-raised'], + boxShadow: shadowVars['--cl-shadow-md'], color: colorVars['--cl-color-foreground'], opacity: { default: 1, diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index ccefa49f64c..0003f61e72c 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -420,17 +420,21 @@ const focusDefaults = { export const focusVars = stylex.defineVars(focusDefaults); -// Elevation, in two composable pieces: `raised` is two drop layers that fall away in dark, and -// `ring` is a hairline that is dark on light and light on dark. A lifted surface takes both, a -// popup with a real border takes `raised` alone, and a sheet on the screen edge takes `ring` -// alone. Every layer is a wash of `--cl-color-neutral` (black on light, white on dark), matching -// Figma, where shadows are #000 rather than the foreground. Branched per colour via `light-dark()` +// Elevation, matching the Figma `shadow-sm|md|lg` effect styles: `md` and `lg` are two drop +// layers and a blurred hairline ring, `sm` is the ring alone. Every layer is a wash of +// `--cl-color-neutral` (black on light, white on dark). In dark the drop layers fall away and +// only the ring stays, since a shadow reads as depth against a light page and as nothing against +// a dark one. Branched per colour via `light-dark()` // since a shadow's geometry cannot branch — see the note on `Dialog`'s popup for why // `@media (prefers-color-scheme)` is not the escape hatch. +const drop = (wash: string) => `light-dark(${wash}, transparent)`; +const neutral4 = `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`; +const neutral8 = colorVars['--cl-color-neutral-alpha-200']; + const shadowDefaults = { - '--cl-shadow-raised': `0 12px 12px -7px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 12%, transparent), transparent), - 0 24px 24px -10px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), transparent)`, - '--cl-shadow-ring': `0 0 0 1px light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`, + '--cl-shadow-sm': `0 1px 2px 0 ${neutral8}`, + '--cl-shadow-md': `0 2px 1px -1px ${drop(neutral4)}, 0 4px 8px 0 ${drop(neutral8)}, 0 0 3px 0 ${neutral8}`, + '--cl-shadow-lg': `0 2px 2px -1px ${drop(neutral4)}, 0 8px 16px 0 ${drop(neutral8)}, 0 0 1px 0 ${neutral8}`, }; export const shadowVars = stylex.defineVars(shadowDefaults); From 4f4691cc09fa2c429033a5f7c41385f662d4bef3 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 14:18:54 -0400 Subject: [PATCH 14/15] fix(ui): draw only the highlight ring for mosaic shadows in dark mode --- packages/ui/src/mosaic/tokens.stylex.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 0003f61e72c..6d377b97fb3 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -420,13 +420,10 @@ const focusDefaults = { export const focusVars = stylex.defineVars(focusDefaults); -// Elevation, matching the Figma `shadow-sm|md|lg` effect styles: `md` and `lg` are two drop -// layers and a blurred hairline ring, `sm` is the ring alone. Every layer is a wash of -// `--cl-color-neutral` (black on light, white on dark). In dark the drop layers fall away and -// only the ring stays, since a shadow reads as depth against a light page and as nothing against -// a dark one. Branched per colour via `light-dark()` -// since a shadow's geometry cannot branch — see the note on `Dialog`'s popup for why -// `@media (prefers-color-scheme)` is not the escape hatch. +// ============================================================================= +// Shadow Tokens +// ============================================================================= + const drop = (wash: string) => `light-dark(${wash}, transparent)`; const neutral4 = `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`; const neutral8 = colorVars['--cl-color-neutral-alpha-200']; From 4e7316147b91eee8f39bf84644756877f2588f3a Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 14:19:48 -0400 Subject: [PATCH 15/15] fix(ui): draw only the highlight ring for mosaic shadows in dark mode --- packages/ui/src/mosaic/tokens.stylex.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/mosaic/tokens.stylex.ts b/packages/ui/src/mosaic/tokens.stylex.ts index 6d377b97fb3..9c7cf3bfe40 100644 --- a/packages/ui/src/mosaic/tokens.stylex.ts +++ b/packages/ui/src/mosaic/tokens.stylex.ts @@ -424,14 +424,15 @@ export const focusVars = stylex.defineVars(focusDefaults); // Shadow Tokens // ============================================================================= -const drop = (wash: string) => `light-dark(${wash}, transparent)`; +const light = (wash: string) => `light-dark(${wash}, transparent)`; const neutral4 = `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`; const neutral8 = colorVars['--cl-color-neutral-alpha-200']; +const darkRing = `0 0 0 1px light-dark(transparent, color-mix(in oklab, ${colorVars['--cl-color-neutral']} 10%, transparent))`; const shadowDefaults = { - '--cl-shadow-sm': `0 1px 2px 0 ${neutral8}`, - '--cl-shadow-md': `0 2px 1px -1px ${drop(neutral4)}, 0 4px 8px 0 ${drop(neutral8)}, 0 0 3px 0 ${neutral8}`, - '--cl-shadow-lg': `0 2px 2px -1px ${drop(neutral4)}, 0 8px 16px 0 ${drop(neutral8)}, 0 0 1px 0 ${neutral8}`, + '--cl-shadow-sm': `0 1px 2px 0 ${light(neutral8)}, ${darkRing}`, + '--cl-shadow-md': `0 2px 1px -1px ${light(neutral4)}, 0 4px 8px 0 ${light(neutral8)}, 0 0 3px 0 ${light(neutral8)}, ${darkRing}`, + '--cl-shadow-lg': `0 2px 2px -1px ${light(neutral4)}, 0 8px 16px 0 ${light(neutral8)}, 0 0 1px 0 ${light(neutral8)}, ${darkRing}`, }; export const shadowVars = stylex.defineVars(shadowDefaults);