@agent-scope/tokens 1.17.0 → 1.17.2
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.
- package/README.md +839 -0
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
# @agent-scope/tokens
|
|
2
|
+
|
|
3
|
+
Design token file parser, validator, and resolution engine for Scope.
|
|
4
|
+
|
|
5
|
+
Parses `reactscope.tokens.json` / `.yaml` files into a flat, fully-resolved `Token[]`, and provides lookup, nearest-match search, compliance auditing, impact analysis, theme overlays, and multi-format export.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @agent-scope/tokens
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What it does / when to use it
|
|
18
|
+
|
|
19
|
+
| Need | Use |
|
|
20
|
+
|------|-----|
|
|
21
|
+
| Parse a token file and resolve all `{path.to.token}` references | `parseTokenFile` / `parseTokenFileSync` |
|
|
22
|
+
| Look up a token by path | `TokenResolver.resolve` |
|
|
23
|
+
| Find the token that matches a CSS value | `TokenResolver.match` / `TokenResolver.nearest` |
|
|
24
|
+
| Audit component styles against the token set | `ComplianceEngine` |
|
|
25
|
+
| Predict which components break when a token changes | `ImpactAnalyzer` |
|
|
26
|
+
| Export tokens to CSS / TypeScript / SCSS / Tailwind / Figma | `exportTokens` |
|
|
27
|
+
| Resolve values across dark/brand themes | `ThemeResolver` |
|
|
28
|
+
| Validate a raw token file object | `validateTokenFile` |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Token file format
|
|
33
|
+
|
|
34
|
+
Token files are JSON or YAML. The top-level shape is:
|
|
35
|
+
|
|
36
|
+
```jsonc
|
|
37
|
+
{
|
|
38
|
+
"$schema": "https://reactscope.dev/token-schema.json", // optional
|
|
39
|
+
"version": "0.1", // required
|
|
40
|
+
"meta": { // optional
|
|
41
|
+
"name": "My Design Tokens",
|
|
42
|
+
"lastUpdated": "2024-01-01",
|
|
43
|
+
"updatedBy": "designer@example.com"
|
|
44
|
+
},
|
|
45
|
+
"tokens": { ... }, // required
|
|
46
|
+
"themes": { ... } // optional
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Token tree
|
|
51
|
+
|
|
52
|
+
The `tokens` object is a nested tree. Every leaf node must have `value` and `type`:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"version": "0.1",
|
|
57
|
+
"tokens": {
|
|
58
|
+
"color": {
|
|
59
|
+
"primary": {
|
|
60
|
+
"500": { "value": "#3B82F6", "type": "color" },
|
|
61
|
+
"600": { "value": "#2563EB", "type": "color" }
|
|
62
|
+
},
|
|
63
|
+
"neutral": {
|
|
64
|
+
"0": { "value": "#FFFFFF", "type": "color" },
|
|
65
|
+
"900": { "value": "#111827", "type": "color" }
|
|
66
|
+
},
|
|
67
|
+
"alias": {
|
|
68
|
+
"brand": { "value": "{color.primary.500}", "type": "color" }
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"spacing": {
|
|
72
|
+
"4": { "value": "16px", "type": "dimension" },
|
|
73
|
+
"8": { "value": "32px", "type": "dimension" }
|
|
74
|
+
},
|
|
75
|
+
"typography": {
|
|
76
|
+
"fontFamily": {
|
|
77
|
+
"sans": { "value": "Inter, sans-serif", "type": "fontFamily", "description": "Primary font" }
|
|
78
|
+
},
|
|
79
|
+
"fontWeight": {
|
|
80
|
+
"bold": { "value": 700, "type": "fontWeight" },
|
|
81
|
+
"normal": { "value": 400, "type": "fontWeight" }
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"radius": { "md": { "value": "6px", "type": "dimension" } },
|
|
85
|
+
"shadow": { "md": { "value": "0 4px 6px rgba(0,0,0,0.1)", "type": "shadow" } },
|
|
86
|
+
"motion": {
|
|
87
|
+
"duration": { "fast": { "value": "150ms", "type": "duration" } },
|
|
88
|
+
"easing": { "standard": { "value": "cubic-bezier(0.4, 0, 0.2, 1)", "type": "cubicBezier" } }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Leaf node fields
|
|
95
|
+
|
|
96
|
+
| Field | Type | Required | Description |
|
|
97
|
+
|-------|------|----------|-------------|
|
|
98
|
+
| `value` | `string \| number` | Yes | Raw value. May be a `{path.to.token}` reference. |
|
|
99
|
+
| `type` | `TokenType` | Yes | One of the 8 supported types (see below). |
|
|
100
|
+
| `description` | `string` | No | Human-readable annotation. Passed through to exports. |
|
|
101
|
+
|
|
102
|
+
### Token types
|
|
103
|
+
|
|
104
|
+
| Type | Example value |
|
|
105
|
+
|------|---------------|
|
|
106
|
+
| `color` | `"#3B82F6"` |
|
|
107
|
+
| `dimension` | `"16px"`, `"1.5rem"` |
|
|
108
|
+
| `fontFamily` | `"Inter, sans-serif"` |
|
|
109
|
+
| `fontWeight` | `700` |
|
|
110
|
+
| `number` | `1.5` |
|
|
111
|
+
| `shadow` | `"0 4px 6px rgba(0,0,0,0.1)"` |
|
|
112
|
+
| `duration` | `"150ms"` |
|
|
113
|
+
| `cubicBezier` | `"cubic-bezier(0.4, 0, 0.2, 1)"` |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Reference syntax — `{path.to.token}`
|
|
118
|
+
|
|
119
|
+
A token's `value` can point to another token using curly-brace dot-notation:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
"color": {
|
|
123
|
+
"primary": { "500": { "value": "#3B82F6", "type": "color" } },
|
|
124
|
+
"alias": {
|
|
125
|
+
"brand": { "value": "{color.primary.500}", "type": "color" }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- The parser resolves `{color.primary.500}` → `"#3B82F6"` and stores it in `resolvedValue`.
|
|
131
|
+
- References can chain: `A → B → C` (all fully resolved).
|
|
132
|
+
- Circular references (e.g. `A → B → A`) throw `TokenParseError` with code `"CIRCULAR_REFERENCE"`.
|
|
133
|
+
- References to non-existent paths throw `TokenParseError` with code `"INVALID_REFERENCE"`.
|
|
134
|
+
- `resolvedValue` is **always a string**, even for numeric tokens (`fontWeight: 700` → `"700"`).
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## API reference
|
|
139
|
+
|
|
140
|
+
### `parseTokenFile(input, format?)` — async
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
async function parseTokenFile(
|
|
144
|
+
input: string,
|
|
145
|
+
format?: "json" | "yaml",
|
|
146
|
+
): Promise<ParsedTokens>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Parses a token file (JSON or YAML) and returns a flat resolved token array.
|
|
150
|
+
|
|
151
|
+
- If `format` is omitted, JSON is tried first, then YAML as a fallback.
|
|
152
|
+
- Throws `TokenParseError` on reference or circular reference errors.
|
|
153
|
+
- Throws `TokenValidationError` on schema violations.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { parseTokenFile } from "@agent-scope/tokens";
|
|
157
|
+
import { readFileSync } from "node:fs";
|
|
158
|
+
|
|
159
|
+
const source = readFileSync("tokens.json", "utf8");
|
|
160
|
+
const { tokens, rawFile } = await parseTokenFile(source);
|
|
161
|
+
// tokens: Token[] — flat, resolved
|
|
162
|
+
// rawFile: TokenFile — the validated raw file object
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `parseTokenFileSync(input)` — sync, JSON only
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
function parseTokenFileSync(input: string): ParsedTokens
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Synchronous version. Only supports JSON (YAML requires an async import).
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { parseTokenFileSync } from "@agent-scope/tokens";
|
|
175
|
+
|
|
176
|
+
const { tokens } = parseTokenFileSync(source);
|
|
177
|
+
// tokens[0] → { path: "color.primary.500", value: "#3B82F6", resolvedValue: "#3B82F6", type: "color" }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `ParsedTokens`
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
interface ParsedTokens {
|
|
184
|
+
tokens: Token[];
|
|
185
|
+
rawFile: TokenFile;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### `Token`
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
interface Token {
|
|
193
|
+
path: string; // dot-notation, e.g. "color.primary.500"
|
|
194
|
+
value: string | number; // raw value from the file (may be a reference)
|
|
195
|
+
resolvedValue: string; // fully resolved, always a string
|
|
196
|
+
type: TokenType;
|
|
197
|
+
description?: string;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### `TokenResolver`
|
|
204
|
+
|
|
205
|
+
Wraps a `Token[]` for fast path-based lookup and nearest-match search.
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { TokenResolver } from "@agent-scope/tokens";
|
|
209
|
+
|
|
210
|
+
const resolver = new TokenResolver(tokens);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### `resolve(path)`
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
resolve(path: string): string
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Returns the resolved value for a known token path. Throws `TokenParseError` (`"INVALID_REFERENCE"`) if not found.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
resolver.resolve("color.primary.500"); // "#3B82F6"
|
|
223
|
+
resolver.resolve("spacing.4"); // "16px"
|
|
224
|
+
resolver.resolve("typography.fontWeight.bold"); // "700"
|
|
225
|
+
resolver.resolve("color.alias.brand"); // "#3B82F6" (alias resolved)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### `match(value, type)`
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
match(value: string, type: TokenType): TokenMatch | null
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Returns an exact-match `TokenMatch` if any token of the given type has a `resolvedValue` equal to `value` (case-insensitive for colors). Returns `null` if no exact match exists.
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
resolver.match("#3B82F6", "color");
|
|
238
|
+
// { token: { path: "color.primary.500", ... }, exact: true, distance: 0 }
|
|
239
|
+
|
|
240
|
+
resolver.match("#3b82f6", "color"); // case-insensitive — same result
|
|
241
|
+
resolver.match("16px", "dimension");
|
|
242
|
+
// { token: { path: "spacing.4", ... }, exact: true, distance: 0 }
|
|
243
|
+
|
|
244
|
+
resolver.match("#AABBCC", "color"); // null — no match
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### `nearest(value, type)`
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
nearest(value: string, type: TokenType): TokenMatch
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Returns the `TokenMatch` with the smallest computed distance to `value` among all tokens of the given type. Always returns a result (never null); throws if no tokens of the specified type exist.
|
|
254
|
+
|
|
255
|
+
Distance computation per type:
|
|
256
|
+
- `color` — Euclidean distance in CIE Lab space (perceptual)
|
|
257
|
+
- `dimension` / `duration` — `|parsed numeric difference|`
|
|
258
|
+
- `fontWeight` / `number` — `|numeric difference|`
|
|
259
|
+
- `shadow` / `fontFamily` / `cubicBezier` — string equality (0 or 1)
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// #3A82F5 is perceptually very close to #3B82F6
|
|
263
|
+
resolver.nearest("#3A82F5", "color");
|
|
264
|
+
// { token: { path: "color.primary.500", ... }, exact: false, distance: 0.42 }
|
|
265
|
+
|
|
266
|
+
// 15px — closest to 16px (spacing.4)
|
|
267
|
+
resolver.nearest("15px", "dimension");
|
|
268
|
+
// { token: { path: "spacing.4", resolvedValue: "16px", ... }, exact: false, distance: 1 }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
#### `list(type?, category?)`
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
list(type?: TokenType, category?: string): Token[]
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Returns all tokens, optionally filtered by type and/or category (the first path segment, e.g. `"color"` in `"color.primary.500"`).
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
resolver.list(); // all tokens
|
|
281
|
+
resolver.list("color"); // only color tokens
|
|
282
|
+
resolver.list(undefined, "spacing"); // all tokens in the "spacing" category
|
|
283
|
+
resolver.list("dimension", "spacing"); // dimension tokens in "spacing"
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### `TokenMatch`
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
interface TokenMatch {
|
|
290
|
+
token: Token;
|
|
291
|
+
exact: boolean; // true when resolvedValue === queried value
|
|
292
|
+
distance: number; // 0 for exact; computed distance otherwise
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### `ComplianceEngine`
|
|
299
|
+
|
|
300
|
+
Audits rendered component CSS styles against the resolved token set. Reports per-property compliance status and an aggregate compliance percentage.
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { ComplianceEngine } from "@agent-scope/tokens";
|
|
304
|
+
|
|
305
|
+
const engine = new ComplianceEngine(resolver);
|
|
306
|
+
// With custom tolerances:
|
|
307
|
+
const engine = new ComplianceEngine(resolver, {
|
|
308
|
+
colorTolerance: 3, // default: max CIE Lab distance for on-system
|
|
309
|
+
dimensionTolerance: 2, // default: max px difference for on-system
|
|
310
|
+
fontWeightTolerance: 0, // default: exact match only
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### `audit(styles)`
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
audit(styles: ComputedStyles): ComplianceReport
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Audits a single component's computed styles.
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
const report = engine.audit({
|
|
324
|
+
colors: { background: "#3B82F6", color: "#ffffff" },
|
|
325
|
+
spacing: { paddingTop: "16px", gap: "8px" },
|
|
326
|
+
typography: { fontFamily: "Inter, sans-serif", fontSize: "14px", fontWeight: "700" },
|
|
327
|
+
borders: { borderRadius: "4px" },
|
|
328
|
+
shadows: { boxShadow: "0 1px 3px rgba(0,0,0,0.1)" },
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
report.compliance; // 0.83 (fraction of on-system properties)
|
|
332
|
+
report.total; // 8 (properties audited, excluding skipped values)
|
|
333
|
+
report.onSystem; // 7
|
|
334
|
+
report.offSystem; // 1
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### `ComputedStyles`
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
type ComputedStyles = {
|
|
341
|
+
colors: Record<string, string>; // e.g. { background: "#3B82F6" }
|
|
342
|
+
spacing: Record<string, string>; // e.g. { paddingTop: "16px" }
|
|
343
|
+
typography: Record<string, string>; // fontFamily, fontSize, fontWeight, lineHeight
|
|
344
|
+
borders: Record<string, string>; // borderRadius, borderWidth
|
|
345
|
+
shadows: Record<string, string>; // boxShadow
|
|
346
|
+
};
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Skipped values (not counted toward totals): `"none"`, `"inherit"`, `"initial"`, `"unset"`, `"auto"`, `"transparent"`, `"currentColor"`, `""`, `"normal"`.
|
|
350
|
+
|
|
351
|
+
#### `ComplianceReport`
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
interface ComplianceReport {
|
|
355
|
+
properties: Record<string, PropertyResult>; // per-property result
|
|
356
|
+
total: number; // properties audited
|
|
357
|
+
onSystem: number;
|
|
358
|
+
offSystem: number;
|
|
359
|
+
compliance: number; // onSystem / total (1 when total === 0)
|
|
360
|
+
auditedAt: string; // ISO timestamp
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
#### `PropertyResult` — on_system example
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// report.properties["background"] when background: "#3B82F6" (exact token match)
|
|
368
|
+
{
|
|
369
|
+
property: "background",
|
|
370
|
+
value: "#3B82F6",
|
|
371
|
+
status: "on_system",
|
|
372
|
+
token: "color.primary.500",
|
|
373
|
+
nearest: { token: "color.primary.500", value: "#3B82F6", distance: 0 }
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### `PropertyResult` — OFF_SYSTEM example
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// report.properties["background"] when background: "#FF0000" (no match)
|
|
381
|
+
{
|
|
382
|
+
property: "background",
|
|
383
|
+
value: "#FF0000",
|
|
384
|
+
status: "OFF_SYSTEM",
|
|
385
|
+
// token is absent (undefined) for OFF_SYSTEM results
|
|
386
|
+
nearest: { token: "color.neutral.900", value: "#111827", distance: 74.3 }
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
#### `auditBatch(components)`
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
auditBatch(components: Map<string, ComputedStyles>): BatchReport
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Audits multiple components at once:
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
const batch = engine.auditBatch(new Map([
|
|
400
|
+
["Button", { colors: { background: "#3B82F6" }, spacing: {}, typography: {}, borders: {}, shadows: {} }],
|
|
401
|
+
["Input", { colors: { background: "#FF0000" }, spacing: {}, typography: {}, borders: {}, shadows: {} }],
|
|
402
|
+
]));
|
|
403
|
+
|
|
404
|
+
batch.aggregateCompliance; // 0.5
|
|
405
|
+
batch.components.Button.compliance; // 1
|
|
406
|
+
batch.components.Input.compliance; // 0
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
#### `ComplianceEngine.toJSON(report)`
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
static toJSON(report: ComplianceReport | BatchReport): string
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Serializes a report to indented JSON (2-space).
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
### `ImpactAnalyzer`
|
|
420
|
+
|
|
421
|
+
Analyses the downstream effects of a design token change on audited components.
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import { ImpactAnalyzer } from "@agent-scope/tokens";
|
|
425
|
+
|
|
426
|
+
const analyzer = new ImpactAnalyzer(resolver, componentReports);
|
|
427
|
+
// componentReports: Map<string, ComplianceReport> — from ComplianceEngine.auditBatch
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
#### `impactOf(tokenPath, newValue)`
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
impactOf(tokenPath: string, newValue: string): ImpactReport
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Returns an `ImpactReport` describing which components and properties would be affected by changing the specified token to `newValue`.
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
const report = analyzer.impactOf("color.primary.500", "#1D4ED8");
|
|
440
|
+
|
|
441
|
+
report.tokenPath; // "color.primary.500"
|
|
442
|
+
report.oldValue; // "#3B82F6"
|
|
443
|
+
report.newValue; // "#1D4ED8"
|
|
444
|
+
report.tokenType; // "color"
|
|
445
|
+
report.colorDelta; // CIE Lab distance (only for color tokens)
|
|
446
|
+
report.affectedComponentCount; // e.g. 2
|
|
447
|
+
report.overallSeverity; // "subtle" | "moderate" | "significant" | "none"
|
|
448
|
+
report.components; // AffectedComponent[]
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
#### `ImpactReport`
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
interface ImpactReport {
|
|
455
|
+
tokenPath: string;
|
|
456
|
+
oldValue: string;
|
|
457
|
+
newValue: string;
|
|
458
|
+
tokenType: TokenType;
|
|
459
|
+
affectedComponentCount: number;
|
|
460
|
+
components: AffectedComponent[];
|
|
461
|
+
overallSeverity: VisualSeverity; // max severity across all components
|
|
462
|
+
colorDelta?: number; // CIE Lab distance (color tokens only)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
interface AffectedComponent {
|
|
466
|
+
name: string;
|
|
467
|
+
affectedProperties: string[]; // e.g. ["background", "borderColor"]
|
|
468
|
+
severity: VisualSeverity;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
type VisualSeverity = "none" | "subtle" | "moderate" | "significant";
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Severity thresholds for color tokens (CIE Lab distance):
|
|
475
|
+
- `"none"` — distance 0 (no change)
|
|
476
|
+
- `"subtle"` — distance < 5
|
|
477
|
+
- `"moderate"` — distance < 20
|
|
478
|
+
- `"significant"` — distance ≥ 20
|
|
479
|
+
|
|
480
|
+
For dimension tokens: `≤2px` → subtle, `≤8px` → moderate, `>8px` → significant.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
### `exportTokens(tokens, format, options?)`
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
function exportTokens(
|
|
488
|
+
tokens: Token[],
|
|
489
|
+
format: ExportFormat,
|
|
490
|
+
options?: ExportOptions,
|
|
491
|
+
): string
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Exports a resolved token set to the specified format.
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
type ExportFormat = "css" | "ts" | "scss" | "tailwind" | "flat-json" | "figma";
|
|
498
|
+
|
|
499
|
+
interface ExportOptions {
|
|
500
|
+
themes?: Map<string, Map<string, string>>; // theme name → (tokenPath → overrideValue)
|
|
501
|
+
prefix?: string; // CSS/SCSS: prefix for custom property / variable names
|
|
502
|
+
rootSelector?: string; // CSS: override ":root" selector
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
#### CSS export
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
exportTokens(tokens, "css");
|
|
510
|
+
// :root {
|
|
511
|
+
// --color-primary-500: #3B82F6;
|
|
512
|
+
// --color-primary-600: #2563EB;
|
|
513
|
+
// --spacing-4: 16px;
|
|
514
|
+
// ...
|
|
515
|
+
// }
|
|
516
|
+
|
|
517
|
+
exportTokens(tokens, "css", { prefix: "scope" });
|
|
518
|
+
// :root { --scope-color-primary-500: #3B82F6; ... }
|
|
519
|
+
|
|
520
|
+
exportTokens(tokens, "css", { rootSelector: "html" });
|
|
521
|
+
// html { --color-primary-500: #3B82F6; ... }
|
|
522
|
+
|
|
523
|
+
// With themes:
|
|
524
|
+
exportTokens(tokens, "css", { themes: themeMap });
|
|
525
|
+
// :root { --color-primary-500: #3B82F6; ... }
|
|
526
|
+
// [data-theme="dark"] { --color-primary-500: #60A5FA; ... }
|
|
527
|
+
// [data-theme="brand-b"] { --color-primary-500: #8B5CF6; ... }
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
#### TypeScript export
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
exportTokens(tokens, "ts");
|
|
534
|
+
// // Auto-generated design tokens — do not edit manually
|
|
535
|
+
//
|
|
536
|
+
// export const colorPrimary500 = "#3B82F6" as const;
|
|
537
|
+
// export const colorPrimary600 = "#2563EB" as const;
|
|
538
|
+
// export const spacing4 = "16px" as const;
|
|
539
|
+
// ...
|
|
540
|
+
|
|
541
|
+
// With themes:
|
|
542
|
+
// export const themes = {
|
|
543
|
+
// "dark": { colorPrimary500: "#60A5FA" as const, ... },
|
|
544
|
+
// "brand-b": { colorPrimary500: "#8B5CF6" as const, ... },
|
|
545
|
+
// } as const;
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
#### SCSS export
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
exportTokens(tokens, "scss");
|
|
552
|
+
// // Auto-generated design tokens — do not edit manually
|
|
553
|
+
//
|
|
554
|
+
// $color-primary-500: #3B82F6;
|
|
555
|
+
// $spacing-4: 16px;
|
|
556
|
+
// ...
|
|
557
|
+
|
|
558
|
+
exportTokens(tokens, "scss", { prefix: "tok" });
|
|
559
|
+
// $tok-color-primary-500: #3B82F6;
|
|
560
|
+
|
|
561
|
+
// With themes — emits [data-theme] blocks using CSS custom properties:
|
|
562
|
+
// [data-theme="dark"] { --color-primary-500: #60A5FA; }
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
#### Tailwind export
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
exportTokens(tokens, "tailwind");
|
|
569
|
+
// // Auto-generated design tokens — do not edit manually
|
|
570
|
+
// module.exports = {
|
|
571
|
+
// "theme": {
|
|
572
|
+
// "extend": {
|
|
573
|
+
// "color": { "primary": { "500": "#3B82F6", "600": "#2563EB" } },
|
|
574
|
+
// "spacing": { "4": "16px", "8": "32px" }
|
|
575
|
+
// }
|
|
576
|
+
// }
|
|
577
|
+
// };
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
#### flat-json export
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
exportTokens(tokens, "flat-json");
|
|
584
|
+
// {
|
|
585
|
+
// "color.primary.500": "#3B82F6",
|
|
586
|
+
// "color.primary.600": "#2563EB",
|
|
587
|
+
// "spacing.4": "16px"
|
|
588
|
+
// }
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
#### Figma export
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
exportTokens(tokens, "figma");
|
|
595
|
+
// {
|
|
596
|
+
// "global": {
|
|
597
|
+
// "color": {
|
|
598
|
+
// "primary": {
|
|
599
|
+
// "500": { "value": "#3B82F6", "type": "color" }
|
|
600
|
+
// }
|
|
601
|
+
// },
|
|
602
|
+
// "typography": {
|
|
603
|
+
// "fontFamily": {
|
|
604
|
+
// "sans": { "value": "Inter, sans-serif", "type": "fontFamily", "description": "Primary font" }
|
|
605
|
+
// }
|
|
606
|
+
// }
|
|
607
|
+
// },
|
|
608
|
+
// "dark": { "color": { "primary": { "500": { "value": "#60A5FA", "type": "color" } } } }
|
|
609
|
+
// }
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
### `ThemeResolver`
|
|
615
|
+
|
|
616
|
+
Extends `TokenResolver` with named theme overlays.
|
|
617
|
+
|
|
618
|
+
#### Token file format — themes
|
|
619
|
+
|
|
620
|
+
Two supported theme formats:
|
|
621
|
+
|
|
622
|
+
**Flat override map** (original format):
|
|
623
|
+
|
|
624
|
+
```json
|
|
625
|
+
{
|
|
626
|
+
"version": "0.1",
|
|
627
|
+
"tokens": { ... },
|
|
628
|
+
"themes": {
|
|
629
|
+
"dark": {
|
|
630
|
+
"color.primary.500": "#60A5FA",
|
|
631
|
+
"color.neutral.0": "#0F172A"
|
|
632
|
+
},
|
|
633
|
+
"brand-b": {
|
|
634
|
+
"color.primary.500": "#8B5CF6"
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
**Nested DTCG-style** (structured format):
|
|
641
|
+
|
|
642
|
+
```json
|
|
643
|
+
{
|
|
644
|
+
"version": "0.1",
|
|
645
|
+
"tokens": { ... },
|
|
646
|
+
"themes": {
|
|
647
|
+
"dark": {
|
|
648
|
+
"color": {
|
|
649
|
+
"primary": { "500": { "$value": "#60A5FA" } },
|
|
650
|
+
"neutral": { "0": { "$value": "#0F172A" } }
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
#### `ThemeResolver.fromTokenFile(baseResolver, rawFile)`
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
static fromTokenFile(baseResolver: TokenResolver, rawFile: ThemedTokenFile): ThemeResolver
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Constructs a `ThemeResolver` from a `TokenResolver` and a raw token file (supports both flat and nested formats).
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
import { ThemeResolver } from "@agent-scope/tokens";
|
|
667
|
+
|
|
668
|
+
const { tokens, rawFile } = parseTokenFileSync(source);
|
|
669
|
+
const resolver = new TokenResolver(tokens);
|
|
670
|
+
const themeResolver = ThemeResolver.fromTokenFile(resolver, rawFile);
|
|
671
|
+
|
|
672
|
+
themeResolver.listThemes(); // ["dark", "brand-b"]
|
|
673
|
+
themeResolver.resolveThemed("color.primary.500", "dark"); // "#60A5FA"
|
|
674
|
+
themeResolver.resolveThemed("spacing.4", "dark"); // "16px" (falls back to base)
|
|
675
|
+
themeResolver.resolveAllThemes("color.primary.500");
|
|
676
|
+
// { base: "#3B82F6", dark: "#60A5FA", "brand-b": "#8B5CF6" }
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
#### `ThemeResolver.fromThemeMap(baseResolver, themes)`
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
static fromThemeMap(
|
|
683
|
+
baseResolver: TokenResolver,
|
|
684
|
+
themes: Map<string, Map<string, string>>,
|
|
685
|
+
): ThemeResolver
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
Programmatic construction from a pre-built theme map.
|
|
689
|
+
|
|
690
|
+
#### `resolveThemed(path, themeName)`
|
|
691
|
+
|
|
692
|
+
Returns the value for the path in the given theme, falling back to base if the theme doesn't override it. Throws if the theme name is not registered.
|
|
693
|
+
|
|
694
|
+
#### `resolveAllThemes(path)`
|
|
695
|
+
|
|
696
|
+
Returns `{ base: string, [themeName]: string, ... }` — the resolved value in every theme.
|
|
697
|
+
|
|
698
|
+
#### `buildThemedTokens(themeName)`
|
|
699
|
+
|
|
700
|
+
Returns a full `Token[]` with the theme overrides applied (base values for non-overridden tokens).
|
|
701
|
+
|
|
702
|
+
#### Delegated methods
|
|
703
|
+
|
|
704
|
+
`ThemeResolver` also exposes `resolve(path)` and `list(type?, category?)` which delegate to the underlying `TokenResolver`.
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
### `validateTokenFile(raw)`
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
function validateTokenFile(raw: unknown): asserts raw is TokenFile
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
Validates a raw parsed object against the `TokenFile` schema. Throws `TokenValidationError` with all collected issues if validation fails (collects all errors before throwing, not just the first).
|
|
715
|
+
|
|
716
|
+
Validation rules:
|
|
717
|
+
- Root value must be a non-null object
|
|
718
|
+
- `version` must be a string field
|
|
719
|
+
- `tokens` must be an object field
|
|
720
|
+
- Every leaf node inside `tokens` must have `value` (string or number) and `type` (one of the 8 valid types)
|
|
721
|
+
- `meta`, if present, must be an object
|
|
722
|
+
- `themes`, if present, must be a `Record<string, Record<string, string>>`
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
import { validateTokenFile, TokenValidationError } from "@agent-scope/tokens";
|
|
726
|
+
|
|
727
|
+
try {
|
|
728
|
+
validateTokenFile(raw);
|
|
729
|
+
// raw is now asserted as TokenFile
|
|
730
|
+
} catch (err) {
|
|
731
|
+
if (err instanceof TokenValidationError) {
|
|
732
|
+
for (const error of err.errors) {
|
|
733
|
+
console.error(`${error.path}: ${error.message} [${error.code}]`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
### Error types
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
class TokenParseError extends Error {
|
|
745
|
+
readonly code: "CIRCULAR_REFERENCE" | "INVALID_REFERENCE" | "INVALID_SCHEMA" | "PARSE_ERROR";
|
|
746
|
+
readonly path?: string;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
class TokenValidationError extends Error {
|
|
750
|
+
readonly errors: ValidationError[];
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
interface ValidationError {
|
|
754
|
+
path: string;
|
|
755
|
+
message: string;
|
|
756
|
+
code: string;
|
|
757
|
+
}
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
## Complete example
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
import {
|
|
766
|
+
parseTokenFileSync,
|
|
767
|
+
TokenResolver,
|
|
768
|
+
ComplianceEngine,
|
|
769
|
+
ImpactAnalyzer,
|
|
770
|
+
exportTokens,
|
|
771
|
+
ThemeResolver,
|
|
772
|
+
} from "@agent-scope/tokens";
|
|
773
|
+
import { readFileSync } from "node:fs";
|
|
774
|
+
|
|
775
|
+
// 1. Parse token file
|
|
776
|
+
const source = readFileSync("reactscope.tokens.json", "utf8");
|
|
777
|
+
const { tokens, rawFile } = parseTokenFileSync(source);
|
|
778
|
+
|
|
779
|
+
// 2. Build resolver
|
|
780
|
+
const resolver = new TokenResolver(tokens);
|
|
781
|
+
resolver.resolve("color.primary.500"); // "#3B82F6"
|
|
782
|
+
resolver.match("#3B82F6", "color"); // exact match → TokenMatch
|
|
783
|
+
resolver.nearest("#3A80F0", "color"); // perceptually closest color
|
|
784
|
+
|
|
785
|
+
// 3. Export tokens
|
|
786
|
+
const css = exportTokens(tokens, "css");
|
|
787
|
+
const ts = exportTokens(tokens, "ts");
|
|
788
|
+
const scss = exportTokens(tokens, "scss");
|
|
789
|
+
|
|
790
|
+
// 4. Compliance audit
|
|
791
|
+
const engine = new ComplianceEngine(resolver);
|
|
792
|
+
const report = engine.audit({
|
|
793
|
+
colors: { background: "#3B82F6" },
|
|
794
|
+
spacing: { paddingTop: "16px" },
|
|
795
|
+
typography: { fontFamily: "Inter, sans-serif" },
|
|
796
|
+
borders: { borderRadius: "6px" },
|
|
797
|
+
shadows: { boxShadow: "0 4px 6px rgba(0,0,0,0.1)" },
|
|
798
|
+
});
|
|
799
|
+
console.log(report.compliance); // e.g. 1
|
|
800
|
+
|
|
801
|
+
// 5. Batch audit + impact analysis
|
|
802
|
+
const batchReport = engine.auditBatch(new Map([
|
|
803
|
+
["Button", { colors: { background: "#3B82F6", color: "#FFFFFF" }, spacing: {}, typography: {}, borders: {}, shadows: {} }],
|
|
804
|
+
["Card", { colors: { background: "#FFFFFF" }, spacing: { gap: "32px" }, typography: {}, borders: { borderRadius: "6px" }, shadows: {} }],
|
|
805
|
+
]));
|
|
806
|
+
|
|
807
|
+
const analyzer = new ImpactAnalyzer(resolver, new Map(Object.entries(batchReport.components)));
|
|
808
|
+
const impact = analyzer.impactOf("color.primary.500", "#1D4ED8");
|
|
809
|
+
console.log(impact.affectedComponentCount); // 1
|
|
810
|
+
console.log(impact.overallSeverity); // "moderate"
|
|
811
|
+
|
|
812
|
+
// 6. Theme resolution
|
|
813
|
+
const themeResolver = ThemeResolver.fromTokenFile(resolver, rawFile);
|
|
814
|
+
themeResolver.resolveThemed("color.primary.500", "dark"); // "#60A5FA"
|
|
815
|
+
themeResolver.resolveAllThemes("color.primary.500"); // { base, dark, "brand-b" }
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## Internal architecture
|
|
821
|
+
|
|
822
|
+
| Module | Responsibility |
|
|
823
|
+
|--------|----------------|
|
|
824
|
+
| `types.ts` | All TypeScript types and error classes |
|
|
825
|
+
| `validator.ts` | Schema validation — collects all errors before throwing |
|
|
826
|
+
| `parser.ts` | JSON/YAML parsing → `flattenTokens` → `resolveValue` (DFS with cycle detection) |
|
|
827
|
+
| `resolver.ts` | `TokenResolver` — path lookup, `match`, `nearest`, `list` |
|
|
828
|
+
| `compliance.ts` | `ComplianceEngine` — style auditing against the token set |
|
|
829
|
+
| `impact.ts` | `ImpactAnalyzer` — downstream change analysis |
|
|
830
|
+
| `export.ts` | `exportTokens` — CSS, TS, SCSS, Tailwind, flat-JSON, Figma |
|
|
831
|
+
| `themes.ts` | `ThemeResolver` — flat and DTCG-style theme overlay resolution |
|
|
832
|
+
| `color-utils.ts` | `hexToLab`, `labDistance`, `parseColorToLab` — perceptual color math |
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
## Used by
|
|
837
|
+
|
|
838
|
+
- `@agent-scope/cli` — token compliance commands (`scope tokens compliance`, `scope tokens export`, `scope tokens impact`, `scope tokens preview`)
|
|
839
|
+
- `@agent-scope/site` — type imports for the Scope web UI
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-scope/tokens",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
4
4
|
"description": "Design token file parser, validator, and resolution engine for Scope",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"module": "./dist/index.js",
|
|
21
21
|
"types": "./dist/index.d.ts",
|
|
22
22
|
"files": [
|
|
23
|
-
"dist"
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build": "tsup",
|