diff --git a/.changeset/mosaic-dark-surfaces.md b/.changeset/mosaic-dark-surfaces.md new file mode 100644 index 00000000000..527ee9e69e9 --- /dev/null +++ b/.changeset/mosaic-dark-surfaces.md @@ -0,0 +1,5 @@ +--- +'@clerk/mosaic': minor +--- + +Dark mode gives the profile its own shell color. `--cl-color-background` is now the base layer, near-black in dark mode, and backs the profile shell. A new `--cl-color-surface` backs everything raised on it, such as cards, sections, menus, popovers and controls, and the gray ramp mixes into it. It is derived from `--cl-color-background` (a 6.5 percent mix of `--cl-color-neutral` in dark mode, equal to it in light mode), so retinting `--cl-color-background` retints the shell, the surface and the ramp together, and light mode is unchanged. A profile opened in a dialog now draws a border. Destructive buttons and menu items tint red on hover. diff --git a/packages/mosaic/ARCHITECTURE.md b/packages/mosaic/ARCHITECTURE.md index c070b80702b..4faed268681 100644 --- a/packages/mosaic/ARCHITECTURE.md +++ b/packages/mosaic/ARCHITECTURE.md @@ -18,14 +18,16 @@ Tokens are CSS custom properties declared through `stylex.defineVars` in `tokens // packages/mosaic/src/tokens.stylex.ts export const colorVars = stylex.defineVars({ '--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))', + '--cl-color-brand-foreground': 'light-dark(oklch(1 0 0), var(--cl-color-surface))', // … }); ``` Each exported `*Vars` const in that file is one group (`colorVars`, `radiusVars`, …); the file is the list. `space` is also declared with `defineVars`, but its keys are not `--cl-*` names, so its steps compile to hashed custom properties that are internal, not part of the contract. -There is no gray scale. Every achromatic token (`--cl-color-foreground`, `-border`, `-background-subtle`, …) is `--cl-color-neutral` (black on light, white on dark) mixed into `--cl-color-background` at a fixed percentage per mode, so overriding neutral to a warm gray retints the whole ramp; in dark mode override `--cl-color-background` too, since the ramp mixes toward it. The `--cl--alpha-100|200|300` tokens in `colorVars` are washes (6, 8, and 12 percent of `--cl-color-neutral`, `-negative`, `-positive`, and `-warning` over `transparent`); overriding the solid color retints its washes. +There is no gray scale. Every achromatic token (`--cl-color-foreground`, `-border`, `-background-subtle`, …) is `--cl-color-neutral` (black on light, white on dark) mixed into `--cl-color-surface` at a fixed percentage per mode, so overriding neutral to a warm gray retints the whole ramp. 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. + +Two tiers carry the dark look. `--cl-color-background` is the base: the shell of a settings surface such as the profile, and the darkest layer (`oklch(0.133 0 0)` in dark mode, white in light mode). `--cl-color-surface` is everything raised on it (cards, sections, menus, popovers, dialogs, controls) and the base the gray ramp mixes into. It is derived, not set: in dark mode it is `--cl-color-neutral` mixed 6.5 percent into `--cl-color-background`, and in light mode it equals `--cl-color-background`. So retinting `--cl-color-background` retints the shell, the surface and the whole ramp together; override `--cl-color-surface` directly only to change how far cards sit above the shell. Both are solid, never alpha, so they look the same over any backdrop. 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. @@ -51,7 +53,7 @@ A theme styles a part from CSS, against that class and its attributes: border-radius: 4px; } .cl-item[data-interactive] { - background-color: var(--cl-color-background); + background-color: var(--cl-color-surface); } ``` diff --git a/packages/mosaic/src/components/action-bar/action-bar.styles.ts b/packages/mosaic/src/components/action-bar/action-bar.styles.ts index 238cf4e7b1c..427a7f57507 100644 --- a/packages/mosaic/src/components/action-bar/action-bar.styles.ts +++ b/packages/mosaic/src/components/action-bar/action-bar.styles.ts @@ -30,7 +30,7 @@ export const styles = stylex.create({ paddingBlock: space['1'], paddingInline: space['1'], alignItems: 'center', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], blockSize: space['10'], boxShadow: shadowVars['--cl-shadow-lg'], display: 'flex', diff --git a/packages/mosaic/src/components/avatar/avatar.styles.ts b/packages/mosaic/src/components/avatar/avatar.styles.ts index f6090d3bc97..2da226047e0 100644 --- a/packages/mosaic/src/components/avatar/avatar.styles.ts +++ b/packages/mosaic/src/components/avatar/avatar.styles.ts @@ -73,7 +73,7 @@ export const styles = stylex.create({ borderWidth: '1px', overflow: 'hidden', alignItems: 'center', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], boxSizing: 'border-box', display: 'flex', insetBlockEnd: `calc(${space['2']} * -1)`, diff --git a/packages/mosaic/src/components/button/button.styles.ts b/packages/mosaic/src/components/button/button.styles.ts index 33d2bbd03db..e57e179bcb8 100644 --- a/packages/mosaic/src/components/button/button.styles.ts +++ b/packages/mosaic/src/components/button/button.styles.ts @@ -22,6 +22,7 @@ import { iconScope } from '../icon/icon.markers.stylex'; 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 negativePressed = `color-mix(in oklab, ${colorVars['--cl-color-negative-subtle']}, ${colorVars['--cl-color-negative']} 8%)`; const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`; // Interactive states are gated on `:not([data-disabled])`: the button stays hit-testable while @@ -209,11 +210,11 @@ export const variants = stylex.create({ borderColor: colorVars['--cl-color-border'], backgroundColor: { default: 'transparent', - ':not([data-disabled]):active': colorVars['--cl-color-neutral-alpha-200'], - ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-alpha-200'], + ':not([data-disabled]):active': negativePressed, + ':not([data-disabled])[data-open]': negativePressed, '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-neutral-alpha-100'], + ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-negative-subtle'], }, }, color: colorVars['--cl-color-negative'], @@ -248,8 +249,8 @@ export const variants = stylex.create({ 'ghost-negative': { backgroundColor: { default: 'transparent', - ':not([data-disabled]):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%)`, + ':not([data-disabled]):active': negativePressed, + ':not([data-disabled])[data-open]': negativePressed, '@media (hover: hover)': { default: null, ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-negative-subtle'], diff --git a/packages/mosaic/src/components/card/card.styles.ts b/packages/mosaic/src/components/card/card.styles.ts index 1a23a333151..6fb69152f01 100644 --- a/packages/mosaic/src/components/card/card.styles.ts +++ b/packages/mosaic/src/components/card/card.styles.ts @@ -26,7 +26,7 @@ export const root = stylex.create({ card: { borderRadius: radiusVars['--cl-radius-xl'], overflow: 'hidden', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], boxShadow: shadowVars['--cl-shadow-lg'], }, flush: { @@ -38,7 +38,7 @@ export const root = stylex.create({ overlay: { borderRadius: radiusVars['--cl-radius-xl'], overflow: 'hidden', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], boxShadow: shadowVars['--cl-shadow-lg'], }, }); diff --git a/packages/mosaic/src/components/checkbox/checkbox.styles.ts b/packages/mosaic/src/components/checkbox/checkbox.styles.ts index 38c9a7babc2..6cff123de9c 100644 --- a/packages/mosaic/src/components/checkbox/checkbox.styles.ts +++ b/packages/mosaic/src/components/checkbox/checkbox.styles.ts @@ -26,7 +26,7 @@ export const styles = stylex.create({ borderWidth: '1px', appearance: 'none', backgroundColor: { - default: colorVars['--cl-color-background'], + default: colorVars['--cl-color-surface'], ':checked': colorVars['--cl-color-brand'], ':indeterminate': colorVars['--cl-color-brand'], }, diff --git a/packages/mosaic/src/components/combobox/combobox.styles.ts b/packages/mosaic/src/components/combobox/combobox.styles.ts index c60fa5b9d1c..a8bb8e2cab0 100644 --- a/packages/mosaic/src/components/combobox/combobox.styles.ts +++ b/packages/mosaic/src/components/combobox/combobox.styles.ts @@ -20,7 +20,7 @@ export const styles = stylex.create({ borderRadius: radiusVars['--cl-radius-lg'], outline: 'none', overflow: 'hidden', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], boxShadow: shadowVars['--cl-shadow-md'], color: colorVars['--cl-color-foreground'], opacity: { diff --git a/packages/mosaic/src/components/copy-button/copy-button.styles.ts b/packages/mosaic/src/components/copy-button/copy-button.styles.ts index 18c8307d6fa..f84d34f5ea8 100644 --- a/packages/mosaic/src/components/copy-button/copy-button.styles.ts +++ b/packages/mosaic/src/components/copy-button/copy-button.styles.ts @@ -10,7 +10,7 @@ export const styles = stylex.create({ paddingInline: space['1.5'], alignItems: 'center', backgroundColor: colorVars['--cl-color-foreground'], - color: colorVars['--cl-color-background'], + color: colorVars['--cl-color-surface'], // A copy while one is still showing queues behind the `limit`; only the newest is wanted. display: { default: 'flex', ':where([data-limited])': 'none' }, fontSize: typeScaleVars['--cl-text-sm-size'], diff --git a/packages/mosaic/src/components/dialog/dialog.styles.ts b/packages/mosaic/src/components/dialog/dialog.styles.ts index 835010e2796..4f3ee4c1179 100644 --- a/packages/mosaic/src/components/dialog/dialog.styles.ts +++ b/packages/mosaic/src/components/dialog/dialog.styles.ts @@ -218,7 +218,7 @@ export const styles = stylex.create({ inset: 0, // Follows the popup's own radius. borderRadius: 'inherit', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], content: '""', opacity: 'var(--_cl-stack-veil, 0)', pointerEvents: 'none', @@ -465,7 +465,7 @@ export const compactPlacements = stylex.create({ // shared: bottom-aligning there would drag a centered dialog down with it. alignSelf: { [SHEET]: 'end', default: null }, // The sheet's surface, so it runs on through the keyboard padding below; the card only fills it. - backgroundColor: { [SHEET]: colorVars['--cl-color-background'], default: null }, + backgroundColor: { [SHEET]: colorVars['--cl-color-surface'], default: null }, // No drop shadow on the screen edge, only the hairline ring, as on `Drawer`. The card inside // reads the two variables and drops its own shadow and fixed width. boxShadow: { [SHEET]: shadowVars['--cl-shadow-sm'], default: null }, diff --git a/packages/mosaic/src/components/drawer/drawer.styles.ts b/packages/mosaic/src/components/drawer/drawer.styles.ts index cd52390f708..f6f40cf5a8f 100644 --- a/packages/mosaic/src/components/drawer/drawer.styles.ts +++ b/packages/mosaic/src/components/drawer/drawer.styles.ts @@ -82,7 +82,7 @@ export const styles = stylex.create({ borderWidth: { default: null, '@media (forced-colors: active)': '1px' }, outline: 'none', overscrollBehavior: 'contain', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], 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. diff --git a/packages/mosaic/src/components/menu/menu.styles.ts b/packages/mosaic/src/components/menu/menu.styles.ts index 11ec39b5c9a..ee80db82abe 100644 --- a/packages/mosaic/src/components/menu/menu.styles.ts +++ b/packages/mosaic/src/components/menu/menu.styles.ts @@ -25,7 +25,7 @@ export const popup = stylex.create({ base: { borderRadius: radiusVars['--cl-radius-lg'], outline: 'none', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], boxShadow: shadowVars['--cl-shadow-md'], color: colorVars['--cl-color-foreground'], opacity: { @@ -80,6 +80,8 @@ export const viewport = stylex.create({ }, }); +const negativeActive = `light-dark(${colorVars['--cl-color-negative-alpha-200']}, ${colorVars['--cl-color-negative-subtle']})`; + export const item = stylex.create({ base: { borderRadius: '0.375rem', @@ -128,9 +130,9 @@ export const item = stylex.create({ negative: { backgroundColor: { default: 'transparent', - ':is([data-active])': colorVars['--cl-color-negative-alpha-200'], + ':is([data-active])': negativeActive, '@media (hover: hover)': { - ':hover': colorVars['--cl-color-negative-alpha-200'], + ':hover': negativeActive, }, }, color: colorVars['--cl-color-negative'], diff --git a/packages/mosaic/src/components/phone-input/phone-input.styles.ts b/packages/mosaic/src/components/phone-input/phone-input.styles.ts index 23a6f199762..faf0ed7fb91 100644 --- a/packages/mosaic/src/components/phone-input/phone-input.styles.ts +++ b/packages/mosaic/src/components/phone-input/phone-input.styles.ts @@ -41,7 +41,7 @@ export const styles = stylex.create({ popup: { borderRadius: radiusVars['--cl-radius-lg'], overflow: 'hidden', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], boxShadow: shadowVars['--cl-shadow-md'], color: colorVars['--cl-color-foreground'], }, diff --git a/packages/mosaic/src/components/profile/profile.styles.ts b/packages/mosaic/src/components/profile/profile.styles.ts index a7c1219e9cc..c6ccf226a3b 100644 --- a/packages/mosaic/src/components/profile/profile.styles.ts +++ b/packages/mosaic/src/components/profile/profile.styles.ts @@ -104,7 +104,10 @@ export const styles = stylex.create({ }, layoutInDialog: { - borderWidth: '0px', + borderWidth: { + [compact]: '0px', + default: '1px', + }, blockSize: 'auto', boxShadow: { [compact]: 'none', @@ -120,6 +123,7 @@ export const styles = stylex.create({ layoutFullscreen: { borderRadius: 0, + borderWidth: '0px', boxShadow: 'none', }, diff --git a/packages/mosaic/src/components/radio/radio.styles.ts b/packages/mosaic/src/components/radio/radio.styles.ts index 5d04a7ea2ce..83017f3f927 100644 --- a/packages/mosaic/src/components/radio/radio.styles.ts +++ b/packages/mosaic/src/components/radio/radio.styles.ts @@ -30,7 +30,7 @@ export const styles = stylex.create({ borderWidth: '1px', appearance: 'none', backgroundColor: { - default: colorVars['--cl-color-background'], + default: colorVars['--cl-color-surface'], ':checked': colorVars['--cl-color-brand'], }, cursor: { default: 'pointer', ':disabled': 'not-allowed' }, diff --git a/packages/mosaic/src/components/section/section.styles.ts b/packages/mosaic/src/components/section/section.styles.ts index 3e36f28471f..0904539df9f 100644 --- a/packages/mosaic/src/components/section/section.styles.ts +++ b/packages/mosaic/src/components/section/section.styles.ts @@ -49,7 +49,7 @@ export const styles = stylex.create({ borderStyle: 'solid', borderWidth: '1px', paddingInline: space['4'], - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], display: 'flex', flexDirection: 'column', width: 'auto', diff --git a/packages/mosaic/src/components/segmented-control/segmented-control.styles.ts b/packages/mosaic/src/components/segmented-control/segmented-control.styles.ts index 14657cde42b..36a3c5b213b 100644 --- a/packages/mosaic/src/components/segmented-control/segmented-control.styles.ts +++ b/packages/mosaic/src/components/segmented-control/segmented-control.styles.ts @@ -32,7 +32,7 @@ const scaleIn = stylex.keyframes({ const surface = { borderRadius: radiusVars['--cl-radius-md'], - backgroundColor: { default: colorVars['--cl-color-background'], [forcedColors]: 'Highlight' }, + backgroundColor: { default: colorVars['--cl-color-surface'], [forcedColors]: 'Highlight' }, boxShadow: shadowVars['--cl-shadow-sm'], forcedColorAdjust: 'none', } as const; diff --git a/packages/mosaic/src/components/select/select.styles.ts b/packages/mosaic/src/components/select/select.styles.ts index 2690ccfae9d..f92f6f30a69 100644 --- a/packages/mosaic/src/components/select/select.styles.ts +++ b/packages/mosaic/src/components/select/select.styles.ts @@ -40,7 +40,7 @@ export const popup = stylex.create({ borderStyle: 'solid', borderWidth: '1px', outline: 'none', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], boxShadow: shadowVars['--cl-shadow-md'], color: colorVars['--cl-color-foreground'], opacity: { diff --git a/packages/mosaic/src/components/switch/switch.styles.ts b/packages/mosaic/src/components/switch/switch.styles.ts index ae19cb2bafe..d0ebb94b61a 100644 --- a/packages/mosaic/src/components/switch/switch.styles.ts +++ b/packages/mosaic/src/components/switch/switch.styles.ts @@ -6,7 +6,7 @@ import { switchInputMarker } from './switch.markers.stylex'; const thumbInset = '2px'; const trackOffHover = `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 24%, transparent)`; const trackOnHover = `color-mix(in oklab, ${colorVars['--cl-color-brand']}, ${colorVars['--cl-color-brand-foreground']} 12%)`; -const thumbOff = `light-dark(${colorVars['--cl-color-background']}, ${colorVars['--cl-color-foreground']})`; +const thumbOff = `light-dark(${colorVars['--cl-color-surface']}, ${colorVars['--cl-color-foreground']})`; export const styles = stylex.create({ root: { diff --git a/packages/mosaic/src/components/table/table.styles.ts b/packages/mosaic/src/components/table/table.styles.ts index 2aa2c763c54..2696c57ea7d 100644 --- a/packages/mosaic/src/components/table/table.styles.ts +++ b/packages/mosaic/src/components/table/table.styles.ts @@ -24,7 +24,7 @@ export const styles = stylex.create({ borderStyle: 'solid', borderWidth: '1px', overflow: 'hidden', - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], width: '100%', }, viewport: { @@ -34,7 +34,7 @@ export const styles = stylex.create({ }, table: { '--_cl-table-row-tint': 'transparent', - '--_cl-table-surface': colorVars['--cl-color-background'], + '--_cl-table-surface': colorVars['--cl-color-surface'], borderCollapse: 'collapse', borderSpacing: 0, isolation: 'isolate', diff --git a/packages/mosaic/src/components/toast/toast.styles.ts b/packages/mosaic/src/components/toast/toast.styles.ts index 3017072d8f6..2bf2149f655 100644 --- a/packages/mosaic/src/components/toast/toast.styles.ts +++ b/packages/mosaic/src/components/toast/toast.styles.ts @@ -43,7 +43,7 @@ export const styles = stylex.create({ '@media (prefers-reduced-motion: reduce)': 'none', }, animationTimingFunction: easingVars['--cl-ease-default'], - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], boxShadow: shadowVars['--cl-shadow-lg'], color: colorVars['--cl-color-foreground'], display: { diff --git a/packages/mosaic/src/features/user-profile/user-profile-billing-history-section/user-profile-billing-history-section.styles.ts b/packages/mosaic/src/features/user-profile/user-profile-billing-history-section/user-profile-billing-history-section.styles.ts index 378fb3fb6bf..39a2306dbc3 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-billing-history-section/user-profile-billing-history-section.styles.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-billing-history-section/user-profile-billing-history-section.styles.ts @@ -72,7 +72,7 @@ export const styles = stylex.create({ borderWidth: '1px', paddingBlock: space['1'], paddingInline: space['2'], - backgroundColor: colorVars['--cl-color-background'], + backgroundColor: colorVars['--cl-color-surface'], color: colorVars['--cl-color-foreground'], fontSize: typeScaleVars['--cl-text-xs-size'], lineHeight: typeScaleVars['--cl-text-xs-leading'], diff --git a/packages/mosaic/src/styles/skeleton.styles.ts b/packages/mosaic/src/styles/skeleton.styles.ts index c3c9e64b1ac..617f207393f 100644 --- a/packages/mosaic/src/styles/skeleton.styles.ts +++ b/packages/mosaic/src/styles/skeleton.styles.ts @@ -3,7 +3,7 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, radiusVars } from '../tokens.stylex'; const highlight = - 'light-dark(color-mix(in oklab, var(--cl-color-background) 62%, transparent), color-mix(in oklab, var(--cl-color-neutral) 7%, transparent))'; + 'light-dark(color-mix(in oklab, var(--cl-color-surface) 62%, transparent), color-mix(in oklab, var(--cl-color-neutral) 7%, transparent))'; const translate = stylex.keyframes({ from: { transform: 'translateX(-100%)' }, diff --git a/packages/mosaic/src/styles/tooltip-surface.styles.ts b/packages/mosaic/src/styles/tooltip-surface.styles.ts index 597e37fc867..b0694471cdc 100644 --- a/packages/mosaic/src/styles/tooltip-surface.styles.ts +++ b/packages/mosaic/src/styles/tooltip-surface.styles.ts @@ -10,7 +10,7 @@ export const tooltipSurface = stylex.create({ paddingInline: space['1.5'], alignItems: 'center', backgroundColor: colorVars['--cl-color-foreground'], - color: colorVars['--cl-color-background'], + color: colorVars['--cl-color-surface'], display: 'flex', fontSize: typeScaleVars['--cl-text-sm-size'], lineHeight: typeScaleVars['--cl-text-sm-leading'], diff --git a/packages/mosaic/src/tokens.stylex.ts b/packages/mosaic/src/tokens.stylex.ts index f8f9229d79c..4b55c74860e 100644 --- a/packages/mosaic/src/tokens.stylex.ts +++ b/packages/mosaic/src/tokens.stylex.ts @@ -23,12 +23,12 @@ import * as stylex from '@stylexjs/stylex'; // Color Tokens // ============================================================================= -// Every achromatic token is `--cl-color-neutral` mixed into `--cl-color-background`, so +// Every achromatic token is `--cl-color-neutral` mixed into `--cl-color-surface`, so // retinting neutral (a warm gray, say) retints the whole ramp. The percentages reproduce // the Figma gray scale: grays have zero chroma, so an oklab mix is linear in lightness. -// Dark mode needs its own percentage because its background is a 900, not black. +// Dark mode needs its own percentage because its surface is a 900, not black. const neutralMix = (light: number, dark: number) => - `light-dark(color-mix(in oklab, var(--cl-color-neutral) ${light}%, var(--cl-color-background)), color-mix(in oklab, var(--cl-color-neutral) ${dark}%, var(--cl-color-background)))`; + `light-dark(color-mix(in oklab, var(--cl-color-neutral) ${light}%, var(--cl-color-surface)), color-mix(in oklab, var(--cl-color-neutral) ${dark}%, var(--cl-color-surface)))`; const colorDefaults = { '--cl-color-neutral': 'light-dark(oklch(0 0 0), oklch(1 0 0))', @@ -36,8 +36,10 @@ const colorDefaults = { '--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-background': 'light-dark(oklch(1 0 0), oklch(0.2046 0 0))', + '--cl-color-background': 'light-dark(oklch(1 0 0), oklch(0.133 0 0))', '--cl-color-background-subtle': neutralMix(3, 4.25), + '--cl-color-surface': + 'light-dark(var(--cl-color-background), color-mix(in oklab, var(--cl-color-neutral) 6.5%, var(--cl-color-background)))', '--cl-color-foreground': neutralMix(79.5, 98), '--cl-color-foreground-secondary': neutralMix(44.45, 64.25), @@ -49,7 +51,7 @@ const colorDefaults = { '--cl-color-brand': 'light-dark(oklch(0.2046 0 0), oklch(0.9851 0 0))', '--cl-color-brand-secondary': 'light-dark(oklch(0.4017 0 0), oklch(0.7025 0 0))', '--cl-color-brand-border': 'light-dark(oklch(0.8078 0 0), oklch(0.3904 0 0))', - '--cl-color-brand-foreground': 'light-dark(oklch(1 0 0), oklch(0.2046 0 0))', + '--cl-color-brand-foreground': 'light-dark(oklch(1 0 0), var(--cl-color-surface))', '--cl-color-negative': 'oklch(0.6252 0.2161 26.11)', '--cl-color-negative-foreground': 'oklch(1 0 0)', @@ -75,7 +77,7 @@ const colorDefaults = { '--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': 'var(--cl-color-background)', + '--cl-color-input': 'var(--cl-color-surface)', '--cl-color-input-placeholder': neutralMix(28, 64), '--cl-color-ring': neutralMix(80, 90), @@ -146,7 +148,7 @@ 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-background`, +// individually overridable. The base is itself mixed most of the way toward `--cl-color-surface`, // 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-foreground`, deepening in light // mode and lightening in dark, since that token already carries both. @@ -169,8 +171,8 @@ 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-foreground-secondary']}, ${colorVars['--cl-color-background']} 55%)`, - '--cl-scrollbar-thumb-idle': `color-mix(in oklab, ${scrollbarThumb}, ${colorVars['--cl-color-background']} 45%)`, + '--cl-scrollbar-thumb': `color-mix(in oklab, ${colorVars['--cl-color-foreground-secondary']}, ${colorVars['--cl-color-surface']} 55%)`, + '--cl-scrollbar-thumb-idle': `color-mix(in oklab, ${scrollbarThumb}, ${colorVars['--cl-color-surface']} 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; diff --git a/packages/swingset/src/stories/button.mdx b/packages/swingset/src/stories/button.mdx index 294b0718f3d..5e6987da1cd 100644 --- a/packages/swingset/src/stories/button.mdx +++ b/packages/swingset/src/stories/button.mdx @@ -60,7 +60,7 @@ it to show. Every combination below is the same two props. Hover and focus are part of the matrix — hover a cell, or tab through it, to see them. `filled` shifts its fill and `link` underlines. `outline` and `ghost` share the same neutral hover fill, -except `ghost` under `negative`, which is the one cell that tints by color. The focus ring is +except under `negative`, where both tint by color. The focus ring is the same for every cell.