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..024c541acdd 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); ``` @@ -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." @@ -87,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 @@ -95,14 +98,15 @@ 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-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 @@ -128,7 +132,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. @@ -157,7 +161,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({ @@ -232,7 +236,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). @@ -255,7 +259,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 @@ -312,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/swingset/src/stories/banner.mdx b/packages/swingset/src/stories/banner.mdx index cbccb761c8c..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` | -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-foreground` over whatever it sits on, and `--cl-color-border` draws its hairline. --- 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/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..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,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: neutralAlpha['8'], + 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 6e2580d412d..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 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-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'], @@ -29,23 +35,23 @@ 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'], + backgroundColor: neutralAlpha['6'], + color: colorVars['--cl-color-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..9785fa69b10 100644 --- a/packages/ui/src/mosaic/components/banner/banner.styles.ts +++ b/packages/ui/src/mosaic/components/banner/banner.styles.ts @@ -1,12 +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-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)`; +import { + colorVars, + fontFamilyVars, + fontWeightVars, + neutralAlpha, + radiusVars, + space, + typeScaleVars, +} from '../../tokens.stylex'; export const styles = stylex.create({ root: { @@ -43,23 +45,23 @@ export const styles = stylex.create({ export const rootColors = stylex.create({ neutral: { borderColor: colorVars['--cl-color-border'], - backgroundColor: neutralFill, - color: colorVars['--cl-color-neutral-foreground'], + backgroundColor: neutralAlpha['4'], + color: colorVars['--cl-color-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'], }, }); 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 f096e1e2e57..c1962178145 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,28 +13,15 @@ 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-neutral`: 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 -// 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. -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 +32,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 iconFadedNegative = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-faded']} 50%)`; -const iconFadedOnPrimary = `color-mix(in oklab, ${colorVars['--cl-color-primary-foreground']}, ${colorVars['--cl-color-primary']} 40%)`; +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-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 +171,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,27 +186,27 @@ 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: { - 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-neutral-foreground'], + color: colorVars['--cl-color-foreground'], }, 'filled-negative': { '--_cl-icon-color': { @@ -247,44 +235,44 @@ 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'], 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-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'], 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-neutral-foreground'], + color: colorVars['--cl-color-foreground'], }, 'outline-negative': { '--_cl-icon-color': { @@ -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'], @@ -311,42 +299,42 @@ 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: { 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-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: { 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-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. @@ -361,11 +349,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'], @@ -382,11 +370,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 +387,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 f81e53e5b67..23cb90a3585 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: `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%))`, @@ -32,7 +32,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: `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%))`, @@ -56,14 +56,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', @@ -111,7 +111,7 @@ export const branding = stylex.create({ textAlign: 'center', }, text: { - 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/dialog/dialog.styles.ts b/packages/ui/src/mosaic/components/dialog/dialog.styles.ts index 0d20e81e9f0..40de6430e0e 100644 --- a/packages/ui/src/mosaic/components/dialog/dialog.styles.ts +++ b/packages/ui/src/mosaic/components/dialog/dialog.styles.ts @@ -208,7 +208,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 @@ -223,7 +223,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 @@ -237,7 +237,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 5f8575ef7ac..b861ae91d97 100644 --- a/packages/ui/src/mosaic/components/dialog/dialog.test.tsx +++ b/packages/ui/src/mosaic/components/dialog/dialog.test.tsx @@ -517,7 +517,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/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 9efd3a52470..e02e72a3080 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: { @@ -9,11 +17,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 +39,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"])': neutralAlpha['8'], '@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"])': neutralAlpha['6'], }, }, 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 +118,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 +129,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'], @@ -158,7 +166,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/components/menu/menu.styles.ts b/packages/ui/src/mosaic/components/menu/menu.styles.ts index a81e051855d..cfaf8b7b3c1 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 @@ -15,11 +23,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 +81,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])': neutralAlpha['6'], '@media (hover: hover)': { - ':hover': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`, + ':hover': neutralAlpha['6'], }, }, 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/profile-page.styles.ts b/packages/ui/src/mosaic/profile-page.styles.ts index 5b052541d60..0daf9a280b7 100644 --- a/packages/ui/src/mosaic/profile-page.styles.ts +++ b/packages/ui/src/mosaic/profile-page.styles.ts @@ -30,8 +30,8 @@ export const styles = stylex.create({ // `clip` rather than `hidden`: the page must never become a scroll container itself, or // focusing something in the content column would scroll the whole page instead of the column. overflow: 'clip', - backgroundColor: colorVars['--cl-color-card'], - color: colorVars['--cl-color-card-foreground'], + backgroundColor: colorVars['--cl-color-background'], + color: colorVars['--cl-color-foreground'], containerName: 'cl-profile-page', containerType: 'inline-size', display: 'flex', @@ -127,16 +127,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', @@ -158,7 +158,7 @@ export const styles = stylex.create({ branding: { gap: space['1'], alignItems: 'center', - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], display: { default: 'flex', [profilePageCompact]: 'none', 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 9085dbd66ad..f5a2fab6967 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 @@ -19,52 +19,84 @@ import * as stylex from '@stylexjs/stylex'; // `@media (prefers-color-scheme)` duplication. The `--cl-` prefix namespaces // the vars so they never collide with a host app's own custom properties. +// ============================================================================= +// Gray Scale +// ============================================================================= + +export const gray = stylex.defineVars({ + '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 // ============================================================================= 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-neutral': 'light-dark(oklch(0 0 0), oklch(1 0 0))', - '--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-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-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-background': `light-dark(oklch(1 0 0), ${gray['900']})`, + '--cl-color-background-subtle': `light-dark(${gray['100']}, ${gray['850']})`, - '--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-border': `light-dark(${gray['200']}, ${gray['800']})`, + '--cl-color-border-subtle': `light-dark(${gray['100']}, ${gray['850']})`, - '--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-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-background': 'light-dark(oklch(0.985 0 0), oklch(0.2686 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-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-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-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-input': 'light-dark(oklch(1 0 0), oklch(0.2046 0 0))', - '--cl-color-input-placeholder': '#a3a3a3', + '--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-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-input': `light-dark(oklch(1 0 0), ${gray['850']})`, + '--cl-color-input-placeholder': gray['400'], '--cl-color-ring': 'light-dark(oklch(0.205 0 0), oklch(0.922 0 0))', } as const; 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', @@ -122,9 +154,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; @@ -145,10 +177,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); @@ -178,8 +210,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', @@ -187,12 +217,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({ @@ -282,8 +306,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', @@ -315,7 +337,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', @@ -397,13 +418,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/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 1aa2b4aebd3..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,25 +29,25 @@ 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', }, header: { - backgroundColor: colorVars['--cl-color-border-faded'], + backgroundColor: colorVars['--cl-color-border-subtle'], }, 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 ed30ce9b878..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,18 +18,18 @@ 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', }, header: { - backgroundColor: colorVars['--cl-color-border-faded'], + backgroundColor: colorVars['--cl-color-border-subtle'], }, 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 6d4ba165d7d..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,17 +12,17 @@ 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'], }, media: { - borderColor: colorVars['--cl-color-border-faded'], + borderColor: colorVars['--cl-color-border-subtle'], 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 f87e0b3c8a2..b948f4441ab 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 d00d6427612..bd90380c2c2 100644 --- a/references/mosaic-architecture.md +++ b/references/mosaic-architecture.md @@ -12,19 +12,21 @@ 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))', // … }); ``` 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. + 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`. @@ -50,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); } ``` @@ -90,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'] }, }); @@ -435,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`.