@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,21 @@
1
+ import type { FlatToken } from './reader.ts';
2
+ interface BuildFile {
3
+ destination: string;
4
+ format: (tokens: FlatToken[]) => string;
5
+ filter?: (token: FlatToken) => boolean;
6
+ }
7
+ interface BuildPlatform {
8
+ buildPath: string;
9
+ files: BuildFile[];
10
+ }
11
+ export interface BuildConfig {
12
+ source: string[];
13
+ platforms: Record<string, BuildPlatform>;
14
+ }
15
+ export interface OutputFile {
16
+ destination: string;
17
+ content: string;
18
+ }
19
+ export declare function composeBuild(config: BuildConfig): Promise<OutputFile[]>;
20
+ export declare function writeOutputs(files: readonly OutputFile[]): Promise<void>;
21
+ export {};
@@ -0,0 +1,41 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { detectDuplicateEmittedNames, refuseOnDuplicateNames } from "./collision.js";
4
+ import { readTokens } from "./reader.js";
5
+ export async function composeBuild(config) {
6
+ const tokens = [];
7
+ for (const source of config.source) {
8
+ const raw = JSON.parse(await readFile(source, 'utf8'));
9
+ tokens.push(...readTokens(raw, source));
10
+ }
11
+ refuseOnDuplicateNames(detectDuplicateEmittedNames(tokens));
12
+ const outputs = [];
13
+ for (const platform of Object.values(config.platforms)) {
14
+ for (const file of platform.files) {
15
+ const filter = file.filter;
16
+ const scoped = filter ? tokens.filter((token) => filter(token)) : tokens;
17
+ outputs.push({
18
+ destination: path.join(platform.buildPath, file.destination),
19
+ content: file.format(scoped),
20
+ });
21
+ }
22
+ }
23
+ return outputs;
24
+ }
25
+ export async function writeOutputs(files) {
26
+ const staged = [];
27
+ try {
28
+ for (const file of files) {
29
+ await mkdir(path.dirname(file.destination), { recursive: true });
30
+ const temporary = `${file.destination}.${process.pid}.tmp`;
31
+ await writeFile(temporary, file.content);
32
+ staged.push({ destination: file.destination, temporary });
33
+ }
34
+ for (const { destination, temporary } of staged)
35
+ await rename(temporary, destination);
36
+ }
37
+ finally {
38
+ for (const { temporary } of staged)
39
+ await rm(temporary, { force: true });
40
+ }
41
+ }
@@ -0,0 +1,12 @@
1
+ import type { OutputFile } from './builder.ts';
2
+ import type { FlatToken } from './reader.ts';
3
+ export declare function detectCollidingNames(dtcgOutputs: readonly OutputFile[], cssDestination: string, themingCss: string): string[];
4
+ export declare function refuseOnCollision(collidingNames: readonly string[]): void;
5
+ export declare function detectDuplicateEmittedNames(tokens: readonly FlatToken[]): {
6
+ name: string;
7
+ paths: string[];
8
+ }[];
9
+ export declare function refuseOnDuplicateNames(duplicates: readonly {
10
+ name: string;
11
+ paths: string[];
12
+ }[]): void;
@@ -0,0 +1,46 @@
1
+ import { DuplicateTokenNameRefusal, TokenCollisionRefusal } from "./errors.js";
2
+ import { scanDeclaredCustomProperties } from "./validate.js";
3
+ export function detectCollidingNames(dtcgOutputs, cssDestination, themingCss) {
4
+ const dtcgCssFile = dtcgOutputs.find((file) => file.destination === cssDestination);
5
+ const dtcgNames = dtcgCssFile
6
+ ? scanDeclaredCustomProperties(dtcgCssFile.content)
7
+ : new Set();
8
+ const themingNames = scanDeclaredCustomProperties(themingCss);
9
+ return [...dtcgNames]
10
+ .filter((name) => themingNames.has(name))
11
+ .toSorted((a, b) => a.localeCompare(b));
12
+ }
13
+ export function refuseOnCollision(collidingNames) {
14
+ if (collidingNames.length === 0)
15
+ return;
16
+ const plural = collidingNames.length === 1 ? '' : 's';
17
+ throw new TokenCollisionRefusal(`Your token source and the generated theming layer both declare ${collidingNames.length} ` +
18
+ `name${plural} (${collidingNames.join(', ')}). Nothing was written. Declare an override ` +
19
+ `for ${plural ? 'them' : 'it'} in your own \`@layer overrides\` instead: it wins over ` +
20
+ `both halves regardless of order, and nothing here needs renaming.`);
21
+ }
22
+ export function detectDuplicateEmittedNames(tokens) {
23
+ const pathsByName = new Map();
24
+ for (const token of tokens) {
25
+ const paths = pathsByName.get(token.name) ?? [];
26
+ paths.push(token.path.join('.'));
27
+ pathsByName.set(token.name, paths);
28
+ }
29
+ return pathsByName
30
+ .entries()
31
+ .filter(([, paths]) => paths.length > 1)
32
+ .map(([name, paths]) => ({ name, paths: paths.toSorted((a, b) => a.localeCompare(b)) }))
33
+ .toArray()
34
+ .toSorted((a, b) => a.name.localeCompare(b.name));
35
+ }
36
+ export function refuseOnDuplicateNames(duplicates) {
37
+ if (duplicates.length === 0)
38
+ return;
39
+ const plural = duplicates.length === 1 ? '' : 's';
40
+ const detail = duplicates
41
+ .map((d) => `--nave-${d.name} (from ${d.paths.join(' and ')})`)
42
+ .join('; ');
43
+ throw new DuplicateTokenNameRefusal(`Your token source has ${duplicates.length} name${plural} that resolve${plural ? '' : 's'} ` +
44
+ `to the same emitted custom property after name computation: ${detail}. Nothing was ` +
45
+ `written. Rename one of the colliding paths in your token source.`);
46
+ }
@@ -0,0 +1,3 @@
1
+ export declare function renderShadow(value: unknown): string;
2
+ export declare function unreadableValueReason(type: string, raw: unknown): string | undefined;
3
+ export declare function renderTokenValue(type: string, raw: unknown, at: string): string | number;
@@ -0,0 +1,152 @@
1
+ function isPlainObject(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ const COLOR_FUNCTION_SPACES = new Set([
5
+ 'a98-rgb',
6
+ 'display-p3',
7
+ 'prophoto-rgb',
8
+ 'rec2020',
9
+ 'srgb',
10
+ 'srgb-linear',
11
+ 'xyz',
12
+ 'xyz-d50',
13
+ 'xyz-d65',
14
+ ]);
15
+ const FUNCTION_SPACES = new Map([
16
+ ['hsl', ['', '%', '%']],
17
+ ['hwb', ['', '%', '%']],
18
+ ['lab', ['', '', '']],
19
+ ['lch', ['', '', '']],
20
+ ['oklab', ['', '', '']],
21
+ ['oklch', ['', '', '']],
22
+ ]);
23
+ const DIMENSION_UNITS = new Set(['em', 'px', 'rem']);
24
+ const DURATION_UNITS = new Set(['ms', 's']);
25
+ function isColorObject(raw) {
26
+ const { colorSpace, components, alpha } = raw;
27
+ if (typeof colorSpace !== 'string')
28
+ return false;
29
+ if (!Array.isArray(components) || components.length !== 3)
30
+ return false;
31
+ if (components.some((c) => typeof c !== 'number' || !Number.isFinite(c)))
32
+ return false;
33
+ return alpha === undefined || (typeof alpha === 'number' && Number.isFinite(alpha));
34
+ }
35
+ function renderMeasure(raw, field, units, kind) {
36
+ if (isPlainObject(raw)) {
37
+ const { value, unit } = raw;
38
+ if (typeof value === 'number' && typeof unit === 'string' && Number.isFinite(value)) {
39
+ if (!units.has(unit)) {
40
+ throw new TypeError(`DTCG 2025.10 reader: ${kind} "${field}" unit "${unit}" is not one this reader renders; ` +
41
+ `supported: ${[...units].join(', ')}`);
42
+ }
43
+ return `${value}${unit}`;
44
+ }
45
+ }
46
+ throw new TypeError(`DTCG 2025.10 reader: ${kind} "${field}" must be a {value, unit} object, got ${JSON.stringify(raw)}`);
47
+ }
48
+ function renderDimension(raw, field = 'value') {
49
+ return renderMeasure(raw, field, DIMENSION_UNITS, 'dimension');
50
+ }
51
+ function renderDuration(raw) {
52
+ return renderMeasure(raw, 'value', DURATION_UNITS, 'duration');
53
+ }
54
+ function renderColor(raw, field = 'color') {
55
+ if (isPlainObject(raw) && isColorObject(raw)) {
56
+ const { colorSpace, components, alpha } = raw;
57
+ const alphaSuffix = alpha === undefined ? '' : ` / ${alpha}`;
58
+ const suffixes = FUNCTION_SPACES.get(colorSpace);
59
+ if (suffixes !== undefined) {
60
+ const channels = components.map((c, index) => `${c}${suffixes[index] ?? ''}`).join(' ');
61
+ return `${colorSpace}(${channels}${alphaSuffix})`;
62
+ }
63
+ if (COLOR_FUNCTION_SPACES.has(colorSpace)) {
64
+ return `color(${colorSpace} ${components.join(' ')}${alphaSuffix})`;
65
+ }
66
+ throw new TypeError(`DTCG 2025.10 reader: "${field}" colorSpace "${colorSpace}" is not one this reader renders; ` +
67
+ `supported: ${[...FUNCTION_SPACES.keys(), ...COLOR_FUNCTION_SPACES].toSorted((a, b) => a.localeCompare(b)).join(', ')}`);
68
+ }
69
+ throw new TypeError(`DTCG 2025.10 reader: "${field}" must be a {colorSpace, components, alpha?} object, got ${JSON.stringify(raw)}`);
70
+ }
71
+ const BARE_FAMILY_RE = /^-?[A-Za-z_][\w-]*$/;
72
+ function renderFontFamily(raw) {
73
+ const families = Array.isArray(raw) ? raw : [raw];
74
+ return families
75
+ .map((family) => {
76
+ if (typeof family !== 'string') {
77
+ throw new TypeError(`DTCG 2025.10 reader: fontFamily entries must be strings, got ${JSON.stringify(family)}`);
78
+ }
79
+ return BARE_FAMILY_RE.test(family) ? family : `'${family}'`;
80
+ })
81
+ .join(', ');
82
+ }
83
+ function renderShadowLayer(layer) {
84
+ if (!isPlainObject(layer)) {
85
+ throw new TypeError(`DTCG 2025.10 reader: shadow layer must be a plain object, got ${JSON.stringify(layer)}`);
86
+ }
87
+ return [
88
+ renderDimension(layer.offsetX, 'offsetX'),
89
+ renderDimension(layer.offsetY, 'offsetY'),
90
+ renderDimension(layer.blur, 'blur'),
91
+ renderDimension(layer.spread, 'spread'),
92
+ renderColor(layer.color, 'color'),
93
+ ].join(' ');
94
+ }
95
+ export function renderShadow(value) {
96
+ if (Array.isArray(value)) {
97
+ if (value.length === 0)
98
+ return 'none';
99
+ return value.map((layer) => renderShadowLayer(layer)).join(', ');
100
+ }
101
+ return renderShadowLayer(value);
102
+ }
103
+ function renderCubicBezier(value) {
104
+ if (!Array.isArray(value) || value.length !== 4) {
105
+ throw new TypeError(`DTCG 2025.10 reader: cubicBezier value must be a 4-element array, got ${JSON.stringify(value)}`);
106
+ }
107
+ if (!value.every((n) => typeof n === 'number' && Number.isFinite(n))) {
108
+ throw new TypeError(`DTCG 2025.10 reader: cubicBezier value must be a 4-element numeric array, got ${JSON.stringify(value)}`);
109
+ }
110
+ const [x1, , x2] = value;
111
+ if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) {
112
+ throw new TypeError(`DTCG 2025.10 reader: cubicBezier x-coordinates (x1, x2) must be within [0, 1] — CSS cubic-bezier() ` +
113
+ `rejects an out-of-range x, got ${JSON.stringify(value)}`);
114
+ }
115
+ return `cubic-bezier(${value.join(', ')})`;
116
+ }
117
+ export function unreadableValueReason(type, raw) {
118
+ try {
119
+ renderTokenValue(type, raw, '');
120
+ return undefined;
121
+ }
122
+ catch (error) {
123
+ return error instanceof Error ? error.message : String(error);
124
+ }
125
+ }
126
+ export function renderTokenValue(type, raw, at) {
127
+ switch (type) {
128
+ case 'color': {
129
+ return typeof raw === 'string' ? raw : renderColor(raw);
130
+ }
131
+ case 'cubicBezier': {
132
+ return renderCubicBezier(raw);
133
+ }
134
+ case 'dimension': {
135
+ return renderDimension(raw);
136
+ }
137
+ case 'duration': {
138
+ return renderDuration(raw);
139
+ }
140
+ case 'fontFamily': {
141
+ return renderFontFamily(raw);
142
+ }
143
+ case 'shadow': {
144
+ return renderShadow(raw);
145
+ }
146
+ default: {
147
+ if (typeof raw === 'string' || typeof raw === 'number')
148
+ return raw;
149
+ throw new TypeError(`DTCG 2025.10 reader: unreadable value at "${at}": ${JSON.stringify(raw)}`);
150
+ }
151
+ }
152
+ }
@@ -0,0 +1,12 @@
1
+ export type CoreVersionProbe = {
2
+ status: 'resolved';
3
+ version: string;
4
+ } | {
5
+ status: 'not-installed';
6
+ } | {
7
+ status: 'unresolvable';
8
+ } | {
9
+ message: string;
10
+ status: 'unreadable';
11
+ };
12
+ export declare function resolveInstalledCoreVersion(): Promise<CoreVersionProbe>;
@@ -0,0 +1,40 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ function canResolveCoreDefaultEntry() {
4
+ try {
5
+ import.meta.resolve('@navecss/core');
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ export async function resolveInstalledCoreVersion() {
13
+ let resolved;
14
+ try {
15
+ resolved = import.meta.resolve('@navecss/core/package.json');
16
+ }
17
+ catch (error) {
18
+ const code = error.code;
19
+ if (code === 'ERR_PACKAGE_PATH_NOT_EXPORTED')
20
+ return { status: 'unresolvable' };
21
+ if (code === 'ERR_MODULE_NOT_FOUND' && !canResolveCoreDefaultEntry()) {
22
+ return { status: 'not-installed' };
23
+ }
24
+ return { message: error.message, status: 'unreadable' };
25
+ }
26
+ try {
27
+ const raw = await readFile(fileURLToPath(resolved), 'utf8');
28
+ const parsed = JSON.parse(raw);
29
+ if (typeof parsed.version !== 'string') {
30
+ return {
31
+ message: '"@navecss/core"\'s package.json has no "version" string field',
32
+ status: 'unreadable',
33
+ };
34
+ }
35
+ return { status: 'resolved', version: parsed.version };
36
+ }
37
+ catch (error) {
38
+ return { message: error.message, status: 'unreadable' };
39
+ }
40
+ }
@@ -0,0 +1,2 @@
1
+ export declare function collectDescriptions(source: unknown, path_?: readonly string[], out?: Map<string, string>): ReadonlyMap<string, string>;
2
+ export declare function collectDescriptionsFromSources(sourcePaths: readonly string[]): ReadonlyMap<string, string>[];
@@ -0,0 +1,23 @@
1
+ import { readFileSync } from 'node:fs';
2
+ function isPlainObject(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ export function collectDescriptions(source, path_ = [], out = new Map()) {
6
+ if (!isPlainObject(source)) {
7
+ throw new Error(`dtcg-descriptions: expected an object at "${path_.join('.')}", got ${JSON.stringify(source)}`);
8
+ }
9
+ if (typeof source.$description === 'string')
10
+ out.set(path_.join('.'), source.$description);
11
+ for (const [key, child] of Object.entries(source)) {
12
+ if (key.startsWith('$'))
13
+ continue;
14
+ collectDescriptions(child, [...path_, key], out);
15
+ }
16
+ return out;
17
+ }
18
+ export function collectDescriptionsFromSources(sourcePaths) {
19
+ return sourcePaths.map((sourcePath) => {
20
+ const parsed = JSON.parse(readFileSync(sourcePath, 'utf8'));
21
+ return collectDescriptions(parsed);
22
+ });
23
+ }
@@ -0,0 +1,10 @@
1
+ import type { DraftShapedNode } from './dtcg-shape.ts';
2
+ export interface MalformedNode {
3
+ readonly path: string;
4
+ readonly reason: string | undefined;
5
+ readonly shape: string;
6
+ readonly type: string;
7
+ readonly value: unknown;
8
+ }
9
+ export declare function classifyMalformed(path: string, type: string, value: unknown): MalformedNode | undefined;
10
+ export declare function formatUnreadableNodesRefusal(drafts: readonly DraftShapedNode[], malformed: readonly MalformedNode[], sourceName: string | undefined): string;
@@ -0,0 +1,56 @@
1
+ import { unreadableValueReason } from "./composite-value.js";
2
+ import { formatDraftSection, isAlias, READER, sourceWhere, SPEC_POINTER } from "./dtcg-shape.js";
3
+ const ACCEPTED_SHAPES = {
4
+ color: 'a "color" value is a CSS colour string, or an object carrying a "colorSpace" and a "components" array (an "alpha" number is optional).',
5
+ cubicBezier: 'a "cubicBezier" value is a 4-element numeric array [x1, y1, x2, y2], with x1 and x2 each within [0, 1].',
6
+ dimension: 'a "dimension" value is an object carrying a numeric "value" and a "unit"; this package accepts "px", "rem" and "em".',
7
+ duration: 'a "duration" value is an object carrying a numeric "value" and a "unit"; the format defines "ms" and "s".',
8
+ fontFamily: 'a "fontFamily" value is an array of family-name strings, or a single family name written as a bare string.',
9
+ fontWeight: 'a "fontWeight" value is a JSON number, or one of the format\'s own weight keywords ("bold", "light", "semi-bold" and the rest) written as a string.',
10
+ number: 'a "number" value is a JSON number.',
11
+ shadow: 'a "shadow" value is one layer object, or an array of layer objects, each carrying "offsetX", "offsetY" and "blur" as {value, unit} objects, "spread" as a {value, unit} object required on every layer, and "color" as a {colorSpace, components, alpha?} object.',
12
+ };
13
+ const REASON_PREFIX = `${READER} `;
14
+ const AT_FRAGMENT_RE = / at "[^"]*"/;
15
+ const NO_DIAGNOSIS_PREFIX = 'unreadable value';
16
+ function embeddedReason(raw) {
17
+ const withoutPrefix = raw.startsWith(REASON_PREFIX) ? raw.slice(REASON_PREFIX.length) : raw;
18
+ const text = withoutPrefix.replace(AT_FRAGMENT_RE, '');
19
+ return text.startsWith(NO_DIAGNOSIS_PREFIX) ? undefined : text;
20
+ }
21
+ export function classifyMalformed(path, type, value) {
22
+ if (isAlias(value))
23
+ return undefined;
24
+ const raw = unreadableValueReason(type, value);
25
+ if (raw === undefined)
26
+ return undefined;
27
+ return {
28
+ path,
29
+ reason: embeddedReason(raw),
30
+ shape: ACCEPTED_SHAPES[type] ?? ACCEPTED_SHAPES.dimension,
31
+ type,
32
+ value,
33
+ };
34
+ }
35
+ function formatMalformedSection(nodes, sourceName) {
36
+ const where = sourceWhere(sourceName);
37
+ const count = nodes.length === 1 ? '1 node' : `${nodes.length} nodes`;
38
+ const lines = nodes.map((node) => {
39
+ const reasonLine = node.reason === undefined ? '' : `\n ${node.reason}`;
40
+ return (` - "${node.path}" ($type "${node.type}"), $value ${JSON.stringify(node.value)}` +
41
+ `${reasonLine}\n` +
42
+ ` Its own $type accepts: ${node.shape}`);
43
+ });
44
+ return [
45
+ `${READER} ${count} in ${where} ${nodes.length === 1 ? 'is' : 'are'} written in neither the accepted 2025.10 shape nor that earlier, pre-stable revision of the format, for their own $type. Each node below is followed by the shape its own $type accepts; there is no single corrective act to name here, because a malformed value can be wrong in more than one way.`,
46
+ '',
47
+ ...lines,
48
+ ].join('\n');
49
+ }
50
+ export function formatUnreadableNodesRefusal(drafts, malformed, sourceName) {
51
+ const sections = [
52
+ ...(drafts.length > 0 ? [formatDraftSection(drafts, sourceName)] : []),
53
+ ...(malformed.length > 0 ? [formatMalformedSection(malformed, sourceName)] : []),
54
+ ];
55
+ return [...sections, SPEC_POINTER].join('\n\n');
56
+ }
@@ -0,0 +1,2 @@
1
+ import type { OutputFile } from './builder.ts';
2
+ export declare function composeDtcgOutputs(sourcePath: string, outDir: string): Promise<OutputFile[]>;
@@ -0,0 +1,32 @@
1
+ import { composeBuild } from "./builder.js";
2
+ import { formatCssTokens, formatJsBreakpoints, formatJsTokens, formatTsBreakpoints, formatTsDeclarations, } from "./formats.js";
3
+ const CONSUMER_LAYER = 'tokens.presets';
4
+ export function composeDtcgOutputs(sourcePath, outDir) {
5
+ return composeBuild({
6
+ source: [sourcePath],
7
+ platforms: {
8
+ css: {
9
+ buildPath: outDir,
10
+ files: [
11
+ {
12
+ destination: 'tokens.css',
13
+ format: (tokens) => formatCssTokens(tokens, CONSUMER_LAYER),
14
+ },
15
+ ],
16
+ },
17
+ js: {
18
+ buildPath: outDir,
19
+ files: [
20
+ { destination: 'tokens.js', format: formatJsTokens },
21
+ { destination: 'tokens.d.ts', format: formatTsDeclarations },
22
+ { destination: 'breakpoints.js', format: formatJsBreakpoints },
23
+ {
24
+ destination: 'breakpoints.d.ts',
25
+ format: formatTsBreakpoints,
26
+ filter: (token) => token.path[0] === 'breakpoint',
27
+ },
28
+ ],
29
+ },
30
+ },
31
+ });
32
+ }
@@ -0,0 +1,13 @@
1
+ export declare const SPEC_POINTER = "Specification: Design Tokens Format Module 2025.10, published 28 October 2025.";
2
+ export declare const READER = "DTCG 2025.10 reader:";
3
+ export declare const isAlias: (value: unknown) => boolean;
4
+ export interface DraftShapedNode {
5
+ readonly conversion: string;
6
+ readonly path: string;
7
+ readonly type: string;
8
+ readonly value: unknown;
9
+ }
10
+ export declare function classifyDraftShape(path: string, type: string, value: unknown): DraftShapedNode | undefined;
11
+ export declare function refuseTypeReason(type: string, path: string): string | undefined;
12
+ export declare function sourceWhere(sourceName: string | undefined): string;
13
+ export declare function formatDraftSection(drafts: readonly DraftShapedNode[], sourceName: string | undefined): string;
@@ -0,0 +1,113 @@
1
+ const SPEC_NAME = 'Design Tokens Format Module 2025.10';
2
+ export const SPEC_POINTER = `Specification: ${SPEC_NAME}, published 28 October 2025.`;
3
+ export const READER = 'DTCG 2025.10 reader:';
4
+ const ACCEPTED_TYPES = new Set([
5
+ 'color',
6
+ 'cubicBezier',
7
+ 'dimension',
8
+ 'duration',
9
+ 'fontFamily',
10
+ 'fontWeight',
11
+ 'number',
12
+ 'shadow',
13
+ ]);
14
+ const STRUCTURAL_REFUSALS = new Map([
15
+ ['gradient', 'a gradient carries no direction, so it cannot render to one CSS value'],
16
+ ['transition', 'a transition names no CSS property, so it cannot render to one CSS value'],
17
+ [
18
+ 'typography',
19
+ "a typography's members do not fit the CSS font shorthand, so it cannot render to one CSS value",
20
+ ],
21
+ ]);
22
+ const SCOPE_REFUSALS = new Set(['border', 'strokeStyle']);
23
+ const SCOPE_RETURN_TRIGGER = 'Returns on the first consumer request, or at 0.2.0, whichever comes first.';
24
+ const FONT_WEIGHT_KEYWORDS = new Set([
25
+ 'black',
26
+ 'bold',
27
+ 'book',
28
+ 'demi-bold',
29
+ 'extra-black',
30
+ 'extra-bold',
31
+ 'extra-light',
32
+ 'hairline',
33
+ 'heavy',
34
+ 'light',
35
+ 'medium',
36
+ 'normal',
37
+ 'regular',
38
+ 'semi-bold',
39
+ 'thin',
40
+ 'ultra-black',
41
+ 'ultra-bold',
42
+ 'ultra-light',
43
+ ]);
44
+ const ALIAS_RE = /^\{[^{}]+\}$/;
45
+ function isPlainObject(value) {
46
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
47
+ }
48
+ export const isAlias = (value) => typeof value === 'string' && ALIAS_RE.test(value);
49
+ const CONVERSIONS = {
50
+ dimension: 'wrap the value in an object carrying a numeric "value" and a "unit", as in {"value": 16, "unit": "px"}. The unit is required even at zero, and the format defines "px" and "rem".',
51
+ duration: 'wrap the value in an object carrying a numeric "value" and a "unit", as in {"value": 200, "unit": "ms"}. The format defines "ms" and "s" for this type.',
52
+ fontFamily: 'write the stack as an array with one family name per element, as in ["system-ui", "Segoe UI", "sans-serif"], and leave a single family as a bare string. Detecting a draft-shaped fontFamily by its comma is a HEURISTIC and not a shape rule: a bare string is a lawful single family name under 2025.10, so a comma is the only signal that a value naming five families means five, and a family name that genuinely contains a comma is caught here in error.',
53
+ fontWeight: 'drop the quotes and write the weight as a JSON number from 1 to 1000, as in 400. The format\'s own weight keywords ("bold", "light", "semi-bold" and the rest) stay quoted strings and are accepted as they are.',
54
+ number: 'drop the quotes and write the value as a JSON number, as in 14 rather than "14".',
55
+ shadow: 'give each layer typed sub-fields: "offsetX", "offsetY", "blur" and "spread" each become an object carrying a numeric "value" and a "unit", "color" becomes an object carrying a "colorSpace" and a "components" array, and "spread" is required on every layer rather than omitted when it is zero.',
56
+ };
57
+ function isDraftShadowLayer(layer) {
58
+ if (!isPlainObject(layer))
59
+ return false;
60
+ if (!('spread' in layer))
61
+ return true;
62
+ return ['blur', 'color', 'offsetX', 'offsetY', 'spread'].some((field) => typeof layer[field] === 'string');
63
+ }
64
+ const DRAFT_WROTE_A_STRING = new Set(['dimension', 'duration', 'fontWeight', 'number']);
65
+ function isDraftShaped(type, value) {
66
+ if (isAlias(value))
67
+ return false;
68
+ if (type === 'shadow') {
69
+ return Array.isArray(value)
70
+ ? value.some((entry) => isDraftShadowLayer(entry))
71
+ : isDraftShadowLayer(value);
72
+ }
73
+ if (typeof value !== 'string')
74
+ return false;
75
+ if (type === 'fontFamily')
76
+ return value.includes(',');
77
+ if (type === 'fontWeight')
78
+ return !FONT_WEIGHT_KEYWORDS.has(value);
79
+ return DRAFT_WROTE_A_STRING.has(type);
80
+ }
81
+ export function classifyDraftShape(path, type, value) {
82
+ if (!isDraftShaped(type, value))
83
+ return undefined;
84
+ return { conversion: CONVERSIONS[type] ?? CONVERSIONS.dimension, path, type, value };
85
+ }
86
+ export function refuseTypeReason(type, path) {
87
+ const structural = STRUCTURAL_REFUSALS.get(type);
88
+ if (structural !== undefined) {
89
+ return `${READER} "$type": "${type}" at "${path}" is not read by this package, on a structural ground: ${structural}, and this pipeline emits one custom property per token. ${SPEC_POINTER}`;
90
+ }
91
+ if (SCOPE_REFUSALS.has(type)) {
92
+ return `${READER} "$type": "${type}" at "${path}" is not read at 0.1.0, on a scope ground rather than a structural one: it renders cleanly and is cheap to add, and no consumer has asked for it. ${SCOPE_RETURN_TRIGGER} ${SPEC_POINTER}`;
93
+ }
94
+ if (!ACCEPTED_TYPES.has(type)) {
95
+ return `${READER} "$type": "${type}" at "${path}" is not a type the ${SPEC_NAME} defines. ${SPEC_POINTER}`;
96
+ }
97
+ return undefined;
98
+ }
99
+ const ANONYMOUS_SOURCE = 'the token source read by this build';
100
+ export function sourceWhere(sourceName) {
101
+ return sourceName === undefined ? ANONYMOUS_SOURCE : `"${sourceName}"`;
102
+ }
103
+ export function formatDraftSection(drafts, sourceName) {
104
+ const where = sourceWhere(sourceName);
105
+ const count = drafts.length === 1 ? '1 node' : `${drafts.length} nodes`;
106
+ const lines = drafts.map((node) => ` - "${node.path}" ($type "${node.type}"), $value ${JSON.stringify(node.value)}\n` +
107
+ ` To convert it: ${node.conversion}`);
108
+ return [
109
+ `${READER} ${count} in ${where} ${drafts.length === 1 ? 'is' : 'are'} written in the pre-stable draft shape of the Design Tokens Format Module. That is an earlier revision of this same format, not a broken file, and this reader reads 2025.10 only. Each node below carries the edit that brings it up to date.`,
110
+ '',
111
+ ...lines,
112
+ ].join('\n');
113
+ }
@@ -0,0 +1,23 @@
1
+ export declare class UsageError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export interface DtcgRefusedNode {
5
+ readonly conversion: string;
6
+ readonly path: string;
7
+ readonly type: string;
8
+ readonly value: unknown;
9
+ }
10
+ export declare class DtcgShapeRefusal extends Error {
11
+ readonly nodes: readonly DtcgRefusedNode[];
12
+ constructor(message: string, nodes: readonly DtcgRefusedNode[]);
13
+ }
14
+ export declare class TokenCollisionRefusal extends Error {
15
+ constructor(message: string);
16
+ }
17
+ export declare class DuplicateTokenNameRefusal extends Error {
18
+ constructor(message: string);
19
+ }
20
+ export declare class MissingContractTokensError extends Error {
21
+ readonly missing: readonly string[];
22
+ constructor(missing: readonly string[]);
23
+ }
@@ -0,0 +1,34 @@
1
+ export class UsageError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'UsageError';
5
+ }
6
+ }
7
+ export class DtcgShapeRefusal extends Error {
8
+ nodes;
9
+ constructor(message, nodes) {
10
+ super(message);
11
+ this.name = 'DtcgShapeRefusal';
12
+ this.nodes = nodes;
13
+ }
14
+ }
15
+ export class TokenCollisionRefusal extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = 'TokenCollisionRefusal';
19
+ }
20
+ }
21
+ export class DuplicateTokenNameRefusal extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'DuplicateTokenNameRefusal';
25
+ }
26
+ }
27
+ export class MissingContractTokensError extends Error {
28
+ missing;
29
+ constructor(missing) {
30
+ super(`Consumer build is missing ${missing.length} core contract token(s): ${missing.join(', ')}`);
31
+ this.name = 'MissingContractTokensError';
32
+ this.missing = missing;
33
+ }
34
+ }