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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-gray-scale.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
30 changes: 17 additions & 13 deletions .claude/skills/mosaic/references/stylex.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,17 @@ 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);
```

- **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."
Expand All @@ -87,22 +90,23 @@ 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
`--cl-color-ring` and nothing else — a `--cl-focus-outline-color` alias beside it
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
Expand All @@ -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.

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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).
Expand All @@ -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
Expand Down Expand Up @@ -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'],
},
},
```
Expand Down
2 changes: 1 addition & 1 deletion packages/swingset/src/stories/banner.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
4 changes: 2 additions & 2 deletions packages/swingset/src/stories/icon-frame.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ export function CustomSurface() {
return (
<IconFrame
style={{
backgroundColor: colorVars['--cl-color-primary'],
color: colorVars['--cl-color-primary-foreground'],
backgroundColor: colorVars['--cl-color-brand'],
color: colorVars['--cl-color-brand-foreground'],
}}
>
<Icon
Expand Down
2 changes: 1 addition & 1 deletion packages/swingset/src/stories/item.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ The root reflects its state as `data-*` attributes on `.cl-item`, so consumers c
```css
/* Re-theme interactive rows */
.cl-item[data-interactive] {
background-color: var(--cl-color-card);
background-color: var(--cl-color-background);
}

/* Widen the media column on compact rows */
Expand Down
6 changes: 3 additions & 3 deletions packages/swingset/src/stories/scroll-area.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,13 @@ can drive itself from them.
}
.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)));
}
.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)));
}
Expand All @@ -169,7 +169,7 @@ The vars are registered as `<number>`, 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.

Expand Down
6 changes: 3 additions & 3 deletions packages/swingset/src/stories/scroll-area.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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)));
}
Expand Down
8 changes: 4 additions & 4 deletions packages/ui/src/mosaic/components/avatar/avatar.styles.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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%',
Expand Down Expand Up @@ -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)`,
Expand Down
32 changes: 19 additions & 13 deletions packages/ui/src/mosaic/components/badge/badge.styles.ts
Original file line number Diff line number Diff line change
@@ -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'],
Expand All @@ -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'],
},
});
30 changes: 16 additions & 14 deletions packages/ui/src/mosaic/components/banner/banner.styles.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -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'] },
});
Loading
Loading