@bridge-ds/core 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/index.d.ts +1 -0
- package/dist/audit/index.js +269 -23
- package/dist/audit/index.js.map +1 -1
- package/dist/audit/missing-pieces.d.ts +43 -0
- package/dist/component-spec-BzfBkYRD.js +1122 -0
- package/dist/component-spec-BzfBkYRD.js.map +1 -0
- package/dist/github-client.d.ts +9 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1685 -1432
- package/dist/index.js.map +1 -1
- package/dist/node/index.js +3 -3
- package/dist/{component-spec-SKuywZqr.js → pr-composer-D2SZm7cg.js} +822 -1444
- package/dist/pr-composer-D2SZm7cg.js.map +1 -0
- package/dist/prod-branch-D5Tl87Wf.js +266 -0
- package/dist/prod-branch-D5Tl87Wf.js.map +1 -0
- package/dist/prod-branch-github.d.ts +139 -0
- package/dist/prod-branch.d.ts +257 -0
- package/dist/{report-iPSnsjYi.js → report-DY_peRy4.js} +24 -24
- package/dist/{report-iPSnsjYi.js.map → report-DY_peRy4.js.map} +1 -1
- package/dist/token-proposal.d.ts +72 -0
- package/package.json +1 -1
- package/dist/component-spec-SKuywZqr.js.map +0 -1
- package/dist/layered-BnbmToGs.js +0 -500
- package/dist/layered-BnbmToGs.js.map +0 -1
|
@@ -0,0 +1,1122 @@
|
|
|
1
|
+
import { bX as tokenBump, aI as changeKind, M as LayeredTokenSet, aA as aliasOf, bI as rgbaToDtcg, bc as hexToRgba, n as parseTokenFile, l as emptyTokenFile, s as serializeTokenFile, z as moveToken, r as removeToken, bf as inheritedTypeAt, u as upsertToken, D as readTokenAt, aN as comboKey, bH as renderedColor, aV as contrastRatio, w as rgbaToHex, U as classifyRisk, V as RISK_LABELS, ba as globToRegExp, a1 as slugify, Q as validateLayered } from "./pr-composer-D2SZm7cg.js";
|
|
2
|
+
function changesetBump(changes) {
|
|
3
|
+
return tokenBump(changes) ?? "patch";
|
|
4
|
+
}
|
|
5
|
+
function formatChangeset(packageName, bump, summary, details = "") {
|
|
6
|
+
return `---
|
|
7
|
+
"${packageName}": ${bump}
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
${summary.trim()}
|
|
11
|
+
${details ? `
|
|
12
|
+
${details.trim()}
|
|
13
|
+
` : ""}`;
|
|
14
|
+
}
|
|
15
|
+
function composeChangeset(changes, opts) {
|
|
16
|
+
const bump = changesetBump(changes);
|
|
17
|
+
const name = opts.branch.replace(/^tokens\//, "ds-bridge-").replace(/[^a-z0-9-]/gi, "-");
|
|
18
|
+
const list = changes.slice(0, 20).map((c) => `- ${changeKind(c)} \`${(c.target ?? c.current).path}\``).join("\n");
|
|
19
|
+
const more = changes.length > 20 ? `
|
|
20
|
+
- and ${changes.length - 20} more` : "";
|
|
21
|
+
const summary = opts.source ? `${opts.title} (${opts.source}).` : `${opts.title}.`;
|
|
22
|
+
return {
|
|
23
|
+
path: `.changeset/${name}.md`,
|
|
24
|
+
content: formatChangeset(opts.packageName, bump, summary, `${list}${more}`)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function coerceValue(input, type, existing) {
|
|
28
|
+
if (typeof input !== "string") return input;
|
|
29
|
+
const s = input.trim();
|
|
30
|
+
if (aliasOf(s)) return s;
|
|
31
|
+
if (s.startsWith("{") || s.startsWith("[")) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(s);
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const keepString = typeof existing === "string" && !aliasOf(existing);
|
|
38
|
+
switch (type) {
|
|
39
|
+
case "color": {
|
|
40
|
+
const m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s) ?? /^#([0-9a-f]{3})$/i.exec(s);
|
|
41
|
+
if (!m) return s;
|
|
42
|
+
const hex6 = m[1].length === 3 ? [...m[1]].map((c) => c + c).join("") : m[1];
|
|
43
|
+
if (keepString) return `#${hex6.toLowerCase()}${m[2] ?? ""}`;
|
|
44
|
+
return rgbaToDtcg(hexToRgba(`#${hex6}${m[2] ?? ""}`));
|
|
45
|
+
}
|
|
46
|
+
case "dimension": {
|
|
47
|
+
const m = /^(-?\d*\.?\d+)\s*(px|rem)?$/.exec(s);
|
|
48
|
+
if (!m || keepString) return s;
|
|
49
|
+
return { value: Number(m[1]), unit: m[2] ?? "px" };
|
|
50
|
+
}
|
|
51
|
+
case "duration": {
|
|
52
|
+
const m = /^(\d*\.?\d+)\s*(ms|s)?$/.exec(s);
|
|
53
|
+
if (!m || keepString) return s;
|
|
54
|
+
return { value: Number(m[1]), unit: m[2] ?? "ms" };
|
|
55
|
+
}
|
|
56
|
+
case "number":
|
|
57
|
+
case "fontWeight":
|
|
58
|
+
return /^-?\d*\.?\d+$/.test(s) ? Number(s) : s;
|
|
59
|
+
default:
|
|
60
|
+
return s;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function applyTokenEdits(base, edits) {
|
|
64
|
+
let set = base;
|
|
65
|
+
const files = new Map(base.files);
|
|
66
|
+
const touched = /* @__PURE__ */ new Set();
|
|
67
|
+
const notes = [];
|
|
68
|
+
const edit = (f, fn) => {
|
|
69
|
+
const doc = files.has(f) ? parseTokenFile(f, files.get(f)) : emptyTokenFile(f);
|
|
70
|
+
if (!fn(doc)) return;
|
|
71
|
+
const text = serializeTokenFile(doc);
|
|
72
|
+
if (text !== files.get(f)) {
|
|
73
|
+
files.set(f, text);
|
|
74
|
+
touched.add(f);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const definingFiles = (path) => [...new Set(set.defs.filter((d) => d.path === path).map((d) => d.file))];
|
|
78
|
+
const pickFiles = (e, all) => {
|
|
79
|
+
const defs = definingFiles(e.path);
|
|
80
|
+
if (e.file) {
|
|
81
|
+
if (!files.has(e.file)) throw new Error(`File "${e.file}" is not a token file. Token files: ${set.fileNames.join(", ")}`);
|
|
82
|
+
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")'}.`);
|
|
83
|
+
return [e.file];
|
|
84
|
+
}
|
|
85
|
+
if (e.mode) {
|
|
86
|
+
const combo = set.normalizeCombo(e.mode);
|
|
87
|
+
const v = set.view(combo).get(e.path);
|
|
88
|
+
if (!v) throw new Error(`"${e.path}" is not defined for ${JSON.stringify(combo)}.`);
|
|
89
|
+
return [v[v.length - 1].file];
|
|
90
|
+
}
|
|
91
|
+
if (!defs.length) throw new Error(`Token "${e.path}" does not exist. Use op "add" to create it.`);
|
|
92
|
+
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.`);
|
|
93
|
+
return defs;
|
|
94
|
+
};
|
|
95
|
+
for (const e of edits) {
|
|
96
|
+
if (!e.path || /[{}]/.test(e.path)) throw new Error(`Invalid token path "${e.path}". Use dots: color.text.muted.`);
|
|
97
|
+
switch (e.op) {
|
|
98
|
+
case "set": {
|
|
99
|
+
if (e.value === void 0 && e.description === void 0 && e.type === void 0) throw new Error(`set ${e.path}: give value, description or type.`);
|
|
100
|
+
for (const f of pickFiles(e, false)) {
|
|
101
|
+
edit(f, (doc) => {
|
|
102
|
+
const cur = readTokenAt(doc, e.path);
|
|
103
|
+
const type = e.type ?? cur.type;
|
|
104
|
+
return upsertToken(doc, e.path, {
|
|
105
|
+
type,
|
|
106
|
+
value: e.value === void 0 ? cur.value : coerceValue(e.value, type, cur.value),
|
|
107
|
+
description: e.description ?? cur.description
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "add": {
|
|
114
|
+
let file = e.file;
|
|
115
|
+
if (!file && e.mode) {
|
|
116
|
+
const combo = set.normalizeCombo(e.mode);
|
|
117
|
+
const parent = e.path.split(".").slice(0, -1).join(".");
|
|
118
|
+
const sib = [...set.view(combo).entries()].filter(([p]) => p.startsWith(`${parent}.`)).map(([, d]) => d[d.length - 1].file);
|
|
119
|
+
file = sib[sib.length - 1];
|
|
120
|
+
}
|
|
121
|
+
if (!file) {
|
|
122
|
+
const keys = e.path.split(".");
|
|
123
|
+
for (let d = keys.length - 1; d > 0 && !file; d--) {
|
|
124
|
+
const prefix = `${keys.slice(0, d).join(".")}.`;
|
|
125
|
+
const fs = [...new Set(set.defs.filter((x) => x.path.startsWith(prefix)).map((x) => x.file))];
|
|
126
|
+
if (fs.length === 1) file = fs[0];
|
|
127
|
+
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).`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!file) throw new Error(`Cannot infer a file for new token "${e.path}". Pass "file".`);
|
|
131
|
+
if (!files.has(file)) throw new Error(`File "${file}" is not a token file. Token files: ${set.fileNames.join(", ")}`);
|
|
132
|
+
if (set.defs.some((d) => d.path === e.path && d.file === file)) throw new Error(`"${e.path}" already exists in ${file}. Use op "set".`);
|
|
133
|
+
if (e.value === void 0) throw new Error(`add ${e.path}: value is required.`);
|
|
134
|
+
edit(file, (doc) => {
|
|
135
|
+
const type = e.type ?? inheritedTypeAt(doc, e.path);
|
|
136
|
+
if (!type && !aliasOf(e.value)) throw new Error(`add ${e.path}: pass "type" (color, dimension, ...); no parent group sets one.`);
|
|
137
|
+
return upsertToken(doc, e.path, { type, value: coerceValue(e.value, type), description: e.description });
|
|
138
|
+
});
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "remove": {
|
|
142
|
+
for (const f of pickFiles(e, true)) edit(f, (doc) => removeToken(doc, e.path));
|
|
143
|
+
const users = set.referencedBy(e.path).filter((u) => u.path !== e.path);
|
|
144
|
+
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.`);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "rename": {
|
|
148
|
+
const to = e.newPath;
|
|
149
|
+
if (!to || /[{}]/.test(to)) throw new Error(`rename ${e.path}: pass a valid newPath.`);
|
|
150
|
+
if (set.defs.some((d) => d.path === to)) throw new Error(`rename: "${to}" already exists.`);
|
|
151
|
+
for (const f of pickFiles(e, true)) edit(f, (doc) => moveToken(doc, e.path, to));
|
|
152
|
+
const from = `"{${e.path}}"`;
|
|
153
|
+
const next = `"{${to}}"`;
|
|
154
|
+
for (const [f, text] of files) {
|
|
155
|
+
if (!text.includes(from)) continue;
|
|
156
|
+
files.set(f, text.split(from).join(next));
|
|
157
|
+
touched.add(f);
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
default:
|
|
162
|
+
throw new Error(`Unknown op "${e.op}". Use set, add, remove or rename.`);
|
|
163
|
+
}
|
|
164
|
+
set = new LayeredTokenSet(base.config, new Map(files));
|
|
165
|
+
}
|
|
166
|
+
return { files, touched: [...touched].filter((f) => files.get(f) !== base.files.get(f)).sort(), notes };
|
|
167
|
+
}
|
|
168
|
+
function checkLayeredPair(set, pair, combo) {
|
|
169
|
+
const base = { fg: pair.foreground, bg: pair.background, min: pair.min, modes: comboKey(combo) };
|
|
170
|
+
const f = set.resolve(pair.foreground, combo);
|
|
171
|
+
const b = set.resolve(pair.background, combo);
|
|
172
|
+
if (f.error || b.error) return { ...base, ratio: null, pass: false, note: f.error ?? b.error };
|
|
173
|
+
const fc = renderedColor(f.value);
|
|
174
|
+
const bc = renderedColor(b.value);
|
|
175
|
+
if (!fc || !bc) return { ...base, ratio: null, pass: false, note: "not a color Bridge can measure (needs srgb or hex)" };
|
|
176
|
+
if (bc.a < 1) return { ...base, ratio: null, pass: false, note: "background is translucent: contrast depends on what is behind it" };
|
|
177
|
+
const ratio = Math.round(contrastRatio(fc, bc) * 100) / 100;
|
|
178
|
+
return { ...base, ratio, pass: ratio >= pair.min, fgValue: rgbaToHex(fc), bgValue: rgbaToHex(bc), ...fc.a < 1 ? { note: "foreground alpha composited over background" } : {} };
|
|
179
|
+
}
|
|
180
|
+
function runLayeredContrast(set, pairs, modes) {
|
|
181
|
+
const combos = set.combos(modes ?? set.config.contrast?.modes);
|
|
182
|
+
return combos.flatMap((c) => pairs.map((p) => checkLayeredPair(set, p, c)));
|
|
183
|
+
}
|
|
184
|
+
function contrastDelta(before, after) {
|
|
185
|
+
const key = (r) => `${r.fg}|${r.bg}|${r.modes}`;
|
|
186
|
+
const prev = new Map(before.map((r) => [key(r), r]));
|
|
187
|
+
return after.map((a) => {
|
|
188
|
+
const b = prev.get(key(a));
|
|
189
|
+
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 };
|
|
190
|
+
}).filter((d) => d.before !== d.after || !d.passAfter);
|
|
191
|
+
}
|
|
192
|
+
const same$1 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
193
|
+
function diffLayered(before, after) {
|
|
194
|
+
const key = (d) => `${d.file}\0${d.path}`;
|
|
195
|
+
const b = new Map(before.defs.map((d) => [key(d), d]));
|
|
196
|
+
const a = new Map(after.defs.map((d) => [key(d), d]));
|
|
197
|
+
const css = (set, d) => set.resolve(d.path, set.comboForFile(d.file)).css;
|
|
198
|
+
const tierOf = (set, f) => {
|
|
199
|
+
const t = set.tierOfFile(f);
|
|
200
|
+
return t ? { tier: t } : {};
|
|
201
|
+
};
|
|
202
|
+
const changes = [];
|
|
203
|
+
const removed = [];
|
|
204
|
+
const added = [];
|
|
205
|
+
for (const [k, d] of b) {
|
|
206
|
+
const n = a.get(k);
|
|
207
|
+
if (!n) {
|
|
208
|
+
removed.push(d);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const fields = [];
|
|
212
|
+
if (!same$1(d.value, n.value)) fields.push("value");
|
|
213
|
+
if (d.type !== n.type) fields.push("type");
|
|
214
|
+
if (d.description !== n.description) fields.push("description");
|
|
215
|
+
if (!same$1(d.extensions, n.extensions)) fields.push("extensions");
|
|
216
|
+
if (!fields.length) continue;
|
|
217
|
+
changes.push({
|
|
218
|
+
kind: "changed",
|
|
219
|
+
path: d.path,
|
|
220
|
+
file: d.file,
|
|
221
|
+
...tierOf(after, d.file),
|
|
222
|
+
...n.type ? { type: n.type } : {},
|
|
223
|
+
fields,
|
|
224
|
+
before: d.value,
|
|
225
|
+
after: n.value,
|
|
226
|
+
beforeCss: css(before, d),
|
|
227
|
+
afterCss: css(after, n),
|
|
228
|
+
...d.description !== n.description ? { descriptionBefore: d.description ?? "", descriptionAfter: n.description ?? "" } : {}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
for (const [k, d] of a) if (!b.has(k)) added.push(d);
|
|
232
|
+
for (const r of [...removed]) {
|
|
233
|
+
const i = added.findIndex((x) => x.file === r.file && same$1(x.value, r.value) && x.type === r.type);
|
|
234
|
+
if (i < 0) continue;
|
|
235
|
+
const n = added.splice(i, 1)[0];
|
|
236
|
+
removed.splice(removed.indexOf(r), 1);
|
|
237
|
+
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) });
|
|
238
|
+
}
|
|
239
|
+
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) });
|
|
240
|
+
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) });
|
|
241
|
+
return changes.sort((x, y) => x.path.localeCompare(y.path) || x.file.localeCompare(y.file));
|
|
242
|
+
}
|
|
243
|
+
function layeredImpact(before, after, direct, limit = 60) {
|
|
244
|
+
const skip = new Set(direct.map((c) => c.path));
|
|
245
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
246
|
+
for (const combo of after.combos()) {
|
|
247
|
+
const paths = /* @__PURE__ */ new Set([...before.view(combo).keys(), ...after.view(combo).keys()]);
|
|
248
|
+
for (const p of paths) {
|
|
249
|
+
if (skip.has(p)) continue;
|
|
250
|
+
const x = before.resolve(p, combo).css;
|
|
251
|
+
const y = after.resolve(p, combo).css;
|
|
252
|
+
if (x === y) continue;
|
|
253
|
+
const e = byPath.get(p);
|
|
254
|
+
if (e) e.modes.push(comboKey(combo));
|
|
255
|
+
else byPath.set(p, { path: p, modes: [comboKey(combo)], before: x, after: y });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const all = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
259
|
+
return { total: all.length, entries: all.slice(0, limit) };
|
|
260
|
+
}
|
|
261
|
+
const toChangeSummary = (c) => ({
|
|
262
|
+
kind: c.kind,
|
|
263
|
+
path: c.path,
|
|
264
|
+
...c.kind === "changed" ? { descriptionOnly: c.fields.length > 0 && c.fields.every((f) => f === "description") } : {}
|
|
265
|
+
});
|
|
266
|
+
function classifyLayered(changes, otherFiles = [], config) {
|
|
267
|
+
const r = classifyRisk({ tokenChanges: changes.map(toChangeSummary), files: otherFiles.map((path) => ({ path })), config });
|
|
268
|
+
return { tier: r.tier, bump: r.bump ?? "patch", reasons: r.reasons.length ? r.reasons : ["No token or guardrail change"], label: r.label };
|
|
269
|
+
}
|
|
270
|
+
const short = (v) => {
|
|
271
|
+
if (v === void 0) return "";
|
|
272
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
273
|
+
return s.length > 60 ? `${s.slice(0, 57)}...` : s;
|
|
274
|
+
};
|
|
275
|
+
const cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
276
|
+
const swatch = (css) => css ? `\`${cell(css)}\`` : "";
|
|
277
|
+
const aliasNote = (v) => typeof v === "string" && v.startsWith("{") ? ` (${cell(short(v))})` : "";
|
|
278
|
+
function changeTable(changes) {
|
|
279
|
+
if (!changes.length) return "_No token changes._";
|
|
280
|
+
const rows = changes.map((c) => {
|
|
281
|
+
const what = c.kind === "renamed" ? `renamed from \`${c.from}\`` : c.kind === "changed" ? c.fields.join(", ") : c.kind;
|
|
282
|
+
const before = c.kind === "added" || c.kind === "renamed" ? "" : `${swatch(c.beforeCss)}${aliasNote(c.before)}`;
|
|
283
|
+
const after = c.kind === "removed" ? "" : `${swatch(c.afterCss)}${aliasNote(c.after)}`;
|
|
284
|
+
return `| \`${c.path}\` | ${what} | ${before} | ${after} | ${c.file} |`;
|
|
285
|
+
});
|
|
286
|
+
return ["| Token | Change | Before | After | File |", "| --- | --- | --- | --- | --- |", ...rows].join("\n");
|
|
287
|
+
}
|
|
288
|
+
function contrastDeltaTable(deltas, limit = 40) {
|
|
289
|
+
if (!deltas.length) return "_No contrast pair moved._";
|
|
290
|
+
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**"} |`);
|
|
291
|
+
const more = deltas.length > limit ? `
|
|
292
|
+
|
|
293
|
+
_${deltas.length - limit} more pair(s) not shown._` : "";
|
|
294
|
+
return ["| Pair | Mode | Before | After | Min | Result |", "| --- | --- | --- | --- | --- | --- |", ...rows].join("\n") + more;
|
|
295
|
+
}
|
|
296
|
+
function composeProposalBody(args) {
|
|
297
|
+
const t = args.tier.tier;
|
|
298
|
+
const box = (n) => `- ${n === t ? "[x]" : "[ ]"} ${RISK_LABELS[n]}`;
|
|
299
|
+
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" : ""})`);
|
|
300
|
+
return [
|
|
301
|
+
"## What changed and why",
|
|
302
|
+
"",
|
|
303
|
+
args.summary.trim(),
|
|
304
|
+
"",
|
|
305
|
+
`Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
|
|
306
|
+
"",
|
|
307
|
+
"## Risk tier",
|
|
308
|
+
"",
|
|
309
|
+
box(1),
|
|
310
|
+
box(2),
|
|
311
|
+
box(3),
|
|
312
|
+
"",
|
|
313
|
+
...args.tier.reasons.map((r) => `- ${r}`),
|
|
314
|
+
`- Semver bump: \`${args.tier.bump}\``,
|
|
315
|
+
"",
|
|
316
|
+
"## Token changes",
|
|
317
|
+
"",
|
|
318
|
+
changeTable(args.changes),
|
|
319
|
+
"",
|
|
320
|
+
"## Contrast before and after",
|
|
321
|
+
"",
|
|
322
|
+
contrastDeltaTable(args.contrast),
|
|
323
|
+
"",
|
|
324
|
+
"## Downstream impact",
|
|
325
|
+
"",
|
|
326
|
+
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._",
|
|
327
|
+
"",
|
|
328
|
+
"## Checks",
|
|
329
|
+
"",
|
|
330
|
+
`- Token validation: ${args.validationErrors ? `**${args.validationErrors} error(s)**` : "no errors"} (DTCG schema, aliases, light and dark parity)`,
|
|
331
|
+
`- Contrast gate: ${args.contrast.some((d) => !d.passAfter) ? "**failing pairs, see table**" : "all pairs pass"}`,
|
|
332
|
+
"",
|
|
333
|
+
"## Authored by",
|
|
334
|
+
"",
|
|
335
|
+
"- [ ] Human",
|
|
336
|
+
"- [x] AI agent (Claude), reviewed by a human before merge",
|
|
337
|
+
"",
|
|
338
|
+
"## Design review checklist",
|
|
339
|
+
"",
|
|
340
|
+
"- [ ] Checked in light and dark themes and in every brand",
|
|
341
|
+
"- [ ] Contrast table reviewed, no pair loosened",
|
|
342
|
+
"- [ ] Changeset bump matches the tier",
|
|
343
|
+
""
|
|
344
|
+
].join("\n");
|
|
345
|
+
}
|
|
346
|
+
const DEFAULT_PROTECTED_BRANCHES = ["main", "master"];
|
|
347
|
+
class ProtectedBranchError extends Error {
|
|
348
|
+
constructor(message) {
|
|
349
|
+
super(message);
|
|
350
|
+
this.name = "ProtectedBranchError";
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function assertSafeBranch(branch, rules = {}) {
|
|
354
|
+
const name = branch.replace(/^refs\/heads\//, "").trim();
|
|
355
|
+
const blocked = /* @__PURE__ */ new Set([...rules.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES, ...rules.base ? [rules.base] : []]);
|
|
356
|
+
if (!name || blocked.has(name)) {
|
|
357
|
+
throw new ProtectedBranchError(`Refusing to write to protected branch "${name}". Bridge only creates new branches and pull requests.`);
|
|
358
|
+
}
|
|
359
|
+
if (rules.branchPrefix && !name.startsWith(rules.branchPrefix)) {
|
|
360
|
+
throw new ProtectedBranchError(`Branch "${name}" must start with "${rules.branchPrefix}".`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const BULK_LIMIT = 60;
|
|
364
|
+
const UNRELEASED = "Unreleased";
|
|
365
|
+
const SUBJECT_PR = [/^Merge pull request #(\d+)/, /\(#(\d+)\)\s*$/];
|
|
366
|
+
function prNumberFromSubject(subject) {
|
|
367
|
+
for (const re of SUBJECT_PR) {
|
|
368
|
+
const m = re.exec(subject);
|
|
369
|
+
if (m) return Number(m[1]);
|
|
370
|
+
}
|
|
371
|
+
return void 0;
|
|
372
|
+
}
|
|
373
|
+
function parseChangesetText(path, text) {
|
|
374
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
|
|
375
|
+
if (!m) return null;
|
|
376
|
+
const bumps = [...m[1].matchAll(/["']?([^"'\s:]+)["']?\s*:\s*(major|minor|patch)/g)].map((x) => ({ pkg: x[1], bump: x[2] }));
|
|
377
|
+
const order = ["major", "minor", "patch"];
|
|
378
|
+
const bump = order.find((b) => bumps.some((x) => x.bump === b));
|
|
379
|
+
const summary = m[2].trim().split(/\r?\n\r?\n/)[0].replace(/\s+/g, " ").trim();
|
|
380
|
+
return { path, ...bump ? { bump } : {}, packages: bumps.map((b) => b.pkg), summary };
|
|
381
|
+
}
|
|
382
|
+
function firstParagraph(text) {
|
|
383
|
+
if (!text) return "";
|
|
384
|
+
const cleaned = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
385
|
+
for (const para of cleaned.split(/\r?\n\s*\r?\n/)) {
|
|
386
|
+
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));
|
|
387
|
+
const s = lines.join(" ").replace(/\s+/g, " ").trim();
|
|
388
|
+
if (s && !/^\|/.test(s)) return s.length > 400 ? `${s.slice(0, 397)}...` : s;
|
|
389
|
+
}
|
|
390
|
+
return "";
|
|
391
|
+
}
|
|
392
|
+
function countChanges(changes) {
|
|
393
|
+
const c = { added: 0, changed: 0, removed: 0, renamed: 0, total: changes.length };
|
|
394
|
+
for (const x of changes) c[x.kind]++;
|
|
395
|
+
return c;
|
|
396
|
+
}
|
|
397
|
+
const shortSha = (sha) => sha.slice(0, 7);
|
|
398
|
+
function titleOf(commit) {
|
|
399
|
+
if (commit.pr?.title) return commit.pr.title;
|
|
400
|
+
if (/^Merge pull request #\d+/.test(commit.subject)) {
|
|
401
|
+
const first = (commit.body ?? "").split(/\r?\n/).find((l) => l.trim());
|
|
402
|
+
if (first) return first.trim();
|
|
403
|
+
}
|
|
404
|
+
return commit.subject;
|
|
405
|
+
}
|
|
406
|
+
function buildHistoryEntry(input) {
|
|
407
|
+
const { config, commit } = input;
|
|
408
|
+
const beforeSet = new LayeredTokenSet(config, input.before);
|
|
409
|
+
const afterSet = new LayeredTokenSet(config, input.after);
|
|
410
|
+
const changes = diffLayered(beforeSet, afterSet);
|
|
411
|
+
if (!changes.length) return null;
|
|
412
|
+
const tier = classifyLayered(changes, [], config);
|
|
413
|
+
const changesets = (input.changesets ?? []).map((c) => parseChangesetText(c.path, c.text)).filter((c) => c !== null);
|
|
414
|
+
const pkg = config.changesets?.package;
|
|
415
|
+
const relevant = changesets.filter((c) => !pkg || !c.packages.length || c.packages.includes(pkg));
|
|
416
|
+
const title = titleOf(commit);
|
|
417
|
+
const why = relevant.map((c) => c.summary).find(Boolean) || firstParagraph(commit.pr?.body) || firstParagraph(commit.body) || title;
|
|
418
|
+
const prNumber = commit.pr?.number ?? prNumberFromSubject(commit.subject);
|
|
419
|
+
const pr = commit.pr ?? (prNumber ? { number: prNumber, title, ...config.repo ? { url: `https://github.com/${config.repo.owner}/${config.repo.name}/pull/${prNumber}` } : {} } : void 0);
|
|
420
|
+
const changesetBump2 = relevant.map((c) => c.bump).find((b) => b === "major");
|
|
421
|
+
const bump = changesetBump2 ?? tier.bump;
|
|
422
|
+
return {
|
|
423
|
+
id: pr ? `pr-${pr.number}` : shortSha(commit.sha),
|
|
424
|
+
sha: commit.sha,
|
|
425
|
+
...commit.parents[0] ? { baseSha: commit.parents[0] } : {},
|
|
426
|
+
date: commit.date,
|
|
427
|
+
author: commit.pr?.author ?? commit.author,
|
|
428
|
+
title,
|
|
429
|
+
why,
|
|
430
|
+
...pr ? { pr } : {},
|
|
431
|
+
changes,
|
|
432
|
+
counts: countChanges(changes),
|
|
433
|
+
tier: tier.tier,
|
|
434
|
+
bump,
|
|
435
|
+
breaking: bump === "major",
|
|
436
|
+
reasons: tier.reasons,
|
|
437
|
+
changesets: relevant,
|
|
438
|
+
...changes.length > BULK_LIMIT ? { bulk: true } : {},
|
|
439
|
+
...input.demo ? { demo: true } : {}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
async function collectHistory(config, backend, opts) {
|
|
443
|
+
const root = config.tokens.root.replace(/\/$/, "");
|
|
444
|
+
const commits = await backend.listCommits({ ref: opts.ref, paths: [root === "." ? "" : root].filter(Boolean), limit: opts.limit ?? 40 });
|
|
445
|
+
const seen = /* @__PURE__ */ new Set();
|
|
446
|
+
const picked = [];
|
|
447
|
+
for (const c of commits) {
|
|
448
|
+
const n = c.pr?.number;
|
|
449
|
+
if (n !== void 0) {
|
|
450
|
+
if (seen.has(n)) continue;
|
|
451
|
+
seen.add(n);
|
|
452
|
+
}
|
|
453
|
+
picked.push(c);
|
|
454
|
+
}
|
|
455
|
+
const entries = [];
|
|
456
|
+
let done = 0;
|
|
457
|
+
for (const c of picked) {
|
|
458
|
+
const parent = c.parents[0];
|
|
459
|
+
const [before, after] = await Promise.all([parent ? backend.tokenFiles(parent) : Promise.resolve(/* @__PURE__ */ new Map()), backend.tokenFiles(c.sha)]);
|
|
460
|
+
let changesets = [];
|
|
461
|
+
if (backend.changesetPaths && backend.readFile && config.changesets) {
|
|
462
|
+
const [was, now] = await Promise.all([parent ? backend.changesetPaths(parent) : Promise.resolve([]), backend.changesetPaths(c.sha)]);
|
|
463
|
+
const added = now.filter((p) => !was.includes(p) && p.endsWith(".md") && !/README\.md$/i.test(p));
|
|
464
|
+
changesets = (await Promise.all(added.map(async (p) => ({ path: p, text: await backend.readFile(c.sha, p) ?? "" })))).filter((x) => x.text);
|
|
465
|
+
}
|
|
466
|
+
const e = buildHistoryEntry({ config, commit: c, before, after, changesets });
|
|
467
|
+
if (e) entries.push(e);
|
|
468
|
+
opts.onProgress?.(++done, picked.length);
|
|
469
|
+
}
|
|
470
|
+
return entries;
|
|
471
|
+
}
|
|
472
|
+
function parseChangelog(text) {
|
|
473
|
+
const out = [];
|
|
474
|
+
const parts = text.split(/^##\s+/m).slice(1);
|
|
475
|
+
for (const p of parts) {
|
|
476
|
+
const [head, ...rest] = p.split(/\r?\n/);
|
|
477
|
+
const version = /v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(head ?? "")?.[1];
|
|
478
|
+
if (!version) continue;
|
|
479
|
+
const body = rest.join("\n");
|
|
480
|
+
const shas = [...new Set([...body.matchAll(/(?:^|[\s[`(])([0-9a-f]{7,40})(?=[\s`\]):]|$)/gm)].map((m) => m[1]))];
|
|
481
|
+
const prs = [...new Set([...body.matchAll(/(?:#|\/pull\/)(\d+)/g)].map((m) => Number(m[1])))];
|
|
482
|
+
const date = /(\d{4}-\d{2}-\d{2})/.exec(head ?? "")?.[1];
|
|
483
|
+
out.push({ version, ...date ? { date } : {}, shas, prs });
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
function releasesFromTags(tags, pkg) {
|
|
488
|
+
const out = [];
|
|
489
|
+
for (const t of tags) {
|
|
490
|
+
const m = /^(?:(@?[^@\s]+)@)?v?(\d+\.\d+\.\d+(?:-[\w.]+)?)$/.exec(t.name);
|
|
491
|
+
if (!m) continue;
|
|
492
|
+
if (pkg && m[1] && m[1] !== pkg) continue;
|
|
493
|
+
out.push({ version: m[2], ...t.sha ? { sha: t.sha } : {}, ...t.date ? { date: t.date } : {} });
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
function assignVersions(entries, releases) {
|
|
498
|
+
const dated = releases.filter((r) => r.date).sort((a, b) => Date.parse(a.date) - Date.parse(b.date));
|
|
499
|
+
return entries.map((e) => {
|
|
500
|
+
const cited = releases.find((r) => r.shas?.some((s) => e.sha.startsWith(s)) || e.pr && r.prs?.includes(e.pr.number));
|
|
501
|
+
const byTag = cited ? void 0 : dated.find((r) => r.sha === e.sha || Date.parse(r.date) >= Date.parse(e.date));
|
|
502
|
+
return { ...e, version: (cited ?? byTag)?.version ?? UNRELEASED };
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const BUMP_RANK = { patch: 0, minor: 1, major: 2 };
|
|
506
|
+
function versionTimeline(entries, releases = []) {
|
|
507
|
+
const groups = /* @__PURE__ */ new Map();
|
|
508
|
+
for (const e of entries) {
|
|
509
|
+
const v = e.version ?? UNRELEASED;
|
|
510
|
+
let g = groups.get(v);
|
|
511
|
+
if (!g) {
|
|
512
|
+
const rel = releases.find((r) => r.version === v);
|
|
513
|
+
g = { version: v, pending: v === UNRELEASED, date: rel?.date ?? e.date, entries: [], prs: [], tokenChanges: 0, breaking: false, bump: "patch", authors: [] };
|
|
514
|
+
groups.set(v, g);
|
|
515
|
+
}
|
|
516
|
+
g.entries.push(e.id);
|
|
517
|
+
if (e.pr && !g.prs.includes(e.pr.number)) g.prs.push(e.pr.number);
|
|
518
|
+
g.tokenChanges += e.counts.total;
|
|
519
|
+
g.breaking ||= e.breaking;
|
|
520
|
+
if (BUMP_RANK[e.bump] > BUMP_RANK[g.bump]) g.bump = e.bump;
|
|
521
|
+
if (!g.authors.includes(e.author)) g.authors.push(e.author);
|
|
522
|
+
if (!releases.some((r) => r.version === v && r.date) && Date.parse(e.date) > Date.parse(g.date)) g.date = e.date;
|
|
523
|
+
}
|
|
524
|
+
return [...groups.values()].sort((a, b) => a.pending ? -1 : b.pending ? 1 : Date.parse(b.date) - Date.parse(a.date));
|
|
525
|
+
}
|
|
526
|
+
const matchesToken = (path, q) => path === q || path.startsWith(`${q.replace(/\.$/, "")}.`);
|
|
527
|
+
function filterHistory(entries, f = {}) {
|
|
528
|
+
const q = f.query?.trim().toLowerCase();
|
|
529
|
+
return entries.filter(
|
|
530
|
+
(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))
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
function fileModes(config, file) {
|
|
534
|
+
for (const layer of config.tokens.layers) {
|
|
535
|
+
const keys = [];
|
|
536
|
+
const marked = layer.files.replace(/\{([^}]+)\}/g, (_, k) => {
|
|
537
|
+
keys.push(k);
|
|
538
|
+
return "";
|
|
539
|
+
});
|
|
540
|
+
const source = globToRegExp(marked).source.replace(/\u0001/g, "([^/.]+)");
|
|
541
|
+
const m = new RegExp(source).exec(file);
|
|
542
|
+
if (m) return Object.fromEntries(keys.map((k, i) => [k, m[i + 1]]));
|
|
543
|
+
}
|
|
544
|
+
return {};
|
|
545
|
+
}
|
|
546
|
+
function modeLabel(modes) {
|
|
547
|
+
const v = Object.values(modes).filter(Boolean);
|
|
548
|
+
if (!v.length) return "All modes";
|
|
549
|
+
const s = v.join(" ");
|
|
550
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
551
|
+
}
|
|
552
|
+
function tokenHistory(entries, path, config) {
|
|
553
|
+
const names = /* @__PURE__ */ new Set([path]);
|
|
554
|
+
const out = [];
|
|
555
|
+
const sorted = [...entries].sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
|
556
|
+
for (const e of sorted) {
|
|
557
|
+
const hits = e.changes.filter((c) => names.has(c.path));
|
|
558
|
+
for (const c of hits) {
|
|
559
|
+
const modes = config ? fileModes(config, c.file) : {};
|
|
560
|
+
out.push({
|
|
561
|
+
entryId: e.id,
|
|
562
|
+
sha: e.sha,
|
|
563
|
+
date: e.date,
|
|
564
|
+
author: e.author,
|
|
565
|
+
title: e.title,
|
|
566
|
+
why: e.why,
|
|
567
|
+
...e.pr ? { pr: e.pr } : {},
|
|
568
|
+
...e.version ? { version: e.version } : {},
|
|
569
|
+
...e.demo ? { demo: true } : {},
|
|
570
|
+
kind: c.kind,
|
|
571
|
+
path: c.path,
|
|
572
|
+
...c.from ? { from: c.from } : {},
|
|
573
|
+
file: c.file,
|
|
574
|
+
modes,
|
|
575
|
+
modeLabel: modeLabel(modes),
|
|
576
|
+
fields: c.fields,
|
|
577
|
+
...c.before !== void 0 ? { before: c.before } : {},
|
|
578
|
+
...c.after !== void 0 ? { after: c.after } : {},
|
|
579
|
+
...c.beforeCss !== void 0 ? { beforeCss: c.beforeCss } : {},
|
|
580
|
+
...c.afterCss !== void 0 ? { afterCss: c.afterCss } : {},
|
|
581
|
+
...c.descriptionBefore !== void 0 ? { descriptionBefore: c.descriptionBefore } : {},
|
|
582
|
+
...c.descriptionAfter !== void 0 ? { descriptionAfter: c.descriptionAfter } : {}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
for (const c of hits) if (c.kind === "renamed" && c.from) names.add(c.from);
|
|
586
|
+
}
|
|
587
|
+
return out;
|
|
588
|
+
}
|
|
589
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
590
|
+
const keyOf = (path, file) => `${path}@${file}`;
|
|
591
|
+
const show = (v) => typeof v === "string" ? v : JSON.stringify(v);
|
|
592
|
+
function defIn(set, path, file) {
|
|
593
|
+
return set.defs.find((d) => d.path === path && d.file === file);
|
|
594
|
+
}
|
|
595
|
+
function sharesCombo(set, a, b) {
|
|
596
|
+
return a === b || set.combos().some((c) => {
|
|
597
|
+
const files = set.layerFiles(c).files;
|
|
598
|
+
return files.includes(a) && files.includes(b);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
function laterTouching(later, path, file, from) {
|
|
602
|
+
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 } : {} }));
|
|
603
|
+
}
|
|
604
|
+
function planRevert(input) {
|
|
605
|
+
const { current, entry } = input;
|
|
606
|
+
const later = (input.later ?? []).filter((e) => e.id !== entry.id && Date.parse(e.date) >= Date.parse(entry.date));
|
|
607
|
+
const res = input.resolutions ?? {};
|
|
608
|
+
const planned = [];
|
|
609
|
+
const conflicts = [];
|
|
610
|
+
const skipped = [];
|
|
611
|
+
const config = current.config;
|
|
612
|
+
const css = (path, file) => current.resolve(path, current.comboForFile(file)).css;
|
|
613
|
+
for (const c of entry.changes) {
|
|
614
|
+
const key = keyOf(c.path, c.file);
|
|
615
|
+
const label = modeLabel(fileModes(config, c.file));
|
|
616
|
+
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 } : {} };
|
|
617
|
+
const conflict = (reason, message, cur, editIfForced, summary) => {
|
|
618
|
+
const resolution = res[key] ?? "keep-current";
|
|
619
|
+
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 });
|
|
620
|
+
if (resolution === "use-before" && editIfForced) planned.push({ key, path: c.path, file: c.file, summary: summary ?? `${editIfForced.op} ${c.path}`, edit: editIfForced });
|
|
621
|
+
};
|
|
622
|
+
const now = defIn(current, c.path, c.file);
|
|
623
|
+
switch (c.kind) {
|
|
624
|
+
case "changed": {
|
|
625
|
+
if (!now) {
|
|
626
|
+
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})`);
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
const valueField = c.fields.includes("value");
|
|
630
|
+
const descField = c.fields.includes("description");
|
|
631
|
+
const cur = { value: now.value, css: css(c.path, c.file) };
|
|
632
|
+
const valueAt = (v) => same(now.value, v);
|
|
633
|
+
const descNow = now.description ?? "";
|
|
634
|
+
if (!valueField && !descField) {
|
|
635
|
+
skipped.push({ key, path: c.path, file: c.file, reason: "Only the type or extensions changed: edit those by hand." });
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
if ((!valueField || valueAt(c.before)) && (!descField || descNow === (c.descriptionBefore ?? ""))) {
|
|
639
|
+
skipped.push({ key, path: c.path, file: c.file, reason: "Already back to the value from before this change." });
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
const valueMoved = valueField && !valueAt(c.after) && !valueAt(c.before);
|
|
643
|
+
const descMoved = descField && descNow !== (c.descriptionAfter ?? "") && descNow !== (c.descriptionBefore ?? "");
|
|
644
|
+
const edit = {
|
|
645
|
+
op: "set",
|
|
646
|
+
path: c.path,
|
|
647
|
+
file: c.file,
|
|
648
|
+
...valueField ? { value: c.before } : {},
|
|
649
|
+
...descField ? { description: c.descriptionBefore ?? "" } : {}
|
|
650
|
+
};
|
|
651
|
+
const summary = valueField ? `Set ${c.path} back to ${show(c.before)} (${label})` : `Put the old description of ${c.path} back (${label})`;
|
|
652
|
+
if (valueMoved || descMoved) {
|
|
653
|
+
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);
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
planned.push({ key, path: c.path, file: c.file, summary, edit });
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case "added": {
|
|
660
|
+
if (!now) {
|
|
661
|
+
skipped.push({ key, path: c.path, file: c.file, reason: "Already removed." });
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
const users = current.referencedBy(c.path).filter((u) => u.path !== c.path && sharesCombo(current, u.file, c.file));
|
|
665
|
+
const edit = { op: "remove", path: c.path, file: c.file };
|
|
666
|
+
const summary = `Remove ${c.path} (${label})`;
|
|
667
|
+
if (!same(now.value, c.after)) {
|
|
668
|
+
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);
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
if (users.length) {
|
|
672
|
+
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);
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
planned.push({ key, path: c.path, file: c.file, summary, edit });
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
case "removed": {
|
|
679
|
+
const edit = { op: "add", path: c.path, file: c.file, value: c.before, ...c.type ? { type: c.type } : {}, ...c.descriptionBefore ? { description: c.descriptionBefore } : {} };
|
|
680
|
+
const summary = `Add ${c.path} back with ${show(c.before)} (${label})`;
|
|
681
|
+
if (now) {
|
|
682
|
+
if (same(now.value, c.before)) skipped.push({ key, path: c.path, file: c.file, reason: "Already back." });
|
|
683
|
+
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) });
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
planned.push({ key, path: c.path, file: c.file, summary, edit });
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
case "renamed": {
|
|
690
|
+
const from = c.from;
|
|
691
|
+
const back = defIn(current, from, c.file);
|
|
692
|
+
if (back && !now) {
|
|
693
|
+
skipped.push({ key, path: c.path, file: c.file, reason: `Already named ${from}.` });
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
if (!now) {
|
|
697
|
+
conflict("renamed-later", `${c.path} no longer exists in ${c.file}: it was renamed or removed again later.`);
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
if (back) {
|
|
701
|
+
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) });
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
if (planned.some((p) => p.edit.op === "rename" && p.path === c.path)) {
|
|
705
|
+
skipped.push({ key, path: c.path, file: c.file, reason: "Covered by the rename above." });
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
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 } });
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const prLink = entry.pr ? entry.pr.url ? `[#${entry.pr.number}](${entry.pr.url})` : `#${entry.pr.number}` : `commit ${entry.sha.slice(0, 7)}`;
|
|
714
|
+
const title = `Revert: ${entry.title}`;
|
|
715
|
+
const lines = [
|
|
716
|
+
`Reverts ${prLink} ("${entry.title}"), merged ${entry.date.slice(0, 10)} by ${entry.author}.`,
|
|
717
|
+
"",
|
|
718
|
+
`Original reason: ${entry.why}`,
|
|
719
|
+
"",
|
|
720
|
+
"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."
|
|
721
|
+
];
|
|
722
|
+
if (conflicts.length) {
|
|
723
|
+
lines.push("", "### Conflicts", "", "| Token | File | Why | Decision |", "| --- | --- | --- | --- |");
|
|
724
|
+
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"} |`);
|
|
725
|
+
}
|
|
726
|
+
if (skipped.length) lines.push("", `${skipped.length} change(s) were already undone on the base branch and are left alone.`);
|
|
727
|
+
return finishPlan({ ...input, kind: "revert", title, prTitle: title, summary: lines.join("\n"), planned, conflicts, skipped, source: entry });
|
|
728
|
+
}
|
|
729
|
+
function planRestore(input) {
|
|
730
|
+
const { current, path } = input;
|
|
731
|
+
const files = [...new Set(current.defs.filter((d) => d.path === path).map((d) => d.file))];
|
|
732
|
+
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.`);
|
|
733
|
+
const file = input.file ?? (files.length === 1 ? files[0] : void 0);
|
|
734
|
+
if (!file) throw new Error(`"${path}" is defined in ${files.join(", ")}. Pass the file (the mode) to restore.`);
|
|
735
|
+
if (!files.includes(file)) throw new Error(`"${path}" is not defined in ${file}. It is defined in: ${files.join(", ")}.`);
|
|
736
|
+
const label = modeLabel(fileModes(current.config, file));
|
|
737
|
+
const from = input.label ? ` from ${input.label}` : "";
|
|
738
|
+
const planned = [{ key: keyOf(path, file), path, file, summary: `Set ${path} to ${show(input.value)} (${label})`, edit: { op: "set", path, file, value: input.value } }];
|
|
739
|
+
const title = `Restore ${path} (${label})${from}`;
|
|
740
|
+
const item = input.item;
|
|
741
|
+
const summary = [
|
|
742
|
+
`Restores \`${path}\` in ${label} to ${show(input.value)}${from}.`,
|
|
743
|
+
...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}.`] : []
|
|
744
|
+
].join("\n");
|
|
745
|
+
return finishPlan({ ...input, kind: "restore", title, prTitle: title, summary, planned, conflicts: [], skipped: [] });
|
|
746
|
+
}
|
|
747
|
+
const issueKey = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
748
|
+
function finishPlan(p) {
|
|
749
|
+
const { current } = p;
|
|
750
|
+
const config = current.config;
|
|
751
|
+
const edits = p.planned.map((x) => x.edit);
|
|
752
|
+
const notes = [];
|
|
753
|
+
const empty = classifyLayered([], [], config);
|
|
754
|
+
const base = {
|
|
755
|
+
kind: p.kind,
|
|
756
|
+
title: p.title,
|
|
757
|
+
prTitle: p.prTitle,
|
|
758
|
+
summary: p.summary,
|
|
759
|
+
...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 } } : {},
|
|
760
|
+
planned: p.planned,
|
|
761
|
+
edits,
|
|
762
|
+
conflicts: p.conflicts,
|
|
763
|
+
skipped: p.skipped,
|
|
764
|
+
branch: `${config.governance.branchPrefix}${slugify(p.title, 48)}`
|
|
765
|
+
};
|
|
766
|
+
const nothing = (why) => ({ ...base, status: "nothing-to-do", why, changes: [], contrast: [], newFailures: [], newErrors: [], tier: empty, files: [], commitMessage: "", prBody: "", notes });
|
|
767
|
+
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.");
|
|
768
|
+
let result;
|
|
769
|
+
try {
|
|
770
|
+
result = applyTokenEdits(current, edits);
|
|
771
|
+
} catch (e) {
|
|
772
|
+
return { ...nothing(e.message), status: "blocked" };
|
|
773
|
+
}
|
|
774
|
+
notes.push(...result.notes);
|
|
775
|
+
const after = new LayeredTokenSet(config, result.files);
|
|
776
|
+
const changes = diffLayered(current, after);
|
|
777
|
+
if (!changes.length) return nothing("The edits leave every token as it is now.");
|
|
778
|
+
const prev = new Set(validateLayered(current).map(issueKey));
|
|
779
|
+
const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prev.has(issueKey(i)));
|
|
780
|
+
const pairs = p.pairs ?? [];
|
|
781
|
+
const modes = config.contrast?.modes;
|
|
782
|
+
const contrast = pairs.length ? contrastDelta(runLayeredContrast(current, pairs, modes), runLayeredContrast(after, pairs, modes)) : [];
|
|
783
|
+
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
784
|
+
const tier = classifyLayered(changes, [], config);
|
|
785
|
+
const impact = layeredImpact(current, after, changes);
|
|
786
|
+
const prBody = composeProposalBody({ summary: p.summary, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: p.tool ?? "Bridge", ...p.author ? { author: p.author } : {} });
|
|
787
|
+
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;
|
|
788
|
+
const files = [...result.touched.map((f) => ({ path: current.repoPath(f), content: result.files.get(f) })), ...changeset ? [changeset] : []];
|
|
789
|
+
const commitMessage = `${p.kind === "revert" ? "revert(tokens)" : "fix(tokens)"}: ${p.title.replace(/^Revert: /, "")}
|
|
790
|
+
|
|
791
|
+
${p.source?.pr ? `Reverts #${p.source.pr.number}. ` : ""}Tier ${tier.tier}. Planned with ${p.tool ?? "Bridge"}.`;
|
|
792
|
+
const blocked = newErrors.length || newFailures.length;
|
|
793
|
+
return {
|
|
794
|
+
...base,
|
|
795
|
+
status: blocked ? "blocked" : "ready",
|
|
796
|
+
...blocked ? { why: [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and ") } : {},
|
|
797
|
+
changes,
|
|
798
|
+
contrast,
|
|
799
|
+
newFailures,
|
|
800
|
+
newErrors,
|
|
801
|
+
tier,
|
|
802
|
+
files,
|
|
803
|
+
...changeset ? { changeset } : {},
|
|
804
|
+
commitMessage,
|
|
805
|
+
prBody,
|
|
806
|
+
notes
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function planToJson(plan) {
|
|
810
|
+
return {
|
|
811
|
+
bridge: "change-plan",
|
|
812
|
+
kind: plan.kind,
|
|
813
|
+
status: plan.status,
|
|
814
|
+
title: plan.prTitle,
|
|
815
|
+
branch: plan.branch,
|
|
816
|
+
tier: plan.tier.tier,
|
|
817
|
+
bump: plan.tier.bump,
|
|
818
|
+
edits: plan.edits,
|
|
819
|
+
conflicts: plan.conflicts.map((c) => ({ key: c.key, reason: c.reason, message: c.message, resolution: c.resolution })),
|
|
820
|
+
files: plan.files,
|
|
821
|
+
commitMessage: plan.commitMessage,
|
|
822
|
+
prBody: plan.prBody
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function packageDirCandidates(config) {
|
|
826
|
+
const parts = config.tokens.root.replace(/\/$/, "").split("/").filter((p) => p && p !== ".");
|
|
827
|
+
const out = [];
|
|
828
|
+
for (let i = parts.length; i >= 0; i--) out.push(parts.slice(0, i).join("/"));
|
|
829
|
+
return out;
|
|
830
|
+
}
|
|
831
|
+
function mergeReleases(changelog, tags) {
|
|
832
|
+
const byVersion = /* @__PURE__ */ new Map();
|
|
833
|
+
for (const r of [...changelog, ...tags]) {
|
|
834
|
+
const prev = byVersion.get(r.version);
|
|
835
|
+
byVersion.set(r.version, prev ? { ...r, ...prev, ...prev.date ? {} : r.date ? { date: r.date } : {}, ...prev.sha ? {} : r.sha ? { sha: r.sha } : {} } : r);
|
|
836
|
+
}
|
|
837
|
+
return [...byVersion.values()];
|
|
838
|
+
}
|
|
839
|
+
function buildReport(args) {
|
|
840
|
+
const entries = assignVersions(args.entries, args.releases).sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
|
841
|
+
return { ...args, entries, timeline: versionTimeline(entries, args.releases) };
|
|
842
|
+
}
|
|
843
|
+
const COMPONENT_SPECS_VERSION = 1;
|
|
844
|
+
const DEFAULT_COMPONENT_SPECS_FILE = "packages/react/component-specs.json";
|
|
845
|
+
const SPEC_FIELDS = [
|
|
846
|
+
"fill",
|
|
847
|
+
"stroke",
|
|
848
|
+
"strokeWeight",
|
|
849
|
+
"strokeTopWeight",
|
|
850
|
+
"strokeRightWeight",
|
|
851
|
+
"strokeBottomWeight",
|
|
852
|
+
"strokeLeftWeight",
|
|
853
|
+
"cornerRadius",
|
|
854
|
+
"paddingTop",
|
|
855
|
+
"paddingRight",
|
|
856
|
+
"paddingBottom",
|
|
857
|
+
"paddingLeft",
|
|
858
|
+
"itemSpacing",
|
|
859
|
+
"counterAxisSpacing",
|
|
860
|
+
"width",
|
|
861
|
+
"height",
|
|
862
|
+
"minWidth",
|
|
863
|
+
"minHeight",
|
|
864
|
+
"opacity",
|
|
865
|
+
"textFill",
|
|
866
|
+
"fontFamily",
|
|
867
|
+
"fontSize",
|
|
868
|
+
"fontWeight",
|
|
869
|
+
"lineHeight",
|
|
870
|
+
"letterSpacing",
|
|
871
|
+
"textDecoration"
|
|
872
|
+
];
|
|
873
|
+
const COLOR_FIELDS = /* @__PURE__ */ new Set(["fill", "stroke", "textFill"]);
|
|
874
|
+
const TRIGGER_LABELS = {
|
|
875
|
+
ON_HOVER: "While hovering",
|
|
876
|
+
ON_PRESS: "While pressing",
|
|
877
|
+
ON_CLICK: "On click",
|
|
878
|
+
MOUSE_ENTER: "Mouse enter",
|
|
879
|
+
MOUSE_LEAVE: "Mouse leave"
|
|
880
|
+
};
|
|
881
|
+
function coverageSummary(file) {
|
|
882
|
+
const list = file.coverage ?? file.components.map((c) => ({ name: c.name, status: "generated" }));
|
|
883
|
+
const generated = list.filter((c) => c.status === "generated").length;
|
|
884
|
+
const drafted = list.filter((c) => c.status === "drafted").length;
|
|
885
|
+
const layout = list.filter((c) => c.status === "layout").length;
|
|
886
|
+
return { generated, drafted, inFigma: generated + drafted, visual: list.length - layout, layout, total: list.length, missing: list.filter((c) => c.status === "unsupported") };
|
|
887
|
+
}
|
|
888
|
+
const LAYOUT_DIRECTIONS = /* @__PURE__ */ new Set(["HORIZONTAL", "VERTICAL"]);
|
|
889
|
+
const PRIMARY = /* @__PURE__ */ new Set(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]);
|
|
890
|
+
const COUNTER = /* @__PURE__ */ new Set(["MIN", "CENTER", "MAX", "STRETCH"]);
|
|
891
|
+
const PART_KINDS = /* @__PURE__ */ new Set(["frame", "text", "icon", "vector"]);
|
|
892
|
+
function validateComponentSpec(spec, tokenTypes) {
|
|
893
|
+
const out = [];
|
|
894
|
+
const at = (where, why) => out.push(`${spec.name ?? "spec"}: ${where}: ${why}`);
|
|
895
|
+
if (typeof spec.name !== "string" || !spec.name) return ["the spec has no name"];
|
|
896
|
+
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`];
|
|
897
|
+
const axisNames = /* @__PURE__ */ new Set();
|
|
898
|
+
for (const a of spec.axes) {
|
|
899
|
+
if (!a.name || axisNames.has(a.name)) at("axes", `"${a.name}" is missing or repeated`);
|
|
900
|
+
axisNames.add(a.name);
|
|
901
|
+
if (!Array.isArray(a.values) || !a.values.length) at(`axis ${a.name}`, "has no values");
|
|
902
|
+
else if (!a.values.includes(a.default)) at(`axis ${a.name}`, `default "${a.default}" is not one of its values`);
|
|
903
|
+
}
|
|
904
|
+
const props = /* @__PURE__ */ new Map();
|
|
905
|
+
for (const p of spec.properties) {
|
|
906
|
+
if (props.has(p.name)) at("properties", `"${p.name}" is repeated`);
|
|
907
|
+
props.set(p.name, p);
|
|
908
|
+
if (p.type !== "TEXT" && p.type !== "BOOLEAN") at(`property ${p.name}`, `type ${String(p.type)} is not TEXT or BOOLEAN`);
|
|
909
|
+
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"}`);
|
|
910
|
+
}
|
|
911
|
+
const parts = /* @__PURE__ */ new Map();
|
|
912
|
+
for (const part of spec.parts) {
|
|
913
|
+
if (!part.id || parts.has(part.id)) at("parts", `id "${part.id}" is missing or repeated`);
|
|
914
|
+
if (!PART_KINDS.has(part.kind)) at(`part ${part.id}`, `kind ${String(part.kind)} is not frame, text, icon or vector`);
|
|
915
|
+
if (part.parent === void 0) {
|
|
916
|
+
if ([...parts.values()].some((x) => x.parent === void 0)) at(`part ${part.id}`, "a second root (every other part needs a parent)");
|
|
917
|
+
} else if (!parts.has(part.parent)) at(`part ${part.id}`, `parent "${part.parent}" is not a part listed before it`);
|
|
918
|
+
else if (parts.get(part.parent).kind !== "frame") at(`part ${part.id}`, `parent "${part.parent}" is not a frame`);
|
|
919
|
+
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");
|
|
920
|
+
if (part.text !== void 0 && props.get(part.text)?.type !== "TEXT") at(`part ${part.id}`, `text property "${part.text}" does not exist`);
|
|
921
|
+
if (part.visible !== void 0 && props.get(part.visible)?.type !== "BOOLEAN") at(`part ${part.id}`, `visibility property "${part.visible}" does not exist`);
|
|
922
|
+
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");
|
|
923
|
+
parts.set(part.id, part);
|
|
924
|
+
}
|
|
925
|
+
if (!spec.parts.length || spec.parts[0].parent !== void 0) at("parts", "the first part must be the root");
|
|
926
|
+
const expected = spec.axes.reduce((n, a) => n * (a.values?.length || 1), 1);
|
|
927
|
+
if (spec.variants.length !== expected) at("variants", `${spec.variants.length} variants for ${expected} combinations of the axes`);
|
|
928
|
+
const seen = /* @__PURE__ */ new Set();
|
|
929
|
+
for (const v of spec.variants) {
|
|
930
|
+
const key = variantKeyOf(v.props ?? {});
|
|
931
|
+
const where = `variant ${key}`;
|
|
932
|
+
if (seen.has(key)) at(where, "is repeated");
|
|
933
|
+
seen.add(key);
|
|
934
|
+
for (const a of spec.axes) if (!a.values?.includes(v.props?.[a.name] ?? "")) at(where, `${a.name} is not one of its values`);
|
|
935
|
+
for (const k of Object.keys(v.props ?? {})) if (!axisNames.has(k)) at(where, `${k} is not an axis`);
|
|
936
|
+
for (const [id, fields] of Object.entries(v.values ?? {})) {
|
|
937
|
+
const part = parts.get(id);
|
|
938
|
+
if (!part) {
|
|
939
|
+
at(where, `values for "${id}", which is not a part`);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
for (const [field, value] of Object.entries(fields ?? {})) {
|
|
943
|
+
if (!SPEC_FIELDS.includes(field)) at(`${where}, ${id}`, `${field} is not a field`);
|
|
944
|
+
else if (value === null) {
|
|
945
|
+
if (!COLOR_FIELDS.has(field)) at(`${where}, ${id}`, `${field} cannot be empty (only colors can be none)`);
|
|
946
|
+
} else if (typeof value === "number") {
|
|
947
|
+
if (COLOR_FIELDS.has(field)) at(`${where}, ${id}`, `${field} cannot be a number`);
|
|
948
|
+
} else if (typeof value !== "string") at(`${where}, ${id}`, `${field} must be a token path, a number or none`);
|
|
949
|
+
else if (field === "textDecoration") {
|
|
950
|
+
if (!/^(UNDERLINE|STRIKETHROUGH|NONE)$/.test(value)) at(`${where}, ${id}`, `${value} is not a text decoration`);
|
|
951
|
+
} else if (tokenTypes) {
|
|
952
|
+
for (const path of value.startsWith("calc(") ? (value.match(/[A-Za-z][\w.-]*/g) ?? []).filter((x) => x !== "calc" && !/^\d|^px$/.test(x)) : [value]) {
|
|
953
|
+
const type = tokenTypes.get(path);
|
|
954
|
+
if (!type) at(`${where}, ${id}`, `${path} is not a token`);
|
|
955
|
+
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 ")}`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
for (const id of v.hidden ?? []) if (!parts.has(id)) at(where, `hides "${id}", which is not a part`);
|
|
961
|
+
for (const id of Object.keys(v.layout ?? {})) if (!parts.has(id)) at(where, `lays out "${id}", which is not a part`);
|
|
962
|
+
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`);
|
|
963
|
+
for (const id of Object.keys(v.slots ?? {})) if (!parts.has(id)) at(where, `shows a component in "${id}", which is not a part`);
|
|
964
|
+
}
|
|
965
|
+
for (const [i, x] of (spec.interactions ?? []).entries()) {
|
|
966
|
+
const where = `interaction ${i + 1}`;
|
|
967
|
+
const axis = spec.axes.find((a) => a.name === x.axis);
|
|
968
|
+
if (!(x.trigger in TRIGGER_LABELS)) at(where, `trigger ${String(x.trigger)} is not one Figma has`);
|
|
969
|
+
if (!axis) at(where, `${x.axis} is not an axis`);
|
|
970
|
+
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}`);
|
|
971
|
+
for (const k of x.variants ?? []) if (!seen.has(k)) at(where, `variant ${k} does not exist`);
|
|
972
|
+
const t = x.transition;
|
|
973
|
+
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");
|
|
974
|
+
}
|
|
975
|
+
return out;
|
|
976
|
+
}
|
|
977
|
+
function FIELD_TOKEN_TYPES(field) {
|
|
978
|
+
if (COLOR_FIELDS.has(field)) return ["color"];
|
|
979
|
+
if (field === "fontFamily") return ["fontFamily"];
|
|
980
|
+
if (field === "fontWeight") return ["fontWeight", "number"];
|
|
981
|
+
if (field === "opacity") return ["number"];
|
|
982
|
+
return ["dimension", "number"];
|
|
983
|
+
}
|
|
984
|
+
const iconComponentName = (name) => `Icon/${name}`;
|
|
985
|
+
function specHash(text) {
|
|
986
|
+
let h = 2166136261;
|
|
987
|
+
for (let i = 0; i < text.length; i++) {
|
|
988
|
+
h ^= text.charCodeAt(i);
|
|
989
|
+
h = Math.imul(h, 16777619);
|
|
990
|
+
}
|
|
991
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
992
|
+
}
|
|
993
|
+
function hashSpec(spec) {
|
|
994
|
+
const { hash: _ignored, example: _example, ...rest } = spec;
|
|
995
|
+
return specHash(JSON.stringify(rest));
|
|
996
|
+
}
|
|
997
|
+
const variantName = (props) => Object.entries(props).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
998
|
+
const variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
|
|
999
|
+
const REDUCED_MOTION_NOTE = "In code, reduced motion turns this off.";
|
|
1000
|
+
function componentMotion(spec) {
|
|
1001
|
+
const keys = new Set(spec.variants.map((v) => variantKeyOf(v.props)));
|
|
1002
|
+
const reactions = [];
|
|
1003
|
+
const triggers = [];
|
|
1004
|
+
for (const x of spec.interactions ?? []) {
|
|
1005
|
+
const only = x.variants ? new Set(x.variants) : void 0;
|
|
1006
|
+
let count = 0;
|
|
1007
|
+
for (const v of spec.variants) {
|
|
1008
|
+
if (v.props[x.axis] !== x.from) continue;
|
|
1009
|
+
const fromKey = variantKeyOf(v.props);
|
|
1010
|
+
if (only && !only.has(fromKey)) continue;
|
|
1011
|
+
const to = { ...v.props, [x.axis]: x.to };
|
|
1012
|
+
const toKey = variantKeyOf(to);
|
|
1013
|
+
if (!keys.has(toKey)) continue;
|
|
1014
|
+
reactions.push({ trigger: x.trigger, label: TRIGGER_LABELS[x.trigger], from: { ...v.props }, to, fromKey, toKey, transition: x.transition });
|
|
1015
|
+
count++;
|
|
1016
|
+
}
|
|
1017
|
+
triggers.push({ ...x, label: TRIGGER_LABELS[x.trigger], count });
|
|
1018
|
+
}
|
|
1019
|
+
const transitions = (spec.interactions ?? []).map((x) => x.transition).filter((t) => !!t);
|
|
1020
|
+
const cannotPlay = [...spec.motion ?? []];
|
|
1021
|
+
const openNote = cannotPlay.find((n) => n.enter || n.exit);
|
|
1022
|
+
const opening = openNote ? { enter: openNote.enter ?? null, exit: openNote.exit ?? null } : void 0;
|
|
1023
|
+
const openTransitions = opening ? [opening.enter, opening.exit].filter((t) => !!t) : [];
|
|
1024
|
+
const tokens = [
|
|
1025
|
+
...new Set([...transitions, ...openTransitions].flatMap((t) => [t.durationToken, t.easingToken]).concat(cannotPlay.map((n) => n.durationToken)).filter((t) => !!t))
|
|
1026
|
+
].sort();
|
|
1027
|
+
const properties = [...new Set(transitions.flatMap((t) => t.properties))].sort();
|
|
1028
|
+
const approximations = [...new Set(transitions.map((t) => t.approximated).filter((t) => !!t))];
|
|
1029
|
+
const animates = transitions.length > 0 || openTransitions.length > 0 || cannotPlay.some((n) => /animation/i.test(n.what));
|
|
1030
|
+
return { name: spec.name, reactions, triggers, tokens, properties, approximations, cannotPlay, opening, reducedMotion: animates ? REDUCED_MOTION_NOTE : void 0 };
|
|
1031
|
+
}
|
|
1032
|
+
function componentsUsingMotionToken(specs, tokenPath, chainOf) {
|
|
1033
|
+
const out = /* @__PURE__ */ new Set();
|
|
1034
|
+
for (const c of specs.components) {
|
|
1035
|
+
for (const t of componentMotion(c).tokens) {
|
|
1036
|
+
if (t === tokenPath || (chainOf?.(t) ?? []).includes(tokenPath)) out.add(c.name);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return [...out].sort();
|
|
1040
|
+
}
|
|
1041
|
+
function parseComponentSpecs(text) {
|
|
1042
|
+
let data;
|
|
1043
|
+
try {
|
|
1044
|
+
data = JSON.parse(text);
|
|
1045
|
+
} catch {
|
|
1046
|
+
throw new Error("component-specs.json is not valid JSON.");
|
|
1047
|
+
}
|
|
1048
|
+
const f = data;
|
|
1049
|
+
if (!f || typeof f !== "object" || !Array.isArray(f.components)) throw new Error('component-specs.json has no "components" list.');
|
|
1050
|
+
if (f.version !== COMPONENT_SPECS_VERSION) throw new Error(`component-specs.json is version ${String(f.version)}; this tool reads version ${COMPONENT_SPECS_VERSION}.`);
|
|
1051
|
+
return f;
|
|
1052
|
+
}
|
|
1053
|
+
const tokenCssVar = (path, prefix = "--", separator = "-") => `${prefix}${path.split(".").join(separator)}`;
|
|
1054
|
+
function figmaCell(entry) {
|
|
1055
|
+
if (!entry) return void 0;
|
|
1056
|
+
if (entry.status === "generated") return { label: "Yes" };
|
|
1057
|
+
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." };
|
|
1058
|
+
if (entry.status === "layout") return { label: "Auto layout", title: "A layout helper: nothing to draw, use auto layout in Figma." };
|
|
1059
|
+
return { label: "Not yet", ...entry.reason ? { title: `Not supported yet: ${entry.reason}` } : {} };
|
|
1060
|
+
}
|
|
1061
|
+
export {
|
|
1062
|
+
toChangeSummary as $,
|
|
1063
|
+
composeChangeset as A,
|
|
1064
|
+
BULK_LIMIT as B,
|
|
1065
|
+
COLOR_FIELDS as C,
|
|
1066
|
+
DEFAULT_PROTECTED_BRANCHES as D,
|
|
1067
|
+
contrastDeltaTable as E,
|
|
1068
|
+
countChanges as F,
|
|
1069
|
+
coverageSummary as G,
|
|
1070
|
+
figmaCell as H,
|
|
1071
|
+
fileModes as I,
|
|
1072
|
+
filterHistory as J,
|
|
1073
|
+
firstParagraph as K,
|
|
1074
|
+
hashSpec as L,
|
|
1075
|
+
iconComponentName as M,
|
|
1076
|
+
modeLabel as N,
|
|
1077
|
+
parseChangesetText as O,
|
|
1078
|
+
ProtectedBranchError as P,
|
|
1079
|
+
parseComponentSpecs as Q,
|
|
1080
|
+
REDUCED_MOTION_NOTE as R,
|
|
1081
|
+
SPEC_FIELDS as S,
|
|
1082
|
+
TRIGGER_LABELS as T,
|
|
1083
|
+
UNRELEASED as U,
|
|
1084
|
+
planRestore as V,
|
|
1085
|
+
planRevert as W,
|
|
1086
|
+
planToJson as X,
|
|
1087
|
+
prNumberFromSubject as Y,
|
|
1088
|
+
specHash as Z,
|
|
1089
|
+
titleOf as _,
|
|
1090
|
+
assertSafeBranch as a,
|
|
1091
|
+
tokenCssVar as a0,
|
|
1092
|
+
tokenHistory as a1,
|
|
1093
|
+
validateComponentSpec as a2,
|
|
1094
|
+
variantName as a3,
|
|
1095
|
+
versionTimeline as a4,
|
|
1096
|
+
parseChangelog as b,
|
|
1097
|
+
collectHistory as c,
|
|
1098
|
+
buildReport as d,
|
|
1099
|
+
applyTokenEdits as e,
|
|
1100
|
+
buildHistoryEntry as f,
|
|
1101
|
+
coerceValue as g,
|
|
1102
|
+
checkLayeredPair as h,
|
|
1103
|
+
formatChangeset as i,
|
|
1104
|
+
diffLayered as j,
|
|
1105
|
+
contrastDelta as k,
|
|
1106
|
+
runLayeredContrast as l,
|
|
1107
|
+
mergeReleases as m,
|
|
1108
|
+
layeredImpact as n,
|
|
1109
|
+
classifyLayered as o,
|
|
1110
|
+
packageDirCandidates as p,
|
|
1111
|
+
composeProposalBody as q,
|
|
1112
|
+
releasesFromTags as r,
|
|
1113
|
+
COMPONENT_SPECS_VERSION as s,
|
|
1114
|
+
DEFAULT_COMPONENT_SPECS_FILE as t,
|
|
1115
|
+
assignVersions as u,
|
|
1116
|
+
variantKeyOf as v,
|
|
1117
|
+
changeTable as w,
|
|
1118
|
+
changesetBump as x,
|
|
1119
|
+
componentMotion as y,
|
|
1120
|
+
componentsUsingMotionToken as z
|
|
1121
|
+
};
|
|
1122
|
+
//# sourceMappingURL=component-spec-BzfBkYRD.js.map
|