@navecss/tokens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +122 -0
  3. package/TOKENS.md +180 -0
  4. package/dist/breakpoints.d.ts +25 -0
  5. package/dist/breakpoints.js +44 -0
  6. package/dist/build-record.json +77 -0
  7. package/dist/core-contract.json +30 -0
  8. package/dist/lib/bin.d.ts +2 -0
  9. package/dist/lib/bin.js +113 -0
  10. package/dist/lib/builder.d.ts +21 -0
  11. package/dist/lib/builder.js +41 -0
  12. package/dist/lib/collision.d.ts +12 -0
  13. package/dist/lib/collision.js +46 -0
  14. package/dist/lib/composite-value.d.ts +3 -0
  15. package/dist/lib/composite-value.js +152 -0
  16. package/dist/lib/core-version.d.ts +12 -0
  17. package/dist/lib/core-version.js +40 -0
  18. package/dist/lib/dtcg-descriptions.d.ts +2 -0
  19. package/dist/lib/dtcg-descriptions.js +23 -0
  20. package/dist/lib/dtcg-malformed.d.ts +10 -0
  21. package/dist/lib/dtcg-malformed.js +56 -0
  22. package/dist/lib/dtcg-outputs.d.ts +2 -0
  23. package/dist/lib/dtcg-outputs.js +32 -0
  24. package/dist/lib/dtcg-shape.d.ts +13 -0
  25. package/dist/lib/dtcg-shape.js +113 -0
  26. package/dist/lib/errors.d.ts +23 -0
  27. package/dist/lib/errors.js +34 -0
  28. package/dist/lib/facade.d.ts +26 -0
  29. package/dist/lib/facade.js +97 -0
  30. package/dist/lib/formats.d.ts +6 -0
  31. package/dist/lib/formats.js +236 -0
  32. package/dist/lib/json-pointer.d.ts +2 -0
  33. package/dist/lib/json-pointer.js +28 -0
  34. package/dist/lib/overrides.d.ts +2 -0
  35. package/dist/lib/overrides.js +59 -0
  36. package/dist/lib/package-root.d.ts +2 -0
  37. package/dist/lib/package-root.js +31 -0
  38. package/dist/lib/reader.d.ts +8 -0
  39. package/dist/lib/reader.js +136 -0
  40. package/dist/lib/theming/adjacency-source.d.ts +20 -0
  41. package/dist/lib/theming/adjacency-source.js +103 -0
  42. package/dist/lib/theming/adjacency.d.ts +17 -0
  43. package/dist/lib/theming/adjacency.js +133 -0
  44. package/dist/lib/theming/build-record.d.ts +45 -0
  45. package/dist/lib/theming/build-record.js +87 -0
  46. package/dist/lib/theming/build-step.d.ts +14 -0
  47. package/dist/lib/theming/build-step.js +91 -0
  48. package/dist/lib/theming/color-math.d.ts +33 -0
  49. package/dist/lib/theming/color-math.js +125 -0
  50. package/dist/lib/theming/comment-open-tracker.d.ts +1 -0
  51. package/dist/lib/theming/comment-open-tracker.js +49 -0
  52. package/dist/lib/theming/consumer-build.d.ts +18 -0
  53. package/dist/lib/theming/consumer-build.js +29 -0
  54. package/dist/lib/theming/contact-sheet.d.ts +22 -0
  55. package/dist/lib/theming/contact-sheet.js +123 -0
  56. package/dist/lib/theming/contrast.d.ts +31 -0
  57. package/dist/lib/theming/contrast.js +133 -0
  58. package/dist/lib/theming/copy-lint.d.ts +13 -0
  59. package/dist/lib/theming/copy-lint.js +98 -0
  60. package/dist/lib/theming/core-contract.d.ts +29 -0
  61. package/dist/lib/theming/core-contract.js +71 -0
  62. package/dist/lib/theming/core-source.d.ts +2 -0
  63. package/dist/lib/theming/core-source.js +18 -0
  64. package/dist/lib/theming/css-color-4.d.ts +19 -0
  65. package/dist/lib/theming/css-color-4.js +66 -0
  66. package/dist/lib/theming/css-named-colours.d.ts +1 -0
  67. package/dist/lib/theming/css-named-colours.js +151 -0
  68. package/dist/lib/theming/descriptions.d.ts +1 -0
  69. package/dist/lib/theming/descriptions.js +21 -0
  70. package/dist/lib/theming/emit.d.ts +10 -0
  71. package/dist/lib/theming/emit.js +62 -0
  72. package/dist/lib/theming/feedback-signal.d.ts +11 -0
  73. package/dist/lib/theming/feedback-signal.js +23 -0
  74. package/dist/lib/theming/guard-message-probes.d.ts +6 -0
  75. package/dist/lib/theming/guard-message-probes.js +96 -0
  76. package/dist/lib/theming/ladder.d.ts +15 -0
  77. package/dist/lib/theming/ladder.js +81 -0
  78. package/dist/lib/theming/neutral.d.ts +12 -0
  79. package/dist/lib/theming/neutral.js +76 -0
  80. package/dist/lib/theming/on-star.d.ts +11 -0
  81. package/dist/lib/theming/on-star.js +87 -0
  82. package/dist/lib/theming/palette-record.d.ts +2 -0
  83. package/dist/lib/theming/palette-record.js +15 -0
  84. package/dist/lib/theming/pipeline.d.ts +43 -0
  85. package/dist/lib/theming/pipeline.js +104 -0
  86. package/dist/lib/theming/ramp.d.ts +34 -0
  87. package/dist/lib/theming/ramp.js +92 -0
  88. package/dist/lib/theming/seed-channel-split.d.ts +8 -0
  89. package/dist/lib/theming/seed-channel-split.js +99 -0
  90. package/dist/lib/theming/seed-form-parsers-css-color-4.d.ts +8 -0
  91. package/dist/lib/theming/seed-form-parsers-css-color-4.js +85 -0
  92. package/dist/lib/theming/seed-form-parsers.d.ts +6 -0
  93. package/dist/lib/theming/seed-form-parsers.js +155 -0
  94. package/dist/lib/theming/seed-ingest.d.ts +15 -0
  95. package/dist/lib/theming/seed-ingest.js +112 -0
  96. package/dist/lib/theming/seed-input.d.ts +22 -0
  97. package/dist/lib/theming/seed-input.js +1 -0
  98. package/dist/lib/theming/seed-refusal.d.ts +21 -0
  99. package/dist/lib/theming/seed-refusal.js +77 -0
  100. package/dist/lib/theming/semantics.d.ts +19 -0
  101. package/dist/lib/theming/semantics.js +71 -0
  102. package/dist/lib/theming/shipped-seeds.d.ts +2 -0
  103. package/dist/lib/theming/shipped-seeds.js +5 -0
  104. package/dist/lib/theming/step-table.d.ts +5 -0
  105. package/dist/lib/theming/step-table.js +26 -0
  106. package/dist/lib/theming/third-party-provenance.d.ts +9 -0
  107. package/dist/lib/theming/third-party-provenance.js +35 -0
  108. package/dist/lib/theming/tokens-source.d.ts +3 -0
  109. package/dist/lib/theming/tokens-source.js +61 -0
  110. package/dist/lib/token-name.d.ts +2 -0
  111. package/dist/lib/token-name.js +28 -0
  112. package/dist/lib/validate-report.d.ts +19 -0
  113. package/dist/lib/validate-report.js +93 -0
  114. package/dist/lib/validate.d.ts +16 -0
  115. package/dist/lib/validate.js +65 -0
  116. package/dist/palette-record.json +293 -0
  117. package/dist/tokens.css +433 -0
  118. package/dist/tokens.d.ts +90 -0
  119. package/dist/tokens.js +88 -0
  120. package/package.json +85 -0
  121. package/tokens.json +938 -0
@@ -0,0 +1,26 @@
1
+ import type { SeedNormalization } from './theming/color-math.ts';
2
+ export { DuplicateTokenNameRefusal, MissingContractTokensError, TokenCollisionRefusal, UsageError, } from './errors.ts';
3
+ export type { SeedNormalization } from './theming/color-math.ts';
4
+ export type { PerStepOverrides } from './theming/pipeline.ts';
5
+ export { SeedIngestRefusal } from './theming/seed-ingest.ts';
6
+ export interface TokensBuildOptions {
7
+ seed: string;
8
+ source?: string;
9
+ overrides?: string;
10
+ outDir: string;
11
+ }
12
+ export interface TokensBuildResult {
13
+ resolvedSeed: string;
14
+ seedNormalization: SeedNormalization;
15
+ files: string[];
16
+ collidingNames: string[];
17
+ }
18
+ export declare function build(options: TokensBuildOptions): Promise<TokensBuildResult>;
19
+ export interface TokensValidateOptions {
20
+ source: string;
21
+ }
22
+ export interface TokensValidateResult {
23
+ exitCode: 0 | 1 | 2;
24
+ output: string[];
25
+ }
26
+ export declare function validate(options: TokensValidateOptions): Promise<TokensValidateResult>;
@@ -0,0 +1,97 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { writeOutputs } from "./builder.js";
5
+ import { detectCollidingNames, refuseOnCollision } from "./collision.js";
6
+ import { resolveInstalledCoreVersion } from "./core-version.js";
7
+ import { composeDtcgOutputs } from "./dtcg-outputs.js";
8
+ import { MissingContractTokensError, UsageError } from "./errors.js";
9
+ import { readOverrides } from "./overrides.js";
10
+ import { findPackageRoot } from "./package-root.js";
11
+ import { formatOklch } from "./theming/color-math.js";
12
+ import { composeConsumerBuild } from "./theming/consumer-build.js";
13
+ import { checkManifestVersionSkew, validateAgainstManifest } from "./theming/core-contract.js";
14
+ import { shippedThemingPropertyNames } from "./theming/emit.js";
15
+ import { parseSeed } from "./theming/seed-ingest.js";
16
+ import { SHIPPED_SEEDS } from "./theming/shipped-seeds.js";
17
+ import { checkManifestFormatSupport, formatValidateReport } from "./validate-report.js";
18
+ import { computeMissing, detectSourceKind, namesFromSource, splitMissingBySupply, } from "./validate.js";
19
+ export { DuplicateTokenNameRefusal, MissingContractTokensError, TokenCollisionRefusal, UsageError, } from "./errors.js";
20
+ export { SeedIngestRefusal } from "./theming/seed-ingest.js";
21
+ const PACKAGE_ROOT = findPackageRoot(import.meta.url);
22
+ async function readManifest() {
23
+ const raw = await readFile(path.join(PACKAGE_ROOT, 'dist', 'core-contract.json'), 'utf8');
24
+ return JSON.parse(raw);
25
+ }
26
+ export async function build(options) {
27
+ const sourcePath = options.source ?? path.join(PACKAGE_ROOT, 'tokens.json');
28
+ if (!existsSync(sourcePath)) {
29
+ throw new UsageError(`no token source found at "${sourcePath}"`);
30
+ }
31
+ const overrides = await readOverrides(options.overrides);
32
+ const manifest = await readManifest();
33
+ const primary = parseSeed(options.seed);
34
+ const sourceContent = await readFile(sourcePath, 'utf8');
35
+ const sourceNames = namesFromSource('json', sourceContent, { path: sourcePath });
36
+ const emittedNames = new Set([...sourceNames, ...shippedThemingPropertyNames()]);
37
+ const missing = validateAgainstManifest(manifest, emittedNames);
38
+ if (missing.length > 0)
39
+ throw new MissingContractTokensError(missing);
40
+ const theming = composeConsumerBuild({
41
+ seedInputs: { primary },
42
+ seeds: {
43
+ danger: SHIPPED_SEEDS.danger,
44
+ declaredTintHue: SHIPPED_SEEDS.declaredTintHue,
45
+ primary: primary.value,
46
+ },
47
+ overrides,
48
+ outDir: options.outDir,
49
+ });
50
+ const dtcgOutputs = await composeDtcgOutputs(sourcePath, options.outDir);
51
+ const cssDestination = path.join(options.outDir, 'tokens.css');
52
+ const collidingNames = detectCollidingNames(dtcgOutputs, cssDestination, theming.css);
53
+ refuseOnCollision(collidingNames);
54
+ const merged = dtcgOutputs.map((file) => file.destination === cssDestination
55
+ ? { ...file, content: `${file.content}\n${theming.css}` }
56
+ : file);
57
+ const allFiles = [...merged, ...theming.files];
58
+ await writeOutputs(allFiles);
59
+ return {
60
+ resolvedSeed: formatOklch(theming.primaryRecord.usedSeed),
61
+ seedNormalization: theming.primaryRecord.normalization,
62
+ files: allFiles
63
+ .map((file) => path.relative(options.outDir, file.destination))
64
+ .toSorted((a, b) => a.localeCompare(b)),
65
+ collidingNames,
66
+ };
67
+ }
68
+ export async function validate(options) {
69
+ const kind = detectSourceKind(options.source);
70
+ if (!kind) {
71
+ throw new UsageError(`"${options.source}" is neither a .css nor a .json file: validate reads the extension and never guesses the format.`);
72
+ }
73
+ if (!existsSync(options.source)) {
74
+ throw new UsageError(`no file found at "${options.source}"`);
75
+ }
76
+ const manifest = await readManifest();
77
+ const content = await readFile(options.source, 'utf8');
78
+ const notDeclared = computeMissing(manifest, kind, content, { path: options.source });
79
+ const { missing, supplied } = splitMissingBySupply(kind, notDeclared);
80
+ const coreProbe = await resolveInstalledCoreVersion();
81
+ const versionSkew = coreProbe.status === 'resolved'
82
+ ? checkManifestVersionSkew(manifest, coreProbe.version)
83
+ : undefined;
84
+ const formatRefusal = checkManifestFormatSupport(manifest);
85
+ const output = formatValidateReport(manifest, missing, versionSkew, { ...coreProbe, supplied });
86
+ let exitCode;
87
+ if (formatRefusal !== undefined)
88
+ exitCode = 1;
89
+ else if (coreProbe.status === 'unreadable')
90
+ exitCode = 2;
91
+ else if (versionSkew !== undefined || missing.length > 0 || coreProbe.status === 'unresolvable') {
92
+ exitCode = 1;
93
+ }
94
+ else
95
+ exitCode = 0;
96
+ return { exitCode, output };
97
+ }
@@ -0,0 +1,6 @@
1
+ import type { FlatToken } from './reader.ts';
2
+ export declare function formatCssTokens(tokens: FlatToken[], layer?: string): string;
3
+ export declare function formatJsTokens(tokens: FlatToken[]): string;
4
+ export declare function formatTsDeclarations(tokens: FlatToken[]): string;
5
+ export declare function formatJsBreakpoints(tokens: FlatToken[]): string;
6
+ export declare function formatTsBreakpoints(tokens: FlatToken[]): string;
@@ -0,0 +1,236 @@
1
+ const HEADER = '/* Nave Design System — generated, do not edit */';
2
+ const LAYER_ORDER_STATEMENT = '@layer tokens.defaults, tokens.presets, reset, atomic, components.nave, components.consumer, overrides;';
3
+ const PREFIX = '--nave-';
4
+ const camelCase = (str) => str.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
5
+ const toEm = (px) => `${Number((px / 16).toFixed(4))}em`;
6
+ const AT_PROPERTY_SYNTAX = {
7
+ color: '<color>',
8
+ dimension: '<length>',
9
+ number: '<number>',
10
+ duration: '<time>',
11
+ fontWeight: '<number>',
12
+ };
13
+ const ABSOLUTE_LENGTH = /^-?(?:\d+|\d*\.\d+)(?:px|cm|mm|in|pt|pc|q)?$/i;
14
+ const isPublic = (token) => !token.path.includes('_primitive') && token.path[0] !== 'breakpoint';
15
+ const isReducibleDuration = (token) => token.type === 'duration' &&
16
+ token.path[0] === 'motion' &&
17
+ token.name !== 'motion-duration-instant';
18
+ function isRegistrable(token) {
19
+ const syntax = AT_PROPERTY_SYNTAX[token.type];
20
+ if (syntax === undefined)
21
+ return false;
22
+ if (syntax !== '<length>')
23
+ return true;
24
+ return ABSOLUTE_LENGTH.test(String(token.value).trim());
25
+ }
26
+ function buildAtProperty(token) {
27
+ return [
28
+ ` @property ${PREFIX}${token.name} {`,
29
+ ` syntax: '${AT_PROPERTY_SYNTAX[token.type]}';`,
30
+ ` inherits: true;`,
31
+ ` initial-value: ${token.value};`,
32
+ ` }`,
33
+ ].join('\n');
34
+ }
35
+ const buildCustomProp = (token) => ` ${PREFIX}${token.name}: ${token.value};`;
36
+ export function formatCssTokens(tokens, layer = 'tokens.defaults') {
37
+ const allPublic = tokens.filter((t) => isPublic(t));
38
+ const atPropertyBlock = allPublic
39
+ .filter((t) => isRegistrable(t))
40
+ .map((t) => buildAtProperty(t))
41
+ .join('\n\n');
42
+ const customPropsBlock = allPublic.map((t) => buildCustomProp(t)).join('\n');
43
+ const reducedMotionBlock = allPublic
44
+ .filter((t) => isReducibleDuration(t))
45
+ .map((t) => ` ${PREFIX}${t.name}: 0.01ms;`)
46
+ .join('\n');
47
+ return [
48
+ HEADER,
49
+ LAYER_ORDER_STATEMENT,
50
+ '',
51
+ '/**',
52
+ ' * @property declarations provide:',
53
+ ' * - Type safety (browsers reject invalid values)',
54
+ ' * - CSS transitions on custom properties (e.g. color tokens)',
55
+ ' * - IDE autocomplete in supported editors',
56
+ ' *',
57
+ ' * Only tokens whose initial-value is computationally independent are',
58
+ ' * registered. Font-relative values (rem/em) cannot be, so those tokens',
59
+ ' * ship as plain custom properties below.',
60
+ ' */',
61
+ atPropertyBlock,
62
+ '',
63
+ '/**',
64
+ ` * Token values scoped under @layer ${layer}.`,
65
+ ' * Override in @layer overrides — no specificity battles, ever.',
66
+ ' */',
67
+ `@layer ${layer} {`,
68
+ ' :root {',
69
+ customPropsBlock,
70
+ ' }',
71
+ '',
72
+ ' /**',
73
+ ' * Collapse all motion durations for users who prefer reduced motion.',
74
+ ' * Any component using --nave-motion-duration-* gets this for free.',
75
+ ' */',
76
+ ' @media (prefers-reduced-motion: reduce) {',
77
+ ' :root {',
78
+ reducedMotionBlock,
79
+ ' }',
80
+ ' }',
81
+ '}',
82
+ '',
83
+ ].join('\n');
84
+ }
85
+ export function formatJsTokens(tokens) {
86
+ const entries = tokens
87
+ .filter((t) => isPublic(t))
88
+ .map((t) => ` '${PREFIX}${t.name}': ${JSON.stringify(t.value)},`)
89
+ .join('\n');
90
+ return [
91
+ HEADER,
92
+ "// This file is the JavaScript entry of @navecss/tokens. For the stylesheet, use: @import '@navecss/tokens/css';",
93
+ '',
94
+ '/** The DTCG 2025.10 token set as a typed const object (e.g. `--nave-font-size-md`).',
95
+ ' * Keys are CSS custom property names. The semantic colour layer',
96
+ ' * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css`',
97
+ ' * and is not part of this export.',
98
+ ' */',
99
+ 'export const tokens = {',
100
+ entries,
101
+ '}',
102
+ '',
103
+ ].join('\n');
104
+ }
105
+ export function formatTsDeclarations(tokens) {
106
+ const keys = tokens
107
+ .filter((t) => isPublic(t))
108
+ .map((t) => ` '${PREFIX}${t.name}': ${typeof t.value === 'number' ? 'number' : 'string'}`)
109
+ .join('\n');
110
+ return [
111
+ HEADER,
112
+ '',
113
+ '/** The DTCG 2025.10 token set as a typed const object (e.g. `--nave-font-size-md`).',
114
+ ' * Keys are CSS custom property names. The semantic colour layer',
115
+ ' * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css`',
116
+ ' * and is not part of this export.',
117
+ ' */',
118
+ 'export declare const tokens: {',
119
+ keys,
120
+ '}',
121
+ '',
122
+ 'export declare type TokenName = keyof typeof tokens',
123
+ 'export declare type TokenValue = typeof tokens[TokenName]',
124
+ '',
125
+ ].join('\n');
126
+ }
127
+ function buildBreakpointMap(tokens) {
128
+ const bpMap = {};
129
+ const bpTokens = tokens.filter((t) => t.path[0] === 'breakpoint');
130
+ for (const t of bpTokens) {
131
+ const key = camelCase(t.path.slice(1).join('-'));
132
+ bpMap[key] = t.value;
133
+ }
134
+ return bpMap;
135
+ }
136
+ export function formatJsBreakpoints(tokens) {
137
+ const bpMap = buildBreakpointMap(tokens);
138
+ if (Object.keys(bpMap).length === 0)
139
+ return '// No breakpoints defined\n';
140
+ const valuesLines = Object.entries(bpMap)
141
+ .map(([k, v]) => ` ${k}: ${v}`)
142
+ .join(',\n');
143
+ const emLines = Object.entries(bpMap)
144
+ .map(([k, v]) => ` ${k}: '${toEm(v)}'`)
145
+ .join(',\n');
146
+ const mediaLines = [];
147
+ if (bpMap.tabletPortrait !== undefined) {
148
+ mediaLines.push(` phoneOnly: '(width < ${toEm(bpMap.tabletPortrait)})'`, ` tabletPortraitUp: '(width >= ${toEm(bpMap.tabletPortrait)})'`);
149
+ }
150
+ if (bpMap.tabletLandscape !== undefined) {
151
+ mediaLines.push(` tabletLandscapeUp: '(width >= ${toEm(bpMap.tabletLandscape)})'`);
152
+ }
153
+ if (bpMap.desktop !== undefined) {
154
+ mediaLines.push(` desktopUp: '(width >= ${toEm(bpMap.desktop)})'`);
155
+ }
156
+ if (bpMap.wide !== undefined) {
157
+ mediaLines.push(` wideUp: '(width >= ${toEm(bpMap.wide)})'`);
158
+ }
159
+ return `/**
160
+ * Nave breakpoints — generated from tokens.json
161
+ * Do not edit directly. Run \`pnpm build\` in packages/tokens to regenerate.
162
+ *
163
+ * breakpoints — raw px boundary values (number, no unit)
164
+ * These are the design decisions. Edit tokens.json to change them.
165
+ *
166
+ * em — boundary values in em units (base: 16px)
167
+ * For reference. The media strings below use these values.
168
+ *
169
+ * media — pre-built Level 4 media query condition strings
170
+ * Use as @media keys in atoms.ts and consumer atoms.
171
+ * Never write breakpoint values as magic numbers in your codebase.
172
+ *
173
+ * Syntax: Level 4 range queries — (width < Xem) / (width >= Xem)
174
+ * phoneOnly and tabletPortraitUp share the same boundary value (37.5em).
175
+ * < is exclusive upper bound, >= is inclusive lower bound.
176
+ * No overlap, no gap, no rounding issues at any device pixel ratio.
177
+ *
178
+ * Why em: resolves against the browser's default font size, a user setting
179
+ * (not page zoom, which scales px and em alike).
180
+ */
181
+
182
+ export const breakpoints = {
183
+ ${valuesLines}
184
+ }
185
+
186
+ export const em = {
187
+ ${emLines}
188
+ }
189
+
190
+ export const media = {
191
+ ${mediaLines.join(',\n')}
192
+ }
193
+ `;
194
+ }
195
+ export function formatTsBreakpoints(tokens) {
196
+ const bpMap = buildBreakpointMap(tokens);
197
+ if (Object.keys(bpMap).length === 0)
198
+ return '// No breakpoints defined\n';
199
+ const bpDecls = Object.entries(bpMap)
200
+ .map(([k, v]) => ` readonly ${k}: ${v}`)
201
+ .join('\n');
202
+ const emDecls = Object.entries(bpMap)
203
+ .map(([k, v]) => ` readonly ${k}: '${toEm(v)}'`)
204
+ .join('\n');
205
+ const mediaDecls = [];
206
+ if (bpMap.tabletPortrait !== undefined) {
207
+ mediaDecls.push(` readonly phoneOnly: '(width < ${toEm(bpMap.tabletPortrait)})'`, ` readonly tabletPortraitUp: '(width >= ${toEm(bpMap.tabletPortrait)})'`);
208
+ }
209
+ if (bpMap.tabletLandscape !== undefined) {
210
+ mediaDecls.push(` readonly tabletLandscapeUp: '(width >= ${toEm(bpMap.tabletLandscape)})'`);
211
+ }
212
+ if (bpMap.desktop !== undefined) {
213
+ mediaDecls.push(` readonly desktopUp: '(width >= ${toEm(bpMap.desktop)})'`);
214
+ }
215
+ if (bpMap.wide !== undefined) {
216
+ mediaDecls.push(` readonly wideUp: '(width >= ${toEm(bpMap.wide)})'`);
217
+ }
218
+ return [
219
+ HEADER,
220
+ '',
221
+ 'export declare const breakpoints: {',
222
+ bpDecls,
223
+ '}',
224
+ 'export declare type BreakpointName = keyof typeof breakpoints',
225
+ '',
226
+ 'export declare const em: {',
227
+ emDecls,
228
+ '}',
229
+ '',
230
+ 'export declare const media: {',
231
+ mediaDecls.join('\n'),
232
+ '}',
233
+ 'export declare type MediaName = keyof typeof media',
234
+ '',
235
+ ].join('\n');
236
+ }
@@ -0,0 +1,2 @@
1
+ export declare function resolveTokenPointer(pointer: string, at: string, isToken: (path: string) => boolean): string;
2
+ export declare function refuseBothReferenceForms(at: string): never;
@@ -0,0 +1,28 @@
1
+ import { READER, SPEC_POINTER } from "./dtcg-shape.js";
2
+ const unescapeSegment = (segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~');
3
+ function pointerToPath(pointer, at) {
4
+ const body = pointer.startsWith('#') ? pointer.slice(1) : pointer;
5
+ if (body !== '' && !body.startsWith('/')) {
6
+ throw new TypeError(`${READER} "${at}" carries a $ref that is not a JSON Pointer ("${pointer}"); a pointer starts at the document root, as in "#/spacing/md". ${SPEC_POINTER}`);
7
+ }
8
+ return body
9
+ .split('/')
10
+ .slice(1)
11
+ .map((segment) => unescapeSegment(segment));
12
+ }
13
+ export function resolveTokenPointer(pointer, at, isToken) {
14
+ const segments = pointerToPath(pointer, at);
15
+ const target = segments.join('.');
16
+ if (isToken(target))
17
+ return target;
18
+ for (let end = segments.length - 1; end > 0; end--) {
19
+ const prefix = segments.slice(0, end).join('.');
20
+ if (isToken(prefix)) {
21
+ throw new TypeError(`${READER} "${at}" carries a $ref pointing into a sub-value of "${prefix}" ("${pointer}"), not at a whole token. Point at the token itself, or give the inner value a token of its own and point at that. ${SPEC_POINTER}`);
22
+ }
23
+ }
24
+ throw new TypeError(`${READER} "${at}" carries a $ref to "${pointer}", which names no token in this source. ${SPEC_POINTER}`);
25
+ }
26
+ export function refuseBothReferenceForms(at) {
27
+ throw new TypeError(`${READER} "${at}" carries both a $ref pointer and its own $value. A node states its value one way or the other: keep one and delete the other. ${SPEC_POINTER}`);
28
+ }
@@ -0,0 +1,2 @@
1
+ import type { PerStepOverrides } from './theming/pipeline.ts';
2
+ export declare function readOverrides(filePath: string | undefined): Promise<PerStepOverrides>;
@@ -0,0 +1,59 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { UsageError } from "./errors.js";
3
+ import { OVERRIDE_SCALE_NAMES } from "./theming/pipeline.js";
4
+ const OVERRIDE_STEP_NAMES = new Set(OVERRIDE_SCALE_NAMES);
5
+ function describeJsonValue(value) {
6
+ if (value === null)
7
+ return 'null';
8
+ if (Array.isArray(value))
9
+ return 'an array';
10
+ return typeof value;
11
+ }
12
+ function isPlainObject(value) {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+ function validatePerStepOverrides(value, filePath) {
16
+ if (!isPlainObject(value)) {
17
+ throw new UsageError(`the per-step override file at "${filePath}" must be a JSON object, got ${describeJsonValue(value)}.`);
18
+ }
19
+ for (const [step, stepOverrides] of Object.entries(value)) {
20
+ if (!OVERRIDE_STEP_NAMES.has(step)) {
21
+ throw new UsageError(`the per-step override file at "${filePath}" names an unrecognised step "${step}" ` +
22
+ `(expected one of: ${OVERRIDE_SCALE_NAMES.join(', ')}).`);
23
+ }
24
+ if (!isPlainObject(stepOverrides)) {
25
+ throw new UsageError(`the per-step override file at "${filePath}"'s "${step}" entry must be an object ` +
26
+ `mapping step numbers to override amounts, got ${describeJsonValue(stepOverrides)}.`);
27
+ }
28
+ for (const [rung, amount] of Object.entries(stepOverrides)) {
29
+ if (!Number.isFinite(Number(rung))) {
30
+ throw new UsageError(`the per-step override file at "${filePath}"'s "${step}" entry has a non-numeric ` +
31
+ `step key "${rung}".`);
32
+ }
33
+ if (typeof amount !== 'number' || !Number.isFinite(amount)) {
34
+ throw new UsageError(`the per-step override file at "${filePath}"'s "${step}.${rung}" value must be a ` +
35
+ `finite number, got ${describeJsonValue(amount)}.`);
36
+ }
37
+ }
38
+ }
39
+ return value;
40
+ }
41
+ export async function readOverrides(filePath) {
42
+ if (!filePath)
43
+ return {};
44
+ let raw;
45
+ try {
46
+ raw = await readFile(filePath, 'utf8');
47
+ }
48
+ catch (error) {
49
+ throw new UsageError(`could not read the per-step override file at "${filePath}": ${error.message}`);
50
+ }
51
+ let parsed;
52
+ try {
53
+ parsed = JSON.parse(raw);
54
+ }
55
+ catch (error) {
56
+ throw new UsageError(`the per-step override file at "${filePath}" is not valid JSON: ${error.message}`);
57
+ }
58
+ return validatePerStepOverrides(parsed, filePath);
59
+ }
@@ -0,0 +1,2 @@
1
+ export declare const OWN_PACKAGE_NAME = "@navecss/tokens";
2
+ export declare function findPackageRoot(fromUrl: string, expectedName?: string): string;
@@ -0,0 +1,31 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ export const OWN_PACKAGE_NAME = '@navecss/tokens';
5
+ function isPackageNamed(manifestPath, expectedName) {
6
+ if (!existsSync(manifestPath))
7
+ return false;
8
+ try {
9
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
10
+ return manifest.name === expectedName;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ export function findPackageRoot(fromUrl, expectedName = OWN_PACKAGE_NAME) {
17
+ let dir = path.dirname(fileURLToPath(fromUrl));
18
+ for (;;) {
19
+ if (isPackageNamed(path.join(dir, 'package.json'), expectedName))
20
+ return dir;
21
+ const parent = path.dirname(dir);
22
+ if (parent === dir) {
23
+ throw new Error(`could not locate ${expectedName}'s own package.json above ${fromUrl}: this module is ` +
24
+ `running outside its own installed directory, which happens when ${expectedName} is ` +
25
+ `bundled into another package's output or copied out of node_modules. Its entry points ` +
26
+ `read files from inside their own installed directory and cannot be bundled; ` +
27
+ `import them as external instead.`);
28
+ }
29
+ dir = parent;
30
+ }
31
+ }
@@ -0,0 +1,8 @@
1
+ export { kebabName } from './token-name.ts';
2
+ export interface FlatToken {
3
+ readonly path: readonly string[];
4
+ readonly name: string;
5
+ readonly type: string;
6
+ readonly value: string | number;
7
+ }
8
+ export declare function readTokens(source: unknown, sourceName?: string): FlatToken[];
@@ -0,0 +1,136 @@
1
+ import { renderTokenValue } from "./composite-value.js";
2
+ import { classifyMalformed, formatUnreadableNodesRefusal } from "./dtcg-malformed.js";
3
+ import { classifyDraftShape, refuseTypeReason } from "./dtcg-shape.js";
4
+ import { DtcgShapeRefusal } from "./errors.js";
5
+ import { refuseBothReferenceForms, resolveTokenPointer } from "./json-pointer.js";
6
+ import { nameFromPath } from "./token-name.js";
7
+ export { kebabName } from "./token-name.js";
8
+ const REF_RE = /^\{([^{}]+)\}$/;
9
+ function isPlainObject(value) {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
11
+ }
12
+ function readTokenNode(node, path, inheritedType) {
13
+ const hasValue = '$value' in node;
14
+ const hasRef = typeof node.$ref === 'string';
15
+ if (!hasValue && !hasRef)
16
+ return undefined;
17
+ const at = path.join('.');
18
+ if (hasValue && hasRef)
19
+ refuseBothReferenceForms(at);
20
+ const type = typeof node.$type === 'string' ? node.$type : inheritedType;
21
+ if (type === undefined) {
22
+ throw new TypeError(`DTCG 2025.10 reader: no resolvable $type for token "${at}"`);
23
+ }
24
+ const token = { path, rawValue: node.$value, type };
25
+ if (hasRef)
26
+ token.ref = node.$ref;
27
+ return token;
28
+ }
29
+ function collectRawTokens(node, path, inheritedType, out) {
30
+ if (!isPlainObject(node)) {
31
+ throw new TypeError(`DTCG 2025.10 reader: expected an object at "${path.join('.')}", got ${JSON.stringify(node)}`);
32
+ }
33
+ if ('value' in node && !('$value' in node)) {
34
+ throw new TypeError(`DTCG 2025.10 reader: legacy token shape at "${path.join('.')}" — carries "value"/"type" ` +
35
+ `instead of "$value"/"$type"`);
36
+ }
37
+ const token = readTokenNode(node, path, inheritedType);
38
+ if (token !== undefined) {
39
+ out.push(token);
40
+ return;
41
+ }
42
+ const groupType = typeof node.$type === 'string' ? node.$type : inheritedType;
43
+ for (const [key, child] of Object.entries(node)) {
44
+ if (key.startsWith('$'))
45
+ continue;
46
+ collectRawTokens(child, [...path, key], groupType, out);
47
+ }
48
+ }
49
+ function classifyUnreadableNode(token) {
50
+ const at = token.path.join('.');
51
+ const typeRefusal = refuseTypeReason(token.type, at);
52
+ if (typeRefusal !== undefined)
53
+ return { kind: 'typeRefusal', typeRefusal };
54
+ if (token.ref !== undefined)
55
+ return undefined;
56
+ const draft = classifyDraftShape(at, token.type, token.rawValue);
57
+ if (draft !== undefined)
58
+ return { draft, kind: 'draft' };
59
+ const malformed = classifyMalformed(at, token.type, token.rawValue);
60
+ return malformed === undefined ? undefined : { kind: 'malformed', malformed };
61
+ }
62
+ function refuseUnreadableNodes(raw, sourceName) {
63
+ const drafts = [];
64
+ const malformed = [];
65
+ const typeRefusals = [];
66
+ for (const token of raw) {
67
+ const found = classifyUnreadableNode(token);
68
+ if (found === undefined)
69
+ continue;
70
+ if (found.kind === 'typeRefusal')
71
+ typeRefusals.push(found.typeRefusal);
72
+ else if (found.kind === 'draft')
73
+ drafts.push(found.draft);
74
+ else
75
+ malformed.push(found.malformed);
76
+ }
77
+ if (typeRefusals.length > 0) {
78
+ throw new DtcgShapeRefusal(typeRefusals.join('\n'), []);
79
+ }
80
+ if (drafts.length > 0 || malformed.length > 0) {
81
+ throw new DtcgShapeRefusal(formatUnreadableNodesRefusal(drafts, malformed, sourceName), drafts);
82
+ }
83
+ }
84
+ function resolveReference(key, targetPath, context) {
85
+ if (context.visiting.has(key)) {
86
+ throw new TypeError(`DTCG 2025.10 reader: reference cycle detected at "${key}" -> "${targetPath}"`);
87
+ }
88
+ const target = context.byPath.get(targetPath);
89
+ if (!target) {
90
+ throw new TypeError(`DTCG 2025.10 reader: "${key}" references unresolved target "${targetPath}"`);
91
+ }
92
+ context.visiting.add(key);
93
+ const value = resolveValue(target, context);
94
+ context.visiting.delete(key);
95
+ return value;
96
+ }
97
+ function resolveValue(token, context) {
98
+ const key = token.path.join('.');
99
+ const cached = context.resolved.get(key);
100
+ if (cached !== undefined)
101
+ return cached;
102
+ let value;
103
+ if (token.ref !== undefined) {
104
+ const targetPath = resolveTokenPointer(token.ref, key, (path) => context.byPath.has(path));
105
+ value = resolveReference(key, targetPath, context);
106
+ }
107
+ else if (typeof token.rawValue === 'string' && REF_RE.test(token.rawValue)) {
108
+ value = resolveReference(key, REF_RE.exec(token.rawValue)[1], context);
109
+ }
110
+ else if (typeof token.rawValue === 'string' &&
111
+ (token.rawValue.includes('{') || token.rawValue.includes('}'))) {
112
+ throw new TypeError(`DTCG 2025.10 reader: "${key}" embeds a reference inside a larger string ("${token.rawValue}") — ` +
113
+ 'only a whole-value alias (curly braces around one dotted path, nothing else) is supported');
114
+ }
115
+ else {
116
+ value = renderTokenValue(token.type, token.rawValue, key);
117
+ }
118
+ context.resolved.set(key, value);
119
+ return value;
120
+ }
121
+ export function readTokens(source, sourceName) {
122
+ const raw = [];
123
+ collectRawTokens(source, [], undefined, raw);
124
+ refuseUnreadableNodes(raw, sourceName);
125
+ const context = {
126
+ byPath: new Map(raw.map((t) => [t.path.join('.'), t])),
127
+ resolved: new Map(),
128
+ visiting: new Set(),
129
+ };
130
+ return raw.map((token) => ({
131
+ path: token.path,
132
+ name: nameFromPath(token.path),
133
+ type: token.type,
134
+ value: resolveValue(token, context),
135
+ }));
136
+ }
@@ -0,0 +1,20 @@
1
+ type PairClass = 'text' | 'non-text';
2
+ export interface Adjacency {
3
+ subject: string;
4
+ against: string;
5
+ class: PairClass;
6
+ }
7
+ export declare function materializeAdjacency(source: unknown): Adjacency[];
8
+ export declare const ADJACENCY: readonly Adjacency[];
9
+ export declare const ADJACENCY_EXCLUSIONS: Readonly<Record<string, string>>;
10
+ export declare const ADJACENCY_OPEN: Readonly<Record<string, string>>;
11
+ declare const measuredOpenSlotsBrand: unique symbol;
12
+ export type MeasuredOpenSlots = readonly string[] & {
13
+ readonly [measuredOpenSlotsBrand]: true;
14
+ };
15
+ export interface OrphanCheckResult {
16
+ open: MeasuredOpenSlots;
17
+ excluded: readonly string[];
18
+ }
19
+ export declare function assertNoOrphanedSemanticSlot(slots: readonly string[], adjacency?: readonly Adjacency[], exclusions?: Readonly<Record<string, string>>, open?: Readonly<Record<string, string>>): OrphanCheckResult;
20
+ export {};