diff --git a/packages/tokens/config/tokens.config.ts b/packages/tokens/config/tokens.config.ts index 081af92..18624c0 100644 --- a/packages/tokens/config/tokens.config.ts +++ b/packages/tokens/config/tokens.config.ts @@ -518,15 +518,80 @@ export const config: TokensConfig = { kind: 'direct', ref: { family: 'neutral', id: '2' }, }, + // Mode-flipping background (SPEC §5.1). Light mode → dark, Dark mode → + // light. Achieved by referencing the opposite end of the neutral spine: + // neutral.12 in light = dark; pivot-mirror makes neutral.12 in dark = light. + inverted: { + kind: 'direct', + ref: { family: 'neutral', id: '12' }, + }, + // Nested-card hierarchy (Apple Settings.app pattern, SPEC §10.D7). + // Used inside a Grouped section where the outer surface is `primary` + // and inner cards alternate between Grouped.Primary and Grouped.Secondary. + // Tier values match Figma fixture per-mode (light: Gray.25/Gray.0/Gray.25; + // dark: Gray.25/Gray.50≈neutral.1+1/Gray.75≈neutral.2+1). + grouped: { + primary: { + kind: 'direct', + ref: { family: 'neutral', id: '1' }, + }, + secondary: { + kind: 'direct', + ref: { family: 'neutral', id: '0' }, + }, + tertiary: { + kind: 'direct', + ref: { family: 'neutral', id: '2' }, + }, + }, }, + // Legacy alias — matches the historical single-tier emission used by + // existing v0.2.x consumers. Effectively `overlay_tiers.base` at the + // legacy stop (40% dark scrim). Retained until next major bump. overlay: { kind: 'direct', ref: { family: 'static', id: 'dark', opacity_stop: 40 }, }, + // Full SPEC §5.1 overlay tier set — frosted-overlay scrims (Light + // sub-collection alpha-on-white). Use these for Modal scrims, Tooltip + // backgrounds, and Sheet overlays. Stops are clamped to the production + // opacity ladder; `base` sits at 48 (closest available to the 50% target). + overlay_tiers: { + ghost: { + kind: 'direct', + ref: { family: 'static', id: 'white', opacity_stop: 1 }, + }, + soft: { + kind: 'direct', + ref: { family: 'static', id: 'white', opacity_stop: 20 }, + }, + base: { + kind: 'direct', + ref: { family: 'static', id: 'white', opacity_stop: 48 }, + }, + strong: { + kind: 'direct', + ref: { family: 'static', id: 'white', opacity_stop: 80 }, + }, + }, + // Legacy alias — single-tier emission for v0.2.x consumers. static: { kind: 'direct', ref: { family: 'static', id: 'white' }, }, + // Mode-invariant statics. `light` is always #ffffff; `dark` is always + // ~#020203. Useful for surfaces that must NOT swap with theme (brand + // hero sections, splash screens, photography overlays). + static_tiers: { + light: { + kind: 'direct', + ref: { family: 'static', id: 'white' }, + }, + dark: { + kind: 'direct', + ref: { family: 'static', id: 'dark' }, + }, + }, }, labels: buildLabels(), diff --git a/packages/tokens/src/generators/semantic-colors.ts b/packages/tokens/src/generators/semantic-colors.ts index e379bb2..2a365cb 100644 --- a/packages/tokens/src/generators/semantic-colors.ts +++ b/packages/tokens/src/generators/semantic-colors.ts @@ -143,16 +143,72 @@ function collectEntries(semantics: SemanticsConfig): SemEntry[] { path: 'backgrounds.neutral.tertiary', def: semantics.backgrounds.neutral.tertiary, }) + // SPEC §5.1: mode-flipping inverted background (always opposite of mode primary). + out.push({ + name: 'bg-inverted', + path: 'backgrounds.neutral.inverted', + def: semantics.backgrounds.neutral.inverted, + }) + // SPEC §5.1 / §10.D7: nested-card grouped hierarchy. + out.push({ + name: 'bg-grouped-primary', + path: 'backgrounds.neutral.grouped.primary', + def: semantics.backgrounds.neutral.grouped.primary, + }) + out.push({ + name: 'bg-grouped-secondary', + path: 'backgrounds.neutral.grouped.secondary', + def: semantics.backgrounds.neutral.grouped.secondary, + }) + out.push({ + name: 'bg-grouped-tertiary', + path: 'backgrounds.neutral.grouped.tertiary', + def: semantics.backgrounds.neutral.grouped.tertiary, + }) + // Legacy alias — kept for v0.2.x backward compat. out.push({ name: 'bg-overlay', path: 'backgrounds.overlay', def: semantics.backgrounds.overlay, }) + // SPEC §5.1: 4-tier overlay scrims for modals, tooltips, sheets. + out.push({ + name: 'bg-overlay-ghost', + path: 'backgrounds.overlay_tiers.ghost', + def: semantics.backgrounds.overlay_tiers.ghost, + }) + out.push({ + name: 'bg-overlay-soft', + path: 'backgrounds.overlay_tiers.soft', + def: semantics.backgrounds.overlay_tiers.soft, + }) + out.push({ + name: 'bg-overlay-base', + path: 'backgrounds.overlay_tiers.base', + def: semantics.backgrounds.overlay_tiers.base, + }) + out.push({ + name: 'bg-overlay-strong', + path: 'backgrounds.overlay_tiers.strong', + def: semantics.backgrounds.overlay_tiers.strong, + }) + // Legacy alias — kept for v0.2.x backward compat. out.push({ name: 'bg-static', path: 'backgrounds.static', def: semantics.backgrounds.static, }) + // SPEC §5.1: mode-invariant statics (always white / always near-black). + out.push({ + name: 'bg-static-light', + path: 'backgrounds.static_tiers.light', + def: semantics.backgrounds.static_tiers.light, + }) + out.push({ + name: 'bg-static-dark', + path: 'backgrounds.static_tiers.dark', + def: semantics.backgrounds.static_tiers.dark, + }) // Labels const labels = semantics.labels diff --git a/packages/tokens/src/types.ts b/packages/tokens/src/types.ts index 172621f..e55e18f 100644 --- a/packages/tokens/src/types.ts +++ b/packages/tokens/src/types.ts @@ -469,14 +469,47 @@ export interface SemanticDefModeBranch { // ─── Semantic tree (config.semantics) ─────────────────────────────────── +/** + * Backgrounds — surface tokens. Per SPEC §5.1, the full skeleton has: + * Neutral.{Primary, Secondary, Tertiary, Inverted} — 4 base tiers + * Neutral.Grouped.{Primary, Secondary, Tertiary} — nested-card pattern (D7) + * Overlay.{Ghost, Soft, Base, Strong} — scrim/frosted-overlay tiers + * Static.{Light, Dark} — mode-invariant whites/darks + * + * `overlay` and `static` are kept as **single-tier aliases** for backward compat + * with v0.2.x consumers (they collapse to `.base` / `.light` respectively). + * `overlay_tiers` and `static_tiers` carry the full SPEC-required tier sets. + */ export interface BackgroundsConfig { neutral: { primary: SemanticDef secondary: SemanticDef tertiary: SemanticDef + /** Mode-flipping background — opposite end of the Gray ladder per mode (SPEC §5.1). */ + inverted: SemanticDef + /** Nested-card hierarchy (Apple Settings.app pattern; SPEC §10.D7). */ + grouped: { + primary: SemanticDef + secondary: SemanticDef + tertiary: SemanticDef + } } + /** Legacy single-tier alias (= overlay_tiers.base). Retained for v0.2.x backward compat. */ overlay: SemanticDef + /** Full SPEC §5.1 overlay tiers — frosted-overlay scrims (Light sub-collection alpha-on-white). */ + overlay_tiers: { + ghost: SemanticDef + soft: SemanticDef + base: SemanticDef + strong: SemanticDef + } + /** Legacy single-tier alias (= static_tiers.light). Retained for v0.2.x backward compat. */ static: SemanticDef + /** Mode-invariant statics — always emit the same hex regardless of theme. */ + static_tiers: { + light: SemanticDef + dark: SemanticDef + } } export type TierSet4 = Record<'primary' | 'secondary' | 'tertiary' | 'quaternary', SemanticDef> diff --git a/packages/tokens/tests/guards/__snapshots__/snapshot-css.test.ts.snap b/packages/tokens/tests/guards/__snapshots__/snapshot-css.test.ts.snap index f1dc3be..6296604 100644 --- a/packages/tokens/tests/guards/__snapshots__/snapshot-css.test.ts.snap +++ b/packages/tokens/tests/guards/__snapshots__/snapshot-css.test.ts.snap @@ -1047,8 +1047,18 @@ exports[`G1 · dist/tokens.css byte-stable matches committed snapshot 1`] = ` --bg-primary: oklch(1 0 0); --bg-secondary: oklch(0.979 0.003 264.5); --bg-tertiary: oklch(0.943 0.013 286.1); + --bg-inverted: oklch(0.174 0.004 286); + --bg-grouped-primary: oklch(0.979 0.003 264.5); + --bg-grouped-secondary: oklch(1 0 0); + --bg-grouped-tertiary: oklch(0.943 0.013 286.1); --bg-overlay: oklch(0.08 0 0 / 0.4); + --bg-overlay-ghost: oklch(1 0 0 / 0.01); + --bg-overlay-soft: oklch(1 0 0 / 0.2); + --bg-overlay-base: oklch(1 0 0 / 0.48); + --bg-overlay-strong: oklch(1 0 0 / 0.8); --bg-static: oklch(1 0 0); + --bg-static-light: oklch(1 0 0); + --bg-static-dark: oklch(0.08 0 0); --label-neutral-primary: oklch(0.648 0.008 247); --label-neutral-secondary: oklch(0.742 0.007 247); --label-neutral-tertiary: oklch(0.83 0.006 247); @@ -1874,8 +1884,18 @@ exports[`G1 · dist/tokens.css byte-stable matches committed snapshot 1`] = ` --bg-primary: oklch(1 0 0); --bg-secondary: oklch(0.976 0.004 271.4); --bg-tertiary: oklch(0.956 0.009 279.7); + --bg-inverted: oklch(0 0 0); + --bg-grouped-primary: oklch(0.976 0.004 271.4); + --bg-grouped-secondary: oklch(1 0 0); + --bg-grouped-tertiary: oklch(0.956 0.009 279.7); --bg-overlay: oklch(0.08 0 0 / 0.4); + --bg-overlay-ghost: oklch(1 0 0 / 0.01); + --bg-overlay-soft: oklch(1 0 0 / 0.2); + --bg-overlay-base: oklch(1 0 0 / 0.48); + --bg-overlay-strong: oklch(1 0 0 / 0.8); --bg-static: oklch(1 0 0); + --bg-static-light: oklch(1 0 0); + --bg-static-dark: oklch(0.08 0 0); --label-neutral-primary: oklch(0.539 0.008 247); --label-neutral-secondary: oklch(0.648 0.007 247); --label-neutral-tertiary: oklch(0.742 0.007 247); @@ -2701,8 +2721,18 @@ exports[`G1 · dist/tokens.css byte-stable matches committed snapshot 1`] = ` --bg-primary: oklch(0.174 0.004 286); --bg-secondary: oklch(0.227 0.004 286.1); --bg-tertiary: oklch(0.261 0.004 286.1); + --bg-inverted: oklch(1 0 0); + --bg-grouped-primary: oklch(0.227 0.004 286.1); + --bg-grouped-secondary: oklch(0.174 0.004 286); + --bg-grouped-tertiary: oklch(0.261 0.004 286.1); --bg-overlay: oklch(0.08 0 0 / 0.4); + --bg-overlay-ghost: oklch(1 0 0 / 0.01); + --bg-overlay-soft: oklch(1 0 0 / 0.2); + --bg-overlay-base: oklch(1 0 0 / 0.48); + --bg-overlay-strong: oklch(1 0 0 / 0.8); --bg-static: oklch(1 0 0); + --bg-static-light: oklch(1 0 0); + --bg-static-dark: oklch(0.08 0 0); --label-neutral-primary: oklch(0.761 0.006 247); --label-neutral-secondary: oklch(0.67 0.007 247); --label-neutral-tertiary: oklch(0.567 0.007 247); @@ -3528,8 +3558,18 @@ exports[`G1 · dist/tokens.css byte-stable matches committed snapshot 1`] = ` --bg-primary: oklch(0 0 0); --bg-secondary: oklch(0.197 0.004 286); --bg-tertiary: oklch(0.241 0.006 286); + --bg-inverted: oklch(1 0 0); + --bg-grouped-primary: oklch(0.197 0.004 286); + --bg-grouped-secondary: oklch(0 0 0); + --bg-grouped-tertiary: oklch(0.241 0.006 286); --bg-overlay: oklch(0.08 0 0 / 0.4); + --bg-overlay-ghost: oklch(1 0 0 / 0.01); + --bg-overlay-soft: oklch(1 0 0 / 0.2); + --bg-overlay-base: oklch(1 0 0 / 0.48); + --bg-overlay-strong: oklch(1 0 0 / 0.8); --bg-static: oklch(1 0 0); + --bg-static-light: oklch(1 0 0); + --bg-static-dark: oklch(0.08 0 0); --label-neutral-primary: oklch(0.841 0.005 247); --label-neutral-secondary: oklch(0.759 0.006 247); --label-neutral-tertiary: oklch(0.668 0.007 247); @@ -4356,8 +4396,18 @@ exports[`G1 · dist/tokens.css byte-stable matches committed snapshot 1`] = ` --bg-primary: oklch(0.174 0.004 286); --bg-secondary: oklch(0.227 0.004 286.1); --bg-tertiary: oklch(0.261 0.004 286.1); + --bg-inverted: oklch(1 0 0); + --bg-grouped-primary: oklch(0.227 0.004 286.1); + --bg-grouped-secondary: oklch(0.174 0.004 286); + --bg-grouped-tertiary: oklch(0.261 0.004 286.1); --bg-overlay: oklch(0.08 0 0 / 0.4); + --bg-overlay-ghost: oklch(1 0 0 / 0.01); + --bg-overlay-soft: oklch(1 0 0 / 0.2); + --bg-overlay-base: oklch(1 0 0 / 0.48); + --bg-overlay-strong: oklch(1 0 0 / 0.8); --bg-static: oklch(1 0 0); + --bg-static-light: oklch(1 0 0); + --bg-static-dark: oklch(0.08 0 0); --label-neutral-primary: oklch(0.761 0.006 247); --label-neutral-secondary: oklch(0.67 0.007 247); --label-neutral-tertiary: oklch(0.567 0.007 247); diff --git a/packages/tokens/tests/guards/__snapshots__/snapshot-esm.test.ts.snap b/packages/tokens/tests/guards/__snapshots__/snapshot-esm.test.ts.snap index 23e09c8..16a09fb 100644 --- a/packages/tokens/tests/guards/__snapshots__/snapshot-esm.test.ts.snap +++ b/packages/tokens/tests/guards/__snapshots__/snapshot-esm.test.ts.snap @@ -817,8 +817,18 @@ export const pinkA99 = 'var(--pink-a99)'; export const bgPrimary = 'var(--bg-primary)'; export const bgSecondary = 'var(--bg-secondary)'; export const bgTertiary = 'var(--bg-tertiary)'; +export const bgInverted = 'var(--bg-inverted)'; +export const bgGroupedPrimary = 'var(--bg-grouped-primary)'; +export const bgGroupedSecondary = 'var(--bg-grouped-secondary)'; +export const bgGroupedTertiary = 'var(--bg-grouped-tertiary)'; export const bgOverlay = 'var(--bg-overlay)'; +export const bgOverlayGhost = 'var(--bg-overlay-ghost)'; +export const bgOverlaySoft = 'var(--bg-overlay-soft)'; +export const bgOverlayBase = 'var(--bg-overlay-base)'; +export const bgOverlayStrong = 'var(--bg-overlay-strong)'; export const bgStatic = 'var(--bg-static)'; +export const bgStaticLight = 'var(--bg-static-light)'; +export const bgStaticDark = 'var(--bg-static-dark)'; export const labelNeutralPrimary = 'var(--label-neutral-primary)'; export const labelNeutralSecondary = 'var(--label-neutral-secondary)'; export const labelNeutralTertiary = 'var(--label-neutral-tertiary)'; @@ -1952,8 +1962,18 @@ export declare const pinkA99: string; export declare const bgPrimary: string; export declare const bgSecondary: string; export declare const bgTertiary: string; +export declare const bgInverted: string; +export declare const bgGroupedPrimary: string; +export declare const bgGroupedSecondary: string; +export declare const bgGroupedTertiary: string; export declare const bgOverlay: string; +export declare const bgOverlayGhost: string; +export declare const bgOverlaySoft: string; +export declare const bgOverlayBase: string; +export declare const bgOverlayStrong: string; export declare const bgStatic: string; +export declare const bgStaticLight: string; +export declare const bgStaticDark: string; export declare const labelNeutralPrimary: string; export declare const labelNeutralSecondary: string; export declare const labelNeutralTertiary: string; diff --git a/packages/tokens/tests/guards/__snapshots__/snapshot-tailwind.test.ts.snap b/packages/tokens/tests/guards/__snapshots__/snapshot-tailwind.test.ts.snap index c23ac8a..cd13fa7 100644 --- a/packages/tokens/tests/guards/__snapshots__/snapshot-tailwind.test.ts.snap +++ b/packages/tokens/tests/guards/__snapshots__/snapshot-tailwind.test.ts.snap @@ -790,8 +790,18 @@ exports[`Tailwind preset · dist/tailwind-preset.css byte-stable matches committ --color-bg-primary: var(--bg-primary); --color-bg-secondary: var(--bg-secondary); --color-bg-tertiary: var(--bg-tertiary); + --color-bg-inverted: var(--bg-inverted); + --color-bg-grouped-primary: var(--bg-grouped-primary); + --color-bg-grouped-secondary: var(--bg-grouped-secondary); + --color-bg-grouped-tertiary: var(--bg-grouped-tertiary); --color-bg-overlay: var(--bg-overlay); + --color-bg-overlay-ghost: var(--bg-overlay-ghost); + --color-bg-overlay-soft: var(--bg-overlay-soft); + --color-bg-overlay-base: var(--bg-overlay-base); + --color-bg-overlay-strong: var(--bg-overlay-strong); --color-bg-static: var(--bg-static); + --color-bg-static-light: var(--bg-static-light); + --color-bg-static-dark: var(--bg-static-dark); --color-label-neutral-primary: var(--label-neutral-primary); --color-label-neutral-secondary: var(--label-neutral-secondary); --color-label-neutral-tertiary: var(--label-neutral-tertiary);