diff --git a/.gitignore b/.gitignore index cec8deb..c8817be 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ storybook-static/ packages/registry/registry/ *storybook.log +.claude/scheduled_tasks.lock diff --git a/CLAUDE.md b/CLAUDE.md index a6b4d28..c909977 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,8 +80,11 @@ Tokens are W3C DTCG JSON files in `packages/tokens/src/`: Style Dictionary outputs: -- `dist/tokens.css` — `:root { --sct-* }` (light theme) -- `dist/tokens-dark.css` — `[data-theme="dark"] { --sct-* }` (dark overrides) +- `dist/tokens.css` — `@layer design-tokens { :root { --sct-* } }` (light theme) + `@layer design-tokens, theme;` ordering declaration +- `dist/tokens-dark.css` — `@layer design-tokens { [data-theme="dark"] { --sct-* } }` (dark overrides) +- `dist/base.css` — `@layer theme { html, :host { font-family: var(--sct-font-family-sans) } }` (base element styles) + +**CSS layer contract:** Tokens live in `@layer design-tokens` (lowest priority); the default element styles live in `@layer theme`. The layer order `design-tokens, theme` is declared at the top of `tokens.css`. Consuming apps that use their own `@layer` declarations **must** import `tokens.css` before any other layered stylesheet, or explicitly redeclare the layer order (`@layer design-tokens, theme, ;`) before their own layers. Unlayered consumer rules always win over both layers regardless of import order. Token names are kebab-case prefixed `sct-`, e.g. `--sct-color-primary`. Dark mode is toggled by setting `data-theme="dark"` on ``. diff --git a/packages/tokens/build.mjs b/packages/tokens/build.mjs index e18cab5..1372eaf 100644 --- a/packages/tokens/build.mjs +++ b/packages/tokens/build.mjs @@ -1,3 +1,4 @@ +import { writeFileSync } from "fs"; import StyleDictionary from "style-dictionary"; import { register } from "@tokens-studio/sd-transforms"; @@ -37,6 +38,35 @@ StyleDictionary.registerTransformGroup({ ], }); +// Custom format: wraps CSS custom properties in @layer design-tokens. +// Accepts options: +// selector — CSS selector (default: ':root') +// layerOrder — if set, prepends "@layer ;" to establish layer order +StyleDictionary.registerFormat({ + name: "css/variables/layered", + format: ({ dictionary, options }) => { + const selector = options.selector ?? ":root"; + const layerOrderDecl = options.layerOrder + ? `@layer ${options.layerOrder};\n\n` + : ""; + // SD v4 with DTCG format (@tokens-studio/sd-transforms) stores the + // transformed value in token.$value; fall back to token.value for + // non-DTCG tokens. + const vars = dictionary.allTokens + .map((token) => ` --${token.name}: ${token.$value ?? token.value};`) + .join("\n"); + return ( + `/**\n * Do not edit directly, this file was auto-generated.\n */\n\n` + + `${layerOrderDecl}` + + `@layer design-tokens {\n` + + ` ${selector} {\n` + + `${vars}\n` + + ` }\n` + + `}\n` + ); + }, +}); + // Light theme (all tokens except dark overrides) const light = new StyleDictionary({ source: ["src/base.tokens.json", "src/semantic.tokens.json"], @@ -47,8 +77,14 @@ const light = new StyleDictionary({ files: [ { destination: "tokens.css", - format: "css/variables", - options: { selector: ":root", outputReferences: false }, + format: "css/variables/layered", + options: { + selector: ":root", + outputReferences: false, + // Declare layer order so consumers don't have to. + // design-tokens is lower priority; theme can override it. + layerOrder: "design-tokens, theme", + }, }, ], }, @@ -86,8 +122,11 @@ const dark = new StyleDictionary({ files: [ { destination: "tokens-dark.css", - format: "css/variables", - options: { selector: '[data-theme="dark"]', outputReferences: false }, + format: "css/variables/layered", + options: { + selector: '[data-theme="dark"]', + outputReferences: false, + }, filter: (token) => token.filePath.includes("semantic-dark"), }, ], @@ -97,4 +136,165 @@ const dark = new StyleDictionary({ await light.buildAllPlatforms(); await dark.buildAllPlatforms(); + +// Generate base.css: applies font-family token to html/:host. +// Lives in its own @layer theme so it can override @layer design-tokens +// without specificity fights, and consumers can override it via unlayered rules. +writeFileSync( + "dist/base.css", + `/** + * Do not edit directly, this file was auto-generated. + */ + +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--sct-font-family-sans, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--sct-font-family-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} +`, +); + console.log("✓ tokens built"); diff --git a/packages/tokens/package.json b/packages/tokens/package.json index 4b0ca5d..8e08b3e 100644 --- a/packages/tokens/package.json +++ b/packages/tokens/package.json @@ -8,7 +8,8 @@ }, "exports": { "./tokens.css": "./dist/tokens.css", - "./tokens-dark.css": "./dist/tokens-dark.css" + "./tokens-dark.css": "./dist/tokens-dark.css", + "./base.css": "./dist/base.css" }, "devDependencies": { "@tokens-studio/sd-transforms": "^2.0.3",