@bridge-ds/core 0.7.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/docs-proposal.d.ts +73 -1
- package/dist/explorations-github.d.ts +2 -0
- package/dist/github-client.d.ts +9 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1744 -1418
- package/dist/index.js.map +1 -1
- package/dist/node/index.js +5 -3
- package/dist/node/index.js.map +1 -1
- package/dist/{component-spec-1z2ih4E3.js → pr-composer-D2SZm7cg.js} +826 -1443
- 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/products.d.ts +9 -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-1z2ih4E3.js.map +0 -1
- package/dist/layered-BnbmToGs.js +0 -500
- package/dist/layered-BnbmToGs.js.map +0 -1
|
@@ -1,4 +1,476 @@
|
|
|
1
|
-
|
|
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) {
|
|
@@ -564,6 +1083,10 @@ const DEFAULT_PRODUCTS = [
|
|
|
564
1083
|
];
|
|
565
1084
|
const PRODUCT_LABEL_PREFIX = "product: ";
|
|
566
1085
|
const productLabel = (key) => `${PRODUCT_LABEL_PREFIX}${key}`;
|
|
1086
|
+
function productMarks(products, p) {
|
|
1087
|
+
if (p.key === defaultProduct(products).key) return { labels: [], line: "" };
|
|
1088
|
+
return { labels: [productLabel(p.key)], line: `**Product:** ${p.name}` };
|
|
1089
|
+
}
|
|
567
1090
|
function productsOf(config) {
|
|
568
1091
|
const list2 = config?.products;
|
|
569
1092
|
return Array.isArray(list2) && list2.length ? list2 : DEFAULT_PRODUCTS;
|
|
@@ -1670,53 +2193,6 @@ function indexByPath(tokens) {
|
|
|
1670
2193
|
for (const t of tokens) m.set(t.path, t);
|
|
1671
2194
|
return m;
|
|
1672
2195
|
}
|
|
1673
|
-
function valueIn(t, ctx) {
|
|
1674
|
-
const axis = TIER_AXIS[t.tier];
|
|
1675
|
-
if (!axis) return { value: t.values[SINGLE_MODE], via: "token" };
|
|
1676
|
-
if (t.tier === "semantic" && ctx.overrides && ctx.brand && ctx.theme) {
|
|
1677
|
-
const o = ctx.overrides.get(`${ctx.brand}|${ctx.theme}|${t.path}`);
|
|
1678
|
-
if (o !== void 0) return { value: o, via: "override" };
|
|
1679
|
-
}
|
|
1680
|
-
const mode = ctx[axis];
|
|
1681
|
-
return { value: mode === void 0 ? void 0 : t.values[mode], via: "token" };
|
|
1682
|
-
}
|
|
1683
|
-
function resolve(index, path, ctx) {
|
|
1684
|
-
const chain = [];
|
|
1685
|
-
const via = [];
|
|
1686
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1687
|
-
let current = path;
|
|
1688
|
-
for (; ; ) {
|
|
1689
|
-
if (seen.has(current)) return { chain: [...chain, current], via, error: "cycle", at: current };
|
|
1690
|
-
seen.add(current);
|
|
1691
|
-
chain.push(current);
|
|
1692
|
-
const t = index.get(current);
|
|
1693
|
-
if (!t) return { chain, via, error: "broken", at: current };
|
|
1694
|
-
const v = valueIn(t, ctx);
|
|
1695
|
-
if (v.value === void 0) return { chain, via, error: "missing-mode", at: current };
|
|
1696
|
-
via.push(v.via);
|
|
1697
|
-
if (!isAlias(v.value)) return { value: v.value, chain, via };
|
|
1698
|
-
current = aliasTarget(v.value);
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
function findReferenceProblems(index, axes) {
|
|
1702
|
-
const out = [];
|
|
1703
|
-
const base = { brand: axes.brand?.[0], theme: axes.theme?.[0], density: axes.density?.[0] };
|
|
1704
|
-
for (const t of index.values()) {
|
|
1705
|
-
const axis = TIER_AXIS[t.tier];
|
|
1706
|
-
const modes = axis ? (axes[axis] ?? Object.keys(t.values)).filter((m) => t.values[m] !== void 0) : [SINGLE_MODE];
|
|
1707
|
-
for (const mode of modes) {
|
|
1708
|
-
const v = t.values[mode];
|
|
1709
|
-
for (const ref of collectRefs(v)) {
|
|
1710
|
-
if (!index.has(ref)) out.push({ path: t.path, mode, kind: "broken", target: ref, chain: [t.path, ref] });
|
|
1711
|
-
}
|
|
1712
|
-
if (isAlias(v)) {
|
|
1713
|
-
const r = resolve(index, t.path, axis ? { ...base, [axis]: mode } : base);
|
|
1714
|
-
if (r.error === "cycle") out.push({ path: t.path, mode, kind: "cycle", target: r.at ?? "", chain: r.chain });
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
return out;
|
|
1719
|
-
}
|
|
1720
2196
|
const has = (segments, ...names) => names.some((n) => segments.includes(n));
|
|
1721
2197
|
function defaultScopes(path, type, tier) {
|
|
1722
2198
|
const s = path.split(".");
|
|
@@ -1755,124 +2231,32 @@ const sameScopes = (a, b) => {
|
|
|
1755
2231
|
return sa.length === sb.length && sa.every((x, i) => x === sb[i]);
|
|
1756
2232
|
};
|
|
1757
2233
|
const effectiveScopes = (t) => t.scopes ?? defaultScopes(t.path, t.type, t.tier);
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
const
|
|
1764
|
-
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
const b = typeof bg === "string" ? hexToRgba(bg) : bg;
|
|
1773
|
-
if (!f || !b) throw new Error("Invalid color");
|
|
1774
|
-
const opaqueBg = { ...b };
|
|
1775
|
-
const l1 = luminance(composite(f, opaqueBg));
|
|
1776
|
-
const l2 = luminance(opaqueBg);
|
|
1777
|
-
const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
|
|
1778
|
-
return (hi + 0.05) / (lo + 0.05);
|
|
1779
|
-
}
|
|
1780
|
-
function contrastLevel(ratio, kind, min = AA_MIN[kind]) {
|
|
1781
|
-
if (ratio < min) return "fail";
|
|
1782
|
-
if (ratio >= AAA_MIN[kind]) return "AAA";
|
|
1783
|
-
return "AA";
|
|
2234
|
+
const normDesc = (d) => (d ?? "").trim();
|
|
2235
|
+
function fieldChanges(a, b) {
|
|
2236
|
+
const out = [];
|
|
2237
|
+
if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
|
|
2238
|
+
if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
|
|
2239
|
+
const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
|
|
2240
|
+
for (const mode of modes) {
|
|
2241
|
+
if (!valuesEqual(b.type, a.values[mode], b.values[mode])) out.push({ field: "value", mode, before: a.values[mode], after: b.values[mode] });
|
|
2242
|
+
}
|
|
2243
|
+
if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
|
|
2244
|
+
const sa = effectiveScopes(a);
|
|
2245
|
+
const sb = effectiveScopes(b);
|
|
2246
|
+
if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
|
|
2247
|
+
return out;
|
|
1784
2248
|
}
|
|
1785
|
-
function
|
|
1786
|
-
return
|
|
1787
|
-
|
|
1788
|
-
themes: set.layout.themes.map((t) => t.mode),
|
|
1789
|
-
density: set.layout.densities[0]?.mode,
|
|
1790
|
-
overrides: set.overrideIndex
|
|
1791
|
-
};
|
|
2249
|
+
function sameToken(a, b) {
|
|
2250
|
+
if (!a || !b) return a === b;
|
|
2251
|
+
return fieldChanges(a, b).length === 0;
|
|
1792
2252
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
return
|
|
1800
|
-
}
|
|
1801
|
-
function evaluatePairs(index, pairs, scope) {
|
|
1802
|
-
const out = [];
|
|
1803
|
-
const brands = scope.brands.length ? scope.brands : [void 0];
|
|
1804
|
-
for (const brand of brands) {
|
|
1805
|
-
for (const theme of scope.themes) {
|
|
1806
|
-
const ctx = { brand, theme, density: scope.density, overrides: scope.overrides };
|
|
1807
|
-
for (const pair of pairs) {
|
|
1808
|
-
const steps = [];
|
|
1809
|
-
const color = (path) => {
|
|
1810
|
-
const r = resolve(index, path, ctx);
|
|
1811
|
-
r.via.forEach((via, i) => steps.push({ path: r.chain[i], via }));
|
|
1812
|
-
if (r.error) return `${path} cannot be resolved (${r.error === "broken" ? `missing ${r.at}` : r.error})`;
|
|
1813
|
-
return renderedColor(r.value) ?? `${path} is not a color`;
|
|
1814
|
-
};
|
|
1815
|
-
const fg = color(pair.foreground);
|
|
1816
|
-
const bg = color(pair.background);
|
|
1817
|
-
if (typeof fg === "string" || typeof bg === "string") {
|
|
1818
|
-
out.push({ pair, brand, theme, steps, error: typeof fg === "string" ? fg : bg });
|
|
1819
|
-
continue;
|
|
1820
|
-
}
|
|
1821
|
-
const ratio = Math.round(contrastRatio(fg, bg) * 100) / 100;
|
|
1822
|
-
out.push({ pair, brand, theme, ratio, level: contrastLevel(ratio, pair.kind, pair.min), steps });
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
return out;
|
|
1827
|
-
}
|
|
1828
|
-
const kindForMin = (min) => min >= AA_MIN.text ? "text" : "ui";
|
|
1829
|
-
const contrastPair = (foreground, background, min = AA_MIN.text) => ({ foreground, background, min, kind: kindForMin(min) });
|
|
1830
|
-
function parseContrastChecks(text) {
|
|
1831
|
-
if (!text) return [];
|
|
1832
|
-
const data = JSON.parse(text);
|
|
1833
|
-
const list2 = Array.isArray(data) ? data : Array.isArray(data.pairs) ? data.pairs : [];
|
|
1834
|
-
const out = [];
|
|
1835
|
-
for (const p of list2) {
|
|
1836
|
-
let fg, bg, min;
|
|
1837
|
-
if (Array.isArray(p)) [fg, bg, min] = p;
|
|
1838
|
-
else if (p && typeof p === "object") {
|
|
1839
|
-
const o = p;
|
|
1840
|
-
fg = o.fg ?? o.foreground;
|
|
1841
|
-
bg = o.bg ?? o.background;
|
|
1842
|
-
min = o.min ?? o.ratio ?? AA_MIN.text;
|
|
1843
|
-
}
|
|
1844
|
-
if (typeof fg !== "string" || typeof bg !== "string" || typeof min !== "number" || !(min > 0)) continue;
|
|
1845
|
-
out.push(contrastPair(fg, bg, min));
|
|
1846
|
-
}
|
|
1847
|
-
return out;
|
|
1848
|
-
}
|
|
1849
|
-
const pairKey = (r) => `${r.brand ?? ""}/${r.theme}:${r.pair.foreground}|${r.pair.background}`;
|
|
1850
|
-
const normDesc = (d) => (d ?? "").trim();
|
|
1851
|
-
function fieldChanges(a, b) {
|
|
1852
|
-
const out = [];
|
|
1853
|
-
if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
|
|
1854
|
-
if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
|
|
1855
|
-
const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
|
|
1856
|
-
for (const mode of modes) {
|
|
1857
|
-
if (!valuesEqual(b.type, a.values[mode], b.values[mode])) out.push({ field: "value", mode, before: a.values[mode], after: b.values[mode] });
|
|
1858
|
-
}
|
|
1859
|
-
if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
|
|
1860
|
-
const sa = effectiveScopes(a);
|
|
1861
|
-
const sb = effectiveScopes(b);
|
|
1862
|
-
if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
|
|
1863
|
-
return out;
|
|
1864
|
-
}
|
|
1865
|
-
function sameToken(a, b) {
|
|
1866
|
-
if (!a || !b) return a === b;
|
|
1867
|
-
return fieldChanges(a, b).length === 0;
|
|
1868
|
-
}
|
|
1869
|
-
function sideChange(from, to) {
|
|
1870
|
-
if (!from && !to) return void 0;
|
|
1871
|
-
if (!from) return { kind: "added", fields: [] };
|
|
1872
|
-
if (!to) return { kind: "removed", fields: [] };
|
|
1873
|
-
const fields = fieldChanges(from, to);
|
|
1874
|
-
if (!fields.length) return void 0;
|
|
1875
|
-
return { kind: fields.some((f) => f.field === "path") ? "renamed" : "changed", fields };
|
|
2253
|
+
function sideChange(from, to) {
|
|
2254
|
+
if (!from && !to) return void 0;
|
|
2255
|
+
if (!from) return { kind: "added", fields: [] };
|
|
2256
|
+
if (!to) return { kind: "removed", fields: [] };
|
|
2257
|
+
const fields = fieldChanges(from, to);
|
|
2258
|
+
if (!fields.length) return void 0;
|
|
2259
|
+
return { kind: fields.some((f) => f.field === "path") ? "renamed" : "changed", fields };
|
|
1876
2260
|
}
|
|
1877
2261
|
function threeWayDiff(base, figma, head) {
|
|
1878
2262
|
const ids = /* @__PURE__ */ new Set([...base?.keys() ?? [], ...figma.keys(), ...head.keys()]);
|
|
@@ -1911,7 +2295,7 @@ const RISK_LABELS = {
|
|
|
1911
2295
|
2: "Tier 2: medium risk, design review then design + tech approval",
|
|
1912
2296
|
3: "Tier 3: token change, needs design + tech approval"
|
|
1913
2297
|
};
|
|
1914
|
-
const BUMP_RANK
|
|
2298
|
+
const BUMP_RANK = { patch: 0, minor: 1, major: 2 };
|
|
1915
2299
|
const LOW = [/\.md$/, /^docs\//, /\.stories\.tsx?$/, /\.docs\.json$/, /\.test\.tsx?$/, /^\.changeset\//];
|
|
1916
2300
|
const isSummary = (c) => "kind" in c;
|
|
1917
2301
|
function summarizeTokenChange(c) {
|
|
@@ -1927,7 +2311,7 @@ function tokenBump(changes) {
|
|
|
1927
2311
|
let bump;
|
|
1928
2312
|
for (const c of changes.map(summarizeTokenChange)) {
|
|
1929
2313
|
const b = c.kind === "removed" || c.kind === "renamed" ? "major" : c.kind === "changed" && c.descriptionOnly ? "patch" : "minor";
|
|
1930
|
-
if (!bump || BUMP_RANK
|
|
2314
|
+
if (!bump || BUMP_RANK[b] > BUMP_RANK[bump]) bump = b;
|
|
1931
2315
|
}
|
|
1932
2316
|
return bump;
|
|
1933
2317
|
}
|
|
@@ -1957,6 +2341,98 @@ function classifyRisk(input) {
|
|
|
1957
2341
|
const tier = reasons[0]?.tier ?? 1;
|
|
1958
2342
|
return { tier, label: RISK_LABELS[tier], reasons: reasons.map((r) => r.text), bump };
|
|
1959
2343
|
}
|
|
2344
|
+
const CONTRAST_CHECKS_FILE = "contrast.checks.json";
|
|
2345
|
+
const AA_MIN = { text: 4.5, "large-text": 3, ui: 3 };
|
|
2346
|
+
const AAA_MIN = { text: 7, "large-text": 4.5, ui: 4.5 };
|
|
2347
|
+
const channel = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
2348
|
+
function luminance(c) {
|
|
2349
|
+
const q = (x) => Math.round(Math.min(1, Math.max(0, x)) * 255) / 255;
|
|
2350
|
+
return 0.2126 * channel(q(c.r)) + 0.7152 * channel(q(c.g)) + 0.0722 * channel(q(c.b));
|
|
2351
|
+
}
|
|
2352
|
+
function composite(fg, bg) {
|
|
2353
|
+
const a = fg.a;
|
|
2354
|
+
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 };
|
|
2355
|
+
}
|
|
2356
|
+
function contrastRatio(fg, bg) {
|
|
2357
|
+
const f = typeof fg === "string" ? hexToRgba(fg) : fg;
|
|
2358
|
+
const b = typeof bg === "string" ? hexToRgba(bg) : bg;
|
|
2359
|
+
if (!f || !b) throw new Error("Invalid color");
|
|
2360
|
+
const opaqueBg = { ...b };
|
|
2361
|
+
const l1 = luminance(composite(f, opaqueBg));
|
|
2362
|
+
const l2 = luminance(opaqueBg);
|
|
2363
|
+
const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
|
|
2364
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
2365
|
+
}
|
|
2366
|
+
function contrastLevel(ratio, kind, min = AA_MIN[kind]) {
|
|
2367
|
+
if (ratio < min) return "fail";
|
|
2368
|
+
if (ratio >= AAA_MIN[kind]) return "AAA";
|
|
2369
|
+
return "AA";
|
|
2370
|
+
}
|
|
2371
|
+
function contrastScope(set) {
|
|
2372
|
+
return {
|
|
2373
|
+
brands: set.layout.brands.map((b) => b.mode),
|
|
2374
|
+
themes: set.layout.themes.map((t) => t.mode),
|
|
2375
|
+
density: set.layout.densities[0]?.mode,
|
|
2376
|
+
overrides: set.overrideIndex
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
2380
|
+
const contextLabel = (r) => r.brand ? `${cap(r.brand)} ${r.theme}` : cap(r.theme);
|
|
2381
|
+
function renderedColor(value) {
|
|
2382
|
+
const c = parseColor(value);
|
|
2383
|
+
const hex = value && typeof value === "object" ? value.hex : void 0;
|
|
2384
|
+
const fromHex = typeof hex === "string" ? hexToRgba(hex) : void 0;
|
|
2385
|
+
return c && fromHex ? { ...fromHex, a: c.a } : c;
|
|
2386
|
+
}
|
|
2387
|
+
function evaluatePairs(index, pairs, scope) {
|
|
2388
|
+
const out = [];
|
|
2389
|
+
const brands = scope.brands.length ? scope.brands : [void 0];
|
|
2390
|
+
for (const brand of brands) {
|
|
2391
|
+
for (const theme of scope.themes) {
|
|
2392
|
+
const ctx = { brand, theme, density: scope.density, overrides: scope.overrides };
|
|
2393
|
+
for (const pair of pairs) {
|
|
2394
|
+
const steps = [];
|
|
2395
|
+
const color = (path) => {
|
|
2396
|
+
const r = resolve(index, path, ctx);
|
|
2397
|
+
r.via.forEach((via, i) => steps.push({ path: r.chain[i], via }));
|
|
2398
|
+
if (r.error) return `${path} cannot be resolved (${r.error === "broken" ? `missing ${r.at}` : r.error})`;
|
|
2399
|
+
return renderedColor(r.value) ?? `${path} is not a color`;
|
|
2400
|
+
};
|
|
2401
|
+
const fg = color(pair.foreground);
|
|
2402
|
+
const bg = color(pair.background);
|
|
2403
|
+
if (typeof fg === "string" || typeof bg === "string") {
|
|
2404
|
+
out.push({ pair, brand, theme, steps, error: typeof fg === "string" ? fg : bg });
|
|
2405
|
+
continue;
|
|
2406
|
+
}
|
|
2407
|
+
const ratio = Math.round(contrastRatio(fg, bg) * 100) / 100;
|
|
2408
|
+
out.push({ pair, brand, theme, ratio, level: contrastLevel(ratio, pair.kind, pair.min), steps });
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
return out;
|
|
2413
|
+
}
|
|
2414
|
+
const kindForMin = (min) => min >= AA_MIN.text ? "text" : "ui";
|
|
2415
|
+
const contrastPair = (foreground, background, min = AA_MIN.text) => ({ foreground, background, min, kind: kindForMin(min) });
|
|
2416
|
+
function parseContrastChecks(text) {
|
|
2417
|
+
if (!text) return [];
|
|
2418
|
+
const data = JSON.parse(text);
|
|
2419
|
+
const list2 = Array.isArray(data) ? data : Array.isArray(data.pairs) ? data.pairs : [];
|
|
2420
|
+
const out = [];
|
|
2421
|
+
for (const p of list2) {
|
|
2422
|
+
let fg, bg, min;
|
|
2423
|
+
if (Array.isArray(p)) [fg, bg, min] = p;
|
|
2424
|
+
else if (p && typeof p === "object") {
|
|
2425
|
+
const o = p;
|
|
2426
|
+
fg = o.fg ?? o.foreground;
|
|
2427
|
+
bg = o.bg ?? o.background;
|
|
2428
|
+
min = o.min ?? o.ratio ?? AA_MIN.text;
|
|
2429
|
+
}
|
|
2430
|
+
if (typeof fg !== "string" || typeof bg !== "string" || typeof min !== "number" || !(min > 0)) continue;
|
|
2431
|
+
out.push(contrastPair(fg, bg, min));
|
|
2432
|
+
}
|
|
2433
|
+
return out;
|
|
2434
|
+
}
|
|
2435
|
+
const pairKey = (r) => `${r.brand ?? ""}/${r.theme}:${r.pair.foreground}|${r.pair.background}`;
|
|
1960
2436
|
const RISK_TIER = RISK_LABELS[3];
|
|
1961
2437
|
function slugify(text, max = 40) {
|
|
1962
2438
|
const s = text.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -1992,7 +2468,7 @@ const describe = (set, t, mode) => {
|
|
|
1992
2468
|
const resolved = r.value === void 0 ? "unresolved" : formatValue(t.type, r.value);
|
|
1993
2469
|
return `${formatValue(t.type, v)} (${resolved})`;
|
|
1994
2470
|
};
|
|
1995
|
-
const cell
|
|
2471
|
+
const cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ") || " ";
|
|
1996
2472
|
const code = (s) => s ? `\`${s}\`` : "";
|
|
1997
2473
|
function valueText(head, proposed, c, mode) {
|
|
1998
2474
|
const t = c.target ?? c.current;
|
|
@@ -2069,7 +2545,7 @@ function composePrBody(input) {
|
|
|
2069
2545
|
const extras = [];
|
|
2070
2546
|
if (c.current && c.target && (c.current.description ?? "") !== (c.target.description ?? "")) extras.push("description");
|
|
2071
2547
|
lines.push(
|
|
2072
|
-
`| ${name} | ${TIER_NAMES[t.tier]} | ${kind}${extras.length ? ` (+${extras.join(", ")})` : ""} | ${cell
|
|
2548
|
+
`| ${name} | ${TIER_NAMES[t.tier]} | ${kind}${extras.length ? ` (+${extras.join(", ")})` : ""} | ${cell(valuesCell(head, proposed, c))} | ${contrastCell(c, validation.contrastBefore, validation.contrastAfter)} |`
|
|
2073
2549
|
);
|
|
2074
2550
|
}
|
|
2075
2551
|
lines.push("");
|
|
@@ -2124,1282 +2600,189 @@ ${body}
|
|
|
2124
2600
|
|
|
2125
2601
|
${footer}`;
|
|
2126
2602
|
}
|
|
2127
|
-
function changesetBump(changes) {
|
|
2128
|
-
return tokenBump(changes) ?? "patch";
|
|
2129
|
-
}
|
|
2130
|
-
function formatChangeset(packageName, bump, summary, details = "") {
|
|
2131
|
-
return `---
|
|
2132
|
-
"${packageName}": ${bump}
|
|
2133
|
-
---
|
|
2134
|
-
|
|
2135
|
-
${summary.trim()}
|
|
2136
|
-
${details ? `
|
|
2137
|
-
${details.trim()}
|
|
2138
|
-
` : ""}`;
|
|
2139
|
-
}
|
|
2140
|
-
function composeChangeset(changes, opts) {
|
|
2141
|
-
const bump = changesetBump(changes);
|
|
2142
|
-
const name = opts.branch.replace(/^tokens\//, "ds-bridge-").replace(/[^a-z0-9-]/gi, "-");
|
|
2143
|
-
const list2 = changes.slice(0, 20).map((c) => `- ${changeKind(c)} \`${(c.target ?? c.current).path}\``).join("\n");
|
|
2144
|
-
const more = changes.length > 20 ? `
|
|
2145
|
-
- and ${changes.length - 20} more` : "";
|
|
2146
|
-
const summary = opts.source ? `${opts.title} (${opts.source}).` : `${opts.title}.`;
|
|
2147
|
-
return {
|
|
2148
|
-
path: `.changeset/${name}.md`,
|
|
2149
|
-
content: formatChangeset(opts.packageName, bump, summary, `${list2}${more}`)
|
|
2150
|
-
};
|
|
2151
|
-
}
|
|
2152
|
-
function coerceValue(input, type, existing) {
|
|
2153
|
-
if (typeof input !== "string") return input;
|
|
2154
|
-
const s = input.trim();
|
|
2155
|
-
if (aliasOf(s)) return s;
|
|
2156
|
-
if (s.startsWith("{") || s.startsWith("[")) {
|
|
2157
|
-
try {
|
|
2158
|
-
return JSON.parse(s);
|
|
2159
|
-
} catch {
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
const keepString = typeof existing === "string" && !aliasOf(existing);
|
|
2163
|
-
switch (type) {
|
|
2164
|
-
case "color": {
|
|
2165
|
-
const m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s) ?? /^#([0-9a-f]{3})$/i.exec(s);
|
|
2166
|
-
if (!m) return s;
|
|
2167
|
-
const hex6 = m[1].length === 3 ? [...m[1]].map((c) => c + c).join("") : m[1];
|
|
2168
|
-
if (keepString) return `#${hex6.toLowerCase()}${m[2] ?? ""}`;
|
|
2169
|
-
return rgbaToDtcg(hexToRgba(`#${hex6}${m[2] ?? ""}`));
|
|
2170
|
-
}
|
|
2171
|
-
case "dimension": {
|
|
2172
|
-
const m = /^(-?\d*\.?\d+)\s*(px|rem)?$/.exec(s);
|
|
2173
|
-
if (!m || keepString) return s;
|
|
2174
|
-
return { value: Number(m[1]), unit: m[2] ?? "px" };
|
|
2175
|
-
}
|
|
2176
|
-
case "duration": {
|
|
2177
|
-
const m = /^(\d*\.?\d+)\s*(ms|s)?$/.exec(s);
|
|
2178
|
-
if (!m || keepString) return s;
|
|
2179
|
-
return { value: Number(m[1]), unit: m[2] ?? "ms" };
|
|
2180
|
-
}
|
|
2181
|
-
case "number":
|
|
2182
|
-
case "fontWeight":
|
|
2183
|
-
return /^-?\d*\.?\d+$/.test(s) ? Number(s) : s;
|
|
2184
|
-
default:
|
|
2185
|
-
return s;
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
function applyTokenEdits(base, edits) {
|
|
2189
|
-
let set = base;
|
|
2190
|
-
const files = new Map(base.files);
|
|
2191
|
-
const touched = /* @__PURE__ */ new Set();
|
|
2192
|
-
const notes = [];
|
|
2193
|
-
const edit = (f, fn) => {
|
|
2194
|
-
const doc = files.has(f) ? parseTokenFile(f, files.get(f)) : emptyTokenFile(f);
|
|
2195
|
-
if (!fn(doc)) return;
|
|
2196
|
-
const text = serializeTokenFile(doc);
|
|
2197
|
-
if (text !== files.get(f)) {
|
|
2198
|
-
files.set(f, text);
|
|
2199
|
-
touched.add(f);
|
|
2200
|
-
}
|
|
2201
|
-
};
|
|
2202
|
-
const definingFiles = (path) => [...new Set(set.defs.filter((d) => d.path === path).map((d) => d.file))];
|
|
2203
|
-
const pickFiles = (e, all) => {
|
|
2204
|
-
const defs = definingFiles(e.path);
|
|
2205
|
-
if (e.file) {
|
|
2206
|
-
if (!files.has(e.file)) throw new Error(`File "${e.file}" is not a token file. Token files: ${set.fileNames.join(", ")}`);
|
|
2207
|
-
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")'}.`);
|
|
2208
|
-
return [e.file];
|
|
2209
|
-
}
|
|
2210
|
-
if (e.mode) {
|
|
2211
|
-
const combo = set.normalizeCombo(e.mode);
|
|
2212
|
-
const v = set.view(combo).get(e.path);
|
|
2213
|
-
if (!v) throw new Error(`"${e.path}" is not defined for ${JSON.stringify(combo)}.`);
|
|
2214
|
-
return [v[v.length - 1].file];
|
|
2215
|
-
}
|
|
2216
|
-
if (!defs.length) throw new Error(`Token "${e.path}" does not exist. Use op "add" to create it.`);
|
|
2217
|
-
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.`);
|
|
2218
|
-
return defs;
|
|
2219
|
-
};
|
|
2220
|
-
for (const e of edits) {
|
|
2221
|
-
if (!e.path || /[{}]/.test(e.path)) throw new Error(`Invalid token path "${e.path}". Use dots: color.text.muted.`);
|
|
2222
|
-
switch (e.op) {
|
|
2223
|
-
case "set": {
|
|
2224
|
-
if (e.value === void 0 && e.description === void 0 && e.type === void 0) throw new Error(`set ${e.path}: give value, description or type.`);
|
|
2225
|
-
for (const f of pickFiles(e, false)) {
|
|
2226
|
-
edit(f, (doc) => {
|
|
2227
|
-
const cur = readTokenAt(doc, e.path);
|
|
2228
|
-
const type = e.type ?? cur.type;
|
|
2229
|
-
return upsertToken(doc, e.path, {
|
|
2230
|
-
type,
|
|
2231
|
-
value: e.value === void 0 ? cur.value : coerceValue(e.value, type, cur.value),
|
|
2232
|
-
description: e.description ?? cur.description
|
|
2233
|
-
});
|
|
2234
|
-
});
|
|
2235
|
-
}
|
|
2236
|
-
break;
|
|
2237
|
-
}
|
|
2238
|
-
case "add": {
|
|
2239
|
-
let file = e.file;
|
|
2240
|
-
if (!file && e.mode) {
|
|
2241
|
-
const combo = set.normalizeCombo(e.mode);
|
|
2242
|
-
const parent = e.path.split(".").slice(0, -1).join(".");
|
|
2243
|
-
const sib = [...set.view(combo).entries()].filter(([p]) => p.startsWith(`${parent}.`)).map(([, d]) => d[d.length - 1].file);
|
|
2244
|
-
file = sib[sib.length - 1];
|
|
2245
|
-
}
|
|
2246
|
-
if (!file) {
|
|
2247
|
-
const keys = e.path.split(".");
|
|
2248
|
-
for (let d = keys.length - 1; d > 0 && !file; d--) {
|
|
2249
|
-
const prefix = `${keys.slice(0, d).join(".")}.`;
|
|
2250
|
-
const fs = [...new Set(set.defs.filter((x) => x.path.startsWith(prefix)).map((x) => x.file))];
|
|
2251
|
-
if (fs.length === 1) file = fs[0];
|
|
2252
|
-
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).`);
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
if (!file) throw new Error(`Cannot infer a file for new token "${e.path}". Pass "file".`);
|
|
2256
|
-
if (!files.has(file)) throw new Error(`File "${file}" is not a token file. Token files: ${set.fileNames.join(", ")}`);
|
|
2257
|
-
if (set.defs.some((d) => d.path === e.path && d.file === file)) throw new Error(`"${e.path}" already exists in ${file}. Use op "set".`);
|
|
2258
|
-
if (e.value === void 0) throw new Error(`add ${e.path}: value is required.`);
|
|
2259
|
-
edit(file, (doc) => {
|
|
2260
|
-
const type = e.type ?? inheritedTypeAt(doc, e.path);
|
|
2261
|
-
if (!type && !aliasOf(e.value)) throw new Error(`add ${e.path}: pass "type" (color, dimension, ...); no parent group sets one.`);
|
|
2262
|
-
return upsertToken(doc, e.path, { type, value: coerceValue(e.value, type), description: e.description });
|
|
2263
|
-
});
|
|
2264
|
-
break;
|
|
2265
|
-
}
|
|
2266
|
-
case "remove": {
|
|
2267
|
-
for (const f of pickFiles(e, true)) edit(f, (doc) => removeToken(doc, e.path));
|
|
2268
|
-
const users = set.referencedBy(e.path).filter((u) => u.path !== e.path);
|
|
2269
|
-
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.`);
|
|
2270
|
-
break;
|
|
2271
|
-
}
|
|
2272
|
-
case "rename": {
|
|
2273
|
-
const to = e.newPath;
|
|
2274
|
-
if (!to || /[{}]/.test(to)) throw new Error(`rename ${e.path}: pass a valid newPath.`);
|
|
2275
|
-
if (set.defs.some((d) => d.path === to)) throw new Error(`rename: "${to}" already exists.`);
|
|
2276
|
-
for (const f of pickFiles(e, true)) edit(f, (doc) => moveToken(doc, e.path, to));
|
|
2277
|
-
const from = `"{${e.path}}"`;
|
|
2278
|
-
const next = `"{${to}}"`;
|
|
2279
|
-
for (const [f, text] of files) {
|
|
2280
|
-
if (!text.includes(from)) continue;
|
|
2281
|
-
files.set(f, text.split(from).join(next));
|
|
2282
|
-
touched.add(f);
|
|
2283
|
-
}
|
|
2284
|
-
break;
|
|
2285
|
-
}
|
|
2286
|
-
default:
|
|
2287
|
-
throw new Error(`Unknown op "${e.op}". Use set, add, remove or rename.`);
|
|
2288
|
-
}
|
|
2289
|
-
set = new LayeredTokenSet(base.config, new Map(files));
|
|
2290
|
-
}
|
|
2291
|
-
return { files, touched: [...touched].filter((f) => files.get(f) !== base.files.get(f)).sort(), notes };
|
|
2292
|
-
}
|
|
2293
|
-
function checkLayeredPair(set, pair, combo) {
|
|
2294
|
-
const base = { fg: pair.foreground, bg: pair.background, min: pair.min, modes: comboKey(combo) };
|
|
2295
|
-
const f = set.resolve(pair.foreground, combo);
|
|
2296
|
-
const b = set.resolve(pair.background, combo);
|
|
2297
|
-
if (f.error || b.error) return { ...base, ratio: null, pass: false, note: f.error ?? b.error };
|
|
2298
|
-
const fc = renderedColor(f.value);
|
|
2299
|
-
const bc = renderedColor(b.value);
|
|
2300
|
-
if (!fc || !bc) return { ...base, ratio: null, pass: false, note: "not a color Bridge can measure (needs srgb or hex)" };
|
|
2301
|
-
if (bc.a < 1) return { ...base, ratio: null, pass: false, note: "background is translucent: contrast depends on what is behind it" };
|
|
2302
|
-
const ratio = Math.round(contrastRatio(fc, bc) * 100) / 100;
|
|
2303
|
-
return { ...base, ratio, pass: ratio >= pair.min, fgValue: rgbaToHex(fc), bgValue: rgbaToHex(bc), ...fc.a < 1 ? { note: "foreground alpha composited over background" } : {} };
|
|
2304
|
-
}
|
|
2305
|
-
function runLayeredContrast(set, pairs, modes) {
|
|
2306
|
-
const combos = set.combos(modes ?? set.config.contrast?.modes);
|
|
2307
|
-
return combos.flatMap((c) => pairs.map((p) => checkLayeredPair(set, p, c)));
|
|
2308
|
-
}
|
|
2309
|
-
function contrastDelta(before, after) {
|
|
2310
|
-
const key = (r) => `${r.fg}|${r.bg}|${r.modes}`;
|
|
2311
|
-
const prev = new Map(before.map((r) => [key(r), r]));
|
|
2312
|
-
return after.map((a) => {
|
|
2313
|
-
const b = prev.get(key(a));
|
|
2314
|
-
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 };
|
|
2315
|
-
}).filter((d) => d.before !== d.after || !d.passAfter);
|
|
2316
|
-
}
|
|
2317
|
-
const same$1 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2318
|
-
function diffLayered(before, after) {
|
|
2319
|
-
const key = (d) => `${d.file}\0${d.path}`;
|
|
2320
|
-
const b = new Map(before.defs.map((d) => [key(d), d]));
|
|
2321
|
-
const a = new Map(after.defs.map((d) => [key(d), d]));
|
|
2322
|
-
const css = (set, d) => set.resolve(d.path, set.comboForFile(d.file)).css;
|
|
2323
|
-
const tierOf = (set, f) => {
|
|
2324
|
-
const t = set.tierOfFile(f);
|
|
2325
|
-
return t ? { tier: t } : {};
|
|
2326
|
-
};
|
|
2327
|
-
const changes = [];
|
|
2328
|
-
const removed = [];
|
|
2329
|
-
const added = [];
|
|
2330
|
-
for (const [k, d] of b) {
|
|
2331
|
-
const n = a.get(k);
|
|
2332
|
-
if (!n) {
|
|
2333
|
-
removed.push(d);
|
|
2334
|
-
continue;
|
|
2335
|
-
}
|
|
2336
|
-
const fields = [];
|
|
2337
|
-
if (!same$1(d.value, n.value)) fields.push("value");
|
|
2338
|
-
if (d.type !== n.type) fields.push("type");
|
|
2339
|
-
if (d.description !== n.description) fields.push("description");
|
|
2340
|
-
if (!same$1(d.extensions, n.extensions)) fields.push("extensions");
|
|
2341
|
-
if (!fields.length) continue;
|
|
2342
|
-
changes.push({
|
|
2343
|
-
kind: "changed",
|
|
2344
|
-
path: d.path,
|
|
2345
|
-
file: d.file,
|
|
2346
|
-
...tierOf(after, d.file),
|
|
2347
|
-
...n.type ? { type: n.type } : {},
|
|
2348
|
-
fields,
|
|
2349
|
-
before: d.value,
|
|
2350
|
-
after: n.value,
|
|
2351
|
-
beforeCss: css(before, d),
|
|
2352
|
-
afterCss: css(after, n),
|
|
2353
|
-
...d.description !== n.description ? { descriptionBefore: d.description ?? "", descriptionAfter: n.description ?? "" } : {}
|
|
2354
|
-
});
|
|
2355
|
-
}
|
|
2356
|
-
for (const [k, d] of a) if (!b.has(k)) added.push(d);
|
|
2357
|
-
for (const r of [...removed]) {
|
|
2358
|
-
const i = added.findIndex((x) => x.file === r.file && same$1(x.value, r.value) && x.type === r.type);
|
|
2359
|
-
if (i < 0) continue;
|
|
2360
|
-
const n = added.splice(i, 1)[0];
|
|
2361
|
-
removed.splice(removed.indexOf(r), 1);
|
|
2362
|
-
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) });
|
|
2363
|
-
}
|
|
2364
|
-
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) });
|
|
2365
|
-
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) });
|
|
2366
|
-
return changes.sort((x, y) => x.path.localeCompare(y.path) || x.file.localeCompare(y.file));
|
|
2367
|
-
}
|
|
2368
|
-
function layeredImpact(before, after, direct, limit = 60) {
|
|
2369
|
-
const skip = new Set(direct.map((c) => c.path));
|
|
2370
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
2371
|
-
for (const combo of after.combos()) {
|
|
2372
|
-
const paths = /* @__PURE__ */ new Set([...before.view(combo).keys(), ...after.view(combo).keys()]);
|
|
2373
|
-
for (const p of paths) {
|
|
2374
|
-
if (skip.has(p)) continue;
|
|
2375
|
-
const x = before.resolve(p, combo).css;
|
|
2376
|
-
const y = after.resolve(p, combo).css;
|
|
2377
|
-
if (x === y) continue;
|
|
2378
|
-
const e = byPath.get(p);
|
|
2379
|
-
if (e) e.modes.push(comboKey(combo));
|
|
2380
|
-
else byPath.set(p, { path: p, modes: [comboKey(combo)], before: x, after: y });
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
const all = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
2384
|
-
return { total: all.length, entries: all.slice(0, limit) };
|
|
2385
|
-
}
|
|
2386
|
-
const toChangeSummary = (c) => ({
|
|
2387
|
-
kind: c.kind,
|
|
2388
|
-
path: c.path,
|
|
2389
|
-
...c.kind === "changed" ? { descriptionOnly: c.fields.length > 0 && c.fields.every((f) => f === "description") } : {}
|
|
2390
|
-
});
|
|
2391
|
-
function classifyLayered(changes, otherFiles = [], config) {
|
|
2392
|
-
const r = classifyRisk({ tokenChanges: changes.map(toChangeSummary), files: otherFiles.map((path) => ({ path })), config });
|
|
2393
|
-
return { tier: r.tier, bump: r.bump ?? "patch", reasons: r.reasons.length ? r.reasons : ["No token or guardrail change"], label: r.label };
|
|
2394
|
-
}
|
|
2395
|
-
const short = (v) => {
|
|
2396
|
-
if (v === void 0) return "";
|
|
2397
|
-
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
2398
|
-
return s.length > 60 ? `${s.slice(0, 57)}...` : s;
|
|
2399
|
-
};
|
|
2400
|
-
const cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2401
|
-
const swatch = (css) => css ? `\`${cell(css)}\`` : "";
|
|
2402
|
-
const aliasNote = (v) => typeof v === "string" && v.startsWith("{") ? ` (${cell(short(v))})` : "";
|
|
2403
|
-
function changeTable(changes) {
|
|
2404
|
-
if (!changes.length) return "_No token changes._";
|
|
2405
|
-
const rows = changes.map((c) => {
|
|
2406
|
-
const what = c.kind === "renamed" ? `renamed from \`${c.from}\`` : c.kind === "changed" ? c.fields.join(", ") : c.kind;
|
|
2407
|
-
const before = c.kind === "added" || c.kind === "renamed" ? "" : `${swatch(c.beforeCss)}${aliasNote(c.before)}`;
|
|
2408
|
-
const after = c.kind === "removed" ? "" : `${swatch(c.afterCss)}${aliasNote(c.after)}`;
|
|
2409
|
-
return `| \`${c.path}\` | ${what} | ${before} | ${after} | ${c.file} |`;
|
|
2410
|
-
});
|
|
2411
|
-
return ["| Token | Change | Before | After | File |", "| --- | --- | --- | --- | --- |", ...rows].join("\n");
|
|
2412
|
-
}
|
|
2413
|
-
function contrastDeltaTable(deltas, limit = 40) {
|
|
2414
|
-
if (!deltas.length) return "_No contrast pair moved._";
|
|
2415
|
-
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**"} |`);
|
|
2416
|
-
const more = deltas.length > limit ? `
|
|
2417
|
-
|
|
2418
|
-
_${deltas.length - limit} more pair(s) not shown._` : "";
|
|
2419
|
-
return ["| Pair | Mode | Before | After | Min | Result |", "| --- | --- | --- | --- | --- | --- |", ...rows].join("\n") + more;
|
|
2420
|
-
}
|
|
2421
|
-
function composeProposalBody(args) {
|
|
2422
|
-
const t = args.tier.tier;
|
|
2423
|
-
const box = (n) => `- ${n === t ? "[x]" : "[ ]"} ${RISK_LABELS[n]}`;
|
|
2424
|
-
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" : ""})`);
|
|
2425
|
-
return [
|
|
2426
|
-
"## What changed and why",
|
|
2427
|
-
"",
|
|
2428
|
-
args.summary.trim(),
|
|
2429
|
-
"",
|
|
2430
|
-
`Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
|
|
2431
|
-
"",
|
|
2432
|
-
"## Risk tier",
|
|
2433
|
-
"",
|
|
2434
|
-
box(1),
|
|
2435
|
-
box(2),
|
|
2436
|
-
box(3),
|
|
2437
|
-
"",
|
|
2438
|
-
...args.tier.reasons.map((r) => `- ${r}`),
|
|
2439
|
-
`- Semver bump: \`${args.tier.bump}\``,
|
|
2440
|
-
"",
|
|
2441
|
-
"## Token changes",
|
|
2442
|
-
"",
|
|
2443
|
-
changeTable(args.changes),
|
|
2444
|
-
"",
|
|
2445
|
-
"## Contrast before and after",
|
|
2446
|
-
"",
|
|
2447
|
-
contrastDeltaTable(args.contrast),
|
|
2448
|
-
"",
|
|
2449
|
-
"## Downstream impact",
|
|
2450
|
-
"",
|
|
2451
|
-
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._",
|
|
2452
|
-
"",
|
|
2453
|
-
"## Checks",
|
|
2454
|
-
"",
|
|
2455
|
-
`- Token validation: ${args.validationErrors ? `**${args.validationErrors} error(s)**` : "no errors"} (DTCG schema, aliases, light and dark parity)`,
|
|
2456
|
-
`- Contrast gate: ${args.contrast.some((d) => !d.passAfter) ? "**failing pairs, see table**" : "all pairs pass"}`,
|
|
2457
|
-
"",
|
|
2458
|
-
"## Authored by",
|
|
2459
|
-
"",
|
|
2460
|
-
"- [ ] Human",
|
|
2461
|
-
"- [x] AI agent (Claude), reviewed by a human before merge",
|
|
2462
|
-
"",
|
|
2463
|
-
"## Design review checklist",
|
|
2464
|
-
"",
|
|
2465
|
-
"- [ ] Checked in light and dark themes and in every brand",
|
|
2466
|
-
"- [ ] Contrast table reviewed, no pair loosened",
|
|
2467
|
-
"- [ ] Changeset bump matches the tier",
|
|
2468
|
-
""
|
|
2469
|
-
].join("\n");
|
|
2470
|
-
}
|
|
2471
|
-
const DEFAULT_PROTECTED_BRANCHES = ["main", "master"];
|
|
2472
|
-
class ProtectedBranchError extends Error {
|
|
2473
|
-
constructor(message) {
|
|
2474
|
-
super(message);
|
|
2475
|
-
this.name = "ProtectedBranchError";
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
function assertSafeBranch(branch, rules = {}) {
|
|
2479
|
-
const name = branch.replace(/^refs\/heads\//, "").trim();
|
|
2480
|
-
const blocked = /* @__PURE__ */ new Set([...rules.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES, ...rules.base ? [rules.base] : []]);
|
|
2481
|
-
if (!name || blocked.has(name)) {
|
|
2482
|
-
throw new ProtectedBranchError(`Refusing to write to protected branch "${name}". Bridge only creates new branches and pull requests.`);
|
|
2483
|
-
}
|
|
2484
|
-
if (rules.branchPrefix && !name.startsWith(rules.branchPrefix)) {
|
|
2485
|
-
throw new ProtectedBranchError(`Branch "${name}" must start with "${rules.branchPrefix}".`);
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
const BULK_LIMIT = 60;
|
|
2489
|
-
const UNRELEASED = "Unreleased";
|
|
2490
|
-
const SUBJECT_PR = [/^Merge pull request #(\d+)/, /\(#(\d+)\)\s*$/];
|
|
2491
|
-
function prNumberFromSubject(subject) {
|
|
2492
|
-
for (const re of SUBJECT_PR) {
|
|
2493
|
-
const m = re.exec(subject);
|
|
2494
|
-
if (m) return Number(m[1]);
|
|
2495
|
-
}
|
|
2496
|
-
return void 0;
|
|
2497
|
-
}
|
|
2498
|
-
function parseChangesetText(path, text) {
|
|
2499
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
|
|
2500
|
-
if (!m) return null;
|
|
2501
|
-
const bumps = [...m[1].matchAll(/["']?([^"'\s:]+)["']?\s*:\s*(major|minor|patch)/g)].map((x) => ({ pkg: x[1], bump: x[2] }));
|
|
2502
|
-
const order = ["major", "minor", "patch"];
|
|
2503
|
-
const bump = order.find((b) => bumps.some((x) => x.bump === b));
|
|
2504
|
-
const summary = m[2].trim().split(/\r?\n\r?\n/)[0].replace(/\s+/g, " ").trim();
|
|
2505
|
-
return { path, ...bump ? { bump } : {}, packages: bumps.map((b) => b.pkg), summary };
|
|
2506
|
-
}
|
|
2507
|
-
function firstParagraph(text) {
|
|
2508
|
-
if (!text) return "";
|
|
2509
|
-
const cleaned = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
2510
|
-
for (const para of cleaned.split(/\r?\n\s*\r?\n/)) {
|
|
2511
|
-
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));
|
|
2512
|
-
const s = lines.join(" ").replace(/\s+/g, " ").trim();
|
|
2513
|
-
if (s && !/^\|/.test(s)) return s.length > 400 ? `${s.slice(0, 397)}...` : s;
|
|
2514
|
-
}
|
|
2515
|
-
return "";
|
|
2516
|
-
}
|
|
2517
|
-
function countChanges(changes) {
|
|
2518
|
-
const c = { added: 0, changed: 0, removed: 0, renamed: 0, total: changes.length };
|
|
2519
|
-
for (const x of changes) c[x.kind]++;
|
|
2520
|
-
return c;
|
|
2521
|
-
}
|
|
2522
|
-
const shortSha = (sha) => sha.slice(0, 7);
|
|
2523
|
-
function titleOf(commit) {
|
|
2524
|
-
if (commit.pr?.title) return commit.pr.title;
|
|
2525
|
-
if (/^Merge pull request #\d+/.test(commit.subject)) {
|
|
2526
|
-
const first = (commit.body ?? "").split(/\r?\n/).find((l) => l.trim());
|
|
2527
|
-
if (first) return first.trim();
|
|
2528
|
-
}
|
|
2529
|
-
return commit.subject;
|
|
2530
|
-
}
|
|
2531
|
-
function buildHistoryEntry(input) {
|
|
2532
|
-
const { config, commit } = input;
|
|
2533
|
-
const beforeSet = new LayeredTokenSet(config, input.before);
|
|
2534
|
-
const afterSet = new LayeredTokenSet(config, input.after);
|
|
2535
|
-
const changes = diffLayered(beforeSet, afterSet);
|
|
2536
|
-
if (!changes.length) return null;
|
|
2537
|
-
const tier = classifyLayered(changes, [], config);
|
|
2538
|
-
const changesets = (input.changesets ?? []).map((c) => parseChangesetText(c.path, c.text)).filter((c) => c !== null);
|
|
2539
|
-
const pkg = config.changesets?.package;
|
|
2540
|
-
const relevant = changesets.filter((c) => !pkg || !c.packages.length || c.packages.includes(pkg));
|
|
2541
|
-
const title = titleOf(commit);
|
|
2542
|
-
const why = relevant.map((c) => c.summary).find(Boolean) || firstParagraph(commit.pr?.body) || firstParagraph(commit.body) || title;
|
|
2543
|
-
const prNumber = commit.pr?.number ?? prNumberFromSubject(commit.subject);
|
|
2544
|
-
const pr = commit.pr ?? (prNumber ? { number: prNumber, title, ...config.repo ? { url: `https://github.com/${config.repo.owner}/${config.repo.name}/pull/${prNumber}` } : {} } : void 0);
|
|
2545
|
-
const changesetBump2 = relevant.map((c) => c.bump).find((b) => b === "major");
|
|
2546
|
-
const bump = changesetBump2 ?? tier.bump;
|
|
2547
|
-
return {
|
|
2548
|
-
id: pr ? `pr-${pr.number}` : shortSha(commit.sha),
|
|
2549
|
-
sha: commit.sha,
|
|
2550
|
-
...commit.parents[0] ? { baseSha: commit.parents[0] } : {},
|
|
2551
|
-
date: commit.date,
|
|
2552
|
-
author: commit.pr?.author ?? commit.author,
|
|
2553
|
-
title,
|
|
2554
|
-
why,
|
|
2555
|
-
...pr ? { pr } : {},
|
|
2556
|
-
changes,
|
|
2557
|
-
counts: countChanges(changes),
|
|
2558
|
-
tier: tier.tier,
|
|
2559
|
-
bump,
|
|
2560
|
-
breaking: bump === "major",
|
|
2561
|
-
reasons: tier.reasons,
|
|
2562
|
-
changesets: relevant,
|
|
2563
|
-
...changes.length > BULK_LIMIT ? { bulk: true } : {},
|
|
2564
|
-
...input.demo ? { demo: true } : {}
|
|
2565
|
-
};
|
|
2566
|
-
}
|
|
2567
|
-
async function collectHistory(config, backend, opts) {
|
|
2568
|
-
const root = config.tokens.root.replace(/\/$/, "");
|
|
2569
|
-
const commits = await backend.listCommits({ ref: opts.ref, paths: [root === "." ? "" : root].filter(Boolean), limit: opts.limit ?? 40 });
|
|
2570
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2571
|
-
const picked = [];
|
|
2572
|
-
for (const c of commits) {
|
|
2573
|
-
const n = c.pr?.number;
|
|
2574
|
-
if (n !== void 0) {
|
|
2575
|
-
if (seen.has(n)) continue;
|
|
2576
|
-
seen.add(n);
|
|
2577
|
-
}
|
|
2578
|
-
picked.push(c);
|
|
2579
|
-
}
|
|
2580
|
-
const entries = [];
|
|
2581
|
-
let done = 0;
|
|
2582
|
-
for (const c of picked) {
|
|
2583
|
-
const parent = c.parents[0];
|
|
2584
|
-
const [before, after] = await Promise.all([parent ? backend.tokenFiles(parent) : Promise.resolve(/* @__PURE__ */ new Map()), backend.tokenFiles(c.sha)]);
|
|
2585
|
-
let changesets = [];
|
|
2586
|
-
if (backend.changesetPaths && backend.readFile && config.changesets) {
|
|
2587
|
-
const [was, now] = await Promise.all([parent ? backend.changesetPaths(parent) : Promise.resolve([]), backend.changesetPaths(c.sha)]);
|
|
2588
|
-
const added = now.filter((p) => !was.includes(p) && p.endsWith(".md") && !/README\.md$/i.test(p));
|
|
2589
|
-
changesets = (await Promise.all(added.map(async (p) => ({ path: p, text: await backend.readFile(c.sha, p) ?? "" })))).filter((x) => x.text);
|
|
2590
|
-
}
|
|
2591
|
-
const e = buildHistoryEntry({ config, commit: c, before, after, changesets });
|
|
2592
|
-
if (e) entries.push(e);
|
|
2593
|
-
opts.onProgress?.(++done, picked.length);
|
|
2594
|
-
}
|
|
2595
|
-
return entries;
|
|
2596
|
-
}
|
|
2597
|
-
function parseChangelog(text) {
|
|
2598
|
-
const out = [];
|
|
2599
|
-
const parts = text.split(/^##\s+/m).slice(1);
|
|
2600
|
-
for (const p of parts) {
|
|
2601
|
-
const [head, ...rest] = p.split(/\r?\n/);
|
|
2602
|
-
const version = /v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(head ?? "")?.[1];
|
|
2603
|
-
if (!version) continue;
|
|
2604
|
-
const body = rest.join("\n");
|
|
2605
|
-
const shas = [...new Set([...body.matchAll(/(?:^|[\s[`(])([0-9a-f]{7,40})(?=[\s`\]):]|$)/gm)].map((m) => m[1]))];
|
|
2606
|
-
const prs = [...new Set([...body.matchAll(/(?:#|\/pull\/)(\d+)/g)].map((m) => Number(m[1])))];
|
|
2607
|
-
const date = /(\d{4}-\d{2}-\d{2})/.exec(head ?? "")?.[1];
|
|
2608
|
-
out.push({ version, ...date ? { date } : {}, shas, prs });
|
|
2609
|
-
}
|
|
2610
|
-
return out;
|
|
2611
|
-
}
|
|
2612
|
-
function releasesFromTags(tags, pkg) {
|
|
2613
|
-
const out = [];
|
|
2614
|
-
for (const t of tags) {
|
|
2615
|
-
const m = /^(?:(@?[^@\s]+)@)?v?(\d+\.\d+\.\d+(?:-[\w.]+)?)$/.exec(t.name);
|
|
2616
|
-
if (!m) continue;
|
|
2617
|
-
if (pkg && m[1] && m[1] !== pkg) continue;
|
|
2618
|
-
out.push({ version: m[2], ...t.sha ? { sha: t.sha } : {}, ...t.date ? { date: t.date } : {} });
|
|
2619
|
-
}
|
|
2620
|
-
return out;
|
|
2621
|
-
}
|
|
2622
|
-
function assignVersions(entries, releases) {
|
|
2623
|
-
const dated = releases.filter((r) => r.date).sort((a, b) => Date.parse(a.date) - Date.parse(b.date));
|
|
2624
|
-
return entries.map((e) => {
|
|
2625
|
-
const cited = releases.find((r) => r.shas?.some((s) => e.sha.startsWith(s)) || e.pr && r.prs?.includes(e.pr.number));
|
|
2626
|
-
const byTag = cited ? void 0 : dated.find((r) => r.sha === e.sha || Date.parse(r.date) >= Date.parse(e.date));
|
|
2627
|
-
return { ...e, version: (cited ?? byTag)?.version ?? UNRELEASED };
|
|
2628
|
-
});
|
|
2629
|
-
}
|
|
2630
|
-
const BUMP_RANK = { patch: 0, minor: 1, major: 2 };
|
|
2631
|
-
function versionTimeline(entries, releases = []) {
|
|
2632
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2633
|
-
for (const e of entries) {
|
|
2634
|
-
const v = e.version ?? UNRELEASED;
|
|
2635
|
-
let g = groups.get(v);
|
|
2636
|
-
if (!g) {
|
|
2637
|
-
const rel = releases.find((r) => r.version === v);
|
|
2638
|
-
g = { version: v, pending: v === UNRELEASED, date: rel?.date ?? e.date, entries: [], prs: [], tokenChanges: 0, breaking: false, bump: "patch", authors: [] };
|
|
2639
|
-
groups.set(v, g);
|
|
2640
|
-
}
|
|
2641
|
-
g.entries.push(e.id);
|
|
2642
|
-
if (e.pr && !g.prs.includes(e.pr.number)) g.prs.push(e.pr.number);
|
|
2643
|
-
g.tokenChanges += e.counts.total;
|
|
2644
|
-
g.breaking ||= e.breaking;
|
|
2645
|
-
if (BUMP_RANK[e.bump] > BUMP_RANK[g.bump]) g.bump = e.bump;
|
|
2646
|
-
if (!g.authors.includes(e.author)) g.authors.push(e.author);
|
|
2647
|
-
if (!releases.some((r) => r.version === v && r.date) && Date.parse(e.date) > Date.parse(g.date)) g.date = e.date;
|
|
2648
|
-
}
|
|
2649
|
-
return [...groups.values()].sort((a, b) => a.pending ? -1 : b.pending ? 1 : Date.parse(b.date) - Date.parse(a.date));
|
|
2650
|
-
}
|
|
2651
|
-
const matchesToken = (path, q) => path === q || path.startsWith(`${q.replace(/\.$/, "")}.`);
|
|
2652
|
-
function filterHistory(entries, f = {}) {
|
|
2653
|
-
const q = f.query?.trim().toLowerCase();
|
|
2654
|
-
return entries.filter(
|
|
2655
|
-
(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))
|
|
2656
|
-
);
|
|
2657
|
-
}
|
|
2658
|
-
function fileModes(config, file) {
|
|
2659
|
-
for (const layer of config.tokens.layers) {
|
|
2660
|
-
const keys = [];
|
|
2661
|
-
const marked = layer.files.replace(/\{([^}]+)\}/g, (_, k) => {
|
|
2662
|
-
keys.push(k);
|
|
2663
|
-
return "";
|
|
2664
|
-
});
|
|
2665
|
-
const source = globToRegExp(marked).source.replace(/\u0001/g, "([^/.]+)");
|
|
2666
|
-
const m = new RegExp(source).exec(file);
|
|
2667
|
-
if (m) return Object.fromEntries(keys.map((k, i) => [k, m[i + 1]]));
|
|
2668
|
-
}
|
|
2669
|
-
return {};
|
|
2670
|
-
}
|
|
2671
|
-
function modeLabel(modes) {
|
|
2672
|
-
const v = Object.values(modes).filter(Boolean);
|
|
2673
|
-
if (!v.length) return "All modes";
|
|
2674
|
-
const s = v.join(" ");
|
|
2675
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2676
|
-
}
|
|
2677
|
-
function tokenHistory(entries, path, config) {
|
|
2678
|
-
const names = /* @__PURE__ */ new Set([path]);
|
|
2679
|
-
const out = [];
|
|
2680
|
-
const sorted = [...entries].sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
|
2681
|
-
for (const e of sorted) {
|
|
2682
|
-
const hits = e.changes.filter((c) => names.has(c.path));
|
|
2683
|
-
for (const c of hits) {
|
|
2684
|
-
const modes = config ? fileModes(config, c.file) : {};
|
|
2685
|
-
out.push({
|
|
2686
|
-
entryId: e.id,
|
|
2687
|
-
sha: e.sha,
|
|
2688
|
-
date: e.date,
|
|
2689
|
-
author: e.author,
|
|
2690
|
-
title: e.title,
|
|
2691
|
-
why: e.why,
|
|
2692
|
-
...e.pr ? { pr: e.pr } : {},
|
|
2693
|
-
...e.version ? { version: e.version } : {},
|
|
2694
|
-
...e.demo ? { demo: true } : {},
|
|
2695
|
-
kind: c.kind,
|
|
2696
|
-
path: c.path,
|
|
2697
|
-
...c.from ? { from: c.from } : {},
|
|
2698
|
-
file: c.file,
|
|
2699
|
-
modes,
|
|
2700
|
-
modeLabel: modeLabel(modes),
|
|
2701
|
-
fields: c.fields,
|
|
2702
|
-
...c.before !== void 0 ? { before: c.before } : {},
|
|
2703
|
-
...c.after !== void 0 ? { after: c.after } : {},
|
|
2704
|
-
...c.beforeCss !== void 0 ? { beforeCss: c.beforeCss } : {},
|
|
2705
|
-
...c.afterCss !== void 0 ? { afterCss: c.afterCss } : {},
|
|
2706
|
-
...c.descriptionBefore !== void 0 ? { descriptionBefore: c.descriptionBefore } : {},
|
|
2707
|
-
...c.descriptionAfter !== void 0 ? { descriptionAfter: c.descriptionAfter } : {}
|
|
2708
|
-
});
|
|
2709
|
-
}
|
|
2710
|
-
for (const c of hits) if (c.kind === "renamed" && c.from) names.add(c.from);
|
|
2711
|
-
}
|
|
2712
|
-
return out;
|
|
2713
|
-
}
|
|
2714
|
-
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2715
|
-
const keyOf = (path, file) => `${path}@${file}`;
|
|
2716
|
-
const show = (v) => typeof v === "string" ? v : JSON.stringify(v);
|
|
2717
|
-
function defIn(set, path, file) {
|
|
2718
|
-
return set.defs.find((d) => d.path === path && d.file === file);
|
|
2719
|
-
}
|
|
2720
|
-
function sharesCombo(set, a, b) {
|
|
2721
|
-
return a === b || set.combos().some((c) => {
|
|
2722
|
-
const files = set.layerFiles(c).files;
|
|
2723
|
-
return files.includes(a) && files.includes(b);
|
|
2724
|
-
});
|
|
2725
|
-
}
|
|
2726
|
-
function laterTouching(later, path, file, from) {
|
|
2727
|
-
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 } : {} }));
|
|
2728
|
-
}
|
|
2729
|
-
function planRevert(input) {
|
|
2730
|
-
const { current, entry } = input;
|
|
2731
|
-
const later = (input.later ?? []).filter((e) => e.id !== entry.id && Date.parse(e.date) >= Date.parse(entry.date));
|
|
2732
|
-
const res = input.resolutions ?? {};
|
|
2733
|
-
const planned = [];
|
|
2734
|
-
const conflicts = [];
|
|
2735
|
-
const skipped = [];
|
|
2736
|
-
const config = current.config;
|
|
2737
|
-
const css = (path, file) => current.resolve(path, current.comboForFile(file)).css;
|
|
2738
|
-
for (const c of entry.changes) {
|
|
2739
|
-
const key = keyOf(c.path, c.file);
|
|
2740
|
-
const label = modeLabel(fileModes(config, c.file));
|
|
2741
|
-
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 } : {} };
|
|
2742
|
-
const conflict = (reason, message, cur, editIfForced, summary) => {
|
|
2743
|
-
const resolution = res[key] ?? "keep-current";
|
|
2744
|
-
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 });
|
|
2745
|
-
if (resolution === "use-before" && editIfForced) planned.push({ key, path: c.path, file: c.file, summary: summary ?? `${editIfForced.op} ${c.path}`, edit: editIfForced });
|
|
2746
|
-
};
|
|
2747
|
-
const now = defIn(current, c.path, c.file);
|
|
2748
|
-
switch (c.kind) {
|
|
2749
|
-
case "changed": {
|
|
2750
|
-
if (!now) {
|
|
2751
|
-
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})`);
|
|
2752
|
-
break;
|
|
2753
|
-
}
|
|
2754
|
-
const valueField = c.fields.includes("value");
|
|
2755
|
-
const descField = c.fields.includes("description");
|
|
2756
|
-
const cur = { value: now.value, css: css(c.path, c.file) };
|
|
2757
|
-
const valueAt = (v) => same(now.value, v);
|
|
2758
|
-
const descNow = now.description ?? "";
|
|
2759
|
-
if (!valueField && !descField) {
|
|
2760
|
-
skipped.push({ key, path: c.path, file: c.file, reason: "Only the type or extensions changed: edit those by hand." });
|
|
2761
|
-
break;
|
|
2762
|
-
}
|
|
2763
|
-
if ((!valueField || valueAt(c.before)) && (!descField || descNow === (c.descriptionBefore ?? ""))) {
|
|
2764
|
-
skipped.push({ key, path: c.path, file: c.file, reason: "Already back to the value from before this change." });
|
|
2765
|
-
break;
|
|
2766
|
-
}
|
|
2767
|
-
const valueMoved = valueField && !valueAt(c.after) && !valueAt(c.before);
|
|
2768
|
-
const descMoved = descField && descNow !== (c.descriptionAfter ?? "") && descNow !== (c.descriptionBefore ?? "");
|
|
2769
|
-
const edit = {
|
|
2770
|
-
op: "set",
|
|
2771
|
-
path: c.path,
|
|
2772
|
-
file: c.file,
|
|
2773
|
-
...valueField ? { value: c.before } : {},
|
|
2774
|
-
...descField ? { description: c.descriptionBefore ?? "" } : {}
|
|
2775
|
-
};
|
|
2776
|
-
const summary = valueField ? `Set ${c.path} back to ${show(c.before)} (${label})` : `Put the old description of ${c.path} back (${label})`;
|
|
2777
|
-
if (valueMoved || descMoved) {
|
|
2778
|
-
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);
|
|
2779
|
-
break;
|
|
2780
|
-
}
|
|
2781
|
-
planned.push({ key, path: c.path, file: c.file, summary, edit });
|
|
2782
|
-
break;
|
|
2783
|
-
}
|
|
2784
|
-
case "added": {
|
|
2785
|
-
if (!now) {
|
|
2786
|
-
skipped.push({ key, path: c.path, file: c.file, reason: "Already removed." });
|
|
2787
|
-
break;
|
|
2788
|
-
}
|
|
2789
|
-
const users = current.referencedBy(c.path).filter((u) => u.path !== c.path && sharesCombo(current, u.file, c.file));
|
|
2790
|
-
const edit = { op: "remove", path: c.path, file: c.file };
|
|
2791
|
-
const summary = `Remove ${c.path} (${label})`;
|
|
2792
|
-
if (!same(now.value, c.after)) {
|
|
2793
|
-
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);
|
|
2794
|
-
break;
|
|
2795
|
-
}
|
|
2796
|
-
if (users.length) {
|
|
2797
|
-
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);
|
|
2798
|
-
break;
|
|
2799
|
-
}
|
|
2800
|
-
planned.push({ key, path: c.path, file: c.file, summary, edit });
|
|
2801
|
-
break;
|
|
2802
|
-
}
|
|
2803
|
-
case "removed": {
|
|
2804
|
-
const edit = { op: "add", path: c.path, file: c.file, value: c.before, ...c.type ? { type: c.type } : {}, ...c.descriptionBefore ? { description: c.descriptionBefore } : {} };
|
|
2805
|
-
const summary = `Add ${c.path} back with ${show(c.before)} (${label})`;
|
|
2806
|
-
if (now) {
|
|
2807
|
-
if (same(now.value, c.before)) skipped.push({ key, path: c.path, file: c.file, reason: "Already back." });
|
|
2808
|
-
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) });
|
|
2809
|
-
break;
|
|
2810
|
-
}
|
|
2811
|
-
planned.push({ key, path: c.path, file: c.file, summary, edit });
|
|
2812
|
-
break;
|
|
2813
|
-
}
|
|
2814
|
-
case "renamed": {
|
|
2815
|
-
const from = c.from;
|
|
2816
|
-
const back = defIn(current, from, c.file);
|
|
2817
|
-
if (back && !now) {
|
|
2818
|
-
skipped.push({ key, path: c.path, file: c.file, reason: `Already named ${from}.` });
|
|
2819
|
-
break;
|
|
2820
|
-
}
|
|
2821
|
-
if (!now) {
|
|
2822
|
-
conflict("renamed-later", `${c.path} no longer exists in ${c.file}: it was renamed or removed again later.`);
|
|
2823
|
-
break;
|
|
2824
|
-
}
|
|
2825
|
-
if (back) {
|
|
2826
|
-
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) });
|
|
2827
|
-
break;
|
|
2828
|
-
}
|
|
2829
|
-
if (planned.some((p) => p.edit.op === "rename" && p.path === c.path)) {
|
|
2830
|
-
skipped.push({ key, path: c.path, file: c.file, reason: "Covered by the rename above." });
|
|
2831
|
-
break;
|
|
2832
|
-
}
|
|
2833
|
-
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 } });
|
|
2834
|
-
break;
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
}
|
|
2838
|
-
const prLink = entry.pr ? entry.pr.url ? `[#${entry.pr.number}](${entry.pr.url})` : `#${entry.pr.number}` : `commit ${entry.sha.slice(0, 7)}`;
|
|
2839
|
-
const title = `Revert: ${entry.title}`;
|
|
2840
|
-
const lines = [
|
|
2841
|
-
`Reverts ${prLink} ("${entry.title}"), merged ${entry.date.slice(0, 10)} by ${entry.author}.`,
|
|
2842
|
-
"",
|
|
2843
|
-
`Original reason: ${entry.why}`,
|
|
2844
|
-
"",
|
|
2845
|
-
"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."
|
|
2846
|
-
];
|
|
2847
|
-
if (conflicts.length) {
|
|
2848
|
-
lines.push("", "### Conflicts", "", "| Token | File | Why | Decision |", "| --- | --- | --- | --- |");
|
|
2849
|
-
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"} |`);
|
|
2850
|
-
}
|
|
2851
|
-
if (skipped.length) lines.push("", `${skipped.length} change(s) were already undone on the base branch and are left alone.`);
|
|
2852
|
-
return finishPlan({ ...input, kind: "revert", title, prTitle: title, summary: lines.join("\n"), planned, conflicts, skipped, source: entry });
|
|
2853
|
-
}
|
|
2854
|
-
function planRestore(input) {
|
|
2855
|
-
const { current, path } = input;
|
|
2856
|
-
const files = [...new Set(current.defs.filter((d) => d.path === path).map((d) => d.file))];
|
|
2857
|
-
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.`);
|
|
2858
|
-
const file = input.file ?? (files.length === 1 ? files[0] : void 0);
|
|
2859
|
-
if (!file) throw new Error(`"${path}" is defined in ${files.join(", ")}. Pass the file (the mode) to restore.`);
|
|
2860
|
-
if (!files.includes(file)) throw new Error(`"${path}" is not defined in ${file}. It is defined in: ${files.join(", ")}.`);
|
|
2861
|
-
const label = modeLabel(fileModes(current.config, file));
|
|
2862
|
-
const from = input.label ? ` from ${input.label}` : "";
|
|
2863
|
-
const planned = [{ key: keyOf(path, file), path, file, summary: `Set ${path} to ${show(input.value)} (${label})`, edit: { op: "set", path, file, value: input.value } }];
|
|
2864
|
-
const title = `Restore ${path} (${label})${from}`;
|
|
2865
|
-
const item = input.item;
|
|
2866
|
-
const summary = [
|
|
2867
|
-
`Restores \`${path}\` in ${label} to ${show(input.value)}${from}.`,
|
|
2868
|
-
...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}.`] : []
|
|
2869
|
-
].join("\n");
|
|
2870
|
-
return finishPlan({ ...input, kind: "restore", title, prTitle: title, summary, planned, conflicts: [], skipped: [] });
|
|
2871
|
-
}
|
|
2872
|
-
const issueKey = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
2873
|
-
function finishPlan(p) {
|
|
2874
|
-
const { current } = p;
|
|
2875
|
-
const config = current.config;
|
|
2876
|
-
const edits = p.planned.map((x) => x.edit);
|
|
2877
|
-
const notes = [];
|
|
2878
|
-
const empty = classifyLayered([], [], config);
|
|
2879
|
-
const base = {
|
|
2880
|
-
kind: p.kind,
|
|
2881
|
-
title: p.title,
|
|
2882
|
-
prTitle: p.prTitle,
|
|
2883
|
-
summary: p.summary,
|
|
2884
|
-
...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 } } : {},
|
|
2885
|
-
planned: p.planned,
|
|
2886
|
-
edits,
|
|
2887
|
-
conflicts: p.conflicts,
|
|
2888
|
-
skipped: p.skipped,
|
|
2889
|
-
branch: `${config.governance.branchPrefix}${slugify(p.title, 48)}`
|
|
2890
|
-
};
|
|
2891
|
-
const nothing = (why) => ({ ...base, status: "nothing-to-do", why, changes: [], contrast: [], newFailures: [], newErrors: [], tier: empty, files: [], commitMessage: "", prBody: "", notes });
|
|
2892
|
-
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.");
|
|
2893
|
-
let result;
|
|
2894
|
-
try {
|
|
2895
|
-
result = applyTokenEdits(current, edits);
|
|
2896
|
-
} catch (e) {
|
|
2897
|
-
return { ...nothing(e.message), status: "blocked" };
|
|
2898
|
-
}
|
|
2899
|
-
notes.push(...result.notes);
|
|
2900
|
-
const after = new LayeredTokenSet(config, result.files);
|
|
2901
|
-
const changes = diffLayered(current, after);
|
|
2902
|
-
if (!changes.length) return nothing("The edits leave every token as it is now.");
|
|
2903
|
-
const prev = new Set(validateLayered(current).map(issueKey));
|
|
2904
|
-
const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prev.has(issueKey(i)));
|
|
2905
|
-
const pairs = p.pairs ?? [];
|
|
2906
|
-
const modes = config.contrast?.modes;
|
|
2907
|
-
const contrast = pairs.length ? contrastDelta(runLayeredContrast(current, pairs, modes), runLayeredContrast(after, pairs, modes)) : [];
|
|
2908
|
-
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
2909
|
-
const tier = classifyLayered(changes, [], config);
|
|
2910
|
-
const impact = layeredImpact(current, after, changes);
|
|
2911
|
-
const prBody = composeProposalBody({ summary: p.summary, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: p.tool ?? "Bridge", ...p.author ? { author: p.author } : {} });
|
|
2912
|
-
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;
|
|
2913
|
-
const files = [...result.touched.map((f) => ({ path: current.repoPath(f), content: result.files.get(f) })), ...changeset ? [changeset] : []];
|
|
2914
|
-
const commitMessage2 = `${p.kind === "revert" ? "revert(tokens)" : "fix(tokens)"}: ${p.title.replace(/^Revert: /, "")}
|
|
2915
|
-
|
|
2916
|
-
${p.source?.pr ? `Reverts #${p.source.pr.number}. ` : ""}Tier ${tier.tier}. Planned with ${p.tool ?? "Bridge"}.`;
|
|
2917
|
-
const blocked = newErrors.length || newFailures.length;
|
|
2918
|
-
return {
|
|
2919
|
-
...base,
|
|
2920
|
-
status: blocked ? "blocked" : "ready",
|
|
2921
|
-
...blocked ? { why: [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and ") } : {},
|
|
2922
|
-
changes,
|
|
2923
|
-
contrast,
|
|
2924
|
-
newFailures,
|
|
2925
|
-
newErrors,
|
|
2926
|
-
tier,
|
|
2927
|
-
files,
|
|
2928
|
-
...changeset ? { changeset } : {},
|
|
2929
|
-
commitMessage: commitMessage2,
|
|
2930
|
-
prBody,
|
|
2931
|
-
notes
|
|
2932
|
-
};
|
|
2933
|
-
}
|
|
2934
|
-
function planToJson(plan) {
|
|
2935
|
-
return {
|
|
2936
|
-
bridge: "change-plan",
|
|
2937
|
-
kind: plan.kind,
|
|
2938
|
-
status: plan.status,
|
|
2939
|
-
title: plan.prTitle,
|
|
2940
|
-
branch: plan.branch,
|
|
2941
|
-
tier: plan.tier.tier,
|
|
2942
|
-
bump: plan.tier.bump,
|
|
2943
|
-
edits: plan.edits,
|
|
2944
|
-
conflicts: plan.conflicts.map((c) => ({ key: c.key, reason: c.reason, message: c.message, resolution: c.resolution })),
|
|
2945
|
-
files: plan.files,
|
|
2946
|
-
commitMessage: plan.commitMessage,
|
|
2947
|
-
prBody: plan.prBody
|
|
2948
|
-
};
|
|
2949
|
-
}
|
|
2950
|
-
function packageDirCandidates(config) {
|
|
2951
|
-
const parts = config.tokens.root.replace(/\/$/, "").split("/").filter((p) => p && p !== ".");
|
|
2952
|
-
const out = [];
|
|
2953
|
-
for (let i = parts.length; i >= 0; i--) out.push(parts.slice(0, i).join("/"));
|
|
2954
|
-
return out;
|
|
2955
|
-
}
|
|
2956
|
-
function mergeReleases(changelog, tags) {
|
|
2957
|
-
const byVersion = /* @__PURE__ */ new Map();
|
|
2958
|
-
for (const r of [...changelog, ...tags]) {
|
|
2959
|
-
const prev = byVersion.get(r.version);
|
|
2960
|
-
byVersion.set(r.version, prev ? { ...r, ...prev, ...prev.date ? {} : r.date ? { date: r.date } : {}, ...prev.sha ? {} : r.sha ? { sha: r.sha } : {} } : r);
|
|
2961
|
-
}
|
|
2962
|
-
return [...byVersion.values()];
|
|
2963
|
-
}
|
|
2964
|
-
function buildReport(args) {
|
|
2965
|
-
const entries = assignVersions(args.entries, args.releases).sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
|
2966
|
-
return { ...args, entries, timeline: versionTimeline(entries, args.releases) };
|
|
2967
|
-
}
|
|
2968
|
-
const COMPONENT_SPECS_VERSION = 1;
|
|
2969
|
-
const DEFAULT_COMPONENT_SPECS_FILE = "packages/react/component-specs.json";
|
|
2970
|
-
const SPEC_FIELDS = [
|
|
2971
|
-
"fill",
|
|
2972
|
-
"stroke",
|
|
2973
|
-
"strokeWeight",
|
|
2974
|
-
"strokeTopWeight",
|
|
2975
|
-
"strokeRightWeight",
|
|
2976
|
-
"strokeBottomWeight",
|
|
2977
|
-
"strokeLeftWeight",
|
|
2978
|
-
"cornerRadius",
|
|
2979
|
-
"paddingTop",
|
|
2980
|
-
"paddingRight",
|
|
2981
|
-
"paddingBottom",
|
|
2982
|
-
"paddingLeft",
|
|
2983
|
-
"itemSpacing",
|
|
2984
|
-
"counterAxisSpacing",
|
|
2985
|
-
"width",
|
|
2986
|
-
"height",
|
|
2987
|
-
"minWidth",
|
|
2988
|
-
"minHeight",
|
|
2989
|
-
"opacity",
|
|
2990
|
-
"textFill",
|
|
2991
|
-
"fontFamily",
|
|
2992
|
-
"fontSize",
|
|
2993
|
-
"fontWeight",
|
|
2994
|
-
"lineHeight",
|
|
2995
|
-
"letterSpacing",
|
|
2996
|
-
"textDecoration"
|
|
2997
|
-
];
|
|
2998
|
-
const COLOR_FIELDS = /* @__PURE__ */ new Set(["fill", "stroke", "textFill"]);
|
|
2999
|
-
const TRIGGER_LABELS = {
|
|
3000
|
-
ON_HOVER: "While hovering",
|
|
3001
|
-
ON_PRESS: "While pressing",
|
|
3002
|
-
ON_CLICK: "On click",
|
|
3003
|
-
MOUSE_ENTER: "Mouse enter",
|
|
3004
|
-
MOUSE_LEAVE: "Mouse leave"
|
|
3005
|
-
};
|
|
3006
|
-
function coverageSummary(file) {
|
|
3007
|
-
const list2 = file.coverage ?? file.components.map((c) => ({ name: c.name, status: "generated" }));
|
|
3008
|
-
const generated = list2.filter((c) => c.status === "generated").length;
|
|
3009
|
-
const drafted = list2.filter((c) => c.status === "drafted").length;
|
|
3010
|
-
const layout = list2.filter((c) => c.status === "layout").length;
|
|
3011
|
-
return { generated, drafted, inFigma: generated + drafted, visual: list2.length - layout, layout, total: list2.length, missing: list2.filter((c) => c.status === "unsupported") };
|
|
3012
|
-
}
|
|
3013
|
-
const LAYOUT_DIRECTIONS = /* @__PURE__ */ new Set(["HORIZONTAL", "VERTICAL"]);
|
|
3014
|
-
const PRIMARY = /* @__PURE__ */ new Set(["MIN", "CENTER", "MAX", "SPACE_BETWEEN"]);
|
|
3015
|
-
const COUNTER = /* @__PURE__ */ new Set(["MIN", "CENTER", "MAX", "STRETCH"]);
|
|
3016
|
-
const PART_KINDS = /* @__PURE__ */ new Set(["frame", "text", "icon", "vector"]);
|
|
3017
|
-
function validateComponentSpec(spec, tokenTypes) {
|
|
3018
|
-
const out = [];
|
|
3019
|
-
const at = (where, why) => out.push(`${spec.name ?? "spec"}: ${where}: ${why}`);
|
|
3020
|
-
if (typeof spec.name !== "string" || !spec.name) return ["the spec has no name"];
|
|
3021
|
-
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`];
|
|
3022
|
-
const axisNames = /* @__PURE__ */ new Set();
|
|
3023
|
-
for (const a of spec.axes) {
|
|
3024
|
-
if (!a.name || axisNames.has(a.name)) at("axes", `"${a.name}" is missing or repeated`);
|
|
3025
|
-
axisNames.add(a.name);
|
|
3026
|
-
if (!Array.isArray(a.values) || !a.values.length) at(`axis ${a.name}`, "has no values");
|
|
3027
|
-
else if (!a.values.includes(a.default)) at(`axis ${a.name}`, `default "${a.default}" is not one of its values`);
|
|
3028
|
-
}
|
|
3029
|
-
const props = /* @__PURE__ */ new Map();
|
|
3030
|
-
for (const p of spec.properties) {
|
|
3031
|
-
if (props.has(p.name)) at("properties", `"${p.name}" is repeated`);
|
|
3032
|
-
props.set(p.name, p);
|
|
3033
|
-
if (p.type !== "TEXT" && p.type !== "BOOLEAN") at(`property ${p.name}`, `type ${String(p.type)} is not TEXT or BOOLEAN`);
|
|
3034
|
-
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"}`);
|
|
3035
|
-
}
|
|
3036
|
-
const parts = /* @__PURE__ */ new Map();
|
|
3037
|
-
for (const part of spec.parts) {
|
|
3038
|
-
if (!part.id || parts.has(part.id)) at("parts", `id "${part.id}" is missing or repeated`);
|
|
3039
|
-
if (!PART_KINDS.has(part.kind)) at(`part ${part.id}`, `kind ${String(part.kind)} is not frame, text, icon or vector`);
|
|
3040
|
-
if (part.parent === void 0) {
|
|
3041
|
-
if ([...parts.values()].some((x) => x.parent === void 0)) at(`part ${part.id}`, "a second root (every other part needs a parent)");
|
|
3042
|
-
} else if (!parts.has(part.parent)) at(`part ${part.id}`, `parent "${part.parent}" is not a part listed before it`);
|
|
3043
|
-
else if (parts.get(part.parent).kind !== "frame") at(`part ${part.id}`, `parent "${part.parent}" is not a frame`);
|
|
3044
|
-
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");
|
|
3045
|
-
if (part.text !== void 0 && props.get(part.text)?.type !== "TEXT") at(`part ${part.id}`, `text property "${part.text}" does not exist`);
|
|
3046
|
-
if (part.visible !== void 0 && props.get(part.visible)?.type !== "BOOLEAN") at(`part ${part.id}`, `visibility property "${part.visible}" does not exist`);
|
|
3047
|
-
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");
|
|
3048
|
-
parts.set(part.id, part);
|
|
3049
|
-
}
|
|
3050
|
-
if (!spec.parts.length || spec.parts[0].parent !== void 0) at("parts", "the first part must be the root");
|
|
3051
|
-
const expected = spec.axes.reduce((n, a) => n * (a.values?.length || 1), 1);
|
|
3052
|
-
if (spec.variants.length !== expected) at("variants", `${spec.variants.length} variants for ${expected} combinations of the axes`);
|
|
3053
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3054
|
-
for (const v of spec.variants) {
|
|
3055
|
-
const key = variantKeyOf(v.props ?? {});
|
|
3056
|
-
const where = `variant ${key}`;
|
|
3057
|
-
if (seen.has(key)) at(where, "is repeated");
|
|
3058
|
-
seen.add(key);
|
|
3059
|
-
for (const a of spec.axes) if (!a.values?.includes(v.props?.[a.name] ?? "")) at(where, `${a.name} is not one of its values`);
|
|
3060
|
-
for (const k of Object.keys(v.props ?? {})) if (!axisNames.has(k)) at(where, `${k} is not an axis`);
|
|
3061
|
-
for (const [id, fields] of Object.entries(v.values ?? {})) {
|
|
3062
|
-
const part = parts.get(id);
|
|
3063
|
-
if (!part) {
|
|
3064
|
-
at(where, `values for "${id}", which is not a part`);
|
|
3065
|
-
continue;
|
|
3066
|
-
}
|
|
3067
|
-
for (const [field, value] of Object.entries(fields ?? {})) {
|
|
3068
|
-
if (!SPEC_FIELDS.includes(field)) at(`${where}, ${id}`, `${field} is not a field`);
|
|
3069
|
-
else if (value === null) {
|
|
3070
|
-
if (!COLOR_FIELDS.has(field)) at(`${where}, ${id}`, `${field} cannot be empty (only colors can be none)`);
|
|
3071
|
-
} else if (typeof value === "number") {
|
|
3072
|
-
if (COLOR_FIELDS.has(field)) at(`${where}, ${id}`, `${field} cannot be a number`);
|
|
3073
|
-
} else if (typeof value !== "string") at(`${where}, ${id}`, `${field} must be a token path, a number or none`);
|
|
3074
|
-
else if (field === "textDecoration") {
|
|
3075
|
-
if (!/^(UNDERLINE|STRIKETHROUGH|NONE)$/.test(value)) at(`${where}, ${id}`, `${value} is not a text decoration`);
|
|
3076
|
-
} else if (tokenTypes) {
|
|
3077
|
-
for (const path of value.startsWith("calc(") ? (value.match(/[A-Za-z][\w.-]*/g) ?? []).filter((x) => x !== "calc" && !/^\d|^px$/.test(x)) : [value]) {
|
|
3078
|
-
const type = tokenTypes.get(path);
|
|
3079
|
-
if (!type) at(`${where}, ${id}`, `${path} is not a token`);
|
|
3080
|
-
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 ")}`);
|
|
3081
|
-
}
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
|
-
for (const id of v.hidden ?? []) if (!parts.has(id)) at(where, `hides "${id}", which is not a part`);
|
|
3086
|
-
for (const id of Object.keys(v.layout ?? {})) if (!parts.has(id)) at(where, `lays out "${id}", which is not a part`);
|
|
3087
|
-
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`);
|
|
3088
|
-
for (const id of Object.keys(v.slots ?? {})) if (!parts.has(id)) at(where, `shows a component in "${id}", which is not a part`);
|
|
3089
|
-
}
|
|
3090
|
-
for (const [i, x] of (spec.interactions ?? []).entries()) {
|
|
3091
|
-
const where = `interaction ${i + 1}`;
|
|
3092
|
-
const axis = spec.axes.find((a) => a.name === x.axis);
|
|
3093
|
-
if (!(x.trigger in TRIGGER_LABELS)) at(where, `trigger ${String(x.trigger)} is not one Figma has`);
|
|
3094
|
-
if (!axis) at(where, `${x.axis} is not an axis`);
|
|
3095
|
-
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}`);
|
|
3096
|
-
for (const k of x.variants ?? []) if (!seen.has(k)) at(where, `variant ${k} does not exist`);
|
|
3097
|
-
const t = x.transition;
|
|
3098
|
-
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");
|
|
3099
|
-
}
|
|
3100
|
-
return out;
|
|
3101
|
-
}
|
|
3102
|
-
function FIELD_TOKEN_TYPES(field) {
|
|
3103
|
-
if (COLOR_FIELDS.has(field)) return ["color"];
|
|
3104
|
-
if (field === "fontFamily") return ["fontFamily"];
|
|
3105
|
-
if (field === "fontWeight") return ["fontWeight", "number"];
|
|
3106
|
-
if (field === "opacity") return ["number"];
|
|
3107
|
-
return ["dimension", "number"];
|
|
3108
|
-
}
|
|
3109
|
-
const iconComponentName = (name) => `Icon/${name}`;
|
|
3110
|
-
function specHash(text) {
|
|
3111
|
-
let h = 2166136261;
|
|
3112
|
-
for (let i = 0; i < text.length; i++) {
|
|
3113
|
-
h ^= text.charCodeAt(i);
|
|
3114
|
-
h = Math.imul(h, 16777619);
|
|
3115
|
-
}
|
|
3116
|
-
return (h >>> 0).toString(16).padStart(8, "0");
|
|
3117
|
-
}
|
|
3118
|
-
function hashSpec(spec) {
|
|
3119
|
-
const { hash: _ignored, example: _example, ...rest } = spec;
|
|
3120
|
-
return specHash(JSON.stringify(rest));
|
|
3121
|
-
}
|
|
3122
|
-
const variantName = (props) => Object.entries(props).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
3123
|
-
const variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
|
|
3124
|
-
const REDUCED_MOTION_NOTE = "In code, reduced motion turns this off.";
|
|
3125
|
-
function componentMotion(spec) {
|
|
3126
|
-
const keys = new Set(spec.variants.map((v) => variantKeyOf(v.props)));
|
|
3127
|
-
const reactions = [];
|
|
3128
|
-
const triggers = [];
|
|
3129
|
-
for (const x of spec.interactions ?? []) {
|
|
3130
|
-
const only = x.variants ? new Set(x.variants) : void 0;
|
|
3131
|
-
let count = 0;
|
|
3132
|
-
for (const v of spec.variants) {
|
|
3133
|
-
if (v.props[x.axis] !== x.from) continue;
|
|
3134
|
-
const fromKey = variantKeyOf(v.props);
|
|
3135
|
-
if (only && !only.has(fromKey)) continue;
|
|
3136
|
-
const to = { ...v.props, [x.axis]: x.to };
|
|
3137
|
-
const toKey = variantKeyOf(to);
|
|
3138
|
-
if (!keys.has(toKey)) continue;
|
|
3139
|
-
reactions.push({ trigger: x.trigger, label: TRIGGER_LABELS[x.trigger], from: { ...v.props }, to, fromKey, toKey, transition: x.transition });
|
|
3140
|
-
count++;
|
|
3141
|
-
}
|
|
3142
|
-
triggers.push({ ...x, label: TRIGGER_LABELS[x.trigger], count });
|
|
3143
|
-
}
|
|
3144
|
-
const transitions = (spec.interactions ?? []).map((x) => x.transition).filter((t) => !!t);
|
|
3145
|
-
const cannotPlay = [...spec.motion ?? []];
|
|
3146
|
-
const openNote = cannotPlay.find((n) => n.enter || n.exit);
|
|
3147
|
-
const opening = openNote ? { enter: openNote.enter ?? null, exit: openNote.exit ?? null } : void 0;
|
|
3148
|
-
const openTransitions = opening ? [opening.enter, opening.exit].filter((t) => !!t) : [];
|
|
3149
|
-
const tokens = [
|
|
3150
|
-
...new Set([...transitions, ...openTransitions].flatMap((t) => [t.durationToken, t.easingToken]).concat(cannotPlay.map((n) => n.durationToken)).filter((t) => !!t))
|
|
3151
|
-
].sort();
|
|
3152
|
-
const properties = [...new Set(transitions.flatMap((t) => t.properties))].sort();
|
|
3153
|
-
const approximations = [...new Set(transitions.map((t) => t.approximated).filter((t) => !!t))];
|
|
3154
|
-
const animates = transitions.length > 0 || openTransitions.length > 0 || cannotPlay.some((n) => /animation/i.test(n.what));
|
|
3155
|
-
return { name: spec.name, reactions, triggers, tokens, properties, approximations, cannotPlay, opening, reducedMotion: animates ? REDUCED_MOTION_NOTE : void 0 };
|
|
3156
|
-
}
|
|
3157
|
-
function componentsUsingMotionToken(specs, tokenPath, chainOf) {
|
|
3158
|
-
const out = /* @__PURE__ */ new Set();
|
|
3159
|
-
for (const c of specs.components) {
|
|
3160
|
-
for (const t of componentMotion(c).tokens) {
|
|
3161
|
-
if (t === tokenPath || (chainOf?.(t) ?? []).includes(tokenPath)) out.add(c.name);
|
|
3162
|
-
}
|
|
3163
|
-
}
|
|
3164
|
-
return [...out].sort();
|
|
3165
|
-
}
|
|
3166
|
-
function parseComponentSpecs(text) {
|
|
3167
|
-
let data;
|
|
3168
|
-
try {
|
|
3169
|
-
data = JSON.parse(text);
|
|
3170
|
-
} catch {
|
|
3171
|
-
throw new Error("component-specs.json is not valid JSON.");
|
|
3172
|
-
}
|
|
3173
|
-
const f = data;
|
|
3174
|
-
if (!f || typeof f !== "object" || !Array.isArray(f.components)) throw new Error('component-specs.json has no "components" list.');
|
|
3175
|
-
if (f.version !== COMPONENT_SPECS_VERSION) throw new Error(`component-specs.json is version ${String(f.version)}; this tool reads version ${COMPONENT_SPECS_VERSION}.`);
|
|
3176
|
-
return f;
|
|
3177
|
-
}
|
|
3178
|
-
const tokenCssVar = (path, prefix = "--", separator = "-") => `${prefix}${path.split(".").join(separator)}`;
|
|
3179
|
-
function figmaCell(entry) {
|
|
3180
|
-
if (!entry) return void 0;
|
|
3181
|
-
if (entry.status === "generated") return { label: "Yes" };
|
|
3182
|
-
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." };
|
|
3183
|
-
if (entry.status === "layout") return { label: "Auto layout", title: "A layout helper: nothing to draw, use auto layout in Figma." };
|
|
3184
|
-
return { label: "Not yet", ...entry.reason ? { title: `Not supported yet: ${entry.reason}` } : {} };
|
|
3185
|
-
}
|
|
3186
2603
|
export {
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
2604
|
+
fromPlain as $,
|
|
2605
|
+
effectiveScopes as A,
|
|
2606
|
+
sameScopes as B,
|
|
2607
|
+
defaultScopes as C,
|
|
2608
|
+
readTokenAt as D,
|
|
2609
|
+
EXT_ID as E,
|
|
2610
|
+
modeFile as F,
|
|
2611
|
+
defaultFile as G,
|
|
2612
|
+
CONFIG_FILE as H,
|
|
2613
|
+
loadConfig as I,
|
|
2614
|
+
ConfigError as J,
|
|
2615
|
+
DEFAULT_TOKEN_PATHS as K,
|
|
2616
|
+
selectTokenFiles as L,
|
|
2617
|
+
LayeredTokenSet as M,
|
|
2618
|
+
resolveBranchPreviews as N,
|
|
2619
|
+
tokensPath as O,
|
|
2620
|
+
toConfigJson as P,
|
|
2621
|
+
validateLayered as Q,
|
|
2622
|
+
parseContrastChecks as R,
|
|
3206
2623
|
SINGLE_MODE as S,
|
|
3207
2624
|
TIER_NAMES as T,
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
titleOf as co,
|
|
3371
|
-
toChangeSummary as cp,
|
|
3372
|
-
toMap as cq,
|
|
3373
|
-
tokenBump as cr,
|
|
3374
|
-
tokenHistory as cs,
|
|
3375
|
-
tokenNode as ct,
|
|
3376
|
-
unreadableTeams as cu,
|
|
3377
|
-
valueIn as cv,
|
|
3378
|
-
variantName as cw,
|
|
3379
|
-
versionTimeline as cx,
|
|
3380
|
-
visualImpactProblems as cy,
|
|
3381
|
-
buildReport as d,
|
|
3382
|
-
prNumberFromSubject as e,
|
|
3383
|
-
variantKeyOf as f,
|
|
3384
|
-
layoutOptions as g,
|
|
3385
|
-
hashSpec as h,
|
|
3386
|
-
iconComponentName as i,
|
|
3387
|
-
buildTokenSet as j,
|
|
3388
|
-
defaultContext as k,
|
|
3389
|
-
loadConfig as l,
|
|
3390
|
-
mergeReleases as m,
|
|
3391
|
-
resolve as n,
|
|
3392
|
-
COMPONENT_SPECS_VERSION as o,
|
|
3393
|
-
packageDirCandidates as p,
|
|
3394
|
-
COLOR_FIELDS as q,
|
|
3395
|
-
releasesFromTags as r,
|
|
3396
|
-
specHash as s,
|
|
3397
|
-
tokenCssVar as t,
|
|
3398
|
-
modeDisplayName as u,
|
|
3399
|
-
validateComponentSpec as v,
|
|
3400
|
-
modesOf as w,
|
|
3401
|
-
isMultiMode as x,
|
|
3402
|
-
TIER_AXIS as y,
|
|
3403
|
-
findReferenceProblems as z
|
|
2625
|
+
classifyRisk as U,
|
|
2626
|
+
RISK_LABELS as V,
|
|
2627
|
+
parseJson as W,
|
|
2628
|
+
serializeJson as X,
|
|
2629
|
+
plainEqual as Y,
|
|
2630
|
+
toPlain as Z,
|
|
2631
|
+
getEntry as _,
|
|
2632
|
+
modesOf as a,
|
|
2633
|
+
entryLabel as a$,
|
|
2634
|
+
previewLinks as a0,
|
|
2635
|
+
slugify as a1,
|
|
2636
|
+
readPreviewComment as a2,
|
|
2637
|
+
findPreviewComment as a3,
|
|
2638
|
+
previewsSettled as a4,
|
|
2639
|
+
canShip as a5,
|
|
2640
|
+
whoCanShipSentence as a6,
|
|
2641
|
+
unreadableShipTeams as a7,
|
|
2642
|
+
layoutOptions as a8,
|
|
2643
|
+
buildTokenSet as a9,
|
|
2644
|
+
aliasOf as aA,
|
|
2645
|
+
appForProject as aB,
|
|
2646
|
+
appUrl as aC,
|
|
2647
|
+
approverEntries as aD,
|
|
2648
|
+
approverLists as aE,
|
|
2649
|
+
baseBranchOf as aF,
|
|
2650
|
+
branchName as aG,
|
|
2651
|
+
branchSlug as aH,
|
|
2652
|
+
changeKind as aI,
|
|
2653
|
+
changedModes as aJ,
|
|
2654
|
+
clearRawDeep as aK,
|
|
2655
|
+
colorToCss as aL,
|
|
2656
|
+
colorsEqual as aM,
|
|
2657
|
+
comboKey as aN,
|
|
2658
|
+
commitMessage as aO,
|
|
2659
|
+
compactJson as aP,
|
|
2660
|
+
composePrBody as aQ,
|
|
2661
|
+
composite as aR,
|
|
2662
|
+
contextFor as aS,
|
|
2663
|
+
contrastLevel as aT,
|
|
2664
|
+
contrastPair as aU,
|
|
2665
|
+
contrastRatio as aV,
|
|
2666
|
+
defaultContext as aW,
|
|
2667
|
+
defaultTitle as aX,
|
|
2668
|
+
defineConfig as aY,
|
|
2669
|
+
detectLayout as aZ,
|
|
2670
|
+
entriesFor as a_,
|
|
2671
|
+
productOfBranch as aa,
|
|
2672
|
+
defaultProduct as ab,
|
|
2673
|
+
AAA_MIN as ac,
|
|
2674
|
+
AA_MIN as ad,
|
|
2675
|
+
CONTRAST_CELL_MAX as ae,
|
|
2676
|
+
CONTRAST_CHECKS_FILE as af,
|
|
2677
|
+
DEFAULT_GOVERNANCE as ag,
|
|
2678
|
+
DEFAULT_OVERRIDE as ah,
|
|
2679
|
+
DEFAULT_PRODUCTS as ai,
|
|
2680
|
+
DEFAULT_RIGHTS as aj,
|
|
2681
|
+
DEFAULT_VISUAL_IMPACT as ak,
|
|
2682
|
+
DTCG_TYPES as al,
|
|
2683
|
+
JsonParseError as am,
|
|
2684
|
+
OWNER_GROUPS as an,
|
|
2685
|
+
PREVIEW_APP_LABELS as ao,
|
|
2686
|
+
PREVIEW_COMMENT_MARKER as ap,
|
|
2687
|
+
PREVIEW_PLACEHOLDERS as aq,
|
|
2688
|
+
PRIMITIVE_MODE as ar,
|
|
2689
|
+
PRODUCT_LABEL_PREFIX as as,
|
|
2690
|
+
RIGHTS as at,
|
|
2691
|
+
RIGHT_LABEL as au,
|
|
2692
|
+
RISK_TIER as av,
|
|
2693
|
+
TIER_ORDER as aw,
|
|
2694
|
+
TeamDirectory as ax,
|
|
2695
|
+
VISUAL_CHECK_LABEL as ay,
|
|
2696
|
+
affectedContrast as az,
|
|
2697
|
+
TIER_AXIS as b,
|
|
2698
|
+
valueToCss as b$,
|
|
2699
|
+
expandEntries as b0,
|
|
2700
|
+
expandOwners as b1,
|
|
2701
|
+
fieldChanges as b2,
|
|
2702
|
+
findProduct as b3,
|
|
2703
|
+
findToken as b4,
|
|
2704
|
+
findVercelComment as b5,
|
|
2705
|
+
flattenLayerFile as b6,
|
|
2706
|
+
formatNumber as b7,
|
|
2707
|
+
formatOwnerEntry as b8,
|
|
2708
|
+
getChild as b9,
|
|
2709
|
+
productForPlace as bA,
|
|
2710
|
+
productLabel as bB,
|
|
2711
|
+
productMarks as bC,
|
|
2712
|
+
productProblems as bD,
|
|
2713
|
+
productsOf as bE,
|
|
2714
|
+
projectOfLabel as bF,
|
|
2715
|
+
protectedBranchesOf as bG,
|
|
2716
|
+
renderedColor as bH,
|
|
2717
|
+
rgbaToDtcg as bI,
|
|
2718
|
+
rightsProblems as bJ,
|
|
2719
|
+
sameProducts as bK,
|
|
2720
|
+
sameToken as bL,
|
|
2721
|
+
shipPolicy as bM,
|
|
2722
|
+
snapshotUrl as bN,
|
|
2723
|
+
summarizeTokenChange as bO,
|
|
2724
|
+
tarballAssetName as bP,
|
|
2725
|
+
tarballUrl as bQ,
|
|
2726
|
+
teamKey as bR,
|
|
2727
|
+
teamKeyOf as bS,
|
|
2728
|
+
teamUnreadableMessage as bT,
|
|
2729
|
+
teamsIn as bU,
|
|
2730
|
+
toAlias as bV,
|
|
2731
|
+
toMap as bW,
|
|
2732
|
+
tokenBump as bX,
|
|
2733
|
+
tokenNode as bY,
|
|
2734
|
+
unreadableTeams as bZ,
|
|
2735
|
+
valueIn as b_,
|
|
2736
|
+
globToRegExp as ba,
|
|
2737
|
+
groupsPhrase as bb,
|
|
2738
|
+
hexToRgba as bc,
|
|
2739
|
+
holdersOf as bd,
|
|
2740
|
+
indexByPath as be,
|
|
2741
|
+
inheritedTypeAt as bf,
|
|
2742
|
+
installCommand as bg,
|
|
2743
|
+
installCommandFromPreviewComment as bh,
|
|
2744
|
+
isGuardrail as bi,
|
|
2745
|
+
isTeamEntry as bj,
|
|
2746
|
+
isTokenFile as bk,
|
|
2747
|
+
isVercelComment as bl,
|
|
2748
|
+
kindForMin as bm,
|
|
2749
|
+
layeredParityIssues as bn,
|
|
2750
|
+
luminance as bo,
|
|
2751
|
+
nodesEqual as bp,
|
|
2752
|
+
normalizeRights as bq,
|
|
2753
|
+
overrideKey as br,
|
|
2754
|
+
ownerEntryProblems as bs,
|
|
2755
|
+
pairKey as bt,
|
|
2756
|
+
parseDimension as bu,
|
|
2757
|
+
parseOwnerEntry as bv,
|
|
2758
|
+
parseVercelComment as bw,
|
|
2759
|
+
previewComment as bx,
|
|
2760
|
+
previewLinksOf as by,
|
|
2761
|
+
previewUrl as bz,
|
|
2762
|
+
collectRefs as c,
|
|
2763
|
+
visualImpactProblems as c0,
|
|
2764
|
+
contrastScope as d,
|
|
2765
|
+
evaluatePairs as e,
|
|
2766
|
+
findReferenceProblems as f,
|
|
2767
|
+
contextLabel as g,
|
|
2768
|
+
isAlias as h,
|
|
2769
|
+
isMultiMode as i,
|
|
2770
|
+
aliasTarget as j,
|
|
2771
|
+
flattenTokens as k,
|
|
2772
|
+
emptyTokenFile as l,
|
|
2773
|
+
modeDisplayName as m,
|
|
2774
|
+
parseTokenFile as n,
|
|
2775
|
+
EXT_SCOPES as o,
|
|
2776
|
+
pathId as p,
|
|
2777
|
+
parseColor as q,
|
|
2778
|
+
removeToken as r,
|
|
2779
|
+
serializeTokenFile as s,
|
|
2780
|
+
threeWayDiff as t,
|
|
2781
|
+
upsertToken as u,
|
|
2782
|
+
valuesEqual as v,
|
|
2783
|
+
rgbaToHex as w,
|
|
2784
|
+
formatValue as x,
|
|
2785
|
+
resolve as y,
|
|
2786
|
+
moveToken as z
|
|
3404
2787
|
};
|
|
3405
|
-
//# sourceMappingURL=
|
|
2788
|
+
//# sourceMappingURL=pr-composer-D2SZm7cg.js.map
|