Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ storybook-static/
packages/registry/registry/

*storybook.log
.claude/scheduled_tasks.lock
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, <app-layers...>;`) 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 `<html>`.

Expand Down
208 changes: 204 additions & 4 deletions packages/tokens/build.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { writeFileSync } from "fs";
import StyleDictionary from "style-dictionary";
import { register } from "@tokens-studio/sd-transforms";

Expand Down Expand Up @@ -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 <value>;" 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"],
Expand All @@ -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",
},
},
],
},
Expand Down Expand Up @@ -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"),
},
],
Expand All @@ -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");
3 changes: 2 additions & 1 deletion packages/tokens/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading