@bridge-ds/core 0.8.0 → 0.10.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 (36) hide show
  1. package/README.md +27 -1
  2. package/bridge.config.schema.json +23 -0
  3. package/dist/audit/index.d.ts +1 -0
  4. package/dist/audit/index.js +269 -23
  5. package/dist/audit/index.js.map +1 -1
  6. package/dist/audit/missing-pieces.d.ts +43 -0
  7. package/dist/config.d.ts +28 -0
  8. package/dist/docs-proposal.d.ts +58 -10
  9. package/dist/figma-push-github.d.ts +143 -0
  10. package/dist/figma-push.d.ts +204 -0
  11. package/dist/github-client.d.ts +15 -1
  12. package/dist/index.d.ts +9 -0
  13. package/dist/index.js +1569 -3179
  14. package/dist/index.js.map +1 -1
  15. package/dist/node/git-local.d.ts +15 -0
  16. package/dist/node/index.d.ts +1 -0
  17. package/dist/node/index.js +201 -10
  18. package/dist/node/index.js.map +1 -1
  19. package/dist/node/suggestion-apply-node.d.ts +69 -0
  20. package/dist/{component-spec-SKuywZqr.js → prod-branch-BdMPKffE.js} +1134 -1438
  21. package/dist/prod-branch-BdMPKffE.js.map +1 -0
  22. package/dist/prod-branch-github.d.ts +160 -0
  23. package/dist/prod-branch.d.ts +267 -0
  24. package/dist/{report-iPSnsjYi.js → report-DOnKUw8G.js} +24 -24
  25. package/dist/{report-iPSnsjYi.js.map → report-DOnKUw8G.js.map} +1 -1
  26. package/dist/suggestion-apply-ByBEYtct.js +4802 -0
  27. package/dist/suggestion-apply-ByBEYtct.js.map +1 -0
  28. package/dist/suggestion-apply.d.ts +150 -0
  29. package/dist/suggestion-review.d.ts +70 -0
  30. package/dist/suggestions-github.d.ts +131 -0
  31. package/dist/suggestions.d.ts +236 -0
  32. package/dist/token-proposal.d.ts +123 -0
  33. package/package.json +1 -1
  34. package/dist/component-spec-SKuywZqr.js.map +0 -1
  35. package/dist/layered-BnbmToGs.js +0 -500
  36. package/dist/layered-BnbmToGs.js.map +0 -1
@@ -1,4 +1,476 @@
1
- import { a as collectRefs, i as isAlias, b as aliasTarget, h as hexToRgba, p as parseColor, v as valuesEqual, f as formatValue, L as LayeredTokenSet, d as aliasOf, r as rgbaToDtcg, c as comboKey, e as rgbaToHex, g as globToRegExp, j as validateLayered } from "./layered-BnbmToGs.js";
1
+ const ALIAS_RE = /^\{([^{}]+)\}$/;
2
+ const isAlias = (v) => typeof v === "string" && ALIAS_RE.test(v);
3
+ const aliasTarget = (v) => ALIAS_RE.exec(v)?.[1] ?? v;
4
+ const toAlias = (path) => `{${path}}`;
5
+ function collectRefs(value, out = []) {
6
+ if (isAlias(value)) out.push(aliasTarget(value));
7
+ else if (Array.isArray(value)) value.forEach((v) => collectRefs(v, out));
8
+ else if (value && typeof value === "object") Object.values(value).forEach((v) => collectRefs(v, out));
9
+ return out;
10
+ }
11
+ const clamp01 = (n) => Math.min(1, Math.max(0, n));
12
+ const round = (n, digits) => {
13
+ const f = 10 ** digits;
14
+ return Math.round(n * f) / f;
15
+ };
16
+ function hexToRgba(hex) {
17
+ const m = /^#?([0-9a-f]{3,8})$/i.exec(hex.trim());
18
+ if (!m) return void 0;
19
+ let h = m[1];
20
+ if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
21
+ if (h.length !== 6 && h.length !== 8) return void 0;
22
+ const n = (idx) => parseInt(h.slice(idx, idx + 2), 16) / 255;
23
+ return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) : 1 };
24
+ }
25
+ const to255 = (n) => Math.round(clamp01(n) * 255);
26
+ const rgbaToHex = (c) => "#" + [c.r, c.g, c.b].map((x) => to255(x).toString(16).padStart(2, "0")).join("");
27
+ function parseColor(value) {
28
+ if (typeof value === "string") return isAlias(value) ? void 0 : hexToRgba(value);
29
+ if (!value || typeof value !== "object") return void 0;
30
+ const v = value;
31
+ const alpha = typeof v.alpha === "number" ? v.alpha : 1;
32
+ if (v.colorSpace === "srgb" && Array.isArray(v.components) && v.components.length === 3) {
33
+ const [r, g, b] = v.components;
34
+ if ([r, g, b].every((x) => typeof x === "number")) return { r, g, b, a: alpha };
35
+ }
36
+ if (typeof v.hex === "string") {
37
+ const c = hexToRgba(v.hex);
38
+ if (c) return { ...c, a: alpha };
39
+ }
40
+ return void 0;
41
+ }
42
+ function rgbaToDtcg(c) {
43
+ const out = {
44
+ colorSpace: "srgb",
45
+ components: [round(clamp01(c.r), 3), round(clamp01(c.g), 3), round(clamp01(c.b), 3)]
46
+ };
47
+ if (c.a < 0.9995) out.alpha = round(clamp01(c.a), 3);
48
+ out.hex = rgbaToHex(c);
49
+ return out;
50
+ }
51
+ function colorsEqual(a, b) {
52
+ return rgbaToHex(a) === rgbaToHex(b) && Math.abs(a.a - b.a) < 2e-3;
53
+ }
54
+ function parseDimension(value) {
55
+ if (value && typeof value === "object" && !Array.isArray(value)) {
56
+ const v = value;
57
+ if (typeof v.value === "number" && typeof v.unit === "string") return { value: v.value, unit: v.unit };
58
+ }
59
+ if (typeof value === "string") {
60
+ const m = /^(-?\d+(?:\.\d+)?)(px|rem)$/.exec(value.trim());
61
+ if (m) return { value: Number(m[1]), unit: m[2] };
62
+ }
63
+ if (typeof value === "number") return { value, unit: "px" };
64
+ return void 0;
65
+ }
66
+ const numbersEqual = (a, b) => Math.abs(a - b) < 1e-4;
67
+ function deepEqual(a, b) {
68
+ if (a === b) return true;
69
+ if (typeof a === "number" && typeof b === "number") return numbersEqual(a, b);
70
+ if (typeof a !== typeof b || !a || !b || typeof a !== "object") return false;
71
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
72
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, idx) => deepEqual(v, b[idx]));
73
+ const ka = Object.keys(a).filter((k) => a[k] !== void 0);
74
+ const kb = Object.keys(b).filter((k) => b[k] !== void 0);
75
+ if (ka.length !== kb.length) return false;
76
+ return ka.every((k) => deepEqual(a[k], b[k]));
77
+ }
78
+ function valuesEqual(type, a, b) {
79
+ if (a === void 0 || b === void 0) return a === b;
80
+ if (isAlias(a) || isAlias(b)) return isAlias(a) && isAlias(b) && aliasTarget(a) === aliasTarget(b);
81
+ if (type === "color") {
82
+ const ca = parseColor(a);
83
+ const cb = parseColor(b);
84
+ if (ca && cb) return colorsEqual(ca, cb);
85
+ }
86
+ if (type === "dimension") {
87
+ const da = parseDimension(a);
88
+ const db = parseDimension(b);
89
+ if (da && db) return da.unit === db.unit && numbersEqual(da.value, db.value);
90
+ }
91
+ return deepEqual(a, b);
92
+ }
93
+ function formatValue(type, value) {
94
+ if (value === void 0) return "none";
95
+ if (isAlias(value)) return aliasTarget(value);
96
+ if (type === "color") {
97
+ const c = parseColor(value);
98
+ const hex = value && typeof value === "object" && typeof value.hex === "string" ? value.hex.toLowerCase() : void 0;
99
+ if (c) return c.a < 0.9995 ? `${hex ?? rgbaToHex(c)} @ ${Math.round(c.a * 100)}%` : hex ?? rgbaToHex(c);
100
+ }
101
+ if (type === "dimension") {
102
+ const d = parseDimension(value);
103
+ if (d) return `${d.value}${d.unit}`;
104
+ }
105
+ if (Array.isArray(value)) return value.join(", ");
106
+ if (typeof value === "object" && value) return JSON.stringify(value);
107
+ return String(value);
108
+ }
109
+ const DTCG_TYPES = /* @__PURE__ */ new Set(["color", "dimension", "fontFamily", "fontWeight", "duration", "cubicBezier", "number", "strokeStyle", "border", "transition", "shadow", "gradient", "typography", "fontStyle", "string", "boolean"]);
110
+ const aliasOf = (v) => isAlias(v) ? aliasTarget(v) : null;
111
+ function globToRegExp(glob) {
112
+ let re = "";
113
+ for (let i = 0; i < glob.length; i++) {
114
+ const c = glob[i];
115
+ if (c === "*" && glob[i + 1] === "*") {
116
+ if (glob[i + 2] === "/") {
117
+ re += "(?:.*/)?";
118
+ i += 2;
119
+ } else {
120
+ re += ".*";
121
+ i += 1;
122
+ }
123
+ } else if (c === "*") re += "[^/]*";
124
+ else if (c === "?") re += "[^/]";
125
+ else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
126
+ }
127
+ return new RegExp(`^${re}$`);
128
+ }
129
+ const comboKey = (c) => Object.entries(c).map(([k, v]) => `${k}=${v}`).join(",") || "default";
130
+ function flattenLayerFile(json, file, issues) {
131
+ const out = [];
132
+ const walk = (node, path, inheritedType) => {
133
+ if (!node || typeof node !== "object" || Array.isArray(node)) return;
134
+ const n = node;
135
+ const type = typeof n.$type === "string" ? n.$type : inheritedType;
136
+ if ("$value" in n) {
137
+ out.push({
138
+ path: path.join("."),
139
+ ...type ? { type } : {},
140
+ value: n.$value,
141
+ ...typeof n.$description === "string" ? { description: n.$description } : {},
142
+ ...n.$extensions && typeof n.$extensions === "object" ? { extensions: n.$extensions } : {},
143
+ ...n.$deprecated !== void 0 ? { deprecated: n.$deprecated } : {},
144
+ file
145
+ });
146
+ return;
147
+ }
148
+ for (const [k, v] of Object.entries(n)) {
149
+ if (k.startsWith("$")) continue;
150
+ const p = [...path, k].join(".");
151
+ if (/[{}.]/.test(k)) issues.push({ level: "error", code: "invalid-name", message: `Name "${k}" contains {, } or . which DTCG forbids.`, path: p, file });
152
+ if (v && typeof v === "object" && !Array.isArray(v)) walk(v, [...path, k], type);
153
+ else issues.push({ level: "error", code: "not-a-token", message: `"${p}" is neither a token (no $value) nor a group.`, path: p, file });
154
+ }
155
+ };
156
+ walk(json, []);
157
+ return out;
158
+ }
159
+ const toHex2 = (n) => Math.round(Math.max(0, Math.min(1, n)) * 255).toString(16).padStart(2, "0");
160
+ function compactJson(v) {
161
+ if (Array.isArray(v)) return `[${v.map(compactJson).join(", ")}]`;
162
+ if (v && typeof v === "object") {
163
+ const e = Object.entries(v);
164
+ return e.length ? `{ ${e.map(([k, x]) => `${JSON.stringify(k)}: ${compactJson(x)}`).join(", ")} }` : "{}";
165
+ }
166
+ return JSON.stringify(v);
167
+ }
168
+ function colorToCss(c) {
169
+ if (typeof c === "string") return c;
170
+ if (!c || typeof c !== "object") return String(c);
171
+ const o = c;
172
+ const [r, g, b] = (o.components ?? []).map((x) => typeof x === "number" ? x : 0);
173
+ const alpha = o.alpha ?? 1;
174
+ if (alpha < 1) return `rgb(${[r, g, b].map((x) => Math.round((x ?? 0) * 255)).join(" ")} / ${alpha})`;
175
+ return o.hex ?? `#${[r, g, b].map((x) => toHex2(x ?? 0)).join("")}`;
176
+ }
177
+ const dimToCss = (d) => d && typeof d === "object" && "value" in d ? `${d.value}${d.unit}` : String(d);
178
+ function valueToCss(type, v, ext) {
179
+ if (v === void 0 || v === null) return "";
180
+ switch (type) {
181
+ case "color":
182
+ return colorToCss(v);
183
+ case "dimension":
184
+ case "duration":
185
+ return dimToCss(v);
186
+ case "cubicBezier":
187
+ return Array.isArray(v) ? `cubic-bezier(${v.join(", ")})` : String(v);
188
+ case "fontFamily":
189
+ return Array.isArray(v) ? v.map((n) => /\s/.test(String(n)) ? `"${n}"` : String(n)).join(", ") : String(v);
190
+ case "shadow": {
191
+ if (typeof v === "string") return v;
192
+ const layers = Array.isArray(v) ? v : [v];
193
+ return layers.map((l) => [l.inset ? "inset" : "", dimToCss(l.offsetX), dimToCss(l.offsetY), dimToCss(l.blur), dimToCss(l.spread ?? { value: 0, unit: "px" }), colorToCss(l.color)].filter(Boolean).join(" ")).join(", ");
194
+ }
195
+ default: {
196
+ const unit = ext?.["dev.bridge.unit"];
197
+ if (typeof v === "number" && typeof unit === "string") return `${v}${unit}`;
198
+ return typeof v === "object" ? compactJson(v) : String(v);
199
+ }
200
+ }
201
+ }
202
+ class LayeredTokenSet {
203
+ /** `files`: token file path relative to tokens.root -> file text. */
204
+ constructor(config, files) {
205
+ this.config = config;
206
+ this.files = files;
207
+ this.fileNames = [...files.keys()].sort();
208
+ for (const f of this.fileNames) {
209
+ let json;
210
+ try {
211
+ json = JSON.parse(files.get(f));
212
+ } catch (e) {
213
+ this.issues.push({ level: "error", code: "invalid-json", message: `Invalid JSON: ${e.message}`, file: f });
214
+ continue;
215
+ }
216
+ this.defs.push(...flattenLayerFile(json, f, this.issues));
217
+ }
218
+ }
219
+ config;
220
+ files;
221
+ defs = [];
222
+ issues = [];
223
+ fileNames;
224
+ views = /* @__PURE__ */ new Map();
225
+ get modeNames() {
226
+ return Object.keys(this.config.axes);
227
+ }
228
+ get defaultCombo() {
229
+ return Object.fromEntries(Object.entries(this.config.axes).map(([k, m]) => [k, m.default]));
230
+ }
231
+ /** Every combination of the given modes (all modes when omitted); other modes stay at default. */
232
+ combos(only) {
233
+ let out = [this.defaultCombo];
234
+ for (const [name, m] of Object.entries(this.config.axes)) {
235
+ if (only && !only.includes(name)) continue;
236
+ out = out.flatMap((c) => m.values.map((v) => ({ ...c, [name]: v })));
237
+ }
238
+ return out;
239
+ }
240
+ normalizeCombo(partial = {}) {
241
+ const c = { ...this.defaultCombo };
242
+ for (const [k, v] of Object.entries(partial)) {
243
+ if (v === void 0) continue;
244
+ const m = this.config.axes[k];
245
+ if (!m) throw new Error(`Unknown mode "${k}". Modes: ${this.modeNames.join(", ") || "none"}.`);
246
+ if (!m.values.includes(v)) throw new Error(`Unknown ${k} "${v}". Values: ${m.values.join(", ")}.`);
247
+ c[k] = v;
248
+ }
249
+ return c;
250
+ }
251
+ tierOfFile(file) {
252
+ for (const [tier, dirs] of Object.entries(this.config.tokens.tiers)) {
253
+ if (dirs.some((d) => file === d || file.startsWith(`${d.replace(/\/$/, "")}/`) || globToRegExp(d).test(file))) return tier;
254
+ }
255
+ return void 0;
256
+ }
257
+ /** Tier of a token: the lowest layer that defines it in the default mode (an override does not change its tier). */
258
+ tierOf(path) {
259
+ const d = this.view(this.defaultCombo).get(path)?.[0] ?? this.defs.find((x) => x.path === path);
260
+ return d ? this.tierOfFile(d.file) : void 0;
261
+ }
262
+ /** Files of each layer for a combo, in resolution order. */
263
+ layerFiles(combo) {
264
+ const files = [];
265
+ const missing = [];
266
+ for (const l of this.config.tokens.layers) {
267
+ const pattern = l.files.replace(/\{([^}]+)\}/g, (_, k) => combo[k] ?? `{${k}}`);
268
+ const re = globToRegExp(pattern);
269
+ const hit = this.fileNames.filter((f) => re.test(f));
270
+ if (!hit.length && !l.optional) missing.push(pattern);
271
+ for (const f of hit) if (!files.includes(f)) files.push(f);
272
+ }
273
+ return { files, missing };
274
+ }
275
+ /** path -> definitions in layer order (last one wins) for a combo. */
276
+ view(combo) {
277
+ const key = comboKey(combo);
278
+ let v = this.views.get(key);
279
+ if (!v) {
280
+ const { files, missing } = this.layerFiles(combo);
281
+ const map = /* @__PURE__ */ new Map();
282
+ const byFile = /* @__PURE__ */ new Map();
283
+ for (const d of this.defs) (byFile.get(d.file) ?? byFile.set(d.file, []).get(d.file)).push(d);
284
+ for (const f of files) for (const d of byFile.get(f) ?? []) (map.get(d.path) ?? map.set(d.path, []).get(d.path)).push(d);
285
+ v = { map, missing };
286
+ this.views.set(key, v);
287
+ }
288
+ return v.map;
289
+ }
290
+ missingLayers(combo) {
291
+ this.view(combo);
292
+ return this.views.get(comboKey(combo)).missing;
293
+ }
294
+ /** Every token path defined in any combo. */
295
+ allPaths() {
296
+ return [...new Set(this.defs.map((d) => d.path))].sort();
297
+ }
298
+ defsOf(path) {
299
+ return this.defs.filter((d) => d.path === path);
300
+ }
301
+ /** First combo whose layers include `file`, preferring the default. */
302
+ comboForFile(file) {
303
+ const all = [this.defaultCombo, ...this.combos()];
304
+ return all.find((c) => this.layerFiles(c).files.includes(file)) ?? this.defaultCombo;
305
+ }
306
+ resolve(path, combo) {
307
+ const view = this.view(combo);
308
+ const chain = [path];
309
+ const top = view.get(path);
310
+ if (!top) return { path, value: void 0, css: "", chain, definedIn: [], error: `"${path}" is not defined for ${comboKey(combo)}` };
311
+ const def = top[top.length - 1];
312
+ let type = def.type;
313
+ let error;
314
+ const deref = (target, stack) => {
315
+ if (stack.includes(target)) {
316
+ error = `circular alias: ${[...stack, target].join(" -> ")}`;
317
+ return void 0;
318
+ }
319
+ const t = view.get(target);
320
+ if (!t) {
321
+ error = `broken alias {${target}}`;
322
+ return void 0;
323
+ }
324
+ const td = t[t.length - 1];
325
+ type ??= td.type;
326
+ return walk(td.value, [...stack, target]);
327
+ };
328
+ const walk = (v, stack) => {
329
+ const a = aliasOf(v);
330
+ if (a) return deref(a, stack);
331
+ if (Array.isArray(v)) return v.map((x) => walk(x, stack));
332
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x, stack)]));
333
+ return v;
334
+ };
335
+ let cur = def.value;
336
+ const seen = /* @__PURE__ */ new Set([path]);
337
+ for (let a = aliasOf(cur); a; a = aliasOf(cur)) {
338
+ chain.push(a);
339
+ if (seen.has(a)) break;
340
+ seen.add(a);
341
+ const t = view.get(a);
342
+ if (!t) break;
343
+ cur = t[t.length - 1].value;
344
+ }
345
+ const value = walk(def.value, [path]);
346
+ return { path, ...type ? { type } : {}, value, css: error ? "" : valueToCss(type, value, def.extensions), chain, definedIn: top.map((d) => d.file), ...error ? { error } : {} };
347
+ }
348
+ /** Tokens whose own value aliases `path` directly, across all files. */
349
+ referencedBy(path) {
350
+ const needle = `{${path}}`;
351
+ return this.defs.filter((d) => JSON.stringify(d.value).includes(needle)).map((d) => ({ path: d.path, file: d.file }));
352
+ }
353
+ cssVarName(path) {
354
+ return `${this.config.cssVar.prefix}${path.split(".").join(this.config.cssVar.separator)}`;
355
+ }
356
+ /** Repo-relative path of a token file. */
357
+ repoPath(file) {
358
+ const root = this.config.tokens.root;
359
+ return root === "." || !root ? file : `${root.replace(/\/$/, "")}/${file}`;
360
+ }
361
+ }
362
+ function selectTokenFiles(config, repoFiles) {
363
+ const root = config.tokens.root.replace(/\/$/, "");
364
+ const rel = (f) => root === "." || !root ? f : f.startsWith(`${root}/`) ? f.slice(root.length + 1) : null;
365
+ const inc = config.tokens.include.map(globToRegExp);
366
+ const exc = config.tokens.exclude.map(globToRegExp);
367
+ return repoFiles.filter((f) => f !== config.contrast?.path).map(rel).filter((f) => f !== null && inc.some((r) => r.test(f)) && !exc.some((r) => r.test(f)));
368
+ }
369
+ function checkShape(type, v) {
370
+ if (aliasOf(v)) return null;
371
+ const isObj2 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
372
+ switch (type) {
373
+ case "color":
374
+ if (typeof v === "string") return /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(v) ? "warn:color as a hex string: DTCG 2025.10 uses { colorSpace, components, hex }" : "color must be { colorSpace, components } (or a hex string)";
375
+ if (!isObj2(v) || typeof v.colorSpace !== "string" || !Array.isArray(v.components)) return "color must be { colorSpace, components, alpha?, hex? }";
376
+ if (v.components.length !== 3 || !v.components.every((c) => typeof c === "number" || c === "none" || aliasOf(c))) return "color components must be 3 numbers";
377
+ if (v.alpha !== void 0 && (typeof v.alpha !== "number" || v.alpha < 0 || v.alpha > 1)) return "color alpha must be between 0 and 1";
378
+ if (v.hex !== void 0 && !/^#[0-9a-f]{6}$/i.test(String(v.hex))) return "color hex must be #rrggbb";
379
+ return null;
380
+ case "dimension":
381
+ if (typeof v === "string" || typeof v === "number") return "warn:dimension as a plain value: DTCG 2025.10 uses { value, unit }";
382
+ return isObj2(v) && typeof v.value === "number" && ["px", "rem"].includes(String(v.unit)) ? null : 'dimension must be { value: number, unit: "px" | "rem" }';
383
+ case "duration":
384
+ if (typeof v === "string") return "warn:duration as a string: DTCG 2025.10 uses { value, unit }";
385
+ return isObj2(v) && typeof v.value === "number" && ["ms", "s"].includes(String(v.unit)) ? null : 'duration must be { value: number, unit: "ms" | "s" }';
386
+ case "cubicBezier":
387
+ return Array.isArray(v) && v.length === 4 && v.every((x) => typeof x === "number") && v[0] >= 0 && v[0] <= 1 && v[2] >= 0 && v[2] <= 1 ? null : "cubicBezier must be [x1, y1, x2, y2] with x in 0..1";
388
+ case "fontFamily":
389
+ return typeof v === "string" || Array.isArray(v) && v.every((x) => typeof x === "string") ? null : "fontFamily must be a string or an array of strings";
390
+ case "fontWeight":
391
+ return typeof v === "number" && v >= 1 && v <= 1e3 || typeof v === "string" ? null : "fontWeight must be 1..1000 or a keyword";
392
+ case "number":
393
+ return typeof v === "number" ? null : "number must be a number";
394
+ case "shadow": {
395
+ const layers = Array.isArray(v) ? v : [v];
396
+ return layers.every((l) => isObj2(l) && "color" in l && "offsetX" in l && "offsetY" in l && "blur" in l) ? null : "shadow must be { color, offsetX, offsetY, blur, spread? } or an array of them";
397
+ }
398
+ default:
399
+ return null;
400
+ }
401
+ }
402
+ function validateLayered(set) {
403
+ const issues = [...set.issues];
404
+ for (const d of set.defs) {
405
+ if (d.type && !DTCG_TYPES.has(d.type)) issues.push({ level: "error", code: "unknown-type", message: `Unknown $type "${d.type}".`, path: d.path, file: d.file });
406
+ if (d.type) {
407
+ const s = checkShape(d.type, d.value);
408
+ if (s) issues.push({ level: s.startsWith("warn:") ? "warning" : "error", code: "invalid-value", message: s.replace(/^warn:/, ""), path: d.path, file: d.file });
409
+ }
410
+ }
411
+ const merged = /* @__PURE__ */ new Map();
412
+ const add = (i, combo) => {
413
+ const k = `${i.code}|${i.path}|${i.message}`;
414
+ const prev = merged.get(k);
415
+ if (prev) prev.modes.push(comboKey(combo));
416
+ else merged.set(k, { ...i, modes: [comboKey(combo)] });
417
+ };
418
+ for (const combo of set.combos()) {
419
+ for (const miss of set.missingLayers(combo)) add({ level: "error", code: "missing-layer", message: `No file matches layer "${miss}".` }, combo);
420
+ for (const [path, defs] of set.view(combo)) {
421
+ const r = set.resolve(path, combo);
422
+ const def = defs[defs.length - 1];
423
+ if (r.error) add({ level: "error", code: r.error.startsWith("circular") ? "circular-alias" : "broken-alias", message: r.error, path, file: def.file }, combo);
424
+ else if (!r.type) add({ level: "error", code: "missing-type", message: "No $type on the token, its groups or its alias target.", path, file: def.file }, combo);
425
+ else if (def.type && aliasOf(def.value)) {
426
+ const target = set.view(combo).get(aliasOf(def.value));
427
+ const tt = target?.[target.length - 1]?.type;
428
+ if (tt && tt !== def.type) add({ level: "error", code: "type-mismatch", message: `$type ${def.type} aliases a ${tt} token (${String(def.value)}).`, path, file: def.file }, combo);
429
+ }
430
+ }
431
+ }
432
+ const total = set.combos().length;
433
+ for (const i of merged.values()) {
434
+ if (i.modes.length === total) delete i.modes;
435
+ issues.push(i);
436
+ }
437
+ issues.push(...layeredParityIssues(set));
438
+ return issues;
439
+ }
440
+ function layeredParityIssues(set) {
441
+ const out = [];
442
+ const pathsOf = (file) => new Set(set.defs.filter((d) => d.file === file).map((d) => d.path));
443
+ for (const [mode, m] of Object.entries(set.config.axes)) {
444
+ if (!m.parity) continue;
445
+ for (const layer of set.config.tokens.layers) {
446
+ if (!layer.files.includes(`{${mode}}`) || layer.optional) continue;
447
+ const others = (layer.files.match(/\{([^}]+)\}/g) ?? []).map((p) => p.slice(1, -1)).filter((k) => k !== mode);
448
+ let fixed = [{}];
449
+ for (const o of others) fixed = fixed.flatMap((c) => (set.config.axes[o]?.values ?? []).map((v) => ({ ...c, [o]: v })));
450
+ for (const c of fixed) {
451
+ const filesByValue = m.values.map((v) => {
452
+ const re = globToRegExp(layer.files.replace(/\{([^}]+)\}/g, (_, k) => k === mode ? v : c[k]));
453
+ return { v, files: set.fileNames.filter((f) => re.test(f)) };
454
+ });
455
+ const present = filesByValue.filter((x) => x.files.length);
456
+ if (!present.length) continue;
457
+ for (const x of filesByValue) {
458
+ if (!x.files.length) out.push({ level: "error", code: "mode-parity", message: `${mode} "${x.v}" has no file for layer "${layer.files}" while ${present.map((p) => p.v).join(", ")} do.` });
459
+ }
460
+ const sets = present.map((x) => ({ v: x.v, files: x.files, paths: new Set(x.files.flatMap((f) => [...pathsOf(f)])) }));
461
+ const union = new Set(sets.flatMap((s) => [...s.paths]));
462
+ for (const p of union) {
463
+ const lacking = sets.filter((s) => !s.paths.has(p));
464
+ if (lacking.length) {
465
+ const having = sets.find((s) => s.paths.has(p));
466
+ out.push({ level: "error", code: "mode-parity", message: `Defined for ${mode} ${sets.filter((s) => s.paths.has(p)).map((s) => s.v).join(", ")} but not ${lacking.map((s) => s.v).join(", ")} (${lacking.map((s) => s.files.join(", ")).join("; ")}).`, path: p, file: having.files[0] });
467
+ }
468
+ }
469
+ }
470
+ }
471
+ }
472
+ return out;
473
+ }
2
474
  const EXT_ID = "com.dsbridge.id";
3
475
  const EXT_SCOPES = "com.figma.scopes";
4
476
  const PRIMITIVE_MODE = "value";
@@ -22,6 +494,53 @@ const TIER_NAMES = {
22
494
  component: "Component"
23
495
  };
24
496
  const modeDisplayName = (mode) => mode.charAt(0).toUpperCase() + mode.slice(1);
497
+ function valueIn(t, ctx) {
498
+ const axis = TIER_AXIS[t.tier];
499
+ if (!axis) return { value: t.values[SINGLE_MODE], via: "token" };
500
+ if (t.tier === "semantic" && ctx.overrides && ctx.brand && ctx.theme) {
501
+ const o = ctx.overrides.get(`${ctx.brand}|${ctx.theme}|${t.path}`);
502
+ if (o !== void 0) return { value: o, via: "override" };
503
+ }
504
+ const mode = ctx[axis];
505
+ return { value: mode === void 0 ? void 0 : t.values[mode], via: "token" };
506
+ }
507
+ function resolve(index, path, ctx) {
508
+ const chain = [];
509
+ const via = [];
510
+ const seen = /* @__PURE__ */ new Set();
511
+ let current = path;
512
+ for (; ; ) {
513
+ if (seen.has(current)) return { chain: [...chain, current], via, error: "cycle", at: current };
514
+ seen.add(current);
515
+ chain.push(current);
516
+ const t = index.get(current);
517
+ if (!t) return { chain, via, error: "broken", at: current };
518
+ const v = valueIn(t, ctx);
519
+ if (v.value === void 0) return { chain, via, error: "missing-mode", at: current };
520
+ via.push(v.via);
521
+ if (!isAlias(v.value)) return { value: v.value, chain, via };
522
+ current = aliasTarget(v.value);
523
+ }
524
+ }
525
+ function findReferenceProblems(index, axes) {
526
+ const out = [];
527
+ const base = { brand: axes.brand?.[0], theme: axes.theme?.[0], density: axes.density?.[0] };
528
+ for (const t of index.values()) {
529
+ const axis = TIER_AXIS[t.tier];
530
+ const modes = axis ? (axes[axis] ?? Object.keys(t.values)).filter((m) => t.values[m] !== void 0) : [SINGLE_MODE];
531
+ for (const mode of modes) {
532
+ const v = t.values[mode];
533
+ for (const ref of collectRefs(v)) {
534
+ if (!index.has(ref)) out.push({ path: t.path, mode, kind: "broken", target: ref, chain: [t.path, ref] });
535
+ }
536
+ if (isAlias(v)) {
537
+ const r = resolve(index, t.path, axis ? { ...base, [axis]: mode } : base);
538
+ if (r.error === "cycle") out.push({ path: t.path, mode, kind: "cycle", target: r.at ?? "", chain: r.chain });
539
+ }
540
+ }
541
+ }
542
+ return out;
543
+ }
25
544
  const PREVIEW_APP_LABELS = { docs: "Docs", storybook: "Storybook", admin: "Admin demo" };
26
545
  const PREVIEW_PLACEHOLDERS = ["branch", "rawBranch", "pr", "sha", "sha7"];
27
546
  function branchSlug(branch) {
@@ -543,8 +1062,8 @@ function groupsPhrase(groups) {
543
1062
  return `a ${g.join(" or ")} owner`;
544
1063
  }
545
1064
  function whoCanShipSentence(policy) {
546
- const names = (policy.entries ?? policy.logins).map((l) => entryLabel(l, policy.teams)).join(", ");
547
- return `Only ${groupsPhrase(policy.groups)} can merge this${names ? ` (${names})` : ""}.`;
1065
+ const names2 = (policy.entries ?? policy.logins).map((l) => entryLabel(l, policy.teams)).join(", ");
1066
+ return `Only ${groupsPhrase(policy.groups)} can merge this${names2 ? ` (${names2})` : ""}.`;
548
1067
  }
549
1068
  const DEFAULT_VISUAL_IMPACT = { mode: "auto" };
550
1069
  const VISUAL_CHECK_LABEL = "visual-check";
@@ -656,6 +1175,8 @@ function sameProducts(a, b) {
656
1175
  return norm2(a) === norm2(b);
657
1176
  }
658
1177
  const CONFIG_FILE = "bridge.config.json";
1178
+ const CHANGES_MODES = ["suggest", "direct"];
1179
+ const DEFAULT_CHANGES_MODE = "suggest";
659
1180
  const DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
660
1181
  const DEFAULT_TOKEN_PATHS = {
661
1182
  root: "tokens",
@@ -795,6 +1316,8 @@ function defineConfig(input) {
795
1316
  ...g.rights ? { rights: normalizeRights(g.rights) } : {}
796
1317
  },
797
1318
  products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
1319
+ changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
1320
+ ...input.storybook?.url ? { storybook: { url: input.storybook.url.trim().replace(/\/+$/, "") } } : {},
798
1321
  ...input.audit ? { audit: { ...input.audit } } : {},
799
1322
  ...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
800
1323
  ...input.integrations ? { integrations: { ...input.integrations } } : {},
@@ -917,6 +1440,13 @@ function loadConfig(source) {
917
1440
  }
918
1441
  }
919
1442
  problems.push(...visualImpactProblems(d.visualImpact));
1443
+ if (d.changes !== void 0 && !CHANGES_MODES.includes(d.changes)) problems.push(`"changes" must be "suggest" or "direct" (got ${JSON.stringify(d.changes)})`);
1444
+ const sb = d.storybook;
1445
+ if (sb !== void 0) {
1446
+ if (!isObj(sb) || !isStr(sb.url)) problems.push('"storybook" must be { "url" } with the deployed Storybook address');
1447
+ else if (!/^https:\/\/[^\s/]+/.test(sb.url.trim())) problems.push('"storybook.url" must start with https://, for example https://storybook.example.com');
1448
+ else for (const k of Object.keys(sb)) if (k !== "url") problems.push(`"storybook.${k}" is not a known key`);
1449
+ }
920
1450
  problems.push(...productProblems(d.products));
921
1451
  const audit = d.audit;
922
1452
  if (audit !== void 0) {
@@ -949,6 +1479,13 @@ function loadConfig(source) {
949
1479
  const tokensPath = (config, rel = "") => [config.tokens.root === "." ? "" : config.tokens.root, rel].filter(Boolean).join("/");
950
1480
  const protectedBranchesOf = (config) => [.../* @__PURE__ */ new Set([...config.governance.protectedBranches, ...config.repo ? [config.repo.defaultBranch] : []])];
951
1481
  const baseBranchOf = (config) => config.repo?.defaultBranch ?? "main";
1482
+ const changesMode = (config) => config?.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE;
1483
+ function storybookUrl(config) {
1484
+ const explicit = config?.storybook?.url?.trim();
1485
+ if (explicit) return explicit.replace(/\/+$/, "");
1486
+ const prod = config?.previews?.production?.storybook?.trim();
1487
+ return prod && /^https:\/\//.test(prod) && !/\{\w+\}/.test(prod) ? prod.replace(/\/+$/, "") : void 0;
1488
+ }
952
1489
  function toConfigJson(config, schema = "./packages/core/bridge.config.schema.json") {
953
1490
  return {
954
1491
  $schema: schema,
@@ -965,6 +1502,8 @@ function toConfigJson(config, schema = "./packages/core/bridge.config.schema.jso
965
1502
  ...config.audit ? { audit: config.audit } : {},
966
1503
  ...config.previews ? { previews: config.previews } : {},
967
1504
  ...config.visualImpact ? { visualImpact: config.visualImpact } : {},
1505
+ changes: changesMode(config),
1506
+ ...config.storybook ? { storybook: config.storybook } : {},
968
1507
  ...config.products && !sameProducts(config.products, DEFAULT_PRODUCTS) ? { products: config.products } : {}
969
1508
  };
970
1509
  }
@@ -1674,54 +2213,7 @@ function indexByPath(tokens) {
1674
2213
  for (const t of tokens) m.set(t.path, t);
1675
2214
  return m;
1676
2215
  }
1677
- function valueIn(t, ctx) {
1678
- const axis = TIER_AXIS[t.tier];
1679
- if (!axis) return { value: t.values[SINGLE_MODE], via: "token" };
1680
- if (t.tier === "semantic" && ctx.overrides && ctx.brand && ctx.theme) {
1681
- const o = ctx.overrides.get(`${ctx.brand}|${ctx.theme}|${t.path}`);
1682
- if (o !== void 0) return { value: o, via: "override" };
1683
- }
1684
- const mode = ctx[axis];
1685
- return { value: mode === void 0 ? void 0 : t.values[mode], via: "token" };
1686
- }
1687
- function resolve(index, path, ctx) {
1688
- const chain = [];
1689
- const via = [];
1690
- const seen = /* @__PURE__ */ new Set();
1691
- let current = path;
1692
- for (; ; ) {
1693
- if (seen.has(current)) return { chain: [...chain, current], via, error: "cycle", at: current };
1694
- seen.add(current);
1695
- chain.push(current);
1696
- const t = index.get(current);
1697
- if (!t) return { chain, via, error: "broken", at: current };
1698
- const v = valueIn(t, ctx);
1699
- if (v.value === void 0) return { chain, via, error: "missing-mode", at: current };
1700
- via.push(v.via);
1701
- if (!isAlias(v.value)) return { value: v.value, chain, via };
1702
- current = aliasTarget(v.value);
1703
- }
1704
- }
1705
- function findReferenceProblems(index, axes) {
1706
- const out = [];
1707
- const base = { brand: axes.brand?.[0], theme: axes.theme?.[0], density: axes.density?.[0] };
1708
- for (const t of index.values()) {
1709
- const axis = TIER_AXIS[t.tier];
1710
- const modes = axis ? (axes[axis] ?? Object.keys(t.values)).filter((m) => t.values[m] !== void 0) : [SINGLE_MODE];
1711
- for (const mode of modes) {
1712
- const v = t.values[mode];
1713
- for (const ref of collectRefs(v)) {
1714
- if (!index.has(ref)) out.push({ path: t.path, mode, kind: "broken", target: ref, chain: [t.path, ref] });
1715
- }
1716
- if (isAlias(v)) {
1717
- const r = resolve(index, t.path, axis ? { ...base, [axis]: mode } : base);
1718
- if (r.error === "cycle") out.push({ path: t.path, mode, kind: "cycle", target: r.at ?? "", chain: r.chain });
1719
- }
1720
- }
1721
- }
1722
- return out;
1723
- }
1724
- const has = (segments, ...names) => names.some((n) => segments.includes(n));
2216
+ const has = (segments, ...names2) => names2.some((n) => segments.includes(n));
1725
2217
  function defaultScopes(path, type, tier) {
1726
2218
  const s = path.split(".");
1727
2219
  switch (type) {
@@ -1759,124 +2251,32 @@ const sameScopes = (a, b) => {
1759
2251
  return sa.length === sb.length && sa.every((x, i) => x === sb[i]);
1760
2252
  };
1761
2253
  const effectiveScopes = (t) => t.scopes ?? defaultScopes(t.path, t.type, t.tier);
1762
- const CONTRAST_CHECKS_FILE = "contrast.checks.json";
1763
- const AA_MIN = { text: 4.5, "large-text": 3, ui: 3 };
1764
- const AAA_MIN = { text: 7, "large-text": 4.5, ui: 4.5 };
1765
- const channel = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
1766
- function luminance(c) {
1767
- const q = (x) => Math.round(Math.min(1, Math.max(0, x)) * 255) / 255;
1768
- return 0.2126 * channel(q(c.r)) + 0.7152 * channel(q(c.g)) + 0.0722 * channel(q(c.b));
1769
- }
1770
- function composite(fg, bg) {
1771
- const a = fg.a;
1772
- return { r: fg.r * a + bg.r * (1 - a), g: fg.g * a + bg.g * (1 - a), b: fg.b * a + bg.b * (1 - a), a: 1 };
1773
- }
1774
- function contrastRatio(fg, bg) {
1775
- const f = typeof fg === "string" ? hexToRgba(fg) : fg;
1776
- const b = typeof bg === "string" ? hexToRgba(bg) : bg;
1777
- if (!f || !b) throw new Error("Invalid color");
1778
- const opaqueBg = { ...b };
1779
- const l1 = luminance(composite(f, opaqueBg));
1780
- const l2 = luminance(opaqueBg);
1781
- const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
1782
- return (hi + 0.05) / (lo + 0.05);
1783
- }
1784
- function contrastLevel(ratio, kind, min = AA_MIN[kind]) {
1785
- if (ratio < min) return "fail";
1786
- if (ratio >= AAA_MIN[kind]) return "AAA";
1787
- return "AA";
2254
+ const normDesc = (d) => (d ?? "").trim();
2255
+ function fieldChanges(a, b) {
2256
+ const out = [];
2257
+ if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
2258
+ if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
2259
+ const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
2260
+ for (const mode of modes) {
2261
+ if (!valuesEqual(b.type, a.values[mode], b.values[mode])) out.push({ field: "value", mode, before: a.values[mode], after: b.values[mode] });
2262
+ }
2263
+ if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
2264
+ const sa = effectiveScopes(a);
2265
+ const sb = effectiveScopes(b);
2266
+ if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
2267
+ return out;
1788
2268
  }
1789
- function contrastScope(set) {
1790
- return {
1791
- brands: set.layout.brands.map((b) => b.mode),
1792
- themes: set.layout.themes.map((t) => t.mode),
1793
- density: set.layout.densities[0]?.mode,
1794
- overrides: set.overrideIndex
1795
- };
2269
+ function sameToken(a, b) {
2270
+ if (!a || !b) return a === b;
2271
+ return fieldChanges(a, b).length === 0;
1796
2272
  }
1797
- const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
1798
- const contextLabel = (r) => r.brand ? `${cap(r.brand)} ${r.theme}` : cap(r.theme);
1799
- function renderedColor(value) {
1800
- const c = parseColor(value);
1801
- const hex = value && typeof value === "object" ? value.hex : void 0;
1802
- const fromHex = typeof hex === "string" ? hexToRgba(hex) : void 0;
1803
- return c && fromHex ? { ...fromHex, a: c.a } : c;
1804
- }
1805
- function evaluatePairs(index, pairs, scope) {
1806
- const out = [];
1807
- const brands = scope.brands.length ? scope.brands : [void 0];
1808
- for (const brand of brands) {
1809
- for (const theme of scope.themes) {
1810
- const ctx = { brand, theme, density: scope.density, overrides: scope.overrides };
1811
- for (const pair of pairs) {
1812
- const steps = [];
1813
- const color = (path) => {
1814
- const r = resolve(index, path, ctx);
1815
- r.via.forEach((via, i) => steps.push({ path: r.chain[i], via }));
1816
- if (r.error) return `${path} cannot be resolved (${r.error === "broken" ? `missing ${r.at}` : r.error})`;
1817
- return renderedColor(r.value) ?? `${path} is not a color`;
1818
- };
1819
- const fg = color(pair.foreground);
1820
- const bg = color(pair.background);
1821
- if (typeof fg === "string" || typeof bg === "string") {
1822
- out.push({ pair, brand, theme, steps, error: typeof fg === "string" ? fg : bg });
1823
- continue;
1824
- }
1825
- const ratio = Math.round(contrastRatio(fg, bg) * 100) / 100;
1826
- out.push({ pair, brand, theme, ratio, level: contrastLevel(ratio, pair.kind, pair.min), steps });
1827
- }
1828
- }
1829
- }
1830
- return out;
1831
- }
1832
- const kindForMin = (min) => min >= AA_MIN.text ? "text" : "ui";
1833
- const contrastPair = (foreground, background, min = AA_MIN.text) => ({ foreground, background, min, kind: kindForMin(min) });
1834
- function parseContrastChecks(text) {
1835
- if (!text) return [];
1836
- const data = JSON.parse(text);
1837
- const list2 = Array.isArray(data) ? data : Array.isArray(data.pairs) ? data.pairs : [];
1838
- const out = [];
1839
- for (const p of list2) {
1840
- let fg, bg, min;
1841
- if (Array.isArray(p)) [fg, bg, min] = p;
1842
- else if (p && typeof p === "object") {
1843
- const o = p;
1844
- fg = o.fg ?? o.foreground;
1845
- bg = o.bg ?? o.background;
1846
- min = o.min ?? o.ratio ?? AA_MIN.text;
1847
- }
1848
- if (typeof fg !== "string" || typeof bg !== "string" || typeof min !== "number" || !(min > 0)) continue;
1849
- out.push(contrastPair(fg, bg, min));
1850
- }
1851
- return out;
1852
- }
1853
- const pairKey = (r) => `${r.brand ?? ""}/${r.theme}:${r.pair.foreground}|${r.pair.background}`;
1854
- const normDesc = (d) => (d ?? "").trim();
1855
- function fieldChanges(a, b) {
1856
- const out = [];
1857
- if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
1858
- if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
1859
- const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
1860
- for (const mode of modes) {
1861
- if (!valuesEqual(b.type, a.values[mode], b.values[mode])) out.push({ field: "value", mode, before: a.values[mode], after: b.values[mode] });
1862
- }
1863
- if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
1864
- const sa = effectiveScopes(a);
1865
- const sb = effectiveScopes(b);
1866
- if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
1867
- return out;
1868
- }
1869
- function sameToken(a, b) {
1870
- if (!a || !b) return a === b;
1871
- return fieldChanges(a, b).length === 0;
1872
- }
1873
- function sideChange(from, to) {
1874
- if (!from && !to) return void 0;
1875
- if (!from) return { kind: "added", fields: [] };
1876
- if (!to) return { kind: "removed", fields: [] };
1877
- const fields = fieldChanges(from, to);
1878
- if (!fields.length) return void 0;
1879
- return { kind: fields.some((f) => f.field === "path") ? "renamed" : "changed", fields };
2273
+ function sideChange(from, to) {
2274
+ if (!from && !to) return void 0;
2275
+ if (!from) return { kind: "added", fields: [] };
2276
+ if (!to) return { kind: "removed", fields: [] };
2277
+ const fields = fieldChanges(from, to);
2278
+ if (!fields.length) return void 0;
2279
+ return { kind: fields.some((f) => f.field === "path") ? "renamed" : "changed", fields };
1880
2280
  }
1881
2281
  function threeWayDiff(base, figma, head) {
1882
2282
  const ids = /* @__PURE__ */ new Set([...base?.keys() ?? [], ...figma.keys(), ...head.keys()]);
@@ -1915,7 +2315,7 @@ const RISK_LABELS = {
1915
2315
  2: "Tier 2: medium risk, design review then design + tech approval",
1916
2316
  3: "Tier 3: token change, needs design + tech approval"
1917
2317
  };
1918
- const BUMP_RANK$1 = { patch: 0, minor: 1, major: 2 };
2318
+ const BUMP_RANK = { patch: 0, minor: 1, major: 2 };
1919
2319
  const LOW = [/\.md$/, /^docs\//, /\.stories\.tsx?$/, /\.docs\.json$/, /\.test\.tsx?$/, /^\.changeset\//];
1920
2320
  const isSummary = (c) => "kind" in c;
1921
2321
  function summarizeTokenChange(c) {
@@ -1931,7 +2331,7 @@ function tokenBump(changes) {
1931
2331
  let bump;
1932
2332
  for (const c of changes.map(summarizeTokenChange)) {
1933
2333
  const b = c.kind === "removed" || c.kind === "renamed" ? "major" : c.kind === "changed" && c.descriptionOnly ? "patch" : "minor";
1934
- if (!bump || BUMP_RANK$1[b] > BUMP_RANK$1[bump]) bump = b;
2334
+ if (!bump || BUMP_RANK[b] > BUMP_RANK[bump]) bump = b;
1935
2335
  }
1936
2336
  return bump;
1937
2337
  }
@@ -1961,6 +2361,98 @@ function classifyRisk(input) {
1961
2361
  const tier = reasons[0]?.tier ?? 1;
1962
2362
  return { tier, label: RISK_LABELS[tier], reasons: reasons.map((r) => r.text), bump };
1963
2363
  }
2364
+ const CONTRAST_CHECKS_FILE = "contrast.checks.json";
2365
+ const AA_MIN = { text: 4.5, "large-text": 3, ui: 3 };
2366
+ const AAA_MIN = { text: 7, "large-text": 4.5, ui: 4.5 };
2367
+ const channel = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
2368
+ function luminance(c) {
2369
+ const q = (x) => Math.round(Math.min(1, Math.max(0, x)) * 255) / 255;
2370
+ return 0.2126 * channel(q(c.r)) + 0.7152 * channel(q(c.g)) + 0.0722 * channel(q(c.b));
2371
+ }
2372
+ function composite(fg, bg) {
2373
+ const a = fg.a;
2374
+ return { r: fg.r * a + bg.r * (1 - a), g: fg.g * a + bg.g * (1 - a), b: fg.b * a + bg.b * (1 - a), a: 1 };
2375
+ }
2376
+ function contrastRatio(fg, bg) {
2377
+ const f = typeof fg === "string" ? hexToRgba(fg) : fg;
2378
+ const b = typeof bg === "string" ? hexToRgba(bg) : bg;
2379
+ if (!f || !b) throw new Error("Invalid color");
2380
+ const opaqueBg = { ...b };
2381
+ const l1 = luminance(composite(f, opaqueBg));
2382
+ const l2 = luminance(opaqueBg);
2383
+ const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
2384
+ return (hi + 0.05) / (lo + 0.05);
2385
+ }
2386
+ function contrastLevel(ratio, kind, min = AA_MIN[kind]) {
2387
+ if (ratio < min) return "fail";
2388
+ if (ratio >= AAA_MIN[kind]) return "AAA";
2389
+ return "AA";
2390
+ }
2391
+ function contrastScope(set) {
2392
+ return {
2393
+ brands: set.layout.brands.map((b) => b.mode),
2394
+ themes: set.layout.themes.map((t) => t.mode),
2395
+ density: set.layout.densities[0]?.mode,
2396
+ overrides: set.overrideIndex
2397
+ };
2398
+ }
2399
+ const cap$1 = (s) => s.charAt(0).toUpperCase() + s.slice(1);
2400
+ const contextLabel = (r) => r.brand ? `${cap$1(r.brand)} ${r.theme}` : cap$1(r.theme);
2401
+ function renderedColor(value) {
2402
+ const c = parseColor(value);
2403
+ const hex = value && typeof value === "object" ? value.hex : void 0;
2404
+ const fromHex = typeof hex === "string" ? hexToRgba(hex) : void 0;
2405
+ return c && fromHex ? { ...fromHex, a: c.a } : c;
2406
+ }
2407
+ function evaluatePairs(index, pairs, scope) {
2408
+ const out = [];
2409
+ const brands = scope.brands.length ? scope.brands : [void 0];
2410
+ for (const brand of brands) {
2411
+ for (const theme of scope.themes) {
2412
+ const ctx = { brand, theme, density: scope.density, overrides: scope.overrides };
2413
+ for (const pair of pairs) {
2414
+ const steps = [];
2415
+ const color = (path) => {
2416
+ const r = resolve(index, path, ctx);
2417
+ r.via.forEach((via, i) => steps.push({ path: r.chain[i], via }));
2418
+ if (r.error) return `${path} cannot be resolved (${r.error === "broken" ? `missing ${r.at}` : r.error})`;
2419
+ return renderedColor(r.value) ?? `${path} is not a color`;
2420
+ };
2421
+ const fg = color(pair.foreground);
2422
+ const bg = color(pair.background);
2423
+ if (typeof fg === "string" || typeof bg === "string") {
2424
+ out.push({ pair, brand, theme, steps, error: typeof fg === "string" ? fg : bg });
2425
+ continue;
2426
+ }
2427
+ const ratio = Math.round(contrastRatio(fg, bg) * 100) / 100;
2428
+ out.push({ pair, brand, theme, ratio, level: contrastLevel(ratio, pair.kind, pair.min), steps });
2429
+ }
2430
+ }
2431
+ }
2432
+ return out;
2433
+ }
2434
+ const kindForMin = (min) => min >= AA_MIN.text ? "text" : "ui";
2435
+ const contrastPair = (foreground, background, min = AA_MIN.text) => ({ foreground, background, min, kind: kindForMin(min) });
2436
+ function parseContrastChecks(text) {
2437
+ if (!text) return [];
2438
+ const data = JSON.parse(text);
2439
+ const list2 = Array.isArray(data) ? data : Array.isArray(data.pairs) ? data.pairs : [];
2440
+ const out = [];
2441
+ for (const p of list2) {
2442
+ let fg, bg, min;
2443
+ if (Array.isArray(p)) [fg, bg, min] = p;
2444
+ else if (p && typeof p === "object") {
2445
+ const o = p;
2446
+ fg = o.fg ?? o.foreground;
2447
+ bg = o.bg ?? o.background;
2448
+ min = o.min ?? o.ratio ?? AA_MIN.text;
2449
+ }
2450
+ if (typeof fg !== "string" || typeof bg !== "string" || typeof min !== "number" || !(min > 0)) continue;
2451
+ out.push(contrastPair(fg, bg, min));
2452
+ }
2453
+ return out;
2454
+ }
2455
+ const pairKey = (r) => `${r.brand ?? ""}/${r.theme}:${r.pair.foreground}|${r.pair.background}`;
1964
2456
  const RISK_TIER = RISK_LABELS[3];
1965
2457
  function slugify(text, max = 40) {
1966
2458
  const s = text.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -1996,7 +2488,7 @@ const describe = (set, t, mode) => {
1996
2488
  const resolved = r.value === void 0 ? "unresolved" : formatValue(t.type, r.value);
1997
2489
  return `${formatValue(t.type, v)} (${resolved})`;
1998
2490
  };
1999
- const cell$1 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ") || " ";
2491
+ const cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ") || " ";
2000
2492
  const code = (s) => s ? `\`${s}\`` : "";
2001
2493
  function valueText(head, proposed, c, mode) {
2002
2494
  const t = c.target ?? c.current;
@@ -2073,7 +2565,7 @@ function composePrBody(input) {
2073
2565
  const extras = [];
2074
2566
  if (c.current && c.target && (c.current.description ?? "") !== (c.target.description ?? "")) extras.push("description");
2075
2567
  lines.push(
2076
- `| ${name} | ${TIER_NAMES[t.tier]} | ${kind}${extras.length ? ` (+${extras.join(", ")})` : ""} | ${cell$1(valuesCell(head, proposed, c))} | ${contrastCell(c, validation.contrastBefore, validation.contrastAfter)} |`
2568
+ `| ${name} | ${TIER_NAMES[t.tier]} | ${kind}${extras.length ? ` (+${extras.join(", ")})` : ""} | ${cell(valuesCell(head, proposed, c))} | ${contrastCell(c, validation.contrastBefore, validation.contrastAfter)} |`
2077
2569
  );
2078
2570
  }
2079
2571
  lines.push("");
@@ -2128,1283 +2620,487 @@ ${body}
2128
2620
 
2129
2621
  ${footer}`;
2130
2622
  }
2131
- function changesetBump(changes) {
2132
- return tokenBump(changes) ?? "patch";
2133
- }
2134
- function formatChangeset(packageName, bump, summary, details = "") {
2135
- return `---
2136
- "${packageName}": ${bump}
2137
- ---
2138
-
2139
- ${summary.trim()}
2140
- ${details ? `
2141
- ${details.trim()}
2142
- ` : ""}`;
2143
- }
2144
- function composeChangeset(changes, opts) {
2145
- const bump = changesetBump(changes);
2146
- const name = opts.branch.replace(/^tokens\//, "ds-bridge-").replace(/[^a-z0-9-]/gi, "-");
2147
- const list2 = changes.slice(0, 20).map((c) => `- ${changeKind(c)} \`${(c.target ?? c.current).path}\``).join("\n");
2148
- const more = changes.length > 20 ? `
2149
- - and ${changes.length - 20} more` : "";
2150
- const summary = opts.source ? `${opts.title} (${opts.source}).` : `${opts.title}.`;
2151
- return {
2152
- path: `.changeset/${name}.md`,
2153
- content: formatChangeset(opts.packageName, bump, summary, `${list2}${more}`)
2154
- };
2155
- }
2156
- function coerceValue(input, type, existing) {
2157
- if (typeof input !== "string") return input;
2158
- const s = input.trim();
2159
- if (aliasOf(s)) return s;
2160
- if (s.startsWith("{") || s.startsWith("[")) {
2161
- try {
2162
- return JSON.parse(s);
2163
- } catch {
2164
- }
2165
- }
2166
- const keepString = typeof existing === "string" && !aliasOf(existing);
2167
- switch (type) {
2168
- case "color": {
2169
- const m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s) ?? /^#([0-9a-f]{3})$/i.exec(s);
2170
- if (!m) return s;
2171
- const hex6 = m[1].length === 3 ? [...m[1]].map((c) => c + c).join("") : m[1];
2172
- if (keepString) return `#${hex6.toLowerCase()}${m[2] ?? ""}`;
2173
- return rgbaToDtcg(hexToRgba(`#${hex6}${m[2] ?? ""}`));
2174
- }
2175
- case "dimension": {
2176
- const m = /^(-?\d*\.?\d+)\s*(px|rem)?$/.exec(s);
2177
- if (!m || keepString) return s;
2178
- return { value: Number(m[1]), unit: m[2] ?? "px" };
2179
- }
2180
- case "duration": {
2181
- const m = /^(\d*\.?\d+)\s*(ms|s)?$/.exec(s);
2182
- if (!m || keepString) return s;
2183
- return { value: Number(m[1]), unit: m[2] ?? "ms" };
2184
- }
2185
- case "number":
2186
- case "fontWeight":
2187
- return /^-?\d*\.?\d+$/.test(s) ? Number(s) : s;
2188
- default:
2189
- return s;
2190
- }
2191
- }
2192
- function applyTokenEdits(base, edits) {
2193
- let set = base;
2194
- const files = new Map(base.files);
2195
- const touched = /* @__PURE__ */ new Set();
2196
- const notes = [];
2197
- const edit = (f, fn) => {
2198
- const doc = files.has(f) ? parseTokenFile(f, files.get(f)) : emptyTokenFile(f);
2199
- if (!fn(doc)) return;
2200
- const text = serializeTokenFile(doc);
2201
- if (text !== files.get(f)) {
2202
- files.set(f, text);
2203
- touched.add(f);
2204
- }
2205
- };
2206
- const definingFiles = (path) => [...new Set(set.defs.filter((d) => d.path === path).map((d) => d.file))];
2207
- const pickFiles = (e, all) => {
2208
- const defs = definingFiles(e.path);
2209
- if (e.file) {
2210
- if (!files.has(e.file)) throw new Error(`File "${e.file}" is not a token file. Token files: ${set.fileNames.join(", ")}`);
2211
- if (!defs.includes(e.file)) throw new Error(`"${e.path}" is not defined in ${e.file}. It is defined in: ${defs.join(", ") || 'no file (use op "add")'}.`);
2212
- return [e.file];
2213
- }
2214
- if (e.mode) {
2215
- const combo = set.normalizeCombo(e.mode);
2216
- const v = set.view(combo).get(e.path);
2217
- if (!v) throw new Error(`"${e.path}" is not defined for ${JSON.stringify(combo)}.`);
2218
- return [v[v.length - 1].file];
2219
- }
2220
- if (!defs.length) throw new Error(`Token "${e.path}" does not exist. Use op "add" to create it.`);
2221
- if (defs.length > 1 && !all) throw new Error(`"${e.path}" is defined in ${defs.length} files (${defs.join(", ")}). Pass "file" or "mode" (for example { "theme": "dark" }), or one edit per file.`);
2222
- return defs;
2223
- };
2224
- for (const e of edits) {
2225
- if (!e.path || /[{}]/.test(e.path)) throw new Error(`Invalid token path "${e.path}". Use dots: color.text.muted.`);
2226
- switch (e.op) {
2227
- case "set": {
2228
- if (e.value === void 0 && e.description === void 0 && e.type === void 0) throw new Error(`set ${e.path}: give value, description or type.`);
2229
- for (const f of pickFiles(e, false)) {
2230
- edit(f, (doc) => {
2231
- const cur = readTokenAt(doc, e.path);
2232
- const type = e.type ?? cur.type;
2233
- return upsertToken(doc, e.path, {
2234
- type,
2235
- value: e.value === void 0 ? cur.value : coerceValue(e.value, type, cur.value),
2236
- description: e.description ?? cur.description
2237
- });
2238
- });
2239
- }
2240
- break;
2241
- }
2242
- case "add": {
2243
- let file = e.file;
2244
- if (!file && e.mode) {
2245
- const combo = set.normalizeCombo(e.mode);
2246
- const parent = e.path.split(".").slice(0, -1).join(".");
2247
- const sib = [...set.view(combo).entries()].filter(([p]) => p.startsWith(`${parent}.`)).map(([, d]) => d[d.length - 1].file);
2248
- file = sib[sib.length - 1];
2249
- }
2250
- if (!file) {
2251
- const keys = e.path.split(".");
2252
- for (let d = keys.length - 1; d > 0 && !file; d--) {
2253
- const prefix = `${keys.slice(0, d).join(".")}.`;
2254
- const fs = [...new Set(set.defs.filter((x) => x.path.startsWith(prefix)).map((x) => x.file))];
2255
- if (fs.length === 1) file = fs[0];
2256
- else if (fs.length > 1) throw new Error(`Where should "${e.path}" go? Its group exists in ${fs.join(", ")}. Pass "file" (one add per file for light and dark).`);
2257
- }
2258
- }
2259
- if (!file) throw new Error(`Cannot infer a file for new token "${e.path}". Pass "file".`);
2260
- if (!files.has(file)) throw new Error(`File "${file}" is not a token file. Token files: ${set.fileNames.join(", ")}`);
2261
- if (set.defs.some((d) => d.path === e.path && d.file === file)) throw new Error(`"${e.path}" already exists in ${file}. Use op "set".`);
2262
- if (e.value === void 0) throw new Error(`add ${e.path}: value is required.`);
2263
- edit(file, (doc) => {
2264
- const type = e.type ?? inheritedTypeAt(doc, e.path);
2265
- if (!type && !aliasOf(e.value)) throw new Error(`add ${e.path}: pass "type" (color, dimension, ...); no parent group sets one.`);
2266
- return upsertToken(doc, e.path, { type, value: coerceValue(e.value, type), description: e.description });
2267
- });
2268
- break;
2269
- }
2270
- case "remove": {
2271
- for (const f of pickFiles(e, true)) edit(f, (doc) => removeToken(doc, e.path));
2272
- const users = set.referencedBy(e.path).filter((u) => u.path !== e.path);
2273
- if (users.length) notes.push(`${users.length} token(s) alias ${e.path}: ${users.slice(0, 8).map((u) => u.path).join(", ")}. Validation will flag them as broken unless they are edited too.`);
2274
- break;
2275
- }
2276
- case "rename": {
2277
- const to = e.newPath;
2278
- if (!to || /[{}]/.test(to)) throw new Error(`rename ${e.path}: pass a valid newPath.`);
2279
- if (set.defs.some((d) => d.path === to)) throw new Error(`rename: "${to}" already exists.`);
2280
- for (const f of pickFiles(e, true)) edit(f, (doc) => moveToken(doc, e.path, to));
2281
- const from = `"{${e.path}}"`;
2282
- const next = `"{${to}}"`;
2283
- for (const [f, text] of files) {
2284
- if (!text.includes(from)) continue;
2285
- files.set(f, text.split(from).join(next));
2286
- touched.add(f);
2287
- }
2288
- break;
2289
- }
2290
- default:
2291
- throw new Error(`Unknown op "${e.op}". Use set, add, remove or rename.`);
2292
- }
2293
- set = new LayeredTokenSet(base.config, new Map(files));
2294
- }
2295
- return { files, touched: [...touched].filter((f) => files.get(f) !== base.files.get(f)).sort(), notes };
2296
- }
2297
- function checkLayeredPair(set, pair, combo) {
2298
- const base = { fg: pair.foreground, bg: pair.background, min: pair.min, modes: comboKey(combo) };
2299
- const f = set.resolve(pair.foreground, combo);
2300
- const b = set.resolve(pair.background, combo);
2301
- if (f.error || b.error) return { ...base, ratio: null, pass: false, note: f.error ?? b.error };
2302
- const fc = renderedColor(f.value);
2303
- const bc = renderedColor(b.value);
2304
- if (!fc || !bc) return { ...base, ratio: null, pass: false, note: "not a color Bridge can measure (needs srgb or hex)" };
2305
- if (bc.a < 1) return { ...base, ratio: null, pass: false, note: "background is translucent: contrast depends on what is behind it" };
2306
- const ratio = Math.round(contrastRatio(fc, bc) * 100) / 100;
2307
- return { ...base, ratio, pass: ratio >= pair.min, fgValue: rgbaToHex(fc), bgValue: rgbaToHex(bc), ...fc.a < 1 ? { note: "foreground alpha composited over background" } : {} };
2308
- }
2309
- function runLayeredContrast(set, pairs, modes) {
2310
- const combos = set.combos(modes ?? set.config.contrast?.modes);
2311
- return combos.flatMap((c) => pairs.map((p) => checkLayeredPair(set, p, c)));
2312
- }
2313
- function contrastDelta(before, after) {
2314
- const key = (r) => `${r.fg}|${r.bg}|${r.modes}`;
2315
- const prev = new Map(before.map((r) => [key(r), r]));
2316
- return after.map((a) => {
2317
- const b = prev.get(key(a));
2318
- return { fg: a.fg, bg: a.bg, min: a.min, modes: a.modes, before: b?.ratio ?? null, after: a.ratio, passBefore: b?.pass ?? true, passAfter: a.pass };
2319
- }).filter((d) => d.before !== d.after || !d.passAfter);
2320
- }
2321
- const same$1 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
2322
- function diffLayered(before, after) {
2323
- const key = (d) => `${d.file}\0${d.path}`;
2324
- const b = new Map(before.defs.map((d) => [key(d), d]));
2325
- const a = new Map(after.defs.map((d) => [key(d), d]));
2326
- const css = (set, d) => set.resolve(d.path, set.comboForFile(d.file)).css;
2327
- const tierOf = (set, f) => {
2328
- const t = set.tierOfFile(f);
2329
- return t ? { tier: t } : {};
2330
- };
2331
- const changes = [];
2332
- const removed = [];
2333
- const added = [];
2334
- for (const [k, d] of b) {
2335
- const n = a.get(k);
2336
- if (!n) {
2337
- removed.push(d);
2338
- continue;
2339
- }
2340
- const fields = [];
2341
- if (!same$1(d.value, n.value)) fields.push("value");
2342
- if (d.type !== n.type) fields.push("type");
2343
- if (d.description !== n.description) fields.push("description");
2344
- if (!same$1(d.extensions, n.extensions)) fields.push("extensions");
2345
- if (!fields.length) continue;
2346
- changes.push({
2347
- kind: "changed",
2348
- path: d.path,
2349
- file: d.file,
2350
- ...tierOf(after, d.file),
2351
- ...n.type ? { type: n.type } : {},
2352
- fields,
2353
- before: d.value,
2354
- after: n.value,
2355
- beforeCss: css(before, d),
2356
- afterCss: css(after, n),
2357
- ...d.description !== n.description ? { descriptionBefore: d.description ?? "", descriptionAfter: n.description ?? "" } : {}
2358
- });
2359
- }
2360
- for (const [k, d] of a) if (!b.has(k)) added.push(d);
2361
- for (const r of [...removed]) {
2362
- const i = added.findIndex((x) => x.file === r.file && same$1(x.value, r.value) && x.type === r.type);
2363
- if (i < 0) continue;
2364
- const n = added.splice(i, 1)[0];
2365
- removed.splice(removed.indexOf(r), 1);
2366
- changes.push({ kind: "renamed", path: n.path, from: r.path, file: n.file, ...n.type ? { type: n.type } : {}, fields: [], before: r.value, after: n.value, afterCss: css(after, n), ...tierOf(after, n.file) });
2367
- }
2368
- for (const d of removed) changes.push({ kind: "removed", path: d.path, file: d.file, ...d.type ? { type: d.type } : {}, fields: [], before: d.value, beforeCss: css(before, d), ...d.description ? { descriptionBefore: d.description } : {}, ...tierOf(before, d.file) });
2369
- for (const d of added) changes.push({ kind: "added", path: d.path, file: d.file, ...d.type ? { type: d.type } : {}, fields: [], after: d.value, afterCss: css(after, d), ...d.description ? { descriptionAfter: d.description } : {}, ...tierOf(after, d.file) });
2370
- return changes.sort((x, y) => x.path.localeCompare(y.path) || x.file.localeCompare(y.file));
2371
- }
2372
- function layeredImpact(before, after, direct, limit = 60) {
2373
- const skip = new Set(direct.map((c) => c.path));
2374
- const byPath = /* @__PURE__ */ new Map();
2375
- for (const combo of after.combos()) {
2376
- const paths = /* @__PURE__ */ new Set([...before.view(combo).keys(), ...after.view(combo).keys()]);
2377
- for (const p of paths) {
2378
- if (skip.has(p)) continue;
2379
- const x = before.resolve(p, combo).css;
2380
- const y = after.resolve(p, combo).css;
2381
- if (x === y) continue;
2382
- const e = byPath.get(p);
2383
- if (e) e.modes.push(comboKey(combo));
2384
- else byPath.set(p, { path: p, modes: [comboKey(combo)], before: x, after: y });
2385
- }
2623
+ const PIECES_MARK = "bridge-pieces";
2624
+ const NEEDED_MARK = "bridge-needed-by";
2625
+ const SECTION = "### Missing from the library";
2626
+ const NEEDED_BY_LABEL = "needed-by-product";
2627
+ const hiddenJson = (mark, data) => `<!-- ${mark} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
2628
+ const readHidden = (mark, body) => {
2629
+ if (!body) return void 0;
2630
+ const m = new RegExp(`<!-- ${mark} (\\{[\\s\\S]*?\\}) -->`).exec(body);
2631
+ if (!m) return void 0;
2632
+ try {
2633
+ return JSON.parse(m[1]);
2634
+ } catch {
2635
+ return void 0;
2386
2636
  }
2387
- const all = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
2388
- return { total: all.length, entries: all.slice(0, limit) };
2389
- }
2390
- const toChangeSummary = (c) => ({
2391
- kind: c.kind,
2392
- path: c.path,
2393
- ...c.kind === "changed" ? { descriptionOnly: c.fields.length > 0 && c.fields.every((f) => f === "description") } : {}
2394
- });
2395
- function classifyLayered(changes, otherFiles = [], config) {
2396
- const r = classifyRisk({ tokenChanges: changes.map(toChangeSummary), files: otherFiles.map((path) => ({ path })), config });
2397
- return { tier: r.tier, bump: r.bump ?? "patch", reasons: r.reasons.length ? r.reasons : ["No token or guardrail change"], label: r.label };
2398
- }
2399
- const short = (v) => {
2400
- if (v === void 0) return "";
2401
- const s = typeof v === "string" ? v : JSON.stringify(v);
2402
- return s.length > 60 ? `${s.slice(0, 57)}...` : s;
2403
2637
  };
2404
- const cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
2405
- const swatch = (css) => css ? `\`${cell(css)}\`` : "";
2406
- const aliasNote = (v) => typeof v === "string" && v.startsWith("{") ? ` (${cell(short(v))})` : "";
2407
- function changeTable(changes) {
2408
- if (!changes.length) return "_No token changes._";
2409
- const rows = changes.map((c) => {
2410
- const what = c.kind === "renamed" ? `renamed from \`${c.from}\`` : c.kind === "changed" ? c.fields.join(", ") : c.kind;
2411
- const before = c.kind === "added" || c.kind === "renamed" ? "" : `${swatch(c.beforeCss)}${aliasNote(c.before)}`;
2412
- const after = c.kind === "removed" ? "" : `${swatch(c.afterCss)}${aliasNote(c.after)}`;
2413
- return `| \`${c.path}\` | ${what} | ${before} | ${after} | ${c.file} |`;
2414
- });
2415
- return ["| Token | Change | Before | After | File |", "| --- | --- | --- | --- | --- |", ...rows].join("\n");
2416
- }
2417
- function contrastDeltaTable(deltas, limit = 40) {
2418
- if (!deltas.length) return "_No contrast pair moved._";
2419
- const rows = deltas.slice(0, limit).map((d) => `| \`${d.fg}\` on \`${d.bg}\` | ${d.modes} | ${d.before ?? "n/a"} | ${d.after ?? "n/a"} | ${d.min} | ${d.passAfter ? "pass" : "**fail**"} |`);
2420
- const more = deltas.length > limit ? `
2638
+ const KIND_WORD = { "token-change": "token change", exploration: "library exploration", request: "component request", suggestion: "suggested token change" };
2639
+ function parsePiecesRecord(body) {
2640
+ const r = readHidden(PIECES_MARK, body);
2641
+ if (!r || !Array.isArray(r.pieces) || !Array.isArray(r.links) || typeof r.checkedAt !== "string") return void 0;
2642
+ return r;
2643
+ }
2644
+ function piecesSection(r) {
2645
+ const link = (p) => r.links.find((l) => l.piece === p.id);
2646
+ const lines = r.pieces.length ? r.pieces.map((p) => {
2647
+ const l = link(p);
2648
+ return `- **${p.title}**: ${p.summary}${l ? ` ${cap(KIND_WORD[l.kind])}: #${l.number}.` : " No library branch yet."}`;
2649
+ }) : ["- Nothing: every value and component comes from the library."];
2650
+ return [SECTION, "", `Checked ${r.checkedAt.slice(0, 10)}${r.headSha ? ` on ${r.headSha.slice(0, 7)}` : ""} by Bridge.`, "", ...lines, "", hiddenJson(PIECES_MARK, r)].join("\n");
2651
+ }
2652
+ function withPiecesRecord(body, r) {
2653
+ const b = (body ?? "").trimEnd();
2654
+ const start = b.indexOf(SECTION);
2655
+ const markAt = b.indexOf(`<!-- ${PIECES_MARK} `);
2656
+ if (start >= 0 && markAt > start) {
2657
+ const end = b.indexOf("-->", markAt);
2658
+ return `${b.slice(0, start)}${piecesSection(r)}${end >= 0 ? b.slice(end + 3) : ""}`.trimEnd();
2659
+ }
2660
+ return `${b}${b ? "\n\n" : ""}${piecesSection(r)}`;
2661
+ }
2662
+ function mergePiecesRecord(prev, next) {
2663
+ const links = [...prev?.links ?? []];
2664
+ for (const l of next.links ?? []) {
2665
+ const at = links.findIndex((x) => x.piece === l.piece);
2666
+ if (at >= 0) links[at] = l;
2667
+ else links.push(l);
2668
+ }
2669
+ return { ...next, links };
2670
+ }
2671
+ function withLink(r, l) {
2672
+ return { ...r, links: [...r.links.filter((x) => x.piece !== l.piece), l] };
2673
+ }
2674
+ function neededBySection(n) {
2675
+ const who = n.author ? ` for @${n.author}` : "";
2676
+ const near = n.nearest ? ` The closest the library has today is ${n.nearest}${n.nearestToken ? ` (\`${n.nearestToken}\`)` : ""}.` : "";
2677
+ return [`**Needed by:** [${n.title}](${n.url}) (#${n.pr}) in ${n.productName}${who}.${near}`, "", hiddenJson(NEEDED_MARK, n)].join("\n");
2678
+ }
2679
+ function parseNeededBy(body) {
2680
+ const n = readHidden(NEEDED_MARK, body);
2681
+ return n && typeof n.pr === "number" && typeof n.title === "string" && typeof n.piece === "string" ? n : void 0;
2682
+ }
2683
+ function withNeededBy(body, n) {
2684
+ let b = (body ?? "").trim();
2685
+ const at = b.indexOf("**Needed by:**");
2686
+ const mark = b.indexOf(`<!-- ${NEEDED_MARK} `);
2687
+ if (at >= 0 && mark > at) {
2688
+ const end = b.indexOf("-->", mark);
2689
+ b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
2690
+ }
2691
+ return `${neededBySection(n)}${b ? `
2421
2692
 
2422
- _${deltas.length - limit} more pair(s) not shown._` : "";
2423
- return ["| Pair | Mode | Before | After | Min | Result |", "| --- | --- | --- | --- | --- | --- |", ...rows].join("\n") + more;
2424
- }
2425
- function composeProposalBody(args) {
2426
- const t = args.tier.tier;
2427
- const box = (n) => `- ${n === t ? "[x]" : "[ ]"} ${RISK_LABELS[n]}`;
2428
- const impactList = args.impact.entries.slice(0, 15).map((e) => `- \`${e.path}\`: ${swatch(e.before)} -> ${swatch(e.after)} (${e.modes.length} mode${e.modes.length > 1 ? "s" : ""})`);
2429
- return [
2430
- "## What changed and why",
2431
- "",
2432
- args.summary.trim(),
2433
- "",
2434
- `Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
2435
- "",
2436
- "## Risk tier",
2437
- "",
2438
- box(1),
2439
- box(2),
2440
- box(3),
2441
- "",
2442
- ...args.tier.reasons.map((r) => `- ${r}`),
2443
- `- Semver bump: \`${args.tier.bump}\``,
2444
- "",
2445
- "## Token changes",
2446
- "",
2447
- changeTable(args.changes),
2448
- "",
2449
- "## Contrast before and after",
2450
- "",
2451
- contrastDeltaTable(args.contrast),
2452
- "",
2453
- "## Downstream impact",
2454
- "",
2455
- args.impact.total ? [`${args.impact.total} token(s) resolve to a new value because they alias an edited token.`, "", ...impactList, args.impact.total > 15 ? `- and ${args.impact.total - 15} more` : ""].filter(Boolean).join("\n") : "_No other token changes value._",
2456
- "",
2457
- "## Checks",
2458
- "",
2459
- `- Token validation: ${args.validationErrors ? `**${args.validationErrors} error(s)**` : "no errors"} (DTCG schema, aliases, light and dark parity)`,
2460
- `- Contrast gate: ${args.contrast.some((d) => !d.passAfter) ? "**failing pairs, see table**" : "all pairs pass"}`,
2461
- "",
2462
- "## Authored by",
2463
- "",
2464
- "- [ ] Human",
2465
- "- [x] AI agent (Claude), reviewed by a human before merge",
2466
- "",
2467
- "## Design review checklist",
2468
- "",
2469
- "- [ ] Checked in light and dark themes and in every brand",
2470
- "- [ ] Contrast table reviewed, no pair loosened",
2471
- "- [ ] Changeset bump matches the tier",
2472
- ""
2473
- ].join("\n");
2474
- }
2475
- const DEFAULT_PROTECTED_BRANCHES = ["main", "master"];
2476
- class ProtectedBranchError extends Error {
2477
- constructor(message) {
2478
- super(message);
2479
- this.name = "ProtectedBranchError";
2480
- }
2481
- }
2482
- function assertSafeBranch(branch, rules = {}) {
2483
- const name = branch.replace(/^refs\/heads\//, "").trim();
2484
- const blocked = /* @__PURE__ */ new Set([...rules.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES, ...rules.base ? [rules.base] : []]);
2485
- if (!name || blocked.has(name)) {
2486
- throw new ProtectedBranchError(`Refusing to write to protected branch "${name}". Bridge only creates new branches and pull requests.`);
2487
- }
2488
- if (rules.branchPrefix && !name.startsWith(rules.branchPrefix)) {
2489
- throw new ProtectedBranchError(`Branch "${name}" must start with "${rules.branchPrefix}".`);
2490
- }
2491
- }
2492
- const BULK_LIMIT = 60;
2493
- const UNRELEASED = "Unreleased";
2494
- const SUBJECT_PR = [/^Merge pull request #(\d+)/, /\(#(\d+)\)\s*$/];
2495
- function prNumberFromSubject(subject) {
2496
- for (const re of SUBJECT_PR) {
2497
- const m = re.exec(subject);
2498
- if (m) return Number(m[1]);
2499
- }
2500
- return void 0;
2501
- }
2502
- function parseChangesetText(path, text) {
2503
- const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
2504
- if (!m) return null;
2505
- const bumps = [...m[1].matchAll(/["']?([^"'\s:]+)["']?\s*:\s*(major|minor|patch)/g)].map((x) => ({ pkg: x[1], bump: x[2] }));
2506
- const order = ["major", "minor", "patch"];
2507
- const bump = order.find((b) => bumps.some((x) => x.bump === b));
2508
- const summary = m[2].trim().split(/\r?\n\r?\n/)[0].replace(/\s+/g, " ").trim();
2509
- return { path, ...bump ? { bump } : {}, packages: bumps.map((b) => b.pkg), summary };
2510
- }
2511
- function firstParagraph(text) {
2512
- if (!text) return "";
2513
- const cleaned = text.replace(/<!--[\s\S]*?-->/g, "");
2514
- for (const para of cleaned.split(/\r?\n\s*\r?\n/)) {
2515
- const lines = para.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#{1,6}\s/.test(l) && !/^\s*[-*]\s*\[[ xX]\]/.test(l) && !/^\s*(Co-Authored-By|Claude-Session|Signed-off-by):/i.test(l));
2516
- const s = lines.join(" ").replace(/\s+/g, " ").trim();
2517
- if (s && !/^\|/.test(s)) return s.length > 400 ? `${s.slice(0, 397)}...` : s;
2518
- }
2519
- return "";
2520
- }
2521
- function countChanges(changes) {
2522
- const c = { added: 0, changed: 0, removed: 0, renamed: 0, total: changes.length };
2523
- for (const x of changes) c[x.kind]++;
2524
- return c;
2525
- }
2526
- const shortSha = (sha) => sha.slice(0, 7);
2527
- function titleOf(commit) {
2528
- if (commit.pr?.title) return commit.pr.title;
2529
- if (/^Merge pull request #\d+/.test(commit.subject)) {
2530
- const first = (commit.body ?? "").split(/\r?\n/).find((l) => l.trim());
2531
- if (first) return first.trim();
2532
- }
2533
- return commit.subject;
2534
- }
2535
- function buildHistoryEntry(input) {
2536
- const { config, commit } = input;
2537
- const beforeSet = new LayeredTokenSet(config, input.before);
2538
- const afterSet = new LayeredTokenSet(config, input.after);
2539
- const changes = diffLayered(beforeSet, afterSet);
2540
- if (!changes.length) return null;
2541
- const tier = classifyLayered(changes, [], config);
2542
- const changesets = (input.changesets ?? []).map((c) => parseChangesetText(c.path, c.text)).filter((c) => c !== null);
2543
- const pkg = config.changesets?.package;
2544
- const relevant = changesets.filter((c) => !pkg || !c.packages.length || c.packages.includes(pkg));
2545
- const title = titleOf(commit);
2546
- const why = relevant.map((c) => c.summary).find(Boolean) || firstParagraph(commit.pr?.body) || firstParagraph(commit.body) || title;
2547
- const prNumber = commit.pr?.number ?? prNumberFromSubject(commit.subject);
2548
- const pr = commit.pr ?? (prNumber ? { number: prNumber, title, ...config.repo ? { url: `https://github.com/${config.repo.owner}/${config.repo.name}/pull/${prNumber}` } : {} } : void 0);
2549
- const changesetBump2 = relevant.map((c) => c.bump).find((b) => b === "major");
2550
- const bump = changesetBump2 ?? tier.bump;
2551
- return {
2552
- id: pr ? `pr-${pr.number}` : shortSha(commit.sha),
2553
- sha: commit.sha,
2554
- ...commit.parents[0] ? { baseSha: commit.parents[0] } : {},
2555
- date: commit.date,
2556
- author: commit.pr?.author ?? commit.author,
2557
- title,
2558
- why,
2559
- ...pr ? { pr } : {},
2560
- changes,
2561
- counts: countChanges(changes),
2562
- tier: tier.tier,
2563
- bump,
2564
- breaking: bump === "major",
2565
- reasons: tier.reasons,
2566
- changesets: relevant,
2567
- ...changes.length > BULK_LIMIT ? { bulk: true } : {},
2568
- ...input.demo ? { demo: true } : {}
2693
+ ${b}` : ""}`;
2694
+ }
2695
+ const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
2696
+ const forLine = (b) => `${b.productName} needs it for "${b.title.replace(/"/g, "'")}" (#${b.pr})`;
2697
+ function libraryBranchPlan(piece, branch, opts = {}) {
2698
+ const neededBy = {
2699
+ pr: branch.pr,
2700
+ title: branch.title,
2701
+ branch: branch.branch,
2702
+ url: branch.url,
2703
+ product: branch.productKey,
2704
+ productName: branch.productName,
2705
+ ...branch.author ? { author: branch.author } : {},
2706
+ piece: piece.id,
2707
+ pieceTitle: piece.title,
2708
+ ...piece.nearest ? { nearest: piece.nearest.label, nearestToken: piece.nearest.token } : {}
2569
2709
  };
2570
- }
2571
- async function collectHistory(config, backend, opts) {
2572
- const root = config.tokens.root.replace(/\/$/, "");
2573
- const commits = await backend.listCommits({ ref: opts.ref, paths: [root === "." ? "" : root].filter(Boolean), limit: opts.limit ?? 40 });
2574
- const seen = /* @__PURE__ */ new Set();
2575
- const picked = [];
2576
- for (const c of commits) {
2577
- const n = c.pr?.number;
2578
- if (n !== void 0) {
2579
- if (seen.has(n)) continue;
2580
- seen.add(n);
2581
- }
2582
- picked.push(c);
2583
- }
2584
- const entries = [];
2585
- let done = 0;
2586
- for (const c of picked) {
2587
- const parent = c.parents[0];
2588
- const [before, after] = await Promise.all([parent ? backend.tokenFiles(parent) : Promise.resolve(/* @__PURE__ */ new Map()), backend.tokenFiles(c.sha)]);
2589
- let changesets = [];
2590
- if (backend.changesetPaths && backend.readFile && config.changesets) {
2591
- const [was, now] = await Promise.all([parent ? backend.changesetPaths(parent) : Promise.resolve([]), backend.changesetPaths(c.sha)]);
2592
- const added = now.filter((p) => !was.includes(p) && p.endsWith(".md") && !/README\.md$/i.test(p));
2593
- changesets = (await Promise.all(added.map(async (p) => ({ path: p, text: await backend.readFile(c.sha, p) ?? "" })))).filter((x) => x.text);
2594
- }
2595
- const e = buildHistoryEntry({ config, commit: c, before, after, changesets });
2596
- if (e) entries.push(e);
2597
- opts.onProgress?.(++done, picked.length);
2598
- }
2599
- return entries;
2600
- }
2601
- function parseChangelog(text) {
2602
- const out = [];
2603
- const parts = text.split(/^##\s+/m).slice(1);
2604
- for (const p of parts) {
2605
- const [head, ...rest] = p.split(/\r?\n/);
2606
- const version = /v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(head ?? "")?.[1];
2607
- if (!version) continue;
2608
- const body = rest.join("\n");
2609
- const shas = [...new Set([...body.matchAll(/(?:^|[\s[`(])([0-9a-f]{7,40})(?=[\s`\]):]|$)/gm)].map((m) => m[1]))];
2610
- const prs = [...new Set([...body.matchAll(/(?:#|\/pull\/)(\d+)/g)].map((m) => Number(m[1])))];
2611
- const date = /(\d{4}-\d{2}-\d{2})/.exec(head ?? "")?.[1];
2612
- out.push({ version, ...date ? { date } : {}, shas, prs });
2613
- }
2614
- return out;
2615
- }
2616
- function releasesFromTags(tags, pkg) {
2617
- const out = [];
2618
- for (const t of tags) {
2619
- const m = /^(?:(@?[^@\s]+)@)?v?(\d+\.\d+\.\d+(?:-[\w.]+)?)$/.exec(t.name);
2620
- if (!m) continue;
2621
- if (pkg && m[1] && m[1] !== pkg) continue;
2622
- out.push({ version: m[2], ...t.sha ? { sha: t.sha } : {}, ...t.date ? { date: t.date } : {} });
2623
- }
2624
- return out;
2625
- }
2626
- function assignVersions(entries, releases) {
2627
- const dated = releases.filter((r) => r.date).sort((a, b) => Date.parse(a.date) - Date.parse(b.date));
2628
- return entries.map((e) => {
2629
- const cited = releases.find((r) => r.shas?.some((s) => e.sha.startsWith(s)) || e.pr && r.prs?.includes(e.pr.number));
2630
- const byTag = cited ? void 0 : dated.find((r) => r.sha === e.sha || Date.parse(r.date) >= Date.parse(e.date));
2631
- return { ...e, version: (cited ?? byTag)?.version ?? UNRELEASED };
2632
- });
2633
- }
2634
- const BUMP_RANK = { patch: 0, minor: 1, major: 2 };
2635
- function versionTimeline(entries, releases = []) {
2636
- const groups = /* @__PURE__ */ new Map();
2637
- for (const e of entries) {
2638
- const v = e.version ?? UNRELEASED;
2639
- let g = groups.get(v);
2640
- if (!g) {
2641
- const rel = releases.find((r) => r.version === v);
2642
- g = { version: v, pending: v === UNRELEASED, date: rel?.date ?? e.date, entries: [], prs: [], tokenChanges: 0, breaking: false, bump: "patch", authors: [] };
2643
- groups.set(v, g);
2644
- }
2645
- g.entries.push(e.id);
2646
- if (e.pr && !g.prs.includes(e.pr.number)) g.prs.push(e.pr.number);
2647
- g.tokenChanges += e.counts.total;
2648
- g.breaking ||= e.breaking;
2649
- if (BUMP_RANK[e.bump] > BUMP_RANK[g.bump]) g.bump = e.bump;
2650
- if (!g.authors.includes(e.author)) g.authors.push(e.author);
2651
- if (!releases.some((r) => r.version === v && r.date) && Date.parse(e.date) > Date.parse(g.date)) g.date = e.date;
2652
- }
2653
- return [...groups.values()].sort((a, b) => a.pending ? -1 : b.pending ? 1 : Date.parse(b.date) - Date.parse(a.date));
2654
- }
2655
- const matchesToken = (path, q) => path === q || path.startsWith(`${q.replace(/\.$/, "")}.`);
2656
- function filterHistory(entries, f = {}) {
2657
- const q = f.query?.trim().toLowerCase();
2658
- return entries.filter(
2659
- (e) => (!f.token || e.changes.some((c) => matchesToken(c.path, f.token) || c.from && matchesToken(c.from, f.token))) && (!f.author || e.author.toLowerCase() === f.author.toLowerCase()) && (!f.tier || e.tier === f.tier) && (!f.version || (e.version ?? UNRELEASED) === f.version) && (f.breaking === void 0 || e.breaking === f.breaking) && (!f.since || Date.parse(e.date) >= Date.parse(f.since)) && (!f.until || Date.parse(e.date) <= Date.parse(f.until) + 86399999) && (!q || `${e.title} ${e.why} ${e.pr ? `#${e.pr.number}` : ""} ${e.sha}`.toLowerCase().includes(q))
2660
- );
2661
- }
2662
- function fileModes(config, file) {
2663
- for (const layer of config.tokens.layers) {
2664
- const keys = [];
2665
- const marked = layer.files.replace(/\{([^}]+)\}/g, (_, k) => {
2666
- keys.push(k);
2667
- return "";
2668
- });
2669
- const source = globToRegExp(marked).source.replace(/\u0001/g, "([^/.]+)");
2670
- const m = new RegExp(source).exec(file);
2671
- if (m) return Object.fromEntries(keys.map((k, i) => [k, m[i + 1]]));
2672
- }
2673
- return {};
2674
- }
2675
- function modeLabel(modes) {
2676
- const v = Object.values(modes).filter(Boolean);
2677
- if (!v.length) return "All modes";
2678
- const s = v.join(" ");
2679
- return s.charAt(0).toUpperCase() + s.slice(1);
2680
- }
2681
- function tokenHistory(entries, path, config) {
2682
- const names = /* @__PURE__ */ new Set([path]);
2683
- const out = [];
2684
- const sorted = [...entries].sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
2685
- for (const e of sorted) {
2686
- const hits = e.changes.filter((c) => names.has(c.path));
2687
- for (const c of hits) {
2688
- const modes = config ? fileModes(config, c.file) : {};
2689
- out.push({
2690
- entryId: e.id,
2691
- sha: e.sha,
2692
- date: e.date,
2693
- author: e.author,
2694
- title: e.title,
2695
- why: e.why,
2696
- ...e.pr ? { pr: e.pr } : {},
2697
- ...e.version ? { version: e.version } : {},
2698
- ...e.demo ? { demo: true } : {},
2699
- kind: c.kind,
2700
- path: c.path,
2701
- ...c.from ? { from: c.from } : {},
2702
- file: c.file,
2703
- modes,
2704
- modeLabel: modeLabel(modes),
2705
- fields: c.fields,
2706
- ...c.before !== void 0 ? { before: c.before } : {},
2707
- ...c.after !== void 0 ? { after: c.after } : {},
2708
- ...c.beforeCss !== void 0 ? { beforeCss: c.beforeCss } : {},
2709
- ...c.afterCss !== void 0 ? { afterCss: c.afterCss } : {},
2710
- ...c.descriptionBefore !== void 0 ? { descriptionBefore: c.descriptionBefore } : {},
2711
- ...c.descriptionAfter !== void 0 ? { descriptionAfter: c.descriptionAfter } : {}
2712
- });
2713
- }
2714
- for (const c of hits) if (c.kind === "renamed" && c.from) names.add(c.from);
2715
- }
2716
- return out;
2717
- }
2718
- const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
2719
- const keyOf = (path, file) => `${path}@${file}`;
2720
- const show = (v) => typeof v === "string" ? v : JSON.stringify(v);
2721
- function defIn(set, path, file) {
2722
- return set.defs.find((d) => d.path === path && d.file === file);
2723
- }
2724
- function sharesCombo(set, a, b) {
2725
- return a === b || set.combos().some((c) => {
2726
- const files = set.layerFiles(c).files;
2727
- return files.includes(a) && files.includes(b);
2728
- });
2729
- }
2730
- function laterTouching(later, path, file, from) {
2731
- return later.filter((e) => e.changes.some((c) => c.file === file && (c.path === path || c.from === path || from !== void 0 && (c.path === from || c.from === from)))).sort((a, b) => Date.parse(a.date) - Date.parse(b.date)).map((e) => ({ id: e.id, title: e.title, date: e.date, author: e.author, ...e.pr ? { pr: e.pr.number } : {} }));
2732
- }
2733
- function planRevert(input) {
2734
- const { current, entry } = input;
2735
- const later = (input.later ?? []).filter((e) => e.id !== entry.id && Date.parse(e.date) >= Date.parse(entry.date));
2736
- const res = input.resolutions ?? {};
2737
- const planned = [];
2738
- const conflicts = [];
2739
- const skipped = [];
2740
- const config = current.config;
2741
- const css = (path, file) => current.resolve(path, current.comboForFile(file)).css;
2742
- for (const c of entry.changes) {
2743
- const key = keyOf(c.path, c.file);
2744
- const label = modeLabel(fileModes(config, c.file));
2745
- const original = { ...c.before !== void 0 ? { before: c.before } : {}, ...c.after !== void 0 ? { after: c.after } : {}, ...c.beforeCss ? { beforeCss: c.beforeCss } : {}, ...c.afterCss ? { afterCss: c.afterCss } : {} };
2746
- const conflict = (reason, message, cur, editIfForced, summary) => {
2747
- const resolution = res[key] ?? "keep-current";
2748
- conflicts.push({ key, path: c.path, file: c.file, kind: c.kind, reason, message, original, ...cur ? { current: cur } : {}, later: laterTouching(later, c.path, c.file, c.from), resolution });
2749
- if (resolution === "use-before" && editIfForced) planned.push({ key, path: c.path, file: c.file, summary: summary ?? `${editIfForced.op} ${c.path}`, edit: editIfForced });
2710
+ const near = piece.nearest ? ` The closest the library has today is ${piece.nearest.label} (${piece.nearest.token}, ${piece.nearest.value}).` : "";
2711
+ if (piece.kind === "token" && piece.token) {
2712
+ const t = piece.token;
2713
+ const files = t.files.length ? t.files : [void 0];
2714
+ const edits = files.map((file) => ({
2715
+ op: "add",
2716
+ path: t.path,
2717
+ type: t.type,
2718
+ value: t.value,
2719
+ description: `${cap(piece.name)}: ${forLine(branch)}.${files.length > 1 ? " Same value in every theme: review it in each." : ""}`,
2720
+ ...file ? { file } : {}
2721
+ }));
2722
+ return { kind: "token-change", title: `Add ${t.path}`, summary: `${forLine(branch)}: ${piece.summary}${near}`, edits, neededBy };
2723
+ }
2724
+ if (piece.kind === "variant" && piece.component && opts.mode === "suggest") {
2725
+ const request2 = {
2726
+ component: piece.component,
2727
+ kind: "new-variant",
2728
+ title: piece.title,
2729
+ summary: `${forLine(branch)}: ${piece.summary}${near}`,
2730
+ changes: piece.values.slice(0, 6).map((v) => ({ field: v.property ?? v.category, after: v.value })),
2731
+ source: opts.source ?? "mcp",
2732
+ ...opts.requestedBy ? { requestedBy: opts.requestedBy } : {}
2750
2733
  };
2751
- const now = defIn(current, c.path, c.file);
2752
- switch (c.kind) {
2753
- case "changed": {
2754
- if (!now) {
2755
- conflict("removed-later", `${c.path} was removed from ${c.file} after this change. Restoring it is a new token decision, not a revert.`, void 0, { op: "add", path: c.path, file: c.file, value: c.before, ...c.type ? { type: c.type } : {}, ...c.descriptionBefore ? { description: c.descriptionBefore } : {} }, `Add ${c.path} back with ${show(c.before)} (${label})`);
2756
- break;
2757
- }
2758
- const valueField = c.fields.includes("value");
2759
- const descField = c.fields.includes("description");
2760
- const cur = { value: now.value, css: css(c.path, c.file) };
2761
- const valueAt = (v) => same(now.value, v);
2762
- const descNow = now.description ?? "";
2763
- if (!valueField && !descField) {
2764
- skipped.push({ key, path: c.path, file: c.file, reason: "Only the type or extensions changed: edit those by hand." });
2765
- break;
2766
- }
2767
- if ((!valueField || valueAt(c.before)) && (!descField || descNow === (c.descriptionBefore ?? ""))) {
2768
- skipped.push({ key, path: c.path, file: c.file, reason: "Already back to the value from before this change." });
2769
- break;
2770
- }
2771
- const valueMoved = valueField && !valueAt(c.after) && !valueAt(c.before);
2772
- const descMoved = descField && descNow !== (c.descriptionAfter ?? "") && descNow !== (c.descriptionBefore ?? "");
2773
- const edit = {
2774
- op: "set",
2775
- path: c.path,
2776
- file: c.file,
2777
- ...valueField ? { value: c.before } : {},
2778
- ...descField ? { description: c.descriptionBefore ?? "" } : {}
2779
- };
2780
- const summary = valueField ? `Set ${c.path} back to ${show(c.before)} (${label})` : `Put the old description of ${c.path} back (${label})`;
2781
- if (valueMoved || descMoved) {
2782
- conflict("changed-later", `${c.path} changed again after this change: it is now ${cur.css || show(cur.value)}, not ${c.afterCss || show(c.after)}.`, cur, edit, summary);
2783
- break;
2784
- }
2785
- planned.push({ key, path: c.path, file: c.file, summary, edit });
2786
- break;
2787
- }
2788
- case "added": {
2789
- if (!now) {
2790
- skipped.push({ key, path: c.path, file: c.file, reason: "Already removed." });
2791
- break;
2792
- }
2793
- const users = current.referencedBy(c.path).filter((u) => u.path !== c.path && sharesCombo(current, u.file, c.file));
2794
- const edit = { op: "remove", path: c.path, file: c.file };
2795
- const summary = `Remove ${c.path} (${label})`;
2796
- if (!same(now.value, c.after)) {
2797
- conflict("changed-later", `${c.path} changed after it was added: it is now ${css(c.path, c.file) || show(now.value)}.`, { value: now.value, css: css(c.path, c.file) }, edit, summary);
2798
- break;
2799
- }
2800
- if (users.length) {
2801
- conflict("still-referenced", `${users.length} token(s) alias ${c.path} now (${users.slice(0, 4).map((u) => u.path).join(", ")}${users.length > 4 ? ", ..." : ""}). Removing it would break them.`, { value: now.value, css: css(c.path, c.file) }, edit, summary);
2802
- break;
2803
- }
2804
- planned.push({ key, path: c.path, file: c.file, summary, edit });
2805
- break;
2806
- }
2807
- case "removed": {
2808
- const edit = { op: "add", path: c.path, file: c.file, value: c.before, ...c.type ? { type: c.type } : {}, ...c.descriptionBefore ? { description: c.descriptionBefore } : {} };
2809
- const summary = `Add ${c.path} back with ${show(c.before)} (${label})`;
2810
- if (now) {
2811
- if (same(now.value, c.before)) skipped.push({ key, path: c.path, file: c.file, reason: "Already back." });
2812
- else conflict("recreated-later", `${c.path} was added again later with another value (${css(c.path, c.file) || show(now.value)}).`, { value: now.value, css: css(c.path, c.file) });
2813
- break;
2814
- }
2815
- planned.push({ key, path: c.path, file: c.file, summary, edit });
2816
- break;
2817
- }
2818
- case "renamed": {
2819
- const from = c.from;
2820
- const back = defIn(current, from, c.file);
2821
- if (back && !now) {
2822
- skipped.push({ key, path: c.path, file: c.file, reason: `Already named ${from}.` });
2823
- break;
2824
- }
2825
- if (!now) {
2826
- conflict("renamed-later", `${c.path} no longer exists in ${c.file}: it was renamed or removed again later.`);
2827
- break;
2828
- }
2829
- if (back) {
2830
- conflict("recreated-later", `${from} exists again in ${c.file}, so ${c.path} cannot take its old name back.`, { value: back.value, css: css(from, c.file) });
2831
- break;
2832
- }
2833
- if (planned.some((p) => p.edit.op === "rename" && p.path === c.path)) {
2834
- skipped.push({ key, path: c.path, file: c.file, reason: "Covered by the rename above." });
2835
- break;
2836
- }
2837
- planned.push({ key, path: c.path, file: c.file, summary: `Rename ${c.path} back to ${from} and repoint its aliases`, edit: { op: "rename", path: c.path, newPath: from } });
2838
- break;
2839
- }
2840
- }
2841
- }
2842
- const prLink = entry.pr ? entry.pr.url ? `[#${entry.pr.number}](${entry.pr.url})` : `#${entry.pr.number}` : `commit ${entry.sha.slice(0, 7)}`;
2843
- const title = `Revert: ${entry.title}`;
2844
- const lines = [
2845
- `Reverts ${prLink} ("${entry.title}"), merged ${entry.date.slice(0, 10)} by ${entry.author}.`,
2846
- "",
2847
- `Original reason: ${entry.why}`,
2848
- "",
2849
- "Bridge computed the inverse token edits against the current base branch, not a blind git revert: tokens that changed again since are listed as conflicts and kept unless a reviewer chose to restore them."
2850
- ];
2851
- if (conflicts.length) {
2852
- lines.push("", "### Conflicts", "", "| Token | File | Why | Decision |", "| --- | --- | --- | --- |");
2853
- for (const k of conflicts) lines.push(`| \`${k.path}\` | ${k.file} | ${k.message.replace(/\|/g, "\\|")}${k.later.length ? ` Later: ${k.later.map((l) => l.pr ? `#${l.pr}` : l.id).join(", ")}.` : ""} | ${k.resolution === "use-before" ? "restore the old value" : "keep current"} |`);
2854
- }
2855
- if (skipped.length) lines.push("", `${skipped.length} change(s) were already undone on the base branch and are left alone.`);
2856
- return finishPlan({ ...input, kind: "revert", title, prTitle: title, summary: lines.join("\n"), planned, conflicts, skipped, source: entry });
2857
- }
2858
- function planRestore(input) {
2859
- const { current, path } = input;
2860
- const files = [...new Set(current.defs.filter((d) => d.path === path).map((d) => d.file))];
2861
- if (!files.length) throw new Error(`Token "${path}" does not exist on the base branch. Restoring a removed token is a revert of the change that removed it.`);
2862
- const file = input.file ?? (files.length === 1 ? files[0] : void 0);
2863
- if (!file) throw new Error(`"${path}" is defined in ${files.join(", ")}. Pass the file (the mode) to restore.`);
2864
- if (!files.includes(file)) throw new Error(`"${path}" is not defined in ${file}. It is defined in: ${files.join(", ")}.`);
2865
- const label = modeLabel(fileModes(current.config, file));
2866
- const from = input.label ? ` from ${input.label}` : "";
2867
- const planned = [{ key: keyOf(path, file), path, file, summary: `Set ${path} to ${show(input.value)} (${label})`, edit: { op: "set", path, file, value: input.value } }];
2868
- const title = `Restore ${path} (${label})${from}`;
2869
- const item = input.item;
2870
- const summary = [
2871
- `Restores \`${path}\` in ${label} to ${show(input.value)}${from}.`,
2872
- ...item ? ["", `That value came from ${item.pr ? item.pr.url ? `[#${item.pr.number}](${item.pr.url})` : `#${item.pr.number}` : `commit ${item.sha.slice(0, 7)}`} ("${item.title}"), ${item.date.slice(0, 10)}, by ${item.author}.`] : []
2873
- ].join("\n");
2874
- return finishPlan({ ...input, kind: "restore", title, prTitle: title, summary, planned, conflicts: [], skipped: [] });
2875
- }
2876
- const issueKey = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
2877
- function finishPlan(p) {
2878
- const { current } = p;
2879
- const config = current.config;
2880
- const edits = p.planned.map((x) => x.edit);
2881
- const notes = [];
2882
- const empty = classifyLayered([], [], config);
2883
- const base = {
2884
- kind: p.kind,
2885
- title: p.title,
2886
- prTitle: p.prTitle,
2887
- summary: p.summary,
2888
- ...p.source ? { source: { id: p.source.id, sha: p.source.sha, title: p.source.title, ...p.source.pr ? { pr: p.source.pr } : {}, date: p.source.date, author: p.source.author } } : {},
2889
- planned: p.planned,
2890
- edits,
2891
- conflicts: p.conflicts,
2892
- skipped: p.skipped,
2893
- branch: `${config.governance.branchPrefix}${slugify(p.title, 48)}`
2894
- };
2895
- const nothing = (why) => ({ ...base, status: "nothing-to-do", why, changes: [], contrast: [], newFailures: [], newErrors: [], tier: empty, files: [], commitMessage: "", prBody: "", notes });
2896
- if (!edits.length) return nothing(p.conflicts.length ? 'Every change conflicts with a later one. Choose "restore the old value" on a conflict to include it.' : "Nothing left to undo: the base branch already has the old values.");
2897
- let result;
2898
- try {
2899
- result = applyTokenEdits(current, edits);
2900
- } catch (e) {
2901
- return { ...nothing(e.message), status: "blocked" };
2902
- }
2903
- notes.push(...result.notes);
2904
- const after = new LayeredTokenSet(config, result.files);
2905
- const changes = diffLayered(current, after);
2906
- if (!changes.length) return nothing("The edits leave every token as it is now.");
2907
- const prev = new Set(validateLayered(current).map(issueKey));
2908
- const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prev.has(issueKey(i)));
2909
- const pairs = p.pairs ?? [];
2910
- const modes = config.contrast?.modes;
2911
- const contrast = pairs.length ? contrastDelta(runLayeredContrast(current, pairs, modes), runLayeredContrast(after, pairs, modes)) : [];
2912
- const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
2913
- const tier = classifyLayered(changes, [], config);
2914
- const impact = layeredImpact(current, after, changes);
2915
- const prBody = composeProposalBody({ summary: p.summary, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: p.tool ?? "Bridge", ...p.author ? { author: p.author } : {} });
2916
- const changeset = config.changesets ? { path: `${config.changesets.dir.replace(/\/$/, "")}/bridge-${slugify(p.title, 48)}.md`, content: formatChangeset(config.changesets.package, tier.bump, p.title.replace(/\.?$/, ".")) } : void 0;
2917
- const files = [...result.touched.map((f) => ({ path: current.repoPath(f), content: result.files.get(f) })), ...changeset ? [changeset] : []];
2918
- const commitMessage2 = `${p.kind === "revert" ? "revert(tokens)" : "fix(tokens)"}: ${p.title.replace(/^Revert: /, "")}
2919
-
2920
- ${p.source?.pr ? `Reverts #${p.source.pr.number}. ` : ""}Tier ${tier.tier}. Planned with ${p.tool ?? "Bridge"}.`;
2921
- const blocked = newErrors.length || newFailures.length;
2922
- return {
2923
- ...base,
2924
- status: blocked ? "blocked" : "ready",
2925
- ...blocked ? { why: [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and ") } : {},
2926
- changes,
2927
- contrast,
2928
- newFailures,
2929
- newErrors,
2930
- tier,
2931
- files,
2932
- ...changeset ? { changeset } : {},
2933
- commitMessage: commitMessage2,
2934
- prBody,
2935
- notes
2936
- };
2937
- }
2938
- function planToJson(plan) {
2939
- return {
2940
- bridge: "change-plan",
2941
- kind: plan.kind,
2942
- status: plan.status,
2943
- title: plan.prTitle,
2944
- branch: plan.branch,
2945
- tier: plan.tier.tier,
2946
- bump: plan.tier.bump,
2947
- edits: plan.edits,
2948
- conflicts: plan.conflicts.map((c) => ({ key: c.key, reason: c.reason, message: c.message, resolution: c.resolution })),
2949
- files: plan.files,
2950
- commitMessage: plan.commitMessage,
2951
- prBody: plan.prBody
2734
+ return { kind: "request", title: piece.title, request: request2, neededBy };
2735
+ }
2736
+ if (piece.kind === "variant" && piece.component) {
2737
+ const values = piece.values.map((v) => `${v.property ?? v.category} ${v.value}`).join(", ");
2738
+ const exploration = {
2739
+ name: piece.title,
2740
+ purpose: `${forLine(branch)}: ${/^[aeiou]/i.test(piece.name) ? "an" : "a"} ${piece.name} ${piece.aspect ?? "style"} on ${piece.component} (${values}).${near}`,
2741
+ useCase: "terminal",
2742
+ experimental: [piece.title],
2743
+ source: opts.source === "figma" ? "figma" : opts.source ?? "mcp"
2744
+ };
2745
+ const brief = [
2746
+ `Add the ${piece.name} ${piece.aspect ?? "style"} to ${piece.component} in the library, on this exploration branch.`,
2747
+ `The product branch writes it by hand: ${values}. Express it with tokens (a component or semantic token per value), add it to the component's props, docs and examples, and keep every existing ${piece.component} unchanged.`,
2748
+ piece.nearest ? `Say in the pull request why ${piece.nearest.label} is not enough, or recommend it instead.` : ""
2749
+ ].filter(Boolean).join(" ");
2750
+ return { kind: "exploration", title: piece.title, exploration, neededBy, brief };
2751
+ }
2752
+ const name = piece.component ?? cap(piece.name);
2753
+ const request = {
2754
+ component: name,
2755
+ kind: "new-component",
2756
+ title: `Add ${name}`,
2757
+ summary: `${forLine(branch)}: ${piece.summary}${near}`,
2758
+ changes: piece.values.slice(0, 6).map((v) => ({ field: v.property ?? v.category, after: v.value })),
2759
+ source: opts.source ?? "mcp",
2760
+ ...opts.requestedBy ? { requestedBy: opts.requestedBy } : {}
2952
2761
  };
2953
- }
2954
- function packageDirCandidates(config) {
2955
- const parts = config.tokens.root.replace(/\/$/, "").split("/").filter((p) => p && p !== ".");
2956
- const out = [];
2957
- for (let i = parts.length; i >= 0; i--) out.push(parts.slice(0, i).join("/"));
2958
- return out;
2959
- }
2960
- function mergeReleases(changelog, tags) {
2961
- const byVersion = /* @__PURE__ */ new Map();
2962
- for (const r of [...changelog, ...tags]) {
2963
- const prev = byVersion.get(r.version);
2964
- byVersion.set(r.version, prev ? { ...r, ...prev, ...prev.date ? {} : r.date ? { date: r.date } : {}, ...prev.sha ? {} : r.sha ? { sha: r.sha } : {} } : r);
2965
- }
2966
- return [...byVersion.values()];
2967
- }
2968
- function buildReport(args) {
2969
- const entries = assignVersions(args.entries, args.releases).sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
2970
- return { ...args, entries, timeline: versionTimeline(entries, args.releases) };
2971
- }
2972
- const COMPONENT_SPECS_VERSION = 1;
2973
- const DEFAULT_COMPONENT_SPECS_FILE = "packages/react/component-specs.json";
2974
- const SPEC_FIELDS = [
2975
- "fill",
2976
- "stroke",
2977
- "strokeWeight",
2978
- "strokeTopWeight",
2979
- "strokeRightWeight",
2980
- "strokeBottomWeight",
2981
- "strokeLeftWeight",
2982
- "cornerRadius",
2983
- "paddingTop",
2984
- "paddingRight",
2985
- "paddingBottom",
2986
- "paddingLeft",
2987
- "itemSpacing",
2988
- "counterAxisSpacing",
2989
- "width",
2990
- "height",
2991
- "minWidth",
2992
- "minHeight",
2993
- "opacity",
2994
- "textFill",
2995
- "fontFamily",
2996
- "fontSize",
2997
- "fontWeight",
2998
- "lineHeight",
2999
- "letterSpacing",
3000
- "textDecoration"
3001
- ];
3002
- const COLOR_FIELDS = /* @__PURE__ */ new Set(["fill", "stroke", "textFill"]);
3003
- const TRIGGER_LABELS = {
3004
- ON_HOVER: "While hovering",
3005
- ON_PRESS: "While pressing",
3006
- ON_CLICK: "On click",
3007
- MOUSE_ENTER: "Mouse enter",
3008
- MOUSE_LEAVE: "Mouse leave"
2762
+ return { kind: "request", title: `New component: ${name}`, request, neededBy };
2763
+ }
2764
+ const STATUS_LABEL = {
2765
+ draft: "Draft",
2766
+ "in-review": "In review",
2767
+ changes: "Changes requested",
2768
+ approved: "Approved",
2769
+ shipped: "Shipped",
2770
+ closed: "Closed",
2771
+ exploring: "Being built",
2772
+ requested: "Requested",
2773
+ "in-progress": "In progress",
2774
+ done: "Done",
2775
+ declined: "Declined",
2776
+ outdated: "Outdated"
3009
2777
  };
3010
- function coverageSummary(file) {
3011
- const list2 = file.coverage ?? file.components.map((c) => ({ name: c.name, status: "generated" }));
3012
- const generated = list2.filter((c) => c.status === "generated").length;
3013
- const drafted = list2.filter((c) => c.status === "drafted").length;
3014
- const layout = list2.filter((c) => c.status === "layout").length;
3015
- return { generated, drafted, inFigma: generated + drafted, visual: list2.length - layout, layout, total: list2.length, missing: list2.filter((c) => c.status === "unsupported") };
3016
- }
3017
- const LAYOUT_DIRECTIONS = /* @__PURE__ */ new Set(["HORIZONTAL", "VERTICAL"]);
3018
- const PRIMARY = /* @__PURE__ */ new Set(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]);
3019
- const COUNTER = /* @__PURE__ */ new Set(["MIN", "CENTER", "MAX", "STRETCH"]);
3020
- const PART_KINDS = /* @__PURE__ */ new Set(["frame", "text", "icon", "vector"]);
3021
- function validateComponentSpec(spec, tokenTypes) {
3022
- const out = [];
3023
- const at = (where, why) => out.push(`${spec.name ?? "spec"}: ${where}: ${why}`);
3024
- if (typeof spec.name !== "string" || !spec.name) return ["the spec has no name"];
3025
- if (!Array.isArray(spec.axes) || !Array.isArray(spec.properties) || !Array.isArray(spec.parts) || !Array.isArray(spec.variants) || !Array.isArray(spec.unmapped)) return [`${spec.name}: axes, properties, parts, variants and unmapped must be lists`];
3026
- const axisNames = /* @__PURE__ */ new Set();
3027
- for (const a of spec.axes) {
3028
- if (!a.name || axisNames.has(a.name)) at("axes", `"${a.name}" is missing or repeated`);
3029
- axisNames.add(a.name);
3030
- if (!Array.isArray(a.values) || !a.values.length) at(`axis ${a.name}`, "has no values");
3031
- else if (!a.values.includes(a.default)) at(`axis ${a.name}`, `default "${a.default}" is not one of its values`);
3032
- }
3033
- const props = /* @__PURE__ */ new Map();
3034
- for (const p of spec.properties) {
3035
- if (props.has(p.name)) at("properties", `"${p.name}" is repeated`);
3036
- props.set(p.name, p);
3037
- if (p.type !== "TEXT" && p.type !== "BOOLEAN") at(`property ${p.name}`, `type ${String(p.type)} is not TEXT or BOOLEAN`);
3038
- else if (typeof p.default !== (p.type === "TEXT" ? "string" : "boolean")) at(`property ${p.name}`, `default must be a ${p.type === "TEXT" ? "text" : "true or false"}`);
3039
- }
3040
- const parts = /* @__PURE__ */ new Map();
3041
- for (const part of spec.parts) {
3042
- if (!part.id || parts.has(part.id)) at("parts", `id "${part.id}" is missing or repeated`);
3043
- if (!PART_KINDS.has(part.kind)) at(`part ${part.id}`, `kind ${String(part.kind)} is not frame, text, icon or vector`);
3044
- if (part.parent === void 0) {
3045
- if ([...parts.values()].some((x) => x.parent === void 0)) at(`part ${part.id}`, "a second root (every other part needs a parent)");
3046
- } else if (!parts.has(part.parent)) at(`part ${part.id}`, `parent "${part.parent}" is not a part listed before it`);
3047
- else if (parts.get(part.parent).kind !== "frame") at(`part ${part.id}`, `parent "${part.parent}" is not a frame`);
3048
- if (part.layout && (!LAYOUT_DIRECTIONS.has(part.layout.direction) || !PRIMARY.has(part.layout.primary) || !COUNTER.has(part.layout.counter))) at(`part ${part.id}`, "layout is not a valid auto layout");
3049
- if (part.text !== void 0 && props.get(part.text)?.type !== "TEXT") at(`part ${part.id}`, `text property "${part.text}" does not exist`);
3050
- if (part.visible !== void 0 && props.get(part.visible)?.type !== "BOOLEAN") at(`part ${part.id}`, `visibility property "${part.visible}" does not exist`);
3051
- if (part.kind === "text" && part.text === void 0 && part.characters === void 0 && !part.swap) at(`part ${part.id}`, "a text part needs a TEXT property or its characters");
3052
- parts.set(part.id, part);
3053
- }
3054
- if (!spec.parts.length || spec.parts[0].parent !== void 0) at("parts", "the first part must be the root");
3055
- const expected = spec.axes.reduce((n, a) => n * (a.values?.length || 1), 1);
3056
- if (spec.variants.length !== expected) at("variants", `${spec.variants.length} variants for ${expected} combinations of the axes`);
3057
- const seen = /* @__PURE__ */ new Set();
3058
- for (const v of spec.variants) {
3059
- const key = variantKeyOf(v.props ?? {});
3060
- const where = `variant ${key}`;
3061
- if (seen.has(key)) at(where, "is repeated");
3062
- seen.add(key);
3063
- for (const a of spec.axes) if (!a.values?.includes(v.props?.[a.name] ?? "")) at(where, `${a.name} is not one of its values`);
3064
- for (const k of Object.keys(v.props ?? {})) if (!axisNames.has(k)) at(where, `${k} is not an axis`);
3065
- for (const [id, fields] of Object.entries(v.values ?? {})) {
3066
- const part = parts.get(id);
3067
- if (!part) {
3068
- at(where, `values for "${id}", which is not a part`);
3069
- continue;
3070
- }
3071
- for (const [field, value] of Object.entries(fields ?? {})) {
3072
- if (!SPEC_FIELDS.includes(field)) at(`${where}, ${id}`, `${field} is not a field`);
3073
- else if (value === null) {
3074
- if (!COLOR_FIELDS.has(field)) at(`${where}, ${id}`, `${field} cannot be empty (only colors can be none)`);
3075
- } else if (typeof value === "number") {
3076
- if (COLOR_FIELDS.has(field)) at(`${where}, ${id}`, `${field} cannot be a number`);
3077
- } else if (typeof value !== "string") at(`${where}, ${id}`, `${field} must be a token path, a number or none`);
3078
- else if (field === "textDecoration") {
3079
- if (!/^(UNDERLINE|STRIKETHROUGH|NONE)$/.test(value)) at(`${where}, ${id}`, `${value} is not a text decoration`);
3080
- } else if (tokenTypes) {
3081
- for (const path of value.startsWith("calc(") ? (value.match(/[A-Za-z][\w.-]*/g) ?? []).filter((x) => x !== "calc" && !/^\d|^px$/.test(x)) : [value]) {
3082
- const type = tokenTypes.get(path);
3083
- if (!type) at(`${where}, ${id}`, `${path} is not a token`);
3084
- else if (!value.startsWith("calc(") && !FIELD_TOKEN_TYPES(field).includes(type)) at(`${where}, ${id}`, `${path} is a ${type} token, ${field} needs ${FIELD_TOKEN_TYPES(field).join(" or ")}`);
3085
- }
3086
- }
3087
- }
3088
- }
3089
- for (const id of v.hidden ?? []) if (!parts.has(id)) at(where, `hides "${id}", which is not a part`);
3090
- for (const id of Object.keys(v.layout ?? {})) if (!parts.has(id)) at(where, `lays out "${id}", which is not a part`);
3091
- for (const [id, vec] of Object.entries(v.vectors ?? {})) if (parts.get(id)?.kind !== "vector" || !/^<svg[\s>]/.test(vec.svg)) at(where, `vector "${id}" is not a vector part with an SVG`);
3092
- for (const id of Object.keys(v.slots ?? {})) if (!parts.has(id)) at(where, `shows a component in "${id}", which is not a part`);
3093
- }
3094
- for (const [i, x] of (spec.interactions ?? []).entries()) {
3095
- const where = `interaction ${i + 1}`;
3096
- const axis = spec.axes.find((a) => a.name === x.axis);
3097
- if (!(x.trigger in TRIGGER_LABELS)) at(where, `trigger ${String(x.trigger)} is not one Figma has`);
3098
- if (!axis) at(where, `${x.axis} is not an axis`);
3099
- else if (!axis.values.includes(x.from) || !axis.values.includes(x.to) || x.from === x.to) at(where, `${x.from} to ${x.to} are not two values of ${x.axis}`);
3100
- for (const k of x.variants ?? []) if (!seen.has(k)) at(where, `variant ${k} does not exist`);
3101
- const t = x.transition;
3102
- if (t && (!(t.duration > 0) || t.easing.type === "CUSTOM_CUBIC_BEZIER" && (t.easing.bezier?.length !== 4 || t.easing.bezier.some((n) => typeof n !== "number")))) at(where, "the transition needs a duration and a valid easing");
3103
- }
3104
- return out;
3105
- }
3106
- function FIELD_TOKEN_TYPES(field) {
3107
- if (COLOR_FIELDS.has(field)) return ["color"];
3108
- if (field === "fontFamily") return ["fontFamily"];
3109
- if (field === "fontWeight") return ["fontWeight", "number"];
3110
- if (field === "opacity") return ["number"];
3111
- return ["dimension", "number"];
3112
- }
3113
- const iconComponentName = (name) => `Icon/${name}`;
3114
- function specHash(text) {
3115
- let h = 2166136261;
3116
- for (let i = 0; i < text.length; i++) {
3117
- h ^= text.charCodeAt(i);
3118
- h = Math.imul(h, 16777619);
3119
- }
3120
- return (h >>> 0).toString(16).padStart(8, "0");
3121
- }
3122
- function hashSpec(spec) {
3123
- const { hash: _ignored, example: _example, ...rest } = spec;
3124
- return specHash(JSON.stringify(rest));
3125
- }
3126
- const variantName = (props) => Object.entries(props).map(([k, v]) => `${k}=${v}`).join(", ");
3127
- const variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
3128
- const REDUCED_MOTION_NOTE = "In code, reduced motion turns this off.";
3129
- function componentMotion(spec) {
3130
- const keys = new Set(spec.variants.map((v) => variantKeyOf(v.props)));
3131
- const reactions = [];
3132
- const triggers = [];
3133
- for (const x of spec.interactions ?? []) {
3134
- const only = x.variants ? new Set(x.variants) : void 0;
3135
- let count = 0;
3136
- for (const v of spec.variants) {
3137
- if (v.props[x.axis] !== x.from) continue;
3138
- const fromKey = variantKeyOf(v.props);
3139
- if (only && !only.has(fromKey)) continue;
3140
- const to = { ...v.props, [x.axis]: x.to };
3141
- const toKey = variantKeyOf(to);
3142
- if (!keys.has(toKey)) continue;
3143
- reactions.push({ trigger: x.trigger, label: TRIGGER_LABELS[x.trigger], from: { ...v.props }, to, fromKey, toKey, transition: x.transition });
3144
- count++;
3145
- }
3146
- triggers.push({ ...x, label: TRIGGER_LABELS[x.trigger], count });
3147
- }
3148
- const transitions = (spec.interactions ?? []).map((x) => x.transition).filter((t) => !!t);
3149
- const cannotPlay = [...spec.motion ?? []];
3150
- const openNote = cannotPlay.find((n) => n.enter || n.exit);
3151
- const opening = openNote ? { enter: openNote.enter ?? null, exit: openNote.exit ?? null } : void 0;
3152
- const openTransitions = opening ? [opening.enter, opening.exit].filter((t) => !!t) : [];
3153
- const tokens = [
3154
- ...new Set([...transitions, ...openTransitions].flatMap((t) => [t.durationToken, t.easingToken]).concat(cannotPlay.map((n) => n.durationToken)).filter((t) => !!t))
3155
- ].sort();
3156
- const properties = [...new Set(transitions.flatMap((t) => t.properties))].sort();
3157
- const approximations = [...new Set(transitions.map((t) => t.approximated).filter((t) => !!t))];
3158
- const animates = transitions.length > 0 || openTransitions.length > 0 || cannotPlay.some((n) => /animation/i.test(n.what));
3159
- return { name: spec.name, reactions, triggers, tokens, properties, approximations, cannotPlay, opening, reducedMotion: animates ? REDUCED_MOTION_NOTE : void 0 };
3160
- }
3161
- function componentsUsingMotionToken(specs, tokenPath, chainOf) {
3162
- const out = /* @__PURE__ */ new Set();
3163
- for (const c of specs.components) {
3164
- for (const t of componentMotion(c).tokens) {
3165
- if (t === tokenPath || (chainOf?.(t) ?? []).includes(tokenPath)) out.add(c.name);
3166
- }
3167
- }
3168
- return [...out].sort();
3169
- }
3170
- function parseComponentSpecs(text) {
3171
- let data;
3172
- try {
3173
- data = JSON.parse(text);
3174
- } catch {
3175
- throw new Error("component-specs.json is not valid JSON.");
3176
- }
3177
- const f = data;
3178
- if (!f || typeof f !== "object" || !Array.isArray(f.components)) throw new Error('component-specs.json has no "components" list.');
3179
- if (f.version !== COMPONENT_SPECS_VERSION) throw new Error(`component-specs.json is version ${String(f.version)}; this tool reads version ${COMPONENT_SPECS_VERSION}.`);
3180
- return f;
3181
- }
3182
- const tokenCssVar = (path, prefix = "--", separator = "-") => `${prefix}${path.split(".").join(separator)}`;
3183
- function figmaCell(entry) {
3184
- if (!entry) return void 0;
3185
- if (entry.status === "generated") return { label: "Yes" };
3186
- if (entry.status === "drafted") return { label: "Drafted", title: "Drafted with Claude where the code cannot be read on its own: in Figma, check it against the code." };
3187
- if (entry.status === "layout") return { label: "Auto layout", title: "A layout helper: nothing to draw, use auto layout in Figma." };
3188
- return { label: "Not yet", ...entry.reason ? { title: `Not supported yet: ${entry.reason}` } : {} };
2778
+ const LINK_STATUS_TONE = {
2779
+ draft: "neutral",
2780
+ "in-review": "info",
2781
+ changes: "warning",
2782
+ approved: "success",
2783
+ shipped: "success",
2784
+ closed: "neutral",
2785
+ exploring: "neutral",
2786
+ requested: "neutral",
2787
+ "in-progress": "info",
2788
+ done: "success",
2789
+ declined: "neutral",
2790
+ outdated: "warning"
2791
+ };
2792
+ const names = (people2) => people2.length <= 2 ? people2.map((p) => `@${p}`).join(" and ") : `@${people2[0]} and ${people2.length - 1} more`;
2793
+ const SUGGESTION_LINK_STATUS = {
2794
+ "needs-review": "in-review",
2795
+ "changes-requested": "changes",
2796
+ approved: "approved",
2797
+ applying: "in-progress",
2798
+ applied: "shipped",
2799
+ declined: "declined",
2800
+ outdated: "outdated"
2801
+ };
2802
+ function linkStatus(x) {
2803
+ const make = (key, people2 = [], verb = "reviews") => {
2804
+ const label = STATUS_LABEL[key];
2805
+ const tail = people2.length ? ` · ${names(people2)} ${people2.length === 1 ? verb : verb.replace(/s$/, "")}` : "";
2806
+ return { key, label, people: people2, line: `${label}${tail}` };
2807
+ };
2808
+ if (x.kind === "suggestion") {
2809
+ const key = SUGGESTION_LINK_STATUS[x.status];
2810
+ return make(key, key === "in-progress" ? [...x.assignees ?? []] : [], "builds");
2811
+ }
2812
+ if (x.kind === "request") {
2813
+ if (x.state === "closed") return make(x.stateReason === "not_planned" ? "declined" : "done");
2814
+ return x.implemented ? make("in-progress", [...x.assignees ?? []], "builds") : make("requested", [...x.assignees ?? []], "builds");
2815
+ }
2816
+ if (x.state === "merged") return make("shipped");
2817
+ if (x.state !== "open") return make("closed");
2818
+ if ((x.labels ?? []).includes("exploration")) return make("exploring");
2819
+ if (x.draft) return make("draft");
2820
+ const latest = /* @__PURE__ */ new Map();
2821
+ for (const r of x.reviews ?? []) if (r.user && r.state !== "COMMENTED") latest.set(r.user, r.state);
2822
+ const states = [...latest.values()];
2823
+ if (states.includes("CHANGES_REQUESTED")) return make("changes", [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([u]) => u), "asks");
2824
+ const waiting = [...x.requestedReviewers ?? []];
2825
+ if (states.includes("APPROVED") && !waiting.length) return make("approved");
2826
+ return make("in-review", waiting);
2827
+ }
2828
+ function libraryPreviewState(x) {
2829
+ if (x.kind === "suggestion") {
2830
+ if (x.lifecycle === "closed") return { key: "declined", sentence: `The library declined this suggestion.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
2831
+ if (x.lifecycle === "merged") return x.behindMain === 0 ? { key: "switched", sentence: "Your branch uses the published version.", action: null } : { key: "published", sentence: "The library applied the suggestion. Switch your branch to the published version.", action: { kind: "switch", label: "Switch to the published version" } };
2832
+ return { key: "nothing-yet", sentence: "Nothing to preview yet: owners review the suggestion, then a developer applies it. Your branch keeps its own value until then.", action: null };
2833
+ }
2834
+ if (x.kind === "request") {
2835
+ if (x.lifecycle === "closed") return { key: "declined", sentence: `The library declined this request.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
2836
+ if (x.lifecycle === "merged") return x.behindMain === 0 ? { key: "switched", sentence: "Your branch uses the published version.", action: null } : { key: "published", sentence: "The library built it. Switch your branch to the published version, then ask Claude to use it.", action: { kind: "switch", label: "Switch to the published version" } };
2837
+ return { key: "nothing-yet", sentence: "Nothing to preview yet: the library builds it first. Your branch keeps its own version until then.", action: null };
2838
+ }
2839
+ if (x.lifecycle === "merged") {
2840
+ if (x.behindMain === 0) return { key: "switched", sentence: "Your branch uses the published version.", action: null };
2841
+ return { key: "published", sentence: "The library published it. Switch your branch to the published version.", action: { kind: "switch", label: "Switch to the published version" } };
2842
+ }
2843
+ if (x.lifecycle === "closed") return { key: "declined", sentence: `The library closed it without shipping.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
2844
+ if (!x.hasChanges) return { key: "nothing-yet", sentence: "The library branch has no change yet. Claude adds it there, then your branch can use its preview.", action: null };
2845
+ if (x.missing === 0) return { key: "preview", sentence: "Your branch uses a preview of the library branch. When the library publishes it, switch to the real version here in one click.", action: null };
2846
+ if (x.missing !== null && x.total !== null && x.missing < x.total) return { key: "outdated", sentence: "The library branch changed since your branch took its preview.", action: { kind: "refresh-preview", label: "Get the latest preview" } };
2847
+ return { key: "not-used", sentence: "Your branch does not use the library branch yet.", action: { kind: "use-preview", label: "Use its preview in my branch" } };
2848
+ }
2849
+ const nPieces = (n) => `${n} ${n === 1 ? "piece" : "pieces"}`;
2850
+ function productBranchSentence(r, opts = {}) {
2851
+ if (!r) return { lead: "Not checked against the library yet.", strong: "", rest: " Ask Claude what is missing from the library." };
2852
+ const n = r.pieces.length;
2853
+ if (!n) return { lead: "Built with the library.", strong: " Nothing is missing", rest: ": every value and component comes from it." };
2854
+ const linked = r.pieces.filter((p) => r.links.some((l) => l.piece === p.id));
2855
+ const openers = [...new Set(linked.map((p) => r.links.find((l) => l.piece === p.id)).map((l) => l.agent ? opts.agentName ?? "Claude AI" : l.openedBy && l.openedBy === opts.viewer ? "you" : l.openedBy ? `@${l.openedBy}` : "someone"))];
2856
+ const by = openers.length === 1 ? openers[0] : "your team";
2857
+ const was = n === 1 ? "was" : "were";
2858
+ if (linked.length === n) {
2859
+ const what = n === 1 ? "a library branch for it" : "library branches for them";
2860
+ return { lead: "Built with the library. ", strong: `${nPieces(n)} ${was} missing`, rest: `, and ${by} opened ${what}.` };
2861
+ }
2862
+ if (!linked.length) return { lead: "Built with the library. ", strong: `${nPieces(n)} ${n === 1 ? "is" : "are"} missing`, rest: n === 1 ? ": open a library branch for it." : ": open a library branch for each." };
2863
+ return { lead: "Built with the library. ", strong: `${nPieces(n)} ${was} missing`, rest: `: ${linked.length} ${linked.length === 1 ? "has" : "have"} a library branch, ${n - linked.length} still ${n - linked.length === 1 ? "needs" : "need"} one.` };
2864
+ }
2865
+ function whyItExists(n, opts = {}) {
2866
+ const who = n.author ? opts.names?.[n.author] ?? `@${n.author}` : "A product team";
2867
+ return { who, title: n.title, product: n.productName };
2868
+ }
2869
+ function nearestQuestion(n, opts = {}) {
2870
+ if (!n.nearest) return null;
2871
+ const who = n.author ? opts.names?.[n.author] ?? `@${n.author}` : "the product team";
2872
+ return `It looks a lot like "${n.nearest}". Is a new one worth it, or should ${who} use ${n.nearest}?`;
2873
+ }
2874
+ function suggestNearestComment(n) {
2875
+ if (!n.nearest) return null;
2876
+ return `Let's not add it to the library: ${n.nearest}${n.nearestToken ? ` (\`${n.nearestToken}\`)` : ""} covers it. Use it on ${n.title} (#${n.pr}) instead, then close this branch.`;
2877
+ }
2878
+ function pieceId(parts) {
2879
+ return slugify(parts.filter(Boolean).join(" "), 48);
2880
+ }
2881
+ const TONE_WORDS = ["neutral", "success", "warning", "danger", "info", "error", "critical", "accent", "brand", "primary", "secondary"];
2882
+ function tokenLabel(path) {
2883
+ const parts = path.split(".");
2884
+ const tone = parts.find((p) => TONE_WORDS.includes(p));
2885
+ if (tone) return tone;
2886
+ const meaningful = parts.filter((p) => !/^(color|bg|fg|text|border|default|base|value|\d+)$/.test(p));
2887
+ return meaningful[meaningful.length - 1] ?? parts[parts.length - 1] ?? path;
2888
+ }
2889
+ function proposedTokenPath(nearest, name, category) {
2890
+ const slug = slugify(name, 32);
2891
+ if (!nearest) return `${category === "color" ? "color" : category}.${slug}`;
2892
+ const parts = nearest.split(".");
2893
+ const toneAt = parts.findIndex((p) => TONE_WORDS.includes(p));
2894
+ if (toneAt > 0) return [...parts.slice(0, toneAt), slug, ...parts.slice(toneAt + 1)].join(".");
2895
+ return [...parts.slice(0, -1), slug].join(".");
3189
2896
  }
3190
2897
  export {
3191
- applyTokenEdits as $,
3192
- evaluatePairs as A,
3193
- contrastScope as B,
3194
- CONFIG_FILE as C,
3195
- contextLabel as D,
3196
- pathId as E,
3197
- flattenTokens as F,
3198
- serializeTokenFile as G,
3199
- removeToken as H,
3200
- upsertToken as I,
3201
- threeWayDiff as J,
3202
- emptyTokenFile as K,
3203
- parseTokenFile as L,
3204
- EXT_ID as M,
3205
- EXT_SCOPES as N,
3206
- moveToken as O,
3207
- effectiveScopes as P,
3208
- sameScopes as Q,
3209
- defaultScopes as R,
2898
+ layoutOptions as $,
2899
+ formatValue as A,
2900
+ resolve as B,
2901
+ moveToken as C,
2902
+ effectiveScopes as D,
2903
+ EXT_ID as E,
2904
+ sameScopes as F,
2905
+ defaultScopes as G,
2906
+ readTokenAt as H,
2907
+ modeFile as I,
2908
+ defaultFile as J,
2909
+ selectTokenFiles as K,
2910
+ LayeredTokenSet as L,
2911
+ resolveBranchPreviews as M,
2912
+ loadConfig as N,
2913
+ tokensPath as O,
2914
+ toConfigJson as P,
2915
+ validateLayered as Q,
2916
+ parseContrastChecks as R,
3210
2917
  SINGLE_MODE as S,
3211
2918
  TIER_NAMES as T,
3212
- readTokenAt as U,
3213
- modeFile as V,
3214
- defaultFile as W,
3215
- DEFAULT_PROTECTED_BRANCHES as X,
3216
- ProtectedBranchError as Y,
3217
- ConfigError as Z,
2919
+ classifyRisk as U,
2920
+ CONFIG_FILE as V,
2921
+ RISK_LABELS as W,
2922
+ previewLinks as X,
2923
+ findPreviewComment as Y,
2924
+ previewsSettled as Z,
3218
2925
  DEFAULT_TOKEN_PATHS as _,
3219
- assertSafeBranch as a,
3220
- changedModes as a$,
3221
- buildHistoryEntry as a0,
3222
- resolveBranchPreviews as a1,
3223
- tokensPath as a2,
3224
- coerceValue as a3,
3225
- toConfigJson as a4,
3226
- parseContrastChecks as a5,
3227
- checkLayeredPair as a6,
3228
- classifyRisk as a7,
3229
- formatChangeset as a8,
3230
- RISK_LABELS as a9,
3231
- OWNER_GROUPS as aA,
3232
- PREVIEW_APP_LABELS as aB,
3233
- PREVIEW_COMMENT_MARKER as aC,
3234
- PREVIEW_PLACEHOLDERS as aD,
3235
- PRIMITIVE_MODE as aE,
3236
- PRODUCT_LABEL_PREFIX as aF,
3237
- REDUCED_MOTION_NOTE as aG,
3238
- RIGHTS as aH,
3239
- RIGHT_LABEL as aI,
3240
- RISK_TIER as aJ,
3241
- SPEC_FIELDS as aK,
3242
- TIER_ORDER as aL,
3243
- TRIGGER_LABELS as aM,
3244
- TeamDirectory as aN,
3245
- UNRELEASED as aO,
3246
- VISUAL_CHECK_LABEL as aP,
3247
- affectedContrast as aQ,
3248
- appForProject as aR,
3249
- appUrl as aS,
3250
- approverEntries as aT,
3251
- approverLists as aU,
3252
- assignVersions as aV,
3253
- baseBranchOf as aW,
3254
- branchName as aX,
3255
- branchSlug as aY,
3256
- changeKind as aZ,
3257
- changeTable as a_,
3258
- parseJson as aa,
3259
- serializeJson as ab,
3260
- plainEqual as ac,
3261
- toPlain as ad,
3262
- getEntry as ae,
3263
- fromPlain as af,
3264
- previewLinks as ag,
3265
- slugify as ah,
3266
- readPreviewComment as ai,
3267
- findPreviewComment as aj,
3268
- previewsSettled as ak,
3269
- canShip as al,
3270
- whoCanShipSentence as am,
3271
- unreadableShipTeams as an,
3272
- AAA_MIN as ao,
3273
- AA_MIN as ap,
3274
- BULK_LIMIT as aq,
3275
- CONTRAST_CELL_MAX as ar,
3276
- CONTRAST_CHECKS_FILE as as,
3277
- DEFAULT_COMPONENT_SPECS_FILE as at,
3278
- DEFAULT_GOVERNANCE as au,
3279
- DEFAULT_OVERRIDE as av,
3280
- DEFAULT_PRODUCTS as aw,
3281
- DEFAULT_RIGHTS as ax,
3282
- DEFAULT_VISUAL_IMPACT as ay,
3283
- JsonParseError as az,
3284
- parseChangelog as b,
3285
- planToJson as b$,
3286
- changesetBump as b0,
3287
- classifyLayered as b1,
3288
- clearRawDeep as b2,
3289
- commitMessage as b3,
3290
- componentMotion as b4,
3291
- componentsUsingMotionToken as b5,
3292
- composeChangeset as b6,
3293
- composePrBody as b7,
3294
- composeProposalBody as b8,
3295
- composite as b9,
3296
- formatOwnerEntry as bA,
3297
- getChild as bB,
3298
- groupsPhrase as bC,
3299
- holdersOf as bD,
3300
- indexByPath as bE,
3301
- inheritedTypeAt as bF,
3302
- installCommand as bG,
3303
- installCommandFromPreviewComment as bH,
3304
- isGuardrail as bI,
3305
- isTeamEntry as bJ,
3306
- isTokenFile as bK,
3307
- isVercelComment as bL,
3308
- kindForMin as bM,
3309
- layeredImpact as bN,
3310
- luminance as bO,
3311
- modeLabel as bP,
3312
- nodesEqual as bQ,
3313
- normalizeRights as bR,
3314
- overrideKey as bS,
3315
- ownerEntryProblems as bT,
3316
- pairKey as bU,
3317
- parseChangesetText as bV,
3318
- parseComponentSpecs as bW,
3319
- parseOwnerEntry as bX,
3320
- parseVercelComment as bY,
3321
- planRestore as bZ,
3322
- planRevert as b_,
3323
- contextFor as ba,
3324
- contrastDelta as bb,
3325
- contrastDeltaTable as bc,
3326
- contrastLevel as bd,
3327
- contrastPair as be,
3328
- contrastRatio as bf,
3329
- countChanges as bg,
3330
- coverageSummary as bh,
3331
- defaultProduct as bi,
3332
- defaultTitle as bj,
3333
- defineConfig as bk,
3334
- detectLayout as bl,
3335
- diffLayered as bm,
3336
- entriesFor as bn,
3337
- entryLabel as bo,
3338
- expandEntries as bp,
3339
- expandOwners as bq,
3340
- fieldChanges as br,
3341
- figmaCell as bs,
3342
- fileModes as bt,
3343
- filterHistory as bu,
3344
- findProduct as bv,
3345
- findToken as bw,
3346
- findVercelComment as bx,
3347
- firstParagraph as by,
3348
- formatNumber as bz,
3349
- collectHistory as c,
3350
- previewComment as c0,
3351
- previewLinksOf as c1,
3352
- previewUrl as c2,
3353
- productForPlace as c3,
3354
- productLabel as c4,
3355
- productMarks as c5,
3356
- productOfBranch as c6,
3357
- productProblems as c7,
3358
- productsOf as c8,
3359
- projectOfLabel as c9,
3360
- protectedBranchesOf as ca,
3361
- renderedColor as cb,
3362
- rightsProblems as cc,
3363
- runLayeredContrast as cd,
3364
- sameProducts as ce,
3365
- sameToken as cf,
3366
- shipPolicy as cg,
3367
- snapshotUrl as ch,
3368
- summarizeTokenChange as ci,
3369
- tarballAssetName as cj,
3370
- tarballUrl as ck,
3371
- teamKey as cl,
3372
- teamKeyOf as cm,
3373
- teamUnreadableMessage as cn,
3374
- teamsIn as co,
3375
- titleOf as cp,
3376
- toChangeSummary as cq,
3377
- toMap as cr,
3378
- tokenBump as cs,
3379
- tokenHistory as ct,
3380
- tokenNode as cu,
3381
- unreadableTeams as cv,
3382
- valueIn as cw,
3383
- variantName as cx,
3384
- versionTimeline as cy,
3385
- visualImpactProblems as cz,
3386
- buildReport as d,
3387
- prNumberFromSubject as e,
3388
- variantKeyOf as f,
3389
- layoutOptions as g,
3390
- hashSpec as h,
3391
- iconComponentName as i,
3392
- buildTokenSet as j,
3393
- defaultContext as k,
3394
- loadConfig as l,
3395
- mergeReleases as m,
3396
- resolve as n,
3397
- COMPONENT_SPECS_VERSION as o,
3398
- packageDirCandidates as p,
3399
- COLOR_FIELDS as q,
3400
- releasesFromTags as r,
3401
- specHash as s,
3402
- tokenCssVar as t,
3403
- modeDisplayName as u,
3404
- validateComponentSpec as v,
3405
- modesOf as w,
3406
- isMultiMode as x,
3407
- TIER_AXIS as y,
3408
- findReferenceProblems as z
2926
+ proposedTokenPath as a,
2927
+ contextFor as a$,
2928
+ buildTokenSet as a0,
2929
+ readPreviewComment as a1,
2930
+ linkStatus as a2,
2931
+ libraryPreviewState as a3,
2932
+ parsePiecesRecord as a4,
2933
+ libraryBranchPlan as a5,
2934
+ NEEDED_BY_LABEL as a6,
2935
+ withNeededBy as a7,
2936
+ withLink as a8,
2937
+ withPiecesRecord as a9,
2938
+ RIGHTS as aA,
2939
+ RIGHT_LABEL as aB,
2940
+ RISK_TIER as aC,
2941
+ TIER_ORDER as aD,
2942
+ TeamDirectory as aE,
2943
+ VISUAL_CHECK_LABEL as aF,
2944
+ affectedContrast as aG,
2945
+ aliasOf as aH,
2946
+ appForProject as aI,
2947
+ appUrl as aJ,
2948
+ approverEntries as aK,
2949
+ approverLists as aL,
2950
+ baseBranchOf as aM,
2951
+ branchName as aN,
2952
+ branchSlug as aO,
2953
+ canShip as aP,
2954
+ changeKind as aQ,
2955
+ changedModes as aR,
2956
+ changesMode as aS,
2957
+ clearRawDeep as aT,
2958
+ colorToCss as aU,
2959
+ colorsEqual as aV,
2960
+ comboKey as aW,
2961
+ commitMessage as aX,
2962
+ compactJson as aY,
2963
+ composePrBody as aZ,
2964
+ composite as a_,
2965
+ productBranchSentence as aa,
2966
+ mergePiecesRecord as ab,
2967
+ productOfBranch as ac,
2968
+ defaultProduct as ad,
2969
+ slugify as ae,
2970
+ AAA_MIN as af,
2971
+ AA_MIN as ag,
2972
+ CHANGES_MODES as ah,
2973
+ CONTRAST_CELL_MAX as ai,
2974
+ CONTRAST_CHECKS_FILE as aj,
2975
+ ConfigError as ak,
2976
+ DEFAULT_CHANGES_MODE as al,
2977
+ DEFAULT_GOVERNANCE as am,
2978
+ DEFAULT_OVERRIDE as an,
2979
+ DEFAULT_PRODUCTS as ao,
2980
+ DEFAULT_RIGHTS as ap,
2981
+ DEFAULT_VISUAL_IMPACT as aq,
2982
+ DTCG_TYPES as ar,
2983
+ JsonParseError as as,
2984
+ LINK_STATUS_TONE as at,
2985
+ OWNER_GROUPS as au,
2986
+ PREVIEW_APP_LABELS as av,
2987
+ PREVIEW_COMMENT_MARKER as aw,
2988
+ PREVIEW_PLACEHOLDERS as ax,
2989
+ PRIMITIVE_MODE as ay,
2990
+ PRODUCT_LABEL_PREFIX as az,
2991
+ modesOf as b,
2992
+ sameProducts as b$,
2993
+ contrastLevel as b0,
2994
+ contrastPair as b1,
2995
+ contrastRatio as b2,
2996
+ defaultContext as b3,
2997
+ defaultTitle as b4,
2998
+ defineConfig as b5,
2999
+ detectLayout as b6,
3000
+ entriesFor as b7,
3001
+ entryLabel as b8,
3002
+ expandEntries as b9,
3003
+ nearestQuestion as bA,
3004
+ neededBySection as bB,
3005
+ nodesEqual as bC,
3006
+ normalizeRights as bD,
3007
+ overrideKey as bE,
3008
+ ownerEntryProblems as bF,
3009
+ pairKey as bG,
3010
+ parseDimension as bH,
3011
+ parseJson as bI,
3012
+ parseNeededBy as bJ,
3013
+ parseOwnerEntry as bK,
3014
+ parseVercelComment as bL,
3015
+ piecesSection as bM,
3016
+ plainEqual as bN,
3017
+ previewComment as bO,
3018
+ previewLinksOf as bP,
3019
+ previewUrl as bQ,
3020
+ productForPlace as bR,
3021
+ productLabel as bS,
3022
+ productMarks as bT,
3023
+ productProblems as bU,
3024
+ productsOf as bV,
3025
+ projectOfLabel as bW,
3026
+ protectedBranchesOf as bX,
3027
+ renderedColor as bY,
3028
+ rgbaToDtcg as bZ,
3029
+ rightsProblems as b_,
3030
+ expandOwners as ba,
3031
+ fieldChanges as bb,
3032
+ findProduct as bc,
3033
+ findToken as bd,
3034
+ findVercelComment as be,
3035
+ flattenLayerFile as bf,
3036
+ formatNumber as bg,
3037
+ formatOwnerEntry as bh,
3038
+ fromPlain as bi,
3039
+ getChild as bj,
3040
+ getEntry as bk,
3041
+ globToRegExp as bl,
3042
+ groupsPhrase as bm,
3043
+ hexToRgba as bn,
3044
+ holdersOf as bo,
3045
+ indexByPath as bp,
3046
+ inheritedTypeAt as bq,
3047
+ installCommand as br,
3048
+ installCommandFromPreviewComment as bs,
3049
+ isGuardrail as bt,
3050
+ isTeamEntry as bu,
3051
+ isTokenFile as bv,
3052
+ isVercelComment as bw,
3053
+ kindForMin as bx,
3054
+ layeredParityIssues as by,
3055
+ luminance as bz,
3056
+ TIER_AXIS as c,
3057
+ sameToken as c0,
3058
+ serializeJson as c1,
3059
+ shipPolicy as c2,
3060
+ snapshotUrl as c3,
3061
+ storybookUrl as c4,
3062
+ suggestNearestComment as c5,
3063
+ summarizeTokenChange as c6,
3064
+ tarballAssetName as c7,
3065
+ tarballUrl as c8,
3066
+ teamKey as c9,
3067
+ teamKeyOf as ca,
3068
+ teamUnreadableMessage as cb,
3069
+ teamsIn as cc,
3070
+ toAlias as cd,
3071
+ toMap as ce,
3072
+ toPlain as cf,
3073
+ tokenBump as cg,
3074
+ tokenNode as ch,
3075
+ unreadableShipTeams as ci,
3076
+ unreadableTeams as cj,
3077
+ valueIn as ck,
3078
+ valueToCss as cl,
3079
+ visualImpactProblems as cm,
3080
+ whoCanShipSentence as cn,
3081
+ whyItExists as co,
3082
+ collectRefs as d,
3083
+ evaluatePairs as e,
3084
+ findReferenceProblems as f,
3085
+ contrastScope as g,
3086
+ contextLabel as h,
3087
+ isMultiMode as i,
3088
+ isAlias as j,
3089
+ aliasTarget as k,
3090
+ pathId as l,
3091
+ modeDisplayName as m,
3092
+ flattenTokens as n,
3093
+ threeWayDiff as o,
3094
+ pieceId as p,
3095
+ emptyTokenFile as q,
3096
+ removeToken as r,
3097
+ serializeTokenFile as s,
3098
+ tokenLabel as t,
3099
+ upsertToken as u,
3100
+ valuesEqual as v,
3101
+ parseTokenFile as w,
3102
+ EXT_SCOPES as x,
3103
+ parseColor as y,
3104
+ rgbaToHex as z
3409
3105
  };
3410
- //# sourceMappingURL=component-spec-SKuywZqr.js.map
3106
+ //# sourceMappingURL=prod-branch-BdMPKffE.js.map