From d6591e90fb9eb3b6c10b7628db564cd314dc0426 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 9 Sep 2026 16:23:46 -0400 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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.