@transtyle/ir 0.1.0-alpha.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.
- package/package.json +22 -0
- package/src/index.js +276 -0
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@transtyle/ir",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Transtyle IR: semantic catalog, provenance constants, token-tree helpers. Zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": { "access": "public" },
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/transtyle/transtyle.git",
|
|
17
|
+
"directory": "packages/ir"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/transtyle/transtyle#readme",
|
|
20
|
+
"bugs": "https://github.com/transtyle/transtyle/issues",
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @transtyle/ir — IR constants and token-tree helpers.
|
|
3
|
+
* Walking-skeleton implementation of docs/architecture/ir.md (IR spec v0 draft).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const IR_SPEC = 'v0-draft';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Semantic color roles. Each carries the full role grid (docs/architecture/ir.md
|
|
10
|
+
* #color-the-role-grid, proposal 0001): prominence (solid/tint/outline/text) x
|
|
11
|
+
* interaction state (rest/hover/active/selected) + on-colors.
|
|
12
|
+
*/
|
|
13
|
+
export const COLOR_ROLES = [
|
|
14
|
+
'primary',
|
|
15
|
+
'secondary',
|
|
16
|
+
'accent',
|
|
17
|
+
'success',
|
|
18
|
+
'warning',
|
|
19
|
+
'danger',
|
|
20
|
+
'info',
|
|
21
|
+
'neutral',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** Valid values for `$extensions.transtyle.role.archetype` (docs/architecture/ir.md §archetypes). */
|
|
25
|
+
export const ROLE_ARCHETYPES = ['brand', 'status', 'neutral'];
|
|
26
|
+
|
|
27
|
+
/** Grid cell suffixes appended to `semantic.color.<role>.`, in derivation order. */
|
|
28
|
+
export const GRID_CELLS = [
|
|
29
|
+
'solid',
|
|
30
|
+
'solid-hover',
|
|
31
|
+
'solid-active',
|
|
32
|
+
'solid-selected',
|
|
33
|
+
'tint',
|
|
34
|
+
'tint-hover',
|
|
35
|
+
'tint-active',
|
|
36
|
+
'tint-selected',
|
|
37
|
+
'outline',
|
|
38
|
+
'outline-hover',
|
|
39
|
+
'on-solid',
|
|
40
|
+
'on-tint',
|
|
41
|
+
'text',
|
|
42
|
+
'text-hover',
|
|
43
|
+
'text-active',
|
|
44
|
+
'text-strong',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Content hierarchy rungs under `semantic.color.text.<rung>` (docs/architecture/ir.md). */
|
|
48
|
+
export const TEXT_RUNGS = ['strong', 'base', 'muted', 'subtle', 'disabled', 'inverse'];
|
|
49
|
+
|
|
50
|
+
/** Elevation ladder: surfaces at levels 0-5, shadows at levels 1-4 (F2: scrim stays separate). */
|
|
51
|
+
export const ELEVATION_LEVELS = [0, 1, 2, 3, 4, 5];
|
|
52
|
+
export const SHADOW_LEVELS = [1, 2, 3, 4];
|
|
53
|
+
|
|
54
|
+
/** z-index ladder — key order is the contract; values are catalog defaults unless authored. */
|
|
55
|
+
export const Z_LADDER = [
|
|
56
|
+
'hide',
|
|
57
|
+
'base',
|
|
58
|
+
'dropdown',
|
|
59
|
+
'sticky',
|
|
60
|
+
'banner',
|
|
61
|
+
'overlay',
|
|
62
|
+
'modal',
|
|
63
|
+
'popover',
|
|
64
|
+
'toast',
|
|
65
|
+
'tooltip',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/** Provenance kinds. */
|
|
69
|
+
export const PROVENANCE = {
|
|
70
|
+
AUTHORED: 'authored',
|
|
71
|
+
ALIASED: 'aliased',
|
|
72
|
+
DERIVED: 'derived',
|
|
73
|
+
DEFAULTED: 'defaulted',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Coverage classes (docs/specs/validation-and-coverage.md). */
|
|
77
|
+
export const COVERAGE = ['native', 'derived', 'approximated', 'dropped', 'unsupported'];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* One `dropped` coverage entry per configured mode dimension an exporter
|
|
81
|
+
* doesn't express (docs/architecture/ir.md#modes: "Exporters declare which
|
|
82
|
+
* mode dimensions they can express; inexpressible dimensions surface in the
|
|
83
|
+
* coverage report"). No-op (empty array) when the compile only has the
|
|
84
|
+
* dimensions the exporter already expresses — e.g. a color-scheme-only
|
|
85
|
+
* compile never gets a spurious "density dropped" line.
|
|
86
|
+
*/
|
|
87
|
+
export function droppedDimensions(dimensionNames, expressed) {
|
|
88
|
+
return (dimensionNames ?? [])
|
|
89
|
+
.filter((d) => !expressed.includes(d))
|
|
90
|
+
.map((d) => ({
|
|
91
|
+
variable: `(mode:${d})`,
|
|
92
|
+
slot: '—',
|
|
93
|
+
class: 'dropped',
|
|
94
|
+
note: `${d} mode dimension not expressed by this target`,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Component tier (docs/plan/component-tier.md C2; docs/specs/component-layer.md;
|
|
100
|
+
* generalized by AL2 — docs/proposals/0003-component-catalog-generalization.md).
|
|
101
|
+
* Per component, per token: `defaultFrom` is a bare `semantic.*` path the token
|
|
102
|
+
* aliases when unauthored, or `component:<path>` to default from another
|
|
103
|
+
* component slot. Either way an empty `component.*` tier still compiles,
|
|
104
|
+
* exactly like every other resolve-or-fill slot in the catalog.
|
|
105
|
+
*
|
|
106
|
+
* `control` is the shared **interactive-control** geometry — the one component
|
|
107
|
+
* grouping both reference component-heavy targets converge on *architecturally*,
|
|
108
|
+
* not just nominally: Bootstrap chains `$btn-padding-*` from the shared
|
|
109
|
+
* `$input-btn-padding-*` root, and PrimeNG's Button reads the same `formField`
|
|
110
|
+
* object its inputs do. Buttons then layer on top (authoring `control.*` moves
|
|
111
|
+
* both; authoring `button.*` moves only buttons) — the two-level model both
|
|
112
|
+
* upstreams already implement. Order matters: entries may only `component:`
|
|
113
|
+
* -default from an EARLIER entry in this object.
|
|
114
|
+
*
|
|
115
|
+
* `defaultFrom` is optional. A slot without one exists **only when authored** —
|
|
116
|
+
* appropriate when the meaning is real and shared but no semantic rung expresses
|
|
117
|
+
* it, so inventing a default would mean picking one target's number. That is the
|
|
118
|
+
* case for `tooltip.max-width` below.
|
|
119
|
+
*
|
|
120
|
+
* Deliberately NOT here (evidence passes, see the proposals): the sm/lg size
|
|
121
|
+
* ladder (both targets have one, but they disagree on which rungs — the
|
|
122
|
+
* disagreement is the finding), nav/list/table item padding (correspondence is
|
|
123
|
+
* nominal, not architectural), and — after the 0004 geometry probe — component
|
|
124
|
+
* sizing generally: of ten geometry concepts Bootstrap tokenizes, six are
|
|
125
|
+
* one-sided or false friends and two more disagree architecturally. Exporters
|
|
126
|
+
* derive all of those privately today.
|
|
127
|
+
*/
|
|
128
|
+
export const COMPONENT_CATALOG = {
|
|
129
|
+
control: {
|
|
130
|
+
radius: { type: 'dimension', defaultFrom: 'radius.control' },
|
|
131
|
+
'padding-x': { type: 'dimension', defaultFrom: 'space.4' },
|
|
132
|
+
'padding-y': { type: 'dimension', defaultFrom: 'space.2' },
|
|
133
|
+
},
|
|
134
|
+
button: {
|
|
135
|
+
radius: { type: 'dimension', defaultFrom: 'component:control.radius' },
|
|
136
|
+
'padding-x': { type: 'dimension', defaultFrom: 'component:control.padding-x' },
|
|
137
|
+
'padding-y': { type: 'dimension', defaultFrom: 'component:control.padding-y' },
|
|
138
|
+
},
|
|
139
|
+
/**
|
|
140
|
+
* The overlay measure (proposal 0004). Promoted on the strongest evidence the
|
|
141
|
+
* two-target bar has seen: Bootstrap (`$tooltip-max-width: 200px`) and PrimeNG
|
|
142
|
+
* (`tooltip.root.maxWidth: 12.5rem` — *the same 200px*) independently constrain
|
|
143
|
+
* the same element the same way at the same measure, and PrimeNG carries only
|
|
144
|
+
* two `maxWidth` slots in its entire 2759-slot surface. Two libraries agreeing
|
|
145
|
+
* that this specific element is the one needing a width ceiling, rather than
|
|
146
|
+
* two libraries incidentally having numbers.
|
|
147
|
+
*
|
|
148
|
+
* The decision underneath is typographic — a tooltip is a short line of text
|
|
149
|
+
* and ~200px is roughly a readable measure — which is a design-system opinion,
|
|
150
|
+
* exactly what belongs in a design-system vocabulary.
|
|
151
|
+
*
|
|
152
|
+
* No `defaultFrom`: the IR has no "readable measure" rung, and synthesizing one
|
|
153
|
+
* from either target's number would be inventing catalog vocabulary on one
|
|
154
|
+
* upstream's authority. Unauthored, both exporters keep their own default.
|
|
155
|
+
*/
|
|
156
|
+
tooltip: {
|
|
157
|
+
'max-width': { type: 'dimension' },
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Reserved mode dimension names (docs/architecture/ir.md §reserved-mode-dimensions) — names only, every dimension stays optional. */
|
|
162
|
+
export const RESERVED_MODE_DIMENSIONS = [
|
|
163
|
+
'color-scheme',
|
|
164
|
+
'density',
|
|
165
|
+
'contrast',
|
|
166
|
+
'motion',
|
|
167
|
+
'platform',
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Combine one value per mode dimension into the compound key used to address
|
|
172
|
+
* a compiled mode combo (docs/plan/catalog-revision.md T8) — `["color-scheme",
|
|
173
|
+
* "density"], {"color-scheme":"dark","density":"compact"}` -> `"dark+compact"`.
|
|
174
|
+
* Order is `dimNames`, not object insertion, so both directions of the
|
|
175
|
+
* key<->values mapping are deterministic across the compile.
|
|
176
|
+
*/
|
|
177
|
+
export function comboKey(dimNames, values) {
|
|
178
|
+
return dimNames.map((d) => values[d]).join('+');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The full cross-product of every configured mode dimension's values, in
|
|
183
|
+
* dimension-declaration order. `dimEntries` is `Object.entries(config.modes)`.
|
|
184
|
+
* Returns `[{ key, values: {dimName: value} }, ...]`.
|
|
185
|
+
*/
|
|
186
|
+
export function expandModeMatrix(dimEntries) {
|
|
187
|
+
const dimNames = dimEntries.map(([name]) => name);
|
|
188
|
+
let combos = [{}];
|
|
189
|
+
for (const [name, def] of dimEntries) {
|
|
190
|
+
const next = [];
|
|
191
|
+
for (const combo of combos) for (const v of def.values) next.push({ ...combo, [name]: v });
|
|
192
|
+
combos = next;
|
|
193
|
+
}
|
|
194
|
+
return combos.map((values) => ({ key: comboKey(dimNames, values), values }));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const ALIAS_RE = /^\{([^}]+)\}$/;
|
|
198
|
+
|
|
199
|
+
/** If `value` is a DTCG alias like "{option.color.blue.600}", return the path; else null. */
|
|
200
|
+
export function aliasTarget(value) {
|
|
201
|
+
if (typeof value !== 'string') return null;
|
|
202
|
+
const m = ALIAS_RE.exec(value.trim());
|
|
203
|
+
return m ? m[1] : null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Collect tokens from a merged DTCG(-superset) tree.
|
|
208
|
+
* Returns Map<path, { type, value, modeValues: {dimension: {modeName: value}} }>.
|
|
209
|
+
* Handles group-level $type inheritance and the transtyle.modes extension.
|
|
210
|
+
*/
|
|
211
|
+
export function collectTokens(tree) {
|
|
212
|
+
const out = new Map();
|
|
213
|
+
const walk = (node, path, inheritedType) => {
|
|
214
|
+
if (node === null || typeof node !== 'object' || Array.isArray(node)) return;
|
|
215
|
+
const type = node.$type ?? inheritedType;
|
|
216
|
+
if ('$value' in node) {
|
|
217
|
+
const modeValues = node.$extensions?.['transtyle.modes'] ?? {};
|
|
218
|
+
out.set(path.join('.'), { type, value: node.$value, modeValues });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
for (const [key, child] of Object.entries(node)) {
|
|
222
|
+
if (key.startsWith('$')) continue;
|
|
223
|
+
walk(child, [...path, key], type);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
walk(tree, [], undefined);
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Custom color roles opting into the full grid via `$extensions.transtyle.role`
|
|
232
|
+
* on a `semantic.color.<name>` group (docs/architecture/ir.md §archetypes,
|
|
233
|
+
* plan task T7) — e.g. `{ "solid": {...}, "$extensions": { "transtyle.role":
|
|
234
|
+
* { "archetype": "status" } } }`. Built-in roles are excluded (they don't need
|
|
235
|
+
* the extension). Returns Map<roleName, archetype>.
|
|
236
|
+
*/
|
|
237
|
+
export function collectRoleArchetypes(tree, diagnostics) {
|
|
238
|
+
const out = new Map();
|
|
239
|
+
const colorRoot = tree?.semantic?.color;
|
|
240
|
+
if (!colorRoot || typeof colorRoot !== 'object') return out;
|
|
241
|
+
for (const [name, node] of Object.entries(colorRoot)) {
|
|
242
|
+
if (name.startsWith('$') || COLOR_ROLES.includes(name)) continue;
|
|
243
|
+
const archetype = node?.$extensions?.['transtyle.role']?.archetype;
|
|
244
|
+
if (!archetype) continue;
|
|
245
|
+
if (!ROLE_ARCHETYPES.includes(archetype)) {
|
|
246
|
+
diagnostics?.warn(
|
|
247
|
+
'TST1111',
|
|
248
|
+
`semantic.color.${name}: unknown role archetype "${archetype}" (expected one of ${ROLE_ARCHETYPES.join(', ')}) — role still joins the grid`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
out.set(name, archetype);
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Deep-merge token trees (later wins; conflicts reported via onConflict(path)). */
|
|
257
|
+
export function mergeTrees(trees, onConflict = () => {}) {
|
|
258
|
+
const merged = {};
|
|
259
|
+
const mergeInto = (dst, src, path) => {
|
|
260
|
+
for (const [key, val] of Object.entries(src)) {
|
|
261
|
+
if (val !== null && typeof val === 'object' && !Array.isArray(val) && !('$value' in val)) {
|
|
262
|
+
if (!(key in dst)) dst[key] = {};
|
|
263
|
+
else if ('$value' in dst[key]) {
|
|
264
|
+
onConflict([...path, key].join('.'));
|
|
265
|
+
dst[key] = {};
|
|
266
|
+
}
|
|
267
|
+
mergeInto(dst[key], val, [...path, key]);
|
|
268
|
+
} else {
|
|
269
|
+
if (key in dst && !key.startsWith('$')) onConflict([...path, key].join('.'));
|
|
270
|
+
dst[key] = val;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
for (const t of trees) mergeInto(merged, t, []);
|
|
275
|
+
return merged;
|
|
276
|
+
}
|