From 5638dd2eacd5d9f5ccb2fcefa5a9c861576df585 Mon Sep 17 00:00:00 2001 From: Diogo Esteves Date: Thu, 29 Oct 2026 09:18:47 +0000 Subject: [PATCH 1/2] fix(tokens): export round trip: DESIGN.md spacing steps, typography aliases, no DTCG duplicates The DESIGN.md export wrote Tailwind's spacing unit as `base: 0.25rem`, which a DESIGN.md reader takes as the only step: read back, `padding: "16px"` got "Use `p-[4px]`". It now writes the steps of the scale (`"0.5": 0.125rem` to `"96": 24rem`) from the project's unit. A typography alias was written as `"{typography.font-sans}"`, a reference to a whole level, which onsystem's reader could not resolve and Google's linter read as 22 unknown properties. It is now written as the level. A DTCG file exported into the project is read back by zero config next to the stylesheet, and its `@theme inline` tokens, which have no custom property, were added a second time (42 tokens became 50). mergeTokens now merges a token without a custom property by its name and Tailwind key. --- .changeset/token-export-round-trip.md | 9 +++ docs/tokens.md | 8 +- src/tokens/export.ts | 48 ++++++++++-- src/tokens/index.ts | 14 +++- test/export.test.ts | 107 +++++++++++++++++++++++++- test/tokens.test.ts | 24 ++++++ 6 files changed, 197 insertions(+), 13 deletions(-) create mode 100644 .changeset/token-export-round-trip.md diff --git a/.changeset/token-export-round-trip.md b/.changeset/token-export-round-trip.md new file mode 100644 index 00000000..db45b833 --- /dev/null +++ b/.changeset/token-export-round-trip.md @@ -0,0 +1,9 @@ +--- +'onsystem': patch +--- + +`onsystem export --format design-md` writes Tailwind's spacing unit as the steps of the scale (`"1": 0.25rem`, `"2": 0.5rem` up to `"96": 24rem`) instead of `base: 0.25rem`. DESIGN.md has no multiplier, so a reader, onsystem included, took 4px as the only step: read back in a project with no stylesheet, `padding: "16px"` got the advice "Use `p-[4px]`", and `gap-[8px]` the nearest step `gap-[4px]`. Now 8px and 16px are on the scale, and `p-[13px]` is nearest `p-[12px]`. Export again to rewrite a DESIGN.md written by an earlier version. + +A typography alias in the DESIGN.md export, such as shadcn/ui's `--font-heading: var(--font-sans)`, is written as the level it names (`fontFamily: ...`) instead of `"{typography.font-sans}"`. A reference in the front matter names one value, not a level: onsystem warned "unresolved reference {typography.font-sans}" when it read the file, and DESIGN.md's linter reported one unknown typography property per character of the reference. + +A DTCG file exported into the project with `onsystem export --format dtcg --out design.tokens.json`, which zero config reads back next to the stylesheet, no longer adds tokens. Its `@theme inline` tokens (`rounded-sm`, `font-sans`) have no custom property to merge by, so they now merge with the stylesheet's by name and Tailwind class. In a stock shadcn/ui app, `inspect` listed 50 tokens instead of 42, and a later DESIGN.md export listed the radii twice and warned that they were left out of the front matter. diff --git a/docs/tokens.md b/docs/tokens.md index 158af14d..53223ad9 100644 --- a/docs/tokens.md +++ b/docs/tokens.md @@ -22,7 +22,7 @@ DTCG files are read as the [Format Module 2025.10](https://www.designtokens.org/ - `$root` tokens: the token a group has for itself. `color.accent.$root` is referenced as `{color.accent.$root}` and its custom property is `--color-accent`, - groups that extend another: `"$extends": "{button}"` (or JSON Schema's `"$ref": "#/button"`) gives the group every token and property of `button`, and its own tokens replace the ones at the same path. Circular extension, a reference that resolves to nothing and `$extends` that names a token are reported by `inspect` and left out. -A token is named by its path (`color.brand.500`), unless onsystem's extension keeps another name for it, and its custom property is the path with dashes (`--color-brand-500`, or `--acme-color-brand-500` with `"prefix": "acme"`). Values for other themes can sit under `$extensions.modes` (`{ "dark": "#000" }`). A DTCG file and the CSS generated from it are merged by custom property. +A token is named by its path (`color.brand.500`), unless onsystem's extension keeps another name for it, and its custom property is the path with dashes (`--color-brand-500`, or `--acme-color-brand-500` with `"prefix": "acme"`). Values for other themes can sit under `$extensions.modes` (`{ "dark": "#000" }`). A DTCG file and the CSS generated from it are merged by custom property, and a token that has none (a Tailwind `@theme inline` value such as `--radius-sm`) by its name and Tailwind class. ### onsystem's extension @@ -85,11 +85,13 @@ One group per category (`color`, `spacing`, `radius`, `typography`, `shadow`, `o - An alias stays a reference: `--card: var(--background)` is `"{color.background}"`. - What DTCG has no type for (`color-mix()`, a `var()` no token defines, lengths in `em` or `%`) is written as the CSS string, without `$type`. -Read back as a token source, the file gives the same tokens with the same names: the shadcn demo's `check` reports the same findings from its exported file as from `globals.css`, and names `destructive`, not `color.destructive`. The test suite runs this round trip. +Read back as a token source, the file gives the same tokens with the same names: the shadcn demo's `check` reports the same findings from its exported file as from `globals.css`, and names `destructive`, not `color.destructive`. Zero config reads a `*.tokens.json` anywhere in the project, so a file exported into it is read next to the stylesheet it came from: the two merge, and `inspect` lists as many tokens as before. The test suite runs both. ### DESIGN.md -Front matter in the DESIGN.md schema (`version`, `name`, `colors`, `typography`, `rounded`, `spacing`), then a section per category in the format's order (Colors, Typography, Layout, Elevation & Depth, Shapes, and Other tokens), each with one sentence and a table of the tokens: their value, a column per mode, the class and custom property code uses, and the description. Values the schema cannot hold, such as a `var()` font family, are left out of the front matter and kept in the table. Aliases stay references (`"{colors.background}"`). +Front matter in the DESIGN.md schema (`version`, `name`, `colors`, `typography`, `rounded`, `spacing`), then a section per category in the format's order (Colors, Typography, Layout, Elevation & Depth, Shapes, and Other tokens), each with one sentence and a table of the tokens: their value, a column per mode, the class and custom property code uses, and the description. Values the schema cannot hold, such as a `var()` font family, are left out of the front matter and kept in the table. Aliases stay references (`"{colors.background}"`), except in typography: a reference names one value, and a typography level holds several, so an alias is written as the level it names. + +Tailwind's spacing unit is written as the steps of Tailwind v3's default scale, which v4 still makes: from `"0.5": 0.125rem` to `"96": 24rem`. DESIGN.md has no multiplier: a reader takes each spacing entry as a step, so the unit alone (`base: 0.25rem`) would make 4px the only one. Read back as a token source, the file gives the same colors, radii and spacing steps. A DESIGN.md token names no class, so `check` leaves a length on the scale alone and snaps one off it to the nearest step: `p-[13px]` becomes `p-[12px]`. The test suite runs this round trip. ## Status colors diff --git a/src/tokens/export.ts b/src/tokens/export.ts index d4719e96..f364f1d9 100644 --- a/src/tokens/export.ts +++ b/src/tokens/export.ts @@ -502,7 +502,7 @@ export function toDesignMd(tokens: readonly Token[], options: { name: string }): const lookup = varLookup(tokens); const keys = designKeys(tokens); const front: Record = { version: 'alpha', name: options.name }; - const sections: Record> = {}; + const sections: Record> = {}; const body: string[] = [`# ${options.name}`]; const byName = new Map(tokens.map((t) => [t.name, t])); @@ -510,14 +510,23 @@ export function toDesignMd(tokens: readonly Token[], options: { name: string }): const list = tokens.filter((t) => t.category === section.category); if (!list.length) continue; if (section.key) { - const values: Record = {}; + // A Map keeps the keys in order: an object puts `"1"` before `"0.5"`. + const values = new Map(); + let steps: Map | undefined; for (const token of list) { + if (isSpacingUnit(token)) { + steps = spacingSteps(token, lookup); + continue; + } const key = keys.get(token); if (key === undefined) continue; // An alias stays a reference to a token of the same section that the front matter has. + // A typography level is no single value, which a reference must name: it is written out. const target = token.aliasOf ? byName.get(token.aliasOf) : undefined; const targetKey = + section.key !== 'typography' && target?.category === token.category && + !isSpacingUnit(target) && frontMatterValue(target, section.key, lookup) !== undefined ? keys.get(target) : undefined; @@ -526,13 +535,15 @@ export function toDesignMd(tokens: readonly Token[], options: { name: string }): ? frontMatterValue(token, section.key, lookup) : `{${section.key}.${targetKey}}`; if (value === undefined) continue; - if (key in values) { + if (values.has(key)) { warnings.push(`${token.name} is left out of the front matter: another token has its key`); continue; } - values[key] = value; + values.set(key, value); } - if (Object.keys(values).length) sections[section.key] = values; + // A step the project sets itself (`--spacing-4`) keeps its own value. + for (const [key, value] of steps ?? []) if (!values.has(key)) values.set(key, value); + if (values.size) sections[section.key] = values; } body.push('', `## ${section.heading}`, '', section.intro, '', ...table(list, keys)); } @@ -551,6 +562,33 @@ function designKeys(tokens: readonly Token[]): Map { return out; } +/** Tailwind's spacing unit, `--spacing`, which `p-4` takes 4 times. */ +function isSpacingUnit(token: Token): boolean { + return token.category === 'spacing' && token.tailwind === ''; +} + +/** The steps of Tailwind's default spacing scale, `p-0.5` to `p-96`, in spacing units. */ +const SPACING_STEPS = [ + 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, + 52, 56, 60, 64, 72, 80, 96, +]; + +/** + * Tailwind's spacing unit as the steps it makes: `"4": 1rem` for `p-4`. A DESIGN.md has no + * multiplier: a reader takes each spacing entry as a step, so the unit alone (`base: 0.25rem`) + * would make 4px the only one. + */ +function spacingSteps(token: Token, lookup: Lookup): Map { + const plain = PLAIN_LENGTH.exec(token.value.trim()); + const [amount, unit] = plain + ? [Number(plain[1]), plain[2] ?? 'px'] + : [evaluateLength(token.value, lookup), 'px']; + const steps = new Map(); + if (amount === undefined || !Number.isFinite(amount) || amount <= 0) return steps; + for (const n of SPACING_STEPS) steps.set(String(n), `${round(n * amount)}${unit}`); + return steps; +} + function frontMatterValue( token: Token, key: (typeof FRONT_MATTER_ORDER)[number], diff --git a/src/tokens/index.ts b/src/tokens/index.ts index 2e4d58ca..f3c12d75 100644 --- a/src/tokens/index.ts +++ b/src/tokens/index.ts @@ -144,7 +144,9 @@ function beforeV4(range: unknown): boolean { /** * DTCG files and the CSS generated from them often describe the same tokens. * Merge entries that share a custom property: keep the DTCG name and - * description, and add the Tailwind mapping from CSS. + * description, and add the Tailwind mapping from CSS. A token without one (a + * `@theme inline` value, `rounded-sm`) merges with the token of its name and + * Tailwind key, such as the one a DTCG file exported from the stylesheet has. */ export function mergeTokens(tokens: Token[]): Token[] { // Tailwind's built-in defaults (`--spacing`) only stand in until the project @@ -152,13 +154,17 @@ export function mergeTokens(tokens: Token[]): Token[] { const overridden = new Set( tokens.filter((t) => t.origin !== 'tailwind-default').map((t) => themeKey(t)), ); - const byVar = new Map(); + const byKey = new Map(); const out: Token[] = []; for (const token of tokens) { if (token.origin === 'tailwind-default' && overridden.has(themeKey(token))) continue; - const existing = token.cssVar ? byVar.get(token.cssVar) : undefined; + // Not by category: an exported `-0.025em` has no DTCG type, and reads back as `other`. + const key = + token.cssVar ?? + (token.tailwind === undefined ? undefined : `${token.name}:${token.tailwind}`); + const existing = key === undefined ? undefined : byKey.get(key); if (!existing) { - if (token.cssVar) byVar.set(token.cssVar, token); + if (key !== undefined) byKey.set(key, token); out.push(token); continue; } diff --git a/test/export.test.ts b/test/export.test.ts index ac8dd77e..1c31acc8 100644 --- a/test/export.test.ts +++ b/test/export.test.ts @@ -272,7 +272,10 @@ describe('export --format design-md', () => { 'version: alpha\nname: shadcn-demo\ncolors:\n background: oklch(1 0 0)\n', ); expect(yaml).toContain('rounded:\n sm: 6px\n md: 8px\n lg: 0.625rem\n'); - expect(yaml).toContain('spacing:\n base: 0.25rem\n'); + // DESIGN.md has no multiplier: Tailwind's spacing unit is written as the steps it makes. + expect(yaml).toContain('spacing:\n "0.5": 0.125rem\n "1": 0.25rem\n "1.5": 0.375rem\n'); + expect(yaml).toContain(' "4": 1rem\n'); + expect(yaml).not.toContain('base:'); // `var(--font-geist-sans)` is no font name: the table has it, the front matter does not. expect(yaml).not.toContain('typography'); expect(body.match(/^## .*/gm)).toEqual([ @@ -344,6 +347,108 @@ describe('export --format design-md', () => { }); }); +describe('export, then read back', () => { + const stock = fs.readFileSync( + path.join(import.meta.dirname, 'fixtures/shadcn-neutral/globals.css'), + 'utf8', + ); + const page = `export default function Page() { + return ( +
+ Hello +
+ ); +} +`; + const findings = async (cwd: string) => { + const { stdout } = await run(['check', 'app', '--format', 'json', '--no-cache'], cwd); + return (JSON.parse(stdout) as CheckResult[]).flatMap((r) => r.diagnostics); + }; + + it('keeps the spacing scale in a DESIGN.md, which another project then reads', async () => { + const app = fixture({ + 'package.json': '{"name":"app"}', + 'app/globals.css': stock, + 'app/page.tsx': page, + }); + const file = path.join(app, 'DESIGN.md'); + expect( + await run(['export', '--format', 'design-md', '--out', file, '--no-cache'], app), + ).toMatchObject({ code: 0, stderr: '' }); + const md = fs.readFileSync(file, 'utf8'); + expect(md).toContain('spacing:\n "0.5": 0.125rem\n "1": 0.25rem\n'); + expect(md).toContain(' "96": 24rem\n---\n'); + + // A project with only the DESIGN.md to read tokens from. + const other = fixture({ + 'package.json': '{"name":"other"}', + 'DESIGN.md': md, + 'app/page.tsx': page, + }); + expect((await findings(app)).map((d) => d.suggestion)).toEqual(['gap-2', 'p-3', 'p-4']); + // 8px and 16px are steps of the scale, which a DESIGN.md token names no class for, and 13px + // is nearest 12px. Read as the only step, `base: 0.25rem` made all three `p-[4px]`. + expect((await findings(other)).map((d) => d.message)).toEqual([ + 'Hardcoded spacing `p-[13px]` (13px) is off the scale. Nearest: `p-[12px]` (12px).', + ]); + }); + + it('writes a typography alias as the level it names, not a reference to a whole level', () => { + const { tokens } = parseCssTokens( + `@theme inline { + --font-heading: var(--font-sans); + --font-sans: 'Inter Variable', sans-serif; +}`, + 'app/globals.css', + ); + expect(tokens.find((t) => t.name === 'font-heading')?.aliasOf).toBe('font-sans'); + const { text, warnings } = toDesignMd(tokens, { name: 'Fonts' }); + expect(warnings).toEqual([]); + // In front matter, a reference names one value; `{typography.font-sans}` is a whole level, + // which DESIGN.md's linter read as a string of 22 unknown properties. + expect(text).toContain( + `typography:\n font-heading:\n fontFamily: "'Inter Variable', sans-serif"\n font-sans:\n fontFamily: "'Inter Variable', sans-serif"\n`, + ); + const back = parseDesignMd(text, 'DESIGN.md'); + expect(back.warnings).toEqual([]); + expect(back.tokens.find((t) => t.name === 'typography.font-heading')?.value).toBe( + "fontFamily: 'Inter Variable', sans-serif", + ); + }); + + it('adds no tokens when the stylesheet and a DTCG file exported from it are both read', async () => { + const app = fixture({ + 'package.json': '{"name":"app"}', + 'app/globals.css': stock, + 'app/page.tsx': page, + }); + const designMd = async () => { + const out = path.join(app, 'DESIGN.md'); + const result = await run( + ['export', '--format', 'design-md', '--out', out, '--no-cache'], + app, + ); + return { ...result, text: fs.readFileSync(out, 'utf8') }; + }; + const before = await designMd(); + expect(before).toMatchObject({ code: 0, stderr: '' }); + expect(before.stdout).toMatch(/^Wrote 42 tokens to /); + + // Zero config reads `**/*.tokens.json`, so the export is read back next to the stylesheet. + // Its `@theme inline` tokens (`rounded-sm`, `font-sans`) have no custom property: they merge + // with the stylesheet's by name and Tailwind key. + const dtcg = path.join(app, 'design.tokens.json'); + expect( + await run(['export', '--format', 'dtcg', '--out', dtcg, '--no-cache'], app), + ).toMatchObject({ code: 0, stderr: '' }); + const after = await designMd(); + expect(after).toMatchObject({ code: 0, stderr: '' }); + expect(after.stdout).toMatch(/^Wrote 42 tokens to /); + expect(after.text).toBe(before.text); + expect(await findings(app)).toHaveLength(3); + }); +}); + const tok = (name: string, category: Token['category'], value: string): Token => ({ name, category, diff --git a/test/tokens.test.ts b/test/tokens.test.ts index de8f22bc..b4aa1cd2 100644 --- a/test/tokens.test.ts +++ b/test/tokens.test.ts @@ -16,6 +16,7 @@ import { mergeTokens, parseCssTokens, parseDtcgTokens, + toDtcg, TokenIndex, } from '../src/tokens/index.js'; import { evaluateLength, lengthToPx } from '../src/tokens/units.js'; @@ -398,6 +399,29 @@ describe('CSS custom-property tokens', () => { ]); }); + it('merges a @theme inline token, which has no custom property, by name and Tailwind key', () => { + const css = parseCssTokens( + `@theme inline { + --radius-sm: 6px; + --leading-tight: 1.25; + --tracking-tight: -0.025em; + }`, + 't.css', + ).tokens; + expect(css.map((t) => t.cssVar)).toEqual([undefined, undefined, undefined]); + // The DTCG file exported from them keeps their names and Tailwind keys, and no custom property. + const json = parseDtcgTokens(toDtcg(css).text, 't.json').tokens; + expect(json).toHaveLength(3); + expect(mergeTokens([...css, ...json])).toEqual(css); + // Two tokens of the same key (`tight`) that make other classes stay apart. + const leading = json.filter((t) => t.name === 'leading-tight'); + expect(mergeTokens([...css.filter((t) => t.name !== 'leading-tight'), ...leading])).toEqual([ + css[0], + css[2], + leading[0], + ]); + }); + it('suggests the bare utility for a Tailwind v3 DEFAULT radius', () => { const token: Token = { name: 'radius-DEFAULT', From 5d6b6d684b364d88afd50e93ac82ba3053bcbc9b Mon Sep 17 00:00:00 2001 From: Diogo Esteves Date: Thu, 29 Oct 2026 09:35:17 +0000 Subject: [PATCH 2/2] fix(tokens): keep a merged token's category, and write spacing steps in scale order A DTCG file listed before the stylesheet read -0.025em as other, and the merged token kept that, so tracking-tight left the DESIGN.md typography. Steps the project sets itself are now written in scale order with the generated ones. docs/tokens.md no longer says the read-back gives every spacing step: v4's other steps are off the scale for a project that reads only the DESIGN.md. --- docs/tokens.md | 2 +- src/tokens/export.ts | 7 ++++++- src/tokens/index.ts | 4 ++++ test/export.test.ts | 17 +++++++++++++++++ test/tokens.test.ts | 6 ++++++ 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/tokens.md b/docs/tokens.md index 53223ad9..b057c6c1 100644 --- a/docs/tokens.md +++ b/docs/tokens.md @@ -91,7 +91,7 @@ Read back as a token source, the file gives the same tokens with the same names: Front matter in the DESIGN.md schema (`version`, `name`, `colors`, `typography`, `rounded`, `spacing`), then a section per category in the format's order (Colors, Typography, Layout, Elevation & Depth, Shapes, and Other tokens), each with one sentence and a table of the tokens: their value, a column per mode, the class and custom property code uses, and the description. Values the schema cannot hold, such as a `var()` font family, are left out of the front matter and kept in the table. Aliases stay references (`"{colors.background}"`), except in typography: a reference names one value, and a typography level holds several, so an alias is written as the level it names. -Tailwind's spacing unit is written as the steps of Tailwind v3's default scale, which v4 still makes: from `"0.5": 0.125rem` to `"96": 24rem`. DESIGN.md has no multiplier: a reader takes each spacing entry as a step, so the unit alone (`base: 0.25rem`) would make 4px the only one. Read back as a token source, the file gives the same colors, radii and spacing steps. A DESIGN.md token names no class, so `check` leaves a length on the scale alone and snaps one off it to the nearest step: `p-[13px]` becomes `p-[12px]`. The test suite runs this round trip. +Tailwind's spacing unit is written as the steps of Tailwind v3's default scale, which v4 still makes: from `"0.5": 0.125rem` to `"96": 24rem`. DESIGN.md has no multiplier: a reader takes each spacing entry as a step, so the unit alone (`base: 0.25rem`) would make 4px the only one. Read back as a token source, the file gives the same colors and radii, and those spacing steps: v4's other steps, such as `p-4.5` or `p-13`, are off the scale for a project that reads only the DESIGN.md. A DESIGN.md token names no class, so `check` leaves a length on the scale alone and snaps one off it to the nearest step: `p-[13px]` becomes `p-[12px]`. The test suite runs this round trip. ## Status colors diff --git a/src/tokens/export.ts b/src/tokens/export.ts index f364f1d9..dad18344 100644 --- a/src/tokens/export.ts +++ b/src/tokens/export.ts @@ -543,7 +543,12 @@ export function toDesignMd(tokens: readonly Token[], options: { name: string }): } // A step the project sets itself (`--spacing-4`) keeps its own value. for (const [key, value] of steps ?? []) if (!values.has(key)) values.set(key, value); - if (values.size) sections[section.key] = values; + // In scale order, with the steps the project sets among the others. + const step = (key: string) => (Number.isFinite(Number(key)) ? Number(key) : Infinity); + const ordered = steps + ? new Map([...values].sort(([a], [b]) => step(a) - step(b) || 0)) + : values; + if (ordered.size) sections[section.key] = ordered; } body.push('', `## ${section.heading}`, '', section.intro, '', ...table(list, keys)); } diff --git a/src/tokens/index.ts b/src/tokens/index.ts index f3c12d75..10441991 100644 --- a/src/tokens/index.ts +++ b/src/tokens/index.ts @@ -171,6 +171,10 @@ export function mergeTokens(tokens: Token[]): Token[] { if (existing.tailwind === undefined && token.tailwind !== undefined) { existing.tailwind = token.tailwind; } + // A DTCG file listed before the stylesheet: the stylesheet knows `-0.025em` is letter spacing. + if (existing.category === 'other' && token.category !== 'other') { + existing.category = token.category; + } existing.usage = [...new Set([...token.usage, ...existing.usage])]; existing.description ??= token.description; if (token.modes) existing.modes = { ...token.modes, ...existing.modes }; diff --git a/test/export.test.ts b/test/export.test.ts index 1c31acc8..3f71d554 100644 --- a/test/export.test.ts +++ b/test/export.test.ts @@ -393,6 +393,23 @@ describe('export, then read back', () => { ]); }); + it('writes the spacing steps in scale order, with those the project sets among them', () => { + const { tokens } = parseCssTokens( + `@theme { + --spacing: 0.25rem; + --spacing-18: 4.5rem; + --spacing-4: 1.1rem; +}`, + 'app/globals.css', + ); + const { text } = toDesignMd(tokens, { name: 'Steps' }); + const spacing = /\nspacing:\n((?: {2}.*\n)+)/.exec(text)?.[1] ?? ''; + const keys = [...spacing.matchAll(/^ {2}"?([^":]+)"?:/gm)].map((m) => Number(m[1])); + expect(keys).toEqual([...keys].sort((a, b) => a - b)); + expect(spacing).toContain(' "4": 1.1rem\n'); + expect(spacing).toContain(' "16": 4rem\n "18": 4.5rem\n "20": 5rem\n'); + }); + it('writes a typography alias as the level it names, not a reference to a whole level', () => { const { tokens } = parseCssTokens( `@theme inline { diff --git a/test/tokens.test.ts b/test/tokens.test.ts index b4aa1cd2..7cb04e0a 100644 --- a/test/tokens.test.ts +++ b/test/tokens.test.ts @@ -413,6 +413,12 @@ describe('CSS custom-property tokens', () => { const json = parseDtcgTokens(toDtcg(css).text, 't.json').tokens; expect(json).toHaveLength(3); expect(mergeTokens([...css, ...json])).toEqual(css); + // Listed the other way round, a merged token takes the stylesheet's category: the DTCG file + // has no type for `-0.025em`, so it reads it as `other`. + expect(json[2]?.category).toBe('other'); + expect(mergeTokens([...json, ...css]).map((t) => t.category)).toEqual( + css.map((t) => t.category), + ); // Two tokens of the same key (`tight`) that make other classes stay apart. const leading = json.filter((t) => t.name === 'leading-tight'); expect(mergeTokens([...css.filter((t) => t.name !== 'leading-tight'), ...leading])).toEqual([