@bridge-ds/core 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/bridge.config.schema.json +23 -0
- 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/config.d.ts +28 -0
- package/dist/docs-proposal.d.ts +58 -10
- package/dist/figma-push-github.d.ts +143 -0
- package/dist/figma-push.d.ts +204 -0
- package/dist/github-client.d.ts +15 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1569 -3179
- package/dist/index.js.map +1 -1
- package/dist/node/git-local.d.ts +15 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +201 -10
- package/dist/node/index.js.map +1 -1
- package/dist/node/suggestion-apply-node.d.ts +69 -0
- package/dist/{component-spec-SKuywZqr.js → prod-branch-BdMPKffE.js} +1134 -1438
- package/dist/prod-branch-BdMPKffE.js.map +1 -0
- package/dist/prod-branch-github.d.ts +160 -0
- package/dist/prod-branch.d.ts +267 -0
- package/dist/{report-iPSnsjYi.js → report-DOnKUw8G.js} +24 -24
- package/dist/{report-iPSnsjYi.js.map → report-DOnKUw8G.js.map} +1 -1
- package/dist/suggestion-apply-ByBEYtct.js +4802 -0
- package/dist/suggestion-apply-ByBEYtct.js.map +1 -0
- package/dist/suggestion-apply.d.ts +150 -0
- package/dist/suggestion-review.d.ts +70 -0
- package/dist/suggestions-github.d.ts +131 -0
- package/dist/suggestions.d.ts +236 -0
- package/dist/token-proposal.d.ts +123 -0
- package/package.json +1 -1
- package/dist/component-spec-SKuywZqr.js.map +0 -1
- package/dist/layered-BnbmToGs.js +0 -500
- package/dist/layered-BnbmToGs.js.map +0 -1
|
@@ -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) {
|
|
@@ -543,8 +1062,8 @@ function groupsPhrase(groups) {
|
|
|
543
1062
|
return `a ${g.join(" or ")} owner`;
|
|
544
1063
|
}
|
|
545
1064
|
function whoCanShipSentence(policy) {
|
|
546
|
-
const
|
|
547
|
-
return `Only ${groupsPhrase(policy.groups)} can merge this${
|
|
1065
|
+
const names2 = (policy.entries ?? policy.logins).map((l) => entryLabel(l, policy.teams)).join(", ");
|
|
1066
|
+
return `Only ${groupsPhrase(policy.groups)} can merge this${names2 ? ` (${names2})` : ""}.`;
|
|
548
1067
|
}
|
|
549
1068
|
const DEFAULT_VISUAL_IMPACT = { mode: "auto" };
|
|
550
1069
|
const VISUAL_CHECK_LABEL = "visual-check";
|
|
@@ -656,6 +1175,8 @@ function sameProducts(a, b) {
|
|
|
656
1175
|
return norm2(a) === norm2(b);
|
|
657
1176
|
}
|
|
658
1177
|
const CONFIG_FILE = "bridge.config.json";
|
|
1178
|
+
const CHANGES_MODES = ["suggest", "direct"];
|
|
1179
|
+
const DEFAULT_CHANGES_MODE = "suggest";
|
|
659
1180
|
const DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
|
|
660
1181
|
const DEFAULT_TOKEN_PATHS = {
|
|
661
1182
|
root: "tokens",
|
|
@@ -795,6 +1316,8 @@ function defineConfig(input) {
|
|
|
795
1316
|
...g.rights ? { rights: normalizeRights(g.rights) } : {}
|
|
796
1317
|
},
|
|
797
1318
|
products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
|
|
1319
|
+
changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
|
|
1320
|
+
...input.storybook?.url ? { storybook: { url: input.storybook.url.trim().replace(/\/+$/, "") } } : {},
|
|
798
1321
|
...input.audit ? { audit: { ...input.audit } } : {},
|
|
799
1322
|
...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
|
|
800
1323
|
...input.integrations ? { integrations: { ...input.integrations } } : {},
|
|
@@ -917,6 +1440,13 @@ function loadConfig(source) {
|
|
|
917
1440
|
}
|
|
918
1441
|
}
|
|
919
1442
|
problems.push(...visualImpactProblems(d.visualImpact));
|
|
1443
|
+
if (d.changes !== void 0 && !CHANGES_MODES.includes(d.changes)) problems.push(`"changes" must be "suggest" or "direct" (got ${JSON.stringify(d.changes)})`);
|
|
1444
|
+
const sb = d.storybook;
|
|
1445
|
+
if (sb !== void 0) {
|
|
1446
|
+
if (!isObj(sb) || !isStr(sb.url)) problems.push('"storybook" must be { "url" } with the deployed Storybook address');
|
|
1447
|
+
else if (!/^https:\/\/[^\s/]+/.test(sb.url.trim())) problems.push('"storybook.url" must start with https://, for example https://storybook.example.com');
|
|
1448
|
+
else for (const k of Object.keys(sb)) if (k !== "url") problems.push(`"storybook.${k}" is not a known key`);
|
|
1449
|
+
}
|
|
920
1450
|
problems.push(...productProblems(d.products));
|
|
921
1451
|
const audit = d.audit;
|
|
922
1452
|
if (audit !== void 0) {
|
|
@@ -949,6 +1479,13 @@ function loadConfig(source) {
|
|
|
949
1479
|
const tokensPath = (config, rel = "") => [config.tokens.root === "." ? "" : config.tokens.root, rel].filter(Boolean).join("/");
|
|
950
1480
|
const protectedBranchesOf = (config) => [.../* @__PURE__ */ new Set([...config.governance.protectedBranches, ...config.repo ? [config.repo.defaultBranch] : []])];
|
|
951
1481
|
const baseBranchOf = (config) => config.repo?.defaultBranch ?? "main";
|
|
1482
|
+
const changesMode = (config) => config?.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE;
|
|
1483
|
+
function storybookUrl(config) {
|
|
1484
|
+
const explicit = config?.storybook?.url?.trim();
|
|
1485
|
+
if (explicit) return explicit.replace(/\/+$/, "");
|
|
1486
|
+
const prod = config?.previews?.production?.storybook?.trim();
|
|
1487
|
+
return prod && /^https:\/\//.test(prod) && !/\{\w+\}/.test(prod) ? prod.replace(/\/+$/, "") : void 0;
|
|
1488
|
+
}
|
|
952
1489
|
function toConfigJson(config, schema = "./packages/core/bridge.config.schema.json") {
|
|
953
1490
|
return {
|
|
954
1491
|
$schema: schema,
|
|
@@ -965,6 +1502,8 @@ function toConfigJson(config, schema = "./packages/core/bridge.config.schema.jso
|
|
|
965
1502
|
...config.audit ? { audit: config.audit } : {},
|
|
966
1503
|
...config.previews ? { previews: config.previews } : {},
|
|
967
1504
|
...config.visualImpact ? { visualImpact: config.visualImpact } : {},
|
|
1505
|
+
changes: changesMode(config),
|
|
1506
|
+
...config.storybook ? { storybook: config.storybook } : {},
|
|
968
1507
|
...config.products && !sameProducts(config.products, DEFAULT_PRODUCTS) ? { products: config.products } : {}
|
|
969
1508
|
};
|
|
970
1509
|
}
|
|
@@ -1674,54 +2213,7 @@ function indexByPath(tokens) {
|
|
|
1674
2213
|
for (const t of tokens) m.set(t.path, t);
|
|
1675
2214
|
return m;
|
|
1676
2215
|
}
|
|
1677
|
-
|
|
1678
|
-
const axis = TIER_AXIS[t.tier];
|
|
1679
|
-
if (!axis) return { value: t.values[SINGLE_MODE], via: "token" };
|
|
1680
|
-
if (t.tier === "semantic" && ctx.overrides && ctx.brand && ctx.theme) {
|
|
1681
|
-
const o = ctx.overrides.get(`${ctx.brand}|${ctx.theme}|${t.path}`);
|
|
1682
|
-
if (o !== void 0) return { value: o, via: "override" };
|
|
1683
|
-
}
|
|
1684
|
-
const mode = ctx[axis];
|
|
1685
|
-
return { value: mode === void 0 ? void 0 : t.values[mode], via: "token" };
|
|
1686
|
-
}
|
|
1687
|
-
function resolve(index, path, ctx) {
|
|
1688
|
-
const chain = [];
|
|
1689
|
-
const via = [];
|
|
1690
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1691
|
-
let current = path;
|
|
1692
|
-
for (; ; ) {
|
|
1693
|
-
if (seen.has(current)) return { chain: [...chain, current], via, error: "cycle", at: current };
|
|
1694
|
-
seen.add(current);
|
|
1695
|
-
chain.push(current);
|
|
1696
|
-
const t = index.get(current);
|
|
1697
|
-
if (!t) return { chain, via, error: "broken", at: current };
|
|
1698
|
-
const v = valueIn(t, ctx);
|
|
1699
|
-
if (v.value === void 0) return { chain, via, error: "missing-mode", at: current };
|
|
1700
|
-
via.push(v.via);
|
|
1701
|
-
if (!isAlias(v.value)) return { value: v.value, chain, via };
|
|
1702
|
-
current = aliasTarget(v.value);
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
function findReferenceProblems(index, axes) {
|
|
1706
|
-
const out = [];
|
|
1707
|
-
const base = { brand: axes.brand?.[0], theme: axes.theme?.[0], density: axes.density?.[0] };
|
|
1708
|
-
for (const t of index.values()) {
|
|
1709
|
-
const axis = TIER_AXIS[t.tier];
|
|
1710
|
-
const modes = axis ? (axes[axis] ?? Object.keys(t.values)).filter((m) => t.values[m] !== void 0) : [SINGLE_MODE];
|
|
1711
|
-
for (const mode of modes) {
|
|
1712
|
-
const v = t.values[mode];
|
|
1713
|
-
for (const ref of collectRefs(v)) {
|
|
1714
|
-
if (!index.has(ref)) out.push({ path: t.path, mode, kind: "broken", target: ref, chain: [t.path, ref] });
|
|
1715
|
-
}
|
|
1716
|
-
if (isAlias(v)) {
|
|
1717
|
-
const r = resolve(index, t.path, axis ? { ...base, [axis]: mode } : base);
|
|
1718
|
-
if (r.error === "cycle") out.push({ path: t.path, mode, kind: "cycle", target: r.at ?? "", chain: r.chain });
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
return out;
|
|
1723
|
-
}
|
|
1724
|
-
const has = (segments, ...names) => names.some((n) => segments.includes(n));
|
|
2216
|
+
const has = (segments, ...names2) => names2.some((n) => segments.includes(n));
|
|
1725
2217
|
function defaultScopes(path, type, tier) {
|
|
1726
2218
|
const s = path.split(".");
|
|
1727
2219
|
switch (type) {
|
|
@@ -1759,124 +2251,32 @@ const sameScopes = (a, b) => {
|
|
|
1759
2251
|
return sa.length === sb.length && sa.every((x, i) => x === sb[i]);
|
|
1760
2252
|
};
|
|
1761
2253
|
const effectiveScopes = (t) => t.scopes ?? defaultScopes(t.path, t.type, t.tier);
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
const
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
const b = typeof bg === "string" ? hexToRgba(bg) : bg;
|
|
1777
|
-
if (!f || !b) throw new Error("Invalid color");
|
|
1778
|
-
const opaqueBg = { ...b };
|
|
1779
|
-
const l1 = luminance(composite(f, opaqueBg));
|
|
1780
|
-
const l2 = luminance(opaqueBg);
|
|
1781
|
-
const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
|
|
1782
|
-
return (hi + 0.05) / (lo + 0.05);
|
|
1783
|
-
}
|
|
1784
|
-
function contrastLevel(ratio, kind, min = AA_MIN[kind]) {
|
|
1785
|
-
if (ratio < min) return "fail";
|
|
1786
|
-
if (ratio >= AAA_MIN[kind]) return "AAA";
|
|
1787
|
-
return "AA";
|
|
2254
|
+
const normDesc = (d) => (d ?? "").trim();
|
|
2255
|
+
function fieldChanges(a, b) {
|
|
2256
|
+
const out = [];
|
|
2257
|
+
if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
|
|
2258
|
+
if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
|
|
2259
|
+
const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
|
|
2260
|
+
for (const mode of modes) {
|
|
2261
|
+
if (!valuesEqual(b.type, a.values[mode], b.values[mode])) out.push({ field: "value", mode, before: a.values[mode], after: b.values[mode] });
|
|
2262
|
+
}
|
|
2263
|
+
if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
|
|
2264
|
+
const sa = effectiveScopes(a);
|
|
2265
|
+
const sb = effectiveScopes(b);
|
|
2266
|
+
if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
|
|
2267
|
+
return out;
|
|
1788
2268
|
}
|
|
1789
|
-
function
|
|
1790
|
-
return
|
|
1791
|
-
|
|
1792
|
-
themes: set.layout.themes.map((t) => t.mode),
|
|
1793
|
-
density: set.layout.densities[0]?.mode,
|
|
1794
|
-
overrides: set.overrideIndex
|
|
1795
|
-
};
|
|
2269
|
+
function sameToken(a, b) {
|
|
2270
|
+
if (!a || !b) return a === b;
|
|
2271
|
+
return fieldChanges(a, b).length === 0;
|
|
1796
2272
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
const
|
|
1802
|
-
|
|
1803
|
-
return
|
|
1804
|
-
}
|
|
1805
|
-
function evaluatePairs(index, pairs, scope) {
|
|
1806
|
-
const out = [];
|
|
1807
|
-
const brands = scope.brands.length ? scope.brands : [void 0];
|
|
1808
|
-
for (const brand of brands) {
|
|
1809
|
-
for (const theme of scope.themes) {
|
|
1810
|
-
const ctx = { brand, theme, density: scope.density, overrides: scope.overrides };
|
|
1811
|
-
for (const pair of pairs) {
|
|
1812
|
-
const steps = [];
|
|
1813
|
-
const color = (path) => {
|
|
1814
|
-
const r = resolve(index, path, ctx);
|
|
1815
|
-
r.via.forEach((via, i) => steps.push({ path: r.chain[i], via }));
|
|
1816
|
-
if (r.error) return `${path} cannot be resolved (${r.error === "broken" ? `missing ${r.at}` : r.error})`;
|
|
1817
|
-
return renderedColor(r.value) ?? `${path} is not a color`;
|
|
1818
|
-
};
|
|
1819
|
-
const fg = color(pair.foreground);
|
|
1820
|
-
const bg = color(pair.background);
|
|
1821
|
-
if (typeof fg === "string" || typeof bg === "string") {
|
|
1822
|
-
out.push({ pair, brand, theme, steps, error: typeof fg === "string" ? fg : bg });
|
|
1823
|
-
continue;
|
|
1824
|
-
}
|
|
1825
|
-
const ratio = Math.round(contrastRatio(fg, bg) * 100) / 100;
|
|
1826
|
-
out.push({ pair, brand, theme, ratio, level: contrastLevel(ratio, pair.kind, pair.min), steps });
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
return out;
|
|
1831
|
-
}
|
|
1832
|
-
const kindForMin = (min) => min >= AA_MIN.text ? "text" : "ui";
|
|
1833
|
-
const contrastPair = (foreground, background, min = AA_MIN.text) => ({ foreground, background, min, kind: kindForMin(min) });
|
|
1834
|
-
function parseContrastChecks(text) {
|
|
1835
|
-
if (!text) return [];
|
|
1836
|
-
const data = JSON.parse(text);
|
|
1837
|
-
const list2 = Array.isArray(data) ? data : Array.isArray(data.pairs) ? data.pairs : [];
|
|
1838
|
-
const out = [];
|
|
1839
|
-
for (const p of list2) {
|
|
1840
|
-
let fg, bg, min;
|
|
1841
|
-
if (Array.isArray(p)) [fg, bg, min] = p;
|
|
1842
|
-
else if (p && typeof p === "object") {
|
|
1843
|
-
const o = p;
|
|
1844
|
-
fg = o.fg ?? o.foreground;
|
|
1845
|
-
bg = o.bg ?? o.background;
|
|
1846
|
-
min = o.min ?? o.ratio ?? AA_MIN.text;
|
|
1847
|
-
}
|
|
1848
|
-
if (typeof fg !== "string" || typeof bg !== "string" || typeof min !== "number" || !(min > 0)) continue;
|
|
1849
|
-
out.push(contrastPair(fg, bg, min));
|
|
1850
|
-
}
|
|
1851
|
-
return out;
|
|
1852
|
-
}
|
|
1853
|
-
const pairKey = (r) => `${r.brand ?? ""}/${r.theme}:${r.pair.foreground}|${r.pair.background}`;
|
|
1854
|
-
const normDesc = (d) => (d ?? "").trim();
|
|
1855
|
-
function fieldChanges(a, b) {
|
|
1856
|
-
const out = [];
|
|
1857
|
-
if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
|
|
1858
|
-
if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
|
|
1859
|
-
const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
|
|
1860
|
-
for (const mode of modes) {
|
|
1861
|
-
if (!valuesEqual(b.type, a.values[mode], b.values[mode])) out.push({ field: "value", mode, before: a.values[mode], after: b.values[mode] });
|
|
1862
|
-
}
|
|
1863
|
-
if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
|
|
1864
|
-
const sa = effectiveScopes(a);
|
|
1865
|
-
const sb = effectiveScopes(b);
|
|
1866
|
-
if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
|
|
1867
|
-
return out;
|
|
1868
|
-
}
|
|
1869
|
-
function sameToken(a, b) {
|
|
1870
|
-
if (!a || !b) return a === b;
|
|
1871
|
-
return fieldChanges(a, b).length === 0;
|
|
1872
|
-
}
|
|
1873
|
-
function sideChange(from, to) {
|
|
1874
|
-
if (!from && !to) return void 0;
|
|
1875
|
-
if (!from) return { kind: "added", fields: [] };
|
|
1876
|
-
if (!to) return { kind: "removed", fields: [] };
|
|
1877
|
-
const fields = fieldChanges(from, to);
|
|
1878
|
-
if (!fields.length) return void 0;
|
|
1879
|
-
return { kind: fields.some((f) => f.field === "path") ? "renamed" : "changed", fields };
|
|
2273
|
+
function sideChange(from, to) {
|
|
2274
|
+
if (!from && !to) return void 0;
|
|
2275
|
+
if (!from) return { kind: "added", fields: [] };
|
|
2276
|
+
if (!to) return { kind: "removed", fields: [] };
|
|
2277
|
+
const fields = fieldChanges(from, to);
|
|
2278
|
+
if (!fields.length) return void 0;
|
|
2279
|
+
return { kind: fields.some((f) => f.field === "path") ? "renamed" : "changed", fields };
|
|
1880
2280
|
}
|
|
1881
2281
|
function threeWayDiff(base, figma, head) {
|
|
1882
2282
|
const ids = /* @__PURE__ */ new Set([...base?.keys() ?? [], ...figma.keys(), ...head.keys()]);
|
|
@@ -1915,7 +2315,7 @@ const RISK_LABELS = {
|
|
|
1915
2315
|
2: "Tier 2: medium risk, design review then design + tech approval",
|
|
1916
2316
|
3: "Tier 3: token change, needs design + tech approval"
|
|
1917
2317
|
};
|
|
1918
|
-
const BUMP_RANK
|
|
2318
|
+
const BUMP_RANK = { patch: 0, minor: 1, major: 2 };
|
|
1919
2319
|
const LOW = [/\.md$/, /^docs\//, /\.stories\.tsx?$/, /\.docs\.json$/, /\.test\.tsx?$/, /^\.changeset\//];
|
|
1920
2320
|
const isSummary = (c) => "kind" in c;
|
|
1921
2321
|
function summarizeTokenChange(c) {
|
|
@@ -1931,7 +2331,7 @@ function tokenBump(changes) {
|
|
|
1931
2331
|
let bump;
|
|
1932
2332
|
for (const c of changes.map(summarizeTokenChange)) {
|
|
1933
2333
|
const b = c.kind === "removed" || c.kind === "renamed" ? "major" : c.kind === "changed" && c.descriptionOnly ? "patch" : "minor";
|
|
1934
|
-
if (!bump || BUMP_RANK
|
|
2334
|
+
if (!bump || BUMP_RANK[b] > BUMP_RANK[bump]) bump = b;
|
|
1935
2335
|
}
|
|
1936
2336
|
return bump;
|
|
1937
2337
|
}
|
|
@@ -1961,6 +2361,98 @@ function classifyRisk(input) {
|
|
|
1961
2361
|
const tier = reasons[0]?.tier ?? 1;
|
|
1962
2362
|
return { tier, label: RISK_LABELS[tier], reasons: reasons.map((r) => r.text), bump };
|
|
1963
2363
|
}
|
|
2364
|
+
const CONTRAST_CHECKS_FILE = "contrast.checks.json";
|
|
2365
|
+
const AA_MIN = { text: 4.5, "large-text": 3, ui: 3 };
|
|
2366
|
+
const AAA_MIN = { text: 7, "large-text": 4.5, ui: 4.5 };
|
|
2367
|
+
const channel = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
2368
|
+
function luminance(c) {
|
|
2369
|
+
const q = (x) => Math.round(Math.min(1, Math.max(0, x)) * 255) / 255;
|
|
2370
|
+
return 0.2126 * channel(q(c.r)) + 0.7152 * channel(q(c.g)) + 0.0722 * channel(q(c.b));
|
|
2371
|
+
}
|
|
2372
|
+
function composite(fg, bg) {
|
|
2373
|
+
const a = fg.a;
|
|
2374
|
+
return { r: fg.r * a + bg.r * (1 - a), g: fg.g * a + bg.g * (1 - a), b: fg.b * a + bg.b * (1 - a), a: 1 };
|
|
2375
|
+
}
|
|
2376
|
+
function contrastRatio(fg, bg) {
|
|
2377
|
+
const f = typeof fg === "string" ? hexToRgba(fg) : fg;
|
|
2378
|
+
const b = typeof bg === "string" ? hexToRgba(bg) : bg;
|
|
2379
|
+
if (!f || !b) throw new Error("Invalid color");
|
|
2380
|
+
const opaqueBg = { ...b };
|
|
2381
|
+
const l1 = luminance(composite(f, opaqueBg));
|
|
2382
|
+
const l2 = luminance(opaqueBg);
|
|
2383
|
+
const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
|
|
2384
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
2385
|
+
}
|
|
2386
|
+
function contrastLevel(ratio, kind, min = AA_MIN[kind]) {
|
|
2387
|
+
if (ratio < min) return "fail";
|
|
2388
|
+
if (ratio >= AAA_MIN[kind]) return "AAA";
|
|
2389
|
+
return "AA";
|
|
2390
|
+
}
|
|
2391
|
+
function contrastScope(set) {
|
|
2392
|
+
return {
|
|
2393
|
+
brands: set.layout.brands.map((b) => b.mode),
|
|
2394
|
+
themes: set.layout.themes.map((t) => t.mode),
|
|
2395
|
+
density: set.layout.densities[0]?.mode,
|
|
2396
|
+
overrides: set.overrideIndex
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
const cap$1 = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
2400
|
+
const contextLabel = (r) => r.brand ? `${cap$1(r.brand)} ${r.theme}` : cap$1(r.theme);
|
|
2401
|
+
function renderedColor(value) {
|
|
2402
|
+
const c = parseColor(value);
|
|
2403
|
+
const hex = value && typeof value === "object" ? value.hex : void 0;
|
|
2404
|
+
const fromHex = typeof hex === "string" ? hexToRgba(hex) : void 0;
|
|
2405
|
+
return c && fromHex ? { ...fromHex, a: c.a } : c;
|
|
2406
|
+
}
|
|
2407
|
+
function evaluatePairs(index, pairs, scope) {
|
|
2408
|
+
const out = [];
|
|
2409
|
+
const brands = scope.brands.length ? scope.brands : [void 0];
|
|
2410
|
+
for (const brand of brands) {
|
|
2411
|
+
for (const theme of scope.themes) {
|
|
2412
|
+
const ctx = { brand, theme, density: scope.density, overrides: scope.overrides };
|
|
2413
|
+
for (const pair of pairs) {
|
|
2414
|
+
const steps = [];
|
|
2415
|
+
const color = (path) => {
|
|
2416
|
+
const r = resolve(index, path, ctx);
|
|
2417
|
+
r.via.forEach((via, i) => steps.push({ path: r.chain[i], via }));
|
|
2418
|
+
if (r.error) return `${path} cannot be resolved (${r.error === "broken" ? `missing ${r.at}` : r.error})`;
|
|
2419
|
+
return renderedColor(r.value) ?? `${path} is not a color`;
|
|
2420
|
+
};
|
|
2421
|
+
const fg = color(pair.foreground);
|
|
2422
|
+
const bg = color(pair.background);
|
|
2423
|
+
if (typeof fg === "string" || typeof bg === "string") {
|
|
2424
|
+
out.push({ pair, brand, theme, steps, error: typeof fg === "string" ? fg : bg });
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
const ratio = Math.round(contrastRatio(fg, bg) * 100) / 100;
|
|
2428
|
+
out.push({ pair, brand, theme, ratio, level: contrastLevel(ratio, pair.kind, pair.min), steps });
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return out;
|
|
2433
|
+
}
|
|
2434
|
+
const kindForMin = (min) => min >= AA_MIN.text ? "text" : "ui";
|
|
2435
|
+
const contrastPair = (foreground, background, min = AA_MIN.text) => ({ foreground, background, min, kind: kindForMin(min) });
|
|
2436
|
+
function parseContrastChecks(text) {
|
|
2437
|
+
if (!text) return [];
|
|
2438
|
+
const data = JSON.parse(text);
|
|
2439
|
+
const list2 = Array.isArray(data) ? data : Array.isArray(data.pairs) ? data.pairs : [];
|
|
2440
|
+
const out = [];
|
|
2441
|
+
for (const p of list2) {
|
|
2442
|
+
let fg, bg, min;
|
|
2443
|
+
if (Array.isArray(p)) [fg, bg, min] = p;
|
|
2444
|
+
else if (p && typeof p === "object") {
|
|
2445
|
+
const o = p;
|
|
2446
|
+
fg = o.fg ?? o.foreground;
|
|
2447
|
+
bg = o.bg ?? o.background;
|
|
2448
|
+
min = o.min ?? o.ratio ?? AA_MIN.text;
|
|
2449
|
+
}
|
|
2450
|
+
if (typeof fg !== "string" || typeof bg !== "string" || typeof min !== "number" || !(min > 0)) continue;
|
|
2451
|
+
out.push(contrastPair(fg, bg, min));
|
|
2452
|
+
}
|
|
2453
|
+
return out;
|
|
2454
|
+
}
|
|
2455
|
+
const pairKey = (r) => `${r.brand ?? ""}/${r.theme}:${r.pair.foreground}|${r.pair.background}`;
|
|
1964
2456
|
const RISK_TIER = RISK_LABELS[3];
|
|
1965
2457
|
function slugify(text, max = 40) {
|
|
1966
2458
|
const s = text.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -1996,7 +2488,7 @@ const describe = (set, t, mode) => {
|
|
|
1996
2488
|
const resolved = r.value === void 0 ? "unresolved" : formatValue(t.type, r.value);
|
|
1997
2489
|
return `${formatValue(t.type, v)} (${resolved})`;
|
|
1998
2490
|
};
|
|
1999
|
-
const cell
|
|
2491
|
+
const cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ") || " ";
|
|
2000
2492
|
const code = (s) => s ? `\`${s}\`` : "";
|
|
2001
2493
|
function valueText(head, proposed, c, mode) {
|
|
2002
2494
|
const t = c.target ?? c.current;
|
|
@@ -2073,7 +2565,7 @@ function composePrBody(input) {
|
|
|
2073
2565
|
const extras = [];
|
|
2074
2566
|
if (c.current && c.target && (c.current.description ?? "") !== (c.target.description ?? "")) extras.push("description");
|
|
2075
2567
|
lines.push(
|
|
2076
|
-
`| ${name} | ${TIER_NAMES[t.tier]} | ${kind}${extras.length ? ` (+${extras.join(", ")})` : ""} | ${cell
|
|
2568
|
+
`| ${name} | ${TIER_NAMES[t.tier]} | ${kind}${extras.length ? ` (+${extras.join(", ")})` : ""} | ${cell(valuesCell(head, proposed, c))} | ${contrastCell(c, validation.contrastBefore, validation.contrastAfter)} |`
|
|
2077
2569
|
);
|
|
2078
2570
|
}
|
|
2079
2571
|
lines.push("");
|
|
@@ -2128,1283 +2620,487 @@ ${body}
|
|
|
2128
2620
|
|
|
2129
2621
|
${footer}`;
|
|
2130
2622
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
function composeChangeset(changes, opts) {
|
|
2145
|
-
const bump = changesetBump(changes);
|
|
2146
|
-
const name = opts.branch.replace(/^tokens\//, "ds-bridge-").replace(/[^a-z0-9-]/gi, "-");
|
|
2147
|
-
const list2 = changes.slice(0, 20).map((c) => `- ${changeKind(c)} \`${(c.target ?? c.current).path}\``).join("\n");
|
|
2148
|
-
const more = changes.length > 20 ? `
|
|
2149
|
-
- and ${changes.length - 20} more` : "";
|
|
2150
|
-
const summary = opts.source ? `${opts.title} (${opts.source}).` : `${opts.title}.`;
|
|
2151
|
-
return {
|
|
2152
|
-
path: `.changeset/${name}.md`,
|
|
2153
|
-
content: formatChangeset(opts.packageName, bump, summary, `${list2}${more}`)
|
|
2154
|
-
};
|
|
2155
|
-
}
|
|
2156
|
-
function coerceValue(input, type, existing) {
|
|
2157
|
-
if (typeof input !== "string") return input;
|
|
2158
|
-
const s = input.trim();
|
|
2159
|
-
if (aliasOf(s)) return s;
|
|
2160
|
-
if (s.startsWith("{") || s.startsWith("[")) {
|
|
2161
|
-
try {
|
|
2162
|
-
return JSON.parse(s);
|
|
2163
|
-
} catch {
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
const keepString = typeof existing === "string" && !aliasOf(existing);
|
|
2167
|
-
switch (type) {
|
|
2168
|
-
case "color": {
|
|
2169
|
-
const m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s) ?? /^#([0-9a-f]{3})$/i.exec(s);
|
|
2170
|
-
if (!m) return s;
|
|
2171
|
-
const hex6 = m[1].length === 3 ? [...m[1]].map((c) => c + c).join("") : m[1];
|
|
2172
|
-
if (keepString) return `#${hex6.toLowerCase()}${m[2] ?? ""}`;
|
|
2173
|
-
return rgbaToDtcg(hexToRgba(`#${hex6}${m[2] ?? ""}`));
|
|
2174
|
-
}
|
|
2175
|
-
case "dimension": {
|
|
2176
|
-
const m = /^(-?\d*\.?\d+)\s*(px|rem)?$/.exec(s);
|
|
2177
|
-
if (!m || keepString) return s;
|
|
2178
|
-
return { value: Number(m[1]), unit: m[2] ?? "px" };
|
|
2179
|
-
}
|
|
2180
|
-
case "duration": {
|
|
2181
|
-
const m = /^(\d*\.?\d+)\s*(ms|s)?$/.exec(s);
|
|
2182
|
-
if (!m || keepString) return s;
|
|
2183
|
-
return { value: Number(m[1]), unit: m[2] ?? "ms" };
|
|
2184
|
-
}
|
|
2185
|
-
case "number":
|
|
2186
|
-
case "fontWeight":
|
|
2187
|
-
return /^-?\d*\.?\d+$/.test(s) ? Number(s) : s;
|
|
2188
|
-
default:
|
|
2189
|
-
return s;
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
function applyTokenEdits(base, edits) {
|
|
2193
|
-
let set = base;
|
|
2194
|
-
const files = new Map(base.files);
|
|
2195
|
-
const touched = /* @__PURE__ */ new Set();
|
|
2196
|
-
const notes = [];
|
|
2197
|
-
const edit = (f, fn) => {
|
|
2198
|
-
const doc = files.has(f) ? parseTokenFile(f, files.get(f)) : emptyTokenFile(f);
|
|
2199
|
-
if (!fn(doc)) return;
|
|
2200
|
-
const text = serializeTokenFile(doc);
|
|
2201
|
-
if (text !== files.get(f)) {
|
|
2202
|
-
files.set(f, text);
|
|
2203
|
-
touched.add(f);
|
|
2204
|
-
}
|
|
2205
|
-
};
|
|
2206
|
-
const definingFiles = (path) => [...new Set(set.defs.filter((d) => d.path === path).map((d) => d.file))];
|
|
2207
|
-
const pickFiles = (e, all) => {
|
|
2208
|
-
const defs = definingFiles(e.path);
|
|
2209
|
-
if (e.file) {
|
|
2210
|
-
if (!files.has(e.file)) throw new Error(`File "${e.file}" is not a token file. Token files: ${set.fileNames.join(", ")}`);
|
|
2211
|
-
if (!defs.includes(e.file)) throw new Error(`"${e.path}" is not defined in ${e.file}. It is defined in: ${defs.join(", ") || 'no file (use op "add")'}.`);
|
|
2212
|
-
return [e.file];
|
|
2213
|
-
}
|
|
2214
|
-
if (e.mode) {
|
|
2215
|
-
const combo = set.normalizeCombo(e.mode);
|
|
2216
|
-
const v = set.view(combo).get(e.path);
|
|
2217
|
-
if (!v) throw new Error(`"${e.path}" is not defined for ${JSON.stringify(combo)}.`);
|
|
2218
|
-
return [v[v.length - 1].file];
|
|
2219
|
-
}
|
|
2220
|
-
if (!defs.length) throw new Error(`Token "${e.path}" does not exist. Use op "add" to create it.`);
|
|
2221
|
-
if (defs.length > 1 && !all) throw new Error(`"${e.path}" is defined in ${defs.length} files (${defs.join(", ")}). Pass "file" or "mode" (for example { "theme": "dark" }), or one edit per file.`);
|
|
2222
|
-
return defs;
|
|
2223
|
-
};
|
|
2224
|
-
for (const e of edits) {
|
|
2225
|
-
if (!e.path || /[{}]/.test(e.path)) throw new Error(`Invalid token path "${e.path}". Use dots: color.text.muted.`);
|
|
2226
|
-
switch (e.op) {
|
|
2227
|
-
case "set": {
|
|
2228
|
-
if (e.value === void 0 && e.description === void 0 && e.type === void 0) throw new Error(`set ${e.path}: give value, description or type.`);
|
|
2229
|
-
for (const f of pickFiles(e, false)) {
|
|
2230
|
-
edit(f, (doc) => {
|
|
2231
|
-
const cur = readTokenAt(doc, e.path);
|
|
2232
|
-
const type = e.type ?? cur.type;
|
|
2233
|
-
return upsertToken(doc, e.path, {
|
|
2234
|
-
type,
|
|
2235
|
-
value: e.value === void 0 ? cur.value : coerceValue(e.value, type, cur.value),
|
|
2236
|
-
description: e.description ?? cur.description
|
|
2237
|
-
});
|
|
2238
|
-
});
|
|
2239
|
-
}
|
|
2240
|
-
break;
|
|
2241
|
-
}
|
|
2242
|
-
case "add": {
|
|
2243
|
-
let file = e.file;
|
|
2244
|
-
if (!file && e.mode) {
|
|
2245
|
-
const combo = set.normalizeCombo(e.mode);
|
|
2246
|
-
const parent = e.path.split(".").slice(0, -1).join(".");
|
|
2247
|
-
const sib = [...set.view(combo).entries()].filter(([p]) => p.startsWith(`${parent}.`)).map(([, d]) => d[d.length - 1].file);
|
|
2248
|
-
file = sib[sib.length - 1];
|
|
2249
|
-
}
|
|
2250
|
-
if (!file) {
|
|
2251
|
-
const keys = e.path.split(".");
|
|
2252
|
-
for (let d = keys.length - 1; d > 0 && !file; d--) {
|
|
2253
|
-
const prefix = `${keys.slice(0, d).join(".")}.`;
|
|
2254
|
-
const fs = [...new Set(set.defs.filter((x) => x.path.startsWith(prefix)).map((x) => x.file))];
|
|
2255
|
-
if (fs.length === 1) file = fs[0];
|
|
2256
|
-
else if (fs.length > 1) throw new Error(`Where should "${e.path}" go? Its group exists in ${fs.join(", ")}. Pass "file" (one add per file for light and dark).`);
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
if (!file) throw new Error(`Cannot infer a file for new token "${e.path}". Pass "file".`);
|
|
2260
|
-
if (!files.has(file)) throw new Error(`File "${file}" is not a token file. Token files: ${set.fileNames.join(", ")}`);
|
|
2261
|
-
if (set.defs.some((d) => d.path === e.path && d.file === file)) throw new Error(`"${e.path}" already exists in ${file}. Use op "set".`);
|
|
2262
|
-
if (e.value === void 0) throw new Error(`add ${e.path}: value is required.`);
|
|
2263
|
-
edit(file, (doc) => {
|
|
2264
|
-
const type = e.type ?? inheritedTypeAt(doc, e.path);
|
|
2265
|
-
if (!type && !aliasOf(e.value)) throw new Error(`add ${e.path}: pass "type" (color, dimension, ...); no parent group sets one.`);
|
|
2266
|
-
return upsertToken(doc, e.path, { type, value: coerceValue(e.value, type), description: e.description });
|
|
2267
|
-
});
|
|
2268
|
-
break;
|
|
2269
|
-
}
|
|
2270
|
-
case "remove": {
|
|
2271
|
-
for (const f of pickFiles(e, true)) edit(f, (doc) => removeToken(doc, e.path));
|
|
2272
|
-
const users = set.referencedBy(e.path).filter((u) => u.path !== e.path);
|
|
2273
|
-
if (users.length) notes.push(`${users.length} token(s) alias ${e.path}: ${users.slice(0, 8).map((u) => u.path).join(", ")}. Validation will flag them as broken unless they are edited too.`);
|
|
2274
|
-
break;
|
|
2275
|
-
}
|
|
2276
|
-
case "rename": {
|
|
2277
|
-
const to = e.newPath;
|
|
2278
|
-
if (!to || /[{}]/.test(to)) throw new Error(`rename ${e.path}: pass a valid newPath.`);
|
|
2279
|
-
if (set.defs.some((d) => d.path === to)) throw new Error(`rename: "${to}" already exists.`);
|
|
2280
|
-
for (const f of pickFiles(e, true)) edit(f, (doc) => moveToken(doc, e.path, to));
|
|
2281
|
-
const from = `"{${e.path}}"`;
|
|
2282
|
-
const next = `"{${to}}"`;
|
|
2283
|
-
for (const [f, text] of files) {
|
|
2284
|
-
if (!text.includes(from)) continue;
|
|
2285
|
-
files.set(f, text.split(from).join(next));
|
|
2286
|
-
touched.add(f);
|
|
2287
|
-
}
|
|
2288
|
-
break;
|
|
2289
|
-
}
|
|
2290
|
-
default:
|
|
2291
|
-
throw new Error(`Unknown op "${e.op}". Use set, add, remove or rename.`);
|
|
2292
|
-
}
|
|
2293
|
-
set = new LayeredTokenSet(base.config, new Map(files));
|
|
2294
|
-
}
|
|
2295
|
-
return { files, touched: [...touched].filter((f) => files.get(f) !== base.files.get(f)).sort(), notes };
|
|
2296
|
-
}
|
|
2297
|
-
function checkLayeredPair(set, pair, combo) {
|
|
2298
|
-
const base = { fg: pair.foreground, bg: pair.background, min: pair.min, modes: comboKey(combo) };
|
|
2299
|
-
const f = set.resolve(pair.foreground, combo);
|
|
2300
|
-
const b = set.resolve(pair.background, combo);
|
|
2301
|
-
if (f.error || b.error) return { ...base, ratio: null, pass: false, note: f.error ?? b.error };
|
|
2302
|
-
const fc = renderedColor(f.value);
|
|
2303
|
-
const bc = renderedColor(b.value);
|
|
2304
|
-
if (!fc || !bc) return { ...base, ratio: null, pass: false, note: "not a color Bridge can measure (needs srgb or hex)" };
|
|
2305
|
-
if (bc.a < 1) return { ...base, ratio: null, pass: false, note: "background is translucent: contrast depends on what is behind it" };
|
|
2306
|
-
const ratio = Math.round(contrastRatio(fc, bc) * 100) / 100;
|
|
2307
|
-
return { ...base, ratio, pass: ratio >= pair.min, fgValue: rgbaToHex(fc), bgValue: rgbaToHex(bc), ...fc.a < 1 ? { note: "foreground alpha composited over background" } : {} };
|
|
2308
|
-
}
|
|
2309
|
-
function runLayeredContrast(set, pairs, modes) {
|
|
2310
|
-
const combos = set.combos(modes ?? set.config.contrast?.modes);
|
|
2311
|
-
return combos.flatMap((c) => pairs.map((p) => checkLayeredPair(set, p, c)));
|
|
2312
|
-
}
|
|
2313
|
-
function contrastDelta(before, after) {
|
|
2314
|
-
const key = (r) => `${r.fg}|${r.bg}|${r.modes}`;
|
|
2315
|
-
const prev = new Map(before.map((r) => [key(r), r]));
|
|
2316
|
-
return after.map((a) => {
|
|
2317
|
-
const b = prev.get(key(a));
|
|
2318
|
-
return { fg: a.fg, bg: a.bg, min: a.min, modes: a.modes, before: b?.ratio ?? null, after: a.ratio, passBefore: b?.pass ?? true, passAfter: a.pass };
|
|
2319
|
-
}).filter((d) => d.before !== d.after || !d.passAfter);
|
|
2320
|
-
}
|
|
2321
|
-
const same$1 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2322
|
-
function diffLayered(before, after) {
|
|
2323
|
-
const key = (d) => `${d.file}\0${d.path}`;
|
|
2324
|
-
const b = new Map(before.defs.map((d) => [key(d), d]));
|
|
2325
|
-
const a = new Map(after.defs.map((d) => [key(d), d]));
|
|
2326
|
-
const css = (set, d) => set.resolve(d.path, set.comboForFile(d.file)).css;
|
|
2327
|
-
const tierOf = (set, f) => {
|
|
2328
|
-
const t = set.tierOfFile(f);
|
|
2329
|
-
return t ? { tier: t } : {};
|
|
2330
|
-
};
|
|
2331
|
-
const changes = [];
|
|
2332
|
-
const removed = [];
|
|
2333
|
-
const added = [];
|
|
2334
|
-
for (const [k, d] of b) {
|
|
2335
|
-
const n = a.get(k);
|
|
2336
|
-
if (!n) {
|
|
2337
|
-
removed.push(d);
|
|
2338
|
-
continue;
|
|
2339
|
-
}
|
|
2340
|
-
const fields = [];
|
|
2341
|
-
if (!same$1(d.value, n.value)) fields.push("value");
|
|
2342
|
-
if (d.type !== n.type) fields.push("type");
|
|
2343
|
-
if (d.description !== n.description) fields.push("description");
|
|
2344
|
-
if (!same$1(d.extensions, n.extensions)) fields.push("extensions");
|
|
2345
|
-
if (!fields.length) continue;
|
|
2346
|
-
changes.push({
|
|
2347
|
-
kind: "changed",
|
|
2348
|
-
path: d.path,
|
|
2349
|
-
file: d.file,
|
|
2350
|
-
...tierOf(after, d.file),
|
|
2351
|
-
...n.type ? { type: n.type } : {},
|
|
2352
|
-
fields,
|
|
2353
|
-
before: d.value,
|
|
2354
|
-
after: n.value,
|
|
2355
|
-
beforeCss: css(before, d),
|
|
2356
|
-
afterCss: css(after, n),
|
|
2357
|
-
...d.description !== n.description ? { descriptionBefore: d.description ?? "", descriptionAfter: n.description ?? "" } : {}
|
|
2358
|
-
});
|
|
2359
|
-
}
|
|
2360
|
-
for (const [k, d] of a) if (!b.has(k)) added.push(d);
|
|
2361
|
-
for (const r of [...removed]) {
|
|
2362
|
-
const i = added.findIndex((x) => x.file === r.file && same$1(x.value, r.value) && x.type === r.type);
|
|
2363
|
-
if (i < 0) continue;
|
|
2364
|
-
const n = added.splice(i, 1)[0];
|
|
2365
|
-
removed.splice(removed.indexOf(r), 1);
|
|
2366
|
-
changes.push({ kind: "renamed", path: n.path, from: r.path, file: n.file, ...n.type ? { type: n.type } : {}, fields: [], before: r.value, after: n.value, afterCss: css(after, n), ...tierOf(after, n.file) });
|
|
2367
|
-
}
|
|
2368
|
-
for (const d of removed) changes.push({ kind: "removed", path: d.path, file: d.file, ...d.type ? { type: d.type } : {}, fields: [], before: d.value, beforeCss: css(before, d), ...d.description ? { descriptionBefore: d.description } : {}, ...tierOf(before, d.file) });
|
|
2369
|
-
for (const d of added) changes.push({ kind: "added", path: d.path, file: d.file, ...d.type ? { type: d.type } : {}, fields: [], after: d.value, afterCss: css(after, d), ...d.description ? { descriptionAfter: d.description } : {}, ...tierOf(after, d.file) });
|
|
2370
|
-
return changes.sort((x, y) => x.path.localeCompare(y.path) || x.file.localeCompare(y.file));
|
|
2371
|
-
}
|
|
2372
|
-
function layeredImpact(before, after, direct, limit = 60) {
|
|
2373
|
-
const skip = new Set(direct.map((c) => c.path));
|
|
2374
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
2375
|
-
for (const combo of after.combos()) {
|
|
2376
|
-
const paths = /* @__PURE__ */ new Set([...before.view(combo).keys(), ...after.view(combo).keys()]);
|
|
2377
|
-
for (const p of paths) {
|
|
2378
|
-
if (skip.has(p)) continue;
|
|
2379
|
-
const x = before.resolve(p, combo).css;
|
|
2380
|
-
const y = after.resolve(p, combo).css;
|
|
2381
|
-
if (x === y) continue;
|
|
2382
|
-
const e = byPath.get(p);
|
|
2383
|
-
if (e) e.modes.push(comboKey(combo));
|
|
2384
|
-
else byPath.set(p, { path: p, modes: [comboKey(combo)], before: x, after: y });
|
|
2385
|
-
}
|
|
2623
|
+
const PIECES_MARK = "bridge-pieces";
|
|
2624
|
+
const NEEDED_MARK = "bridge-needed-by";
|
|
2625
|
+
const SECTION = "### Missing from the library";
|
|
2626
|
+
const NEEDED_BY_LABEL = "needed-by-product";
|
|
2627
|
+
const hiddenJson = (mark, data) => `<!-- ${mark} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
2628
|
+
const readHidden = (mark, body) => {
|
|
2629
|
+
if (!body) return void 0;
|
|
2630
|
+
const m = new RegExp(`<!-- ${mark} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
2631
|
+
if (!m) return void 0;
|
|
2632
|
+
try {
|
|
2633
|
+
return JSON.parse(m[1]);
|
|
2634
|
+
} catch {
|
|
2635
|
+
return void 0;
|
|
2386
2636
|
}
|
|
2387
|
-
const all = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
2388
|
-
return { total: all.length, entries: all.slice(0, limit) };
|
|
2389
|
-
}
|
|
2390
|
-
const toChangeSummary = (c) => ({
|
|
2391
|
-
kind: c.kind,
|
|
2392
|
-
path: c.path,
|
|
2393
|
-
...c.kind === "changed" ? { descriptionOnly: c.fields.length > 0 && c.fields.every((f) => f === "description") } : {}
|
|
2394
|
-
});
|
|
2395
|
-
function classifyLayered(changes, otherFiles = [], config) {
|
|
2396
|
-
const r = classifyRisk({ tokenChanges: changes.map(toChangeSummary), files: otherFiles.map((path) => ({ path })), config });
|
|
2397
|
-
return { tier: r.tier, bump: r.bump ?? "patch", reasons: r.reasons.length ? r.reasons : ["No token or guardrail change"], label: r.label };
|
|
2398
|
-
}
|
|
2399
|
-
const short = (v) => {
|
|
2400
|
-
if (v === void 0) return "";
|
|
2401
|
-
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
2402
|
-
return s.length > 60 ? `${s.slice(0, 57)}...` : s;
|
|
2403
2637
|
};
|
|
2404
|
-
const
|
|
2405
|
-
|
|
2406
|
-
const
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
const
|
|
2420
|
-
const
|
|
2638
|
+
const KIND_WORD = { "token-change": "token change", exploration: "library exploration", request: "component request", suggestion: "suggested token change" };
|
|
2639
|
+
function parsePiecesRecord(body) {
|
|
2640
|
+
const r = readHidden(PIECES_MARK, body);
|
|
2641
|
+
if (!r || !Array.isArray(r.pieces) || !Array.isArray(r.links) || typeof r.checkedAt !== "string") return void 0;
|
|
2642
|
+
return r;
|
|
2643
|
+
}
|
|
2644
|
+
function piecesSection(r) {
|
|
2645
|
+
const link = (p) => r.links.find((l) => l.piece === p.id);
|
|
2646
|
+
const lines = r.pieces.length ? r.pieces.map((p) => {
|
|
2647
|
+
const l = link(p);
|
|
2648
|
+
return `- **${p.title}**: ${p.summary}${l ? ` ${cap(KIND_WORD[l.kind])}: #${l.number}.` : " No library branch yet."}`;
|
|
2649
|
+
}) : ["- Nothing: every value and component comes from the library."];
|
|
2650
|
+
return [SECTION, "", `Checked ${r.checkedAt.slice(0, 10)}${r.headSha ? ` on ${r.headSha.slice(0, 7)}` : ""} by Bridge.`, "", ...lines, "", hiddenJson(PIECES_MARK, r)].join("\n");
|
|
2651
|
+
}
|
|
2652
|
+
function withPiecesRecord(body, r) {
|
|
2653
|
+
const b = (body ?? "").trimEnd();
|
|
2654
|
+
const start = b.indexOf(SECTION);
|
|
2655
|
+
const markAt = b.indexOf(`<!-- ${PIECES_MARK} `);
|
|
2656
|
+
if (start >= 0 && markAt > start) {
|
|
2657
|
+
const end = b.indexOf("-->", markAt);
|
|
2658
|
+
return `${b.slice(0, start)}${piecesSection(r)}${end >= 0 ? b.slice(end + 3) : ""}`.trimEnd();
|
|
2659
|
+
}
|
|
2660
|
+
return `${b}${b ? "\n\n" : ""}${piecesSection(r)}`;
|
|
2661
|
+
}
|
|
2662
|
+
function mergePiecesRecord(prev, next) {
|
|
2663
|
+
const links = [...prev?.links ?? []];
|
|
2664
|
+
for (const l of next.links ?? []) {
|
|
2665
|
+
const at = links.findIndex((x) => x.piece === l.piece);
|
|
2666
|
+
if (at >= 0) links[at] = l;
|
|
2667
|
+
else links.push(l);
|
|
2668
|
+
}
|
|
2669
|
+
return { ...next, links };
|
|
2670
|
+
}
|
|
2671
|
+
function withLink(r, l) {
|
|
2672
|
+
return { ...r, links: [...r.links.filter((x) => x.piece !== l.piece), l] };
|
|
2673
|
+
}
|
|
2674
|
+
function neededBySection(n) {
|
|
2675
|
+
const who = n.author ? ` for @${n.author}` : "";
|
|
2676
|
+
const near = n.nearest ? ` The closest the library has today is ${n.nearest}${n.nearestToken ? ` (\`${n.nearestToken}\`)` : ""}.` : "";
|
|
2677
|
+
return [`**Needed by:** [${n.title}](${n.url}) (#${n.pr}) in ${n.productName}${who}.${near}`, "", hiddenJson(NEEDED_MARK, n)].join("\n");
|
|
2678
|
+
}
|
|
2679
|
+
function parseNeededBy(body) {
|
|
2680
|
+
const n = readHidden(NEEDED_MARK, body);
|
|
2681
|
+
return n && typeof n.pr === "number" && typeof n.title === "string" && typeof n.piece === "string" ? n : void 0;
|
|
2682
|
+
}
|
|
2683
|
+
function withNeededBy(body, n) {
|
|
2684
|
+
let b = (body ?? "").trim();
|
|
2685
|
+
const at = b.indexOf("**Needed by:**");
|
|
2686
|
+
const mark = b.indexOf(`<!-- ${NEEDED_MARK} `);
|
|
2687
|
+
if (at >= 0 && mark > at) {
|
|
2688
|
+
const end = b.indexOf("-->", mark);
|
|
2689
|
+
b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
|
|
2690
|
+
}
|
|
2691
|
+
return `${neededBySection(n)}${b ? `
|
|
2421
2692
|
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
const
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
box(1),
|
|
2439
|
-
box(2),
|
|
2440
|
-
box(3),
|
|
2441
|
-
"",
|
|
2442
|
-
...args.tier.reasons.map((r) => `- ${r}`),
|
|
2443
|
-
`- Semver bump: \`${args.tier.bump}\``,
|
|
2444
|
-
"",
|
|
2445
|
-
"## Token changes",
|
|
2446
|
-
"",
|
|
2447
|
-
changeTable(args.changes),
|
|
2448
|
-
"",
|
|
2449
|
-
"## Contrast before and after",
|
|
2450
|
-
"",
|
|
2451
|
-
contrastDeltaTable(args.contrast),
|
|
2452
|
-
"",
|
|
2453
|
-
"## Downstream impact",
|
|
2454
|
-
"",
|
|
2455
|
-
args.impact.total ? [`${args.impact.total} token(s) resolve to a new value because they alias an edited token.`, "", ...impactList, args.impact.total > 15 ? `- and ${args.impact.total - 15} more` : ""].filter(Boolean).join("\n") : "_No other token changes value._",
|
|
2456
|
-
"",
|
|
2457
|
-
"## Checks",
|
|
2458
|
-
"",
|
|
2459
|
-
`- Token validation: ${args.validationErrors ? `**${args.validationErrors} error(s)**` : "no errors"} (DTCG schema, aliases, light and dark parity)`,
|
|
2460
|
-
`- Contrast gate: ${args.contrast.some((d) => !d.passAfter) ? "**failing pairs, see table**" : "all pairs pass"}`,
|
|
2461
|
-
"",
|
|
2462
|
-
"## Authored by",
|
|
2463
|
-
"",
|
|
2464
|
-
"- [ ] Human",
|
|
2465
|
-
"- [x] AI agent (Claude), reviewed by a human before merge",
|
|
2466
|
-
"",
|
|
2467
|
-
"## Design review checklist",
|
|
2468
|
-
"",
|
|
2469
|
-
"- [ ] Checked in light and dark themes and in every brand",
|
|
2470
|
-
"- [ ] Contrast table reviewed, no pair loosened",
|
|
2471
|
-
"- [ ] Changeset bump matches the tier",
|
|
2472
|
-
""
|
|
2473
|
-
].join("\n");
|
|
2474
|
-
}
|
|
2475
|
-
const DEFAULT_PROTECTED_BRANCHES = ["main", "master"];
|
|
2476
|
-
class ProtectedBranchError extends Error {
|
|
2477
|
-
constructor(message) {
|
|
2478
|
-
super(message);
|
|
2479
|
-
this.name = "ProtectedBranchError";
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
function assertSafeBranch(branch, rules = {}) {
|
|
2483
|
-
const name = branch.replace(/^refs\/heads\//, "").trim();
|
|
2484
|
-
const blocked = /* @__PURE__ */ new Set([...rules.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES, ...rules.base ? [rules.base] : []]);
|
|
2485
|
-
if (!name || blocked.has(name)) {
|
|
2486
|
-
throw new ProtectedBranchError(`Refusing to write to protected branch "${name}". Bridge only creates new branches and pull requests.`);
|
|
2487
|
-
}
|
|
2488
|
-
if (rules.branchPrefix && !name.startsWith(rules.branchPrefix)) {
|
|
2489
|
-
throw new ProtectedBranchError(`Branch "${name}" must start with "${rules.branchPrefix}".`);
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2492
|
-
const BULK_LIMIT = 60;
|
|
2493
|
-
const UNRELEASED = "Unreleased";
|
|
2494
|
-
const SUBJECT_PR = [/^Merge pull request #(\d+)/, /\(#(\d+)\)\s*$/];
|
|
2495
|
-
function prNumberFromSubject(subject) {
|
|
2496
|
-
for (const re of SUBJECT_PR) {
|
|
2497
|
-
const m = re.exec(subject);
|
|
2498
|
-
if (m) return Number(m[1]);
|
|
2499
|
-
}
|
|
2500
|
-
return void 0;
|
|
2501
|
-
}
|
|
2502
|
-
function parseChangesetText(path, text) {
|
|
2503
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
|
|
2504
|
-
if (!m) return null;
|
|
2505
|
-
const bumps = [...m[1].matchAll(/["']?([^"'\s:]+)["']?\s*:\s*(major|minor|patch)/g)].map((x) => ({ pkg: x[1], bump: x[2] }));
|
|
2506
|
-
const order = ["major", "minor", "patch"];
|
|
2507
|
-
const bump = order.find((b) => bumps.some((x) => x.bump === b));
|
|
2508
|
-
const summary = m[2].trim().split(/\r?\n\r?\n/)[0].replace(/\s+/g, " ").trim();
|
|
2509
|
-
return { path, ...bump ? { bump } : {}, packages: bumps.map((b) => b.pkg), summary };
|
|
2510
|
-
}
|
|
2511
|
-
function firstParagraph(text) {
|
|
2512
|
-
if (!text) return "";
|
|
2513
|
-
const cleaned = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
2514
|
-
for (const para of cleaned.split(/\r?\n\s*\r?\n/)) {
|
|
2515
|
-
const lines = para.split(/\r?\n/).filter((l) => l.trim() && !/^\s*#{1,6}\s/.test(l) && !/^\s*[-*]\s*\[[ xX]\]/.test(l) && !/^\s*(Co-Authored-By|Claude-Session|Signed-off-by):/i.test(l));
|
|
2516
|
-
const s = lines.join(" ").replace(/\s+/g, " ").trim();
|
|
2517
|
-
if (s && !/^\|/.test(s)) return s.length > 400 ? `${s.slice(0, 397)}...` : s;
|
|
2518
|
-
}
|
|
2519
|
-
return "";
|
|
2520
|
-
}
|
|
2521
|
-
function countChanges(changes) {
|
|
2522
|
-
const c = { added: 0, changed: 0, removed: 0, renamed: 0, total: changes.length };
|
|
2523
|
-
for (const x of changes) c[x.kind]++;
|
|
2524
|
-
return c;
|
|
2525
|
-
}
|
|
2526
|
-
const shortSha = (sha) => sha.slice(0, 7);
|
|
2527
|
-
function titleOf(commit) {
|
|
2528
|
-
if (commit.pr?.title) return commit.pr.title;
|
|
2529
|
-
if (/^Merge pull request #\d+/.test(commit.subject)) {
|
|
2530
|
-
const first = (commit.body ?? "").split(/\r?\n/).find((l) => l.trim());
|
|
2531
|
-
if (first) return first.trim();
|
|
2532
|
-
}
|
|
2533
|
-
return commit.subject;
|
|
2534
|
-
}
|
|
2535
|
-
function buildHistoryEntry(input) {
|
|
2536
|
-
const { config, commit } = input;
|
|
2537
|
-
const beforeSet = new LayeredTokenSet(config, input.before);
|
|
2538
|
-
const afterSet = new LayeredTokenSet(config, input.after);
|
|
2539
|
-
const changes = diffLayered(beforeSet, afterSet);
|
|
2540
|
-
if (!changes.length) return null;
|
|
2541
|
-
const tier = classifyLayered(changes, [], config);
|
|
2542
|
-
const changesets = (input.changesets ?? []).map((c) => parseChangesetText(c.path, c.text)).filter((c) => c !== null);
|
|
2543
|
-
const pkg = config.changesets?.package;
|
|
2544
|
-
const relevant = changesets.filter((c) => !pkg || !c.packages.length || c.packages.includes(pkg));
|
|
2545
|
-
const title = titleOf(commit);
|
|
2546
|
-
const why = relevant.map((c) => c.summary).find(Boolean) || firstParagraph(commit.pr?.body) || firstParagraph(commit.body) || title;
|
|
2547
|
-
const prNumber = commit.pr?.number ?? prNumberFromSubject(commit.subject);
|
|
2548
|
-
const pr = commit.pr ?? (prNumber ? { number: prNumber, title, ...config.repo ? { url: `https://github.com/${config.repo.owner}/${config.repo.name}/pull/${prNumber}` } : {} } : void 0);
|
|
2549
|
-
const changesetBump2 = relevant.map((c) => c.bump).find((b) => b === "major");
|
|
2550
|
-
const bump = changesetBump2 ?? tier.bump;
|
|
2551
|
-
return {
|
|
2552
|
-
id: pr ? `pr-${pr.number}` : shortSha(commit.sha),
|
|
2553
|
-
sha: commit.sha,
|
|
2554
|
-
...commit.parents[0] ? { baseSha: commit.parents[0] } : {},
|
|
2555
|
-
date: commit.date,
|
|
2556
|
-
author: commit.pr?.author ?? commit.author,
|
|
2557
|
-
title,
|
|
2558
|
-
why,
|
|
2559
|
-
...pr ? { pr } : {},
|
|
2560
|
-
changes,
|
|
2561
|
-
counts: countChanges(changes),
|
|
2562
|
-
tier: tier.tier,
|
|
2563
|
-
bump,
|
|
2564
|
-
breaking: bump === "major",
|
|
2565
|
-
reasons: tier.reasons,
|
|
2566
|
-
changesets: relevant,
|
|
2567
|
-
...changes.length > BULK_LIMIT ? { bulk: true } : {},
|
|
2568
|
-
...input.demo ? { demo: true } : {}
|
|
2693
|
+
${b}` : ""}`;
|
|
2694
|
+
}
|
|
2695
|
+
const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
2696
|
+
const forLine = (b) => `${b.productName} needs it for "${b.title.replace(/"/g, "'")}" (#${b.pr})`;
|
|
2697
|
+
function libraryBranchPlan(piece, branch, opts = {}) {
|
|
2698
|
+
const neededBy = {
|
|
2699
|
+
pr: branch.pr,
|
|
2700
|
+
title: branch.title,
|
|
2701
|
+
branch: branch.branch,
|
|
2702
|
+
url: branch.url,
|
|
2703
|
+
product: branch.productKey,
|
|
2704
|
+
productName: branch.productName,
|
|
2705
|
+
...branch.author ? { author: branch.author } : {},
|
|
2706
|
+
piece: piece.id,
|
|
2707
|
+
pieceTitle: piece.title,
|
|
2708
|
+
...piece.nearest ? { nearest: piece.nearest.label, nearestToken: piece.nearest.token } : {}
|
|
2569
2709
|
};
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
}
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
changesets = (await Promise.all(added.map(async (p) => ({ path: p, text: await backend.readFile(c.sha, p) ?? "" })))).filter((x) => x.text);
|
|
2594
|
-
}
|
|
2595
|
-
const e = buildHistoryEntry({ config, commit: c, before, after, changesets });
|
|
2596
|
-
if (e) entries.push(e);
|
|
2597
|
-
opts.onProgress?.(++done, picked.length);
|
|
2598
|
-
}
|
|
2599
|
-
return entries;
|
|
2600
|
-
}
|
|
2601
|
-
function parseChangelog(text) {
|
|
2602
|
-
const out = [];
|
|
2603
|
-
const parts = text.split(/^##\s+/m).slice(1);
|
|
2604
|
-
for (const p of parts) {
|
|
2605
|
-
const [head, ...rest] = p.split(/\r?\n/);
|
|
2606
|
-
const version = /v?(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(head ?? "")?.[1];
|
|
2607
|
-
if (!version) continue;
|
|
2608
|
-
const body = rest.join("\n");
|
|
2609
|
-
const shas = [...new Set([...body.matchAll(/(?:^|[\s[`(])([0-9a-f]{7,40})(?=[\s`\]):]|$)/gm)].map((m) => m[1]))];
|
|
2610
|
-
const prs = [...new Set([...body.matchAll(/(?:#|\/pull\/)(\d+)/g)].map((m) => Number(m[1])))];
|
|
2611
|
-
const date = /(\d{4}-\d{2}-\d{2})/.exec(head ?? "")?.[1];
|
|
2612
|
-
out.push({ version, ...date ? { date } : {}, shas, prs });
|
|
2613
|
-
}
|
|
2614
|
-
return out;
|
|
2615
|
-
}
|
|
2616
|
-
function releasesFromTags(tags, pkg) {
|
|
2617
|
-
const out = [];
|
|
2618
|
-
for (const t of tags) {
|
|
2619
|
-
const m = /^(?:(@?[^@\s]+)@)?v?(\d+\.\d+\.\d+(?:-[\w.]+)?)$/.exec(t.name);
|
|
2620
|
-
if (!m) continue;
|
|
2621
|
-
if (pkg && m[1] && m[1] !== pkg) continue;
|
|
2622
|
-
out.push({ version: m[2], ...t.sha ? { sha: t.sha } : {}, ...t.date ? { date: t.date } : {} });
|
|
2623
|
-
}
|
|
2624
|
-
return out;
|
|
2625
|
-
}
|
|
2626
|
-
function assignVersions(entries, releases) {
|
|
2627
|
-
const dated = releases.filter((r) => r.date).sort((a, b) => Date.parse(a.date) - Date.parse(b.date));
|
|
2628
|
-
return entries.map((e) => {
|
|
2629
|
-
const cited = releases.find((r) => r.shas?.some((s) => e.sha.startsWith(s)) || e.pr && r.prs?.includes(e.pr.number));
|
|
2630
|
-
const byTag = cited ? void 0 : dated.find((r) => r.sha === e.sha || Date.parse(r.date) >= Date.parse(e.date));
|
|
2631
|
-
return { ...e, version: (cited ?? byTag)?.version ?? UNRELEASED };
|
|
2632
|
-
});
|
|
2633
|
-
}
|
|
2634
|
-
const BUMP_RANK = { patch: 0, minor: 1, major: 2 };
|
|
2635
|
-
function versionTimeline(entries, releases = []) {
|
|
2636
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2637
|
-
for (const e of entries) {
|
|
2638
|
-
const v = e.version ?? UNRELEASED;
|
|
2639
|
-
let g = groups.get(v);
|
|
2640
|
-
if (!g) {
|
|
2641
|
-
const rel = releases.find((r) => r.version === v);
|
|
2642
|
-
g = { version: v, pending: v === UNRELEASED, date: rel?.date ?? e.date, entries: [], prs: [], tokenChanges: 0, breaking: false, bump: "patch", authors: [] };
|
|
2643
|
-
groups.set(v, g);
|
|
2644
|
-
}
|
|
2645
|
-
g.entries.push(e.id);
|
|
2646
|
-
if (e.pr && !g.prs.includes(e.pr.number)) g.prs.push(e.pr.number);
|
|
2647
|
-
g.tokenChanges += e.counts.total;
|
|
2648
|
-
g.breaking ||= e.breaking;
|
|
2649
|
-
if (BUMP_RANK[e.bump] > BUMP_RANK[g.bump]) g.bump = e.bump;
|
|
2650
|
-
if (!g.authors.includes(e.author)) g.authors.push(e.author);
|
|
2651
|
-
if (!releases.some((r) => r.version === v && r.date) && Date.parse(e.date) > Date.parse(g.date)) g.date = e.date;
|
|
2652
|
-
}
|
|
2653
|
-
return [...groups.values()].sort((a, b) => a.pending ? -1 : b.pending ? 1 : Date.parse(b.date) - Date.parse(a.date));
|
|
2654
|
-
}
|
|
2655
|
-
const matchesToken = (path, q) => path === q || path.startsWith(`${q.replace(/\.$/, "")}.`);
|
|
2656
|
-
function filterHistory(entries, f = {}) {
|
|
2657
|
-
const q = f.query?.trim().toLowerCase();
|
|
2658
|
-
return entries.filter(
|
|
2659
|
-
(e) => (!f.token || e.changes.some((c) => matchesToken(c.path, f.token) || c.from && matchesToken(c.from, f.token))) && (!f.author || e.author.toLowerCase() === f.author.toLowerCase()) && (!f.tier || e.tier === f.tier) && (!f.version || (e.version ?? UNRELEASED) === f.version) && (f.breaking === void 0 || e.breaking === f.breaking) && (!f.since || Date.parse(e.date) >= Date.parse(f.since)) && (!f.until || Date.parse(e.date) <= Date.parse(f.until) + 86399999) && (!q || `${e.title} ${e.why} ${e.pr ? `#${e.pr.number}` : ""} ${e.sha}`.toLowerCase().includes(q))
|
|
2660
|
-
);
|
|
2661
|
-
}
|
|
2662
|
-
function fileModes(config, file) {
|
|
2663
|
-
for (const layer of config.tokens.layers) {
|
|
2664
|
-
const keys = [];
|
|
2665
|
-
const marked = layer.files.replace(/\{([^}]+)\}/g, (_, k) => {
|
|
2666
|
-
keys.push(k);
|
|
2667
|
-
return "";
|
|
2668
|
-
});
|
|
2669
|
-
const source = globToRegExp(marked).source.replace(/\u0001/g, "([^/.]+)");
|
|
2670
|
-
const m = new RegExp(source).exec(file);
|
|
2671
|
-
if (m) return Object.fromEntries(keys.map((k, i) => [k, m[i + 1]]));
|
|
2672
|
-
}
|
|
2673
|
-
return {};
|
|
2674
|
-
}
|
|
2675
|
-
function modeLabel(modes) {
|
|
2676
|
-
const v = Object.values(modes).filter(Boolean);
|
|
2677
|
-
if (!v.length) return "All modes";
|
|
2678
|
-
const s = v.join(" ");
|
|
2679
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2680
|
-
}
|
|
2681
|
-
function tokenHistory(entries, path, config) {
|
|
2682
|
-
const names = /* @__PURE__ */ new Set([path]);
|
|
2683
|
-
const out = [];
|
|
2684
|
-
const sorted = [...entries].sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
|
2685
|
-
for (const e of sorted) {
|
|
2686
|
-
const hits = e.changes.filter((c) => names.has(c.path));
|
|
2687
|
-
for (const c of hits) {
|
|
2688
|
-
const modes = config ? fileModes(config, c.file) : {};
|
|
2689
|
-
out.push({
|
|
2690
|
-
entryId: e.id,
|
|
2691
|
-
sha: e.sha,
|
|
2692
|
-
date: e.date,
|
|
2693
|
-
author: e.author,
|
|
2694
|
-
title: e.title,
|
|
2695
|
-
why: e.why,
|
|
2696
|
-
...e.pr ? { pr: e.pr } : {},
|
|
2697
|
-
...e.version ? { version: e.version } : {},
|
|
2698
|
-
...e.demo ? { demo: true } : {},
|
|
2699
|
-
kind: c.kind,
|
|
2700
|
-
path: c.path,
|
|
2701
|
-
...c.from ? { from: c.from } : {},
|
|
2702
|
-
file: c.file,
|
|
2703
|
-
modes,
|
|
2704
|
-
modeLabel: modeLabel(modes),
|
|
2705
|
-
fields: c.fields,
|
|
2706
|
-
...c.before !== void 0 ? { before: c.before } : {},
|
|
2707
|
-
...c.after !== void 0 ? { after: c.after } : {},
|
|
2708
|
-
...c.beforeCss !== void 0 ? { beforeCss: c.beforeCss } : {},
|
|
2709
|
-
...c.afterCss !== void 0 ? { afterCss: c.afterCss } : {},
|
|
2710
|
-
...c.descriptionBefore !== void 0 ? { descriptionBefore: c.descriptionBefore } : {},
|
|
2711
|
-
...c.descriptionAfter !== void 0 ? { descriptionAfter: c.descriptionAfter } : {}
|
|
2712
|
-
});
|
|
2713
|
-
}
|
|
2714
|
-
for (const c of hits) if (c.kind === "renamed" && c.from) names.add(c.from);
|
|
2715
|
-
}
|
|
2716
|
-
return out;
|
|
2717
|
-
}
|
|
2718
|
-
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
2719
|
-
const keyOf = (path, file) => `${path}@${file}`;
|
|
2720
|
-
const show = (v) => typeof v === "string" ? v : JSON.stringify(v);
|
|
2721
|
-
function defIn(set, path, file) {
|
|
2722
|
-
return set.defs.find((d) => d.path === path && d.file === file);
|
|
2723
|
-
}
|
|
2724
|
-
function sharesCombo(set, a, b) {
|
|
2725
|
-
return a === b || set.combos().some((c) => {
|
|
2726
|
-
const files = set.layerFiles(c).files;
|
|
2727
|
-
return files.includes(a) && files.includes(b);
|
|
2728
|
-
});
|
|
2729
|
-
}
|
|
2730
|
-
function laterTouching(later, path, file, from) {
|
|
2731
|
-
return later.filter((e) => e.changes.some((c) => c.file === file && (c.path === path || c.from === path || from !== void 0 && (c.path === from || c.from === from)))).sort((a, b) => Date.parse(a.date) - Date.parse(b.date)).map((e) => ({ id: e.id, title: e.title, date: e.date, author: e.author, ...e.pr ? { pr: e.pr.number } : {} }));
|
|
2732
|
-
}
|
|
2733
|
-
function planRevert(input) {
|
|
2734
|
-
const { current, entry } = input;
|
|
2735
|
-
const later = (input.later ?? []).filter((e) => e.id !== entry.id && Date.parse(e.date) >= Date.parse(entry.date));
|
|
2736
|
-
const res = input.resolutions ?? {};
|
|
2737
|
-
const planned = [];
|
|
2738
|
-
const conflicts = [];
|
|
2739
|
-
const skipped = [];
|
|
2740
|
-
const config = current.config;
|
|
2741
|
-
const css = (path, file) => current.resolve(path, current.comboForFile(file)).css;
|
|
2742
|
-
for (const c of entry.changes) {
|
|
2743
|
-
const key = keyOf(c.path, c.file);
|
|
2744
|
-
const label = modeLabel(fileModes(config, c.file));
|
|
2745
|
-
const original = { ...c.before !== void 0 ? { before: c.before } : {}, ...c.after !== void 0 ? { after: c.after } : {}, ...c.beforeCss ? { beforeCss: c.beforeCss } : {}, ...c.afterCss ? { afterCss: c.afterCss } : {} };
|
|
2746
|
-
const conflict = (reason, message, cur, editIfForced, summary) => {
|
|
2747
|
-
const resolution = res[key] ?? "keep-current";
|
|
2748
|
-
conflicts.push({ key, path: c.path, file: c.file, kind: c.kind, reason, message, original, ...cur ? { current: cur } : {}, later: laterTouching(later, c.path, c.file, c.from), resolution });
|
|
2749
|
-
if (resolution === "use-before" && editIfForced) planned.push({ key, path: c.path, file: c.file, summary: summary ?? `${editIfForced.op} ${c.path}`, edit: editIfForced });
|
|
2710
|
+
const near = piece.nearest ? ` The closest the library has today is ${piece.nearest.label} (${piece.nearest.token}, ${piece.nearest.value}).` : "";
|
|
2711
|
+
if (piece.kind === "token" && piece.token) {
|
|
2712
|
+
const t = piece.token;
|
|
2713
|
+
const files = t.files.length ? t.files : [void 0];
|
|
2714
|
+
const edits = files.map((file) => ({
|
|
2715
|
+
op: "add",
|
|
2716
|
+
path: t.path,
|
|
2717
|
+
type: t.type,
|
|
2718
|
+
value: t.value,
|
|
2719
|
+
description: `${cap(piece.name)}: ${forLine(branch)}.${files.length > 1 ? " Same value in every theme: review it in each." : ""}`,
|
|
2720
|
+
...file ? { file } : {}
|
|
2721
|
+
}));
|
|
2722
|
+
return { kind: "token-change", title: `Add ${t.path}`, summary: `${forLine(branch)}: ${piece.summary}${near}`, edits, neededBy };
|
|
2723
|
+
}
|
|
2724
|
+
if (piece.kind === "variant" && piece.component && opts.mode === "suggest") {
|
|
2725
|
+
const request2 = {
|
|
2726
|
+
component: piece.component,
|
|
2727
|
+
kind: "new-variant",
|
|
2728
|
+
title: piece.title,
|
|
2729
|
+
summary: `${forLine(branch)}: ${piece.summary}${near}`,
|
|
2730
|
+
changes: piece.values.slice(0, 6).map((v) => ({ field: v.property ?? v.category, after: v.value })),
|
|
2731
|
+
source: opts.source ?? "mcp",
|
|
2732
|
+
...opts.requestedBy ? { requestedBy: opts.requestedBy } : {}
|
|
2750
2733
|
};
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
...descField ? { description: c.descriptionBefore ?? "" } : {}
|
|
2779
|
-
};
|
|
2780
|
-
const summary = valueField ? `Set ${c.path} back to ${show(c.before)} (${label})` : `Put the old description of ${c.path} back (${label})`;
|
|
2781
|
-
if (valueMoved || descMoved) {
|
|
2782
|
-
conflict("changed-later", `${c.path} changed again after this change: it is now ${cur.css || show(cur.value)}, not ${c.afterCss || show(c.after)}.`, cur, edit, summary);
|
|
2783
|
-
break;
|
|
2784
|
-
}
|
|
2785
|
-
planned.push({ key, path: c.path, file: c.file, summary, edit });
|
|
2786
|
-
break;
|
|
2787
|
-
}
|
|
2788
|
-
case "added": {
|
|
2789
|
-
if (!now) {
|
|
2790
|
-
skipped.push({ key, path: c.path, file: c.file, reason: "Already removed." });
|
|
2791
|
-
break;
|
|
2792
|
-
}
|
|
2793
|
-
const users = current.referencedBy(c.path).filter((u) => u.path !== c.path && sharesCombo(current, u.file, c.file));
|
|
2794
|
-
const edit = { op: "remove", path: c.path, file: c.file };
|
|
2795
|
-
const summary = `Remove ${c.path} (${label})`;
|
|
2796
|
-
if (!same(now.value, c.after)) {
|
|
2797
|
-
conflict("changed-later", `${c.path} changed after it was added: it is now ${css(c.path, c.file) || show(now.value)}.`, { value: now.value, css: css(c.path, c.file) }, edit, summary);
|
|
2798
|
-
break;
|
|
2799
|
-
}
|
|
2800
|
-
if (users.length) {
|
|
2801
|
-
conflict("still-referenced", `${users.length} token(s) alias ${c.path} now (${users.slice(0, 4).map((u) => u.path).join(", ")}${users.length > 4 ? ", ..." : ""}). Removing it would break them.`, { value: now.value, css: css(c.path, c.file) }, edit, summary);
|
|
2802
|
-
break;
|
|
2803
|
-
}
|
|
2804
|
-
planned.push({ key, path: c.path, file: c.file, summary, edit });
|
|
2805
|
-
break;
|
|
2806
|
-
}
|
|
2807
|
-
case "removed": {
|
|
2808
|
-
const edit = { op: "add", path: c.path, file: c.file, value: c.before, ...c.type ? { type: c.type } : {}, ...c.descriptionBefore ? { description: c.descriptionBefore } : {} };
|
|
2809
|
-
const summary = `Add ${c.path} back with ${show(c.before)} (${label})`;
|
|
2810
|
-
if (now) {
|
|
2811
|
-
if (same(now.value, c.before)) skipped.push({ key, path: c.path, file: c.file, reason: "Already back." });
|
|
2812
|
-
else conflict("recreated-later", `${c.path} was added again later with another value (${css(c.path, c.file) || show(now.value)}).`, { value: now.value, css: css(c.path, c.file) });
|
|
2813
|
-
break;
|
|
2814
|
-
}
|
|
2815
|
-
planned.push({ key, path: c.path, file: c.file, summary, edit });
|
|
2816
|
-
break;
|
|
2817
|
-
}
|
|
2818
|
-
case "renamed": {
|
|
2819
|
-
const from = c.from;
|
|
2820
|
-
const back = defIn(current, from, c.file);
|
|
2821
|
-
if (back && !now) {
|
|
2822
|
-
skipped.push({ key, path: c.path, file: c.file, reason: `Already named ${from}.` });
|
|
2823
|
-
break;
|
|
2824
|
-
}
|
|
2825
|
-
if (!now) {
|
|
2826
|
-
conflict("renamed-later", `${c.path} no longer exists in ${c.file}: it was renamed or removed again later.`);
|
|
2827
|
-
break;
|
|
2828
|
-
}
|
|
2829
|
-
if (back) {
|
|
2830
|
-
conflict("recreated-later", `${from} exists again in ${c.file}, so ${c.path} cannot take its old name back.`, { value: back.value, css: css(from, c.file) });
|
|
2831
|
-
break;
|
|
2832
|
-
}
|
|
2833
|
-
if (planned.some((p) => p.edit.op === "rename" && p.path === c.path)) {
|
|
2834
|
-
skipped.push({ key, path: c.path, file: c.file, reason: "Covered by the rename above." });
|
|
2835
|
-
break;
|
|
2836
|
-
}
|
|
2837
|
-
planned.push({ key, path: c.path, file: c.file, summary: `Rename ${c.path} back to ${from} and repoint its aliases`, edit: { op: "rename", path: c.path, newPath: from } });
|
|
2838
|
-
break;
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
|
-
const prLink = entry.pr ? entry.pr.url ? `[#${entry.pr.number}](${entry.pr.url})` : `#${entry.pr.number}` : `commit ${entry.sha.slice(0, 7)}`;
|
|
2843
|
-
const title = `Revert: ${entry.title}`;
|
|
2844
|
-
const lines = [
|
|
2845
|
-
`Reverts ${prLink} ("${entry.title}"), merged ${entry.date.slice(0, 10)} by ${entry.author}.`,
|
|
2846
|
-
"",
|
|
2847
|
-
`Original reason: ${entry.why}`,
|
|
2848
|
-
"",
|
|
2849
|
-
"Bridge computed the inverse token edits against the current base branch, not a blind git revert: tokens that changed again since are listed as conflicts and kept unless a reviewer chose to restore them."
|
|
2850
|
-
];
|
|
2851
|
-
if (conflicts.length) {
|
|
2852
|
-
lines.push("", "### Conflicts", "", "| Token | File | Why | Decision |", "| --- | --- | --- | --- |");
|
|
2853
|
-
for (const k of conflicts) lines.push(`| \`${k.path}\` | ${k.file} | ${k.message.replace(/\|/g, "\\|")}${k.later.length ? ` Later: ${k.later.map((l) => l.pr ? `#${l.pr}` : l.id).join(", ")}.` : ""} | ${k.resolution === "use-before" ? "restore the old value" : "keep current"} |`);
|
|
2854
|
-
}
|
|
2855
|
-
if (skipped.length) lines.push("", `${skipped.length} change(s) were already undone on the base branch and are left alone.`);
|
|
2856
|
-
return finishPlan({ ...input, kind: "revert", title, prTitle: title, summary: lines.join("\n"), planned, conflicts, skipped, source: entry });
|
|
2857
|
-
}
|
|
2858
|
-
function planRestore(input) {
|
|
2859
|
-
const { current, path } = input;
|
|
2860
|
-
const files = [...new Set(current.defs.filter((d) => d.path === path).map((d) => d.file))];
|
|
2861
|
-
if (!files.length) throw new Error(`Token "${path}" does not exist on the base branch. Restoring a removed token is a revert of the change that removed it.`);
|
|
2862
|
-
const file = input.file ?? (files.length === 1 ? files[0] : void 0);
|
|
2863
|
-
if (!file) throw new Error(`"${path}" is defined in ${files.join(", ")}. Pass the file (the mode) to restore.`);
|
|
2864
|
-
if (!files.includes(file)) throw new Error(`"${path}" is not defined in ${file}. It is defined in: ${files.join(", ")}.`);
|
|
2865
|
-
const label = modeLabel(fileModes(current.config, file));
|
|
2866
|
-
const from = input.label ? ` from ${input.label}` : "";
|
|
2867
|
-
const planned = [{ key: keyOf(path, file), path, file, summary: `Set ${path} to ${show(input.value)} (${label})`, edit: { op: "set", path, file, value: input.value } }];
|
|
2868
|
-
const title = `Restore ${path} (${label})${from}`;
|
|
2869
|
-
const item = input.item;
|
|
2870
|
-
const summary = [
|
|
2871
|
-
`Restores \`${path}\` in ${label} to ${show(input.value)}${from}.`,
|
|
2872
|
-
...item ? ["", `That value came from ${item.pr ? item.pr.url ? `[#${item.pr.number}](${item.pr.url})` : `#${item.pr.number}` : `commit ${item.sha.slice(0, 7)}`} ("${item.title}"), ${item.date.slice(0, 10)}, by ${item.author}.`] : []
|
|
2873
|
-
].join("\n");
|
|
2874
|
-
return finishPlan({ ...input, kind: "restore", title, prTitle: title, summary, planned, conflicts: [], skipped: [] });
|
|
2875
|
-
}
|
|
2876
|
-
const issueKey = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
2877
|
-
function finishPlan(p) {
|
|
2878
|
-
const { current } = p;
|
|
2879
|
-
const config = current.config;
|
|
2880
|
-
const edits = p.planned.map((x) => x.edit);
|
|
2881
|
-
const notes = [];
|
|
2882
|
-
const empty = classifyLayered([], [], config);
|
|
2883
|
-
const base = {
|
|
2884
|
-
kind: p.kind,
|
|
2885
|
-
title: p.title,
|
|
2886
|
-
prTitle: p.prTitle,
|
|
2887
|
-
summary: p.summary,
|
|
2888
|
-
...p.source ? { source: { id: p.source.id, sha: p.source.sha, title: p.source.title, ...p.source.pr ? { pr: p.source.pr } : {}, date: p.source.date, author: p.source.author } } : {},
|
|
2889
|
-
planned: p.planned,
|
|
2890
|
-
edits,
|
|
2891
|
-
conflicts: p.conflicts,
|
|
2892
|
-
skipped: p.skipped,
|
|
2893
|
-
branch: `${config.governance.branchPrefix}${slugify(p.title, 48)}`
|
|
2894
|
-
};
|
|
2895
|
-
const nothing = (why) => ({ ...base, status: "nothing-to-do", why, changes: [], contrast: [], newFailures: [], newErrors: [], tier: empty, files: [], commitMessage: "", prBody: "", notes });
|
|
2896
|
-
if (!edits.length) return nothing(p.conflicts.length ? 'Every change conflicts with a later one. Choose "restore the old value" on a conflict to include it.' : "Nothing left to undo: the base branch already has the old values.");
|
|
2897
|
-
let result;
|
|
2898
|
-
try {
|
|
2899
|
-
result = applyTokenEdits(current, edits);
|
|
2900
|
-
} catch (e) {
|
|
2901
|
-
return { ...nothing(e.message), status: "blocked" };
|
|
2902
|
-
}
|
|
2903
|
-
notes.push(...result.notes);
|
|
2904
|
-
const after = new LayeredTokenSet(config, result.files);
|
|
2905
|
-
const changes = diffLayered(current, after);
|
|
2906
|
-
if (!changes.length) return nothing("The edits leave every token as it is now.");
|
|
2907
|
-
const prev = new Set(validateLayered(current).map(issueKey));
|
|
2908
|
-
const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prev.has(issueKey(i)));
|
|
2909
|
-
const pairs = p.pairs ?? [];
|
|
2910
|
-
const modes = config.contrast?.modes;
|
|
2911
|
-
const contrast = pairs.length ? contrastDelta(runLayeredContrast(current, pairs, modes), runLayeredContrast(after, pairs, modes)) : [];
|
|
2912
|
-
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
2913
|
-
const tier = classifyLayered(changes, [], config);
|
|
2914
|
-
const impact = layeredImpact(current, after, changes);
|
|
2915
|
-
const prBody = composeProposalBody({ summary: p.summary, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: p.tool ?? "Bridge", ...p.author ? { author: p.author } : {} });
|
|
2916
|
-
const changeset = config.changesets ? { path: `${config.changesets.dir.replace(/\/$/, "")}/bridge-${slugify(p.title, 48)}.md`, content: formatChangeset(config.changesets.package, tier.bump, p.title.replace(/\.?$/, ".")) } : void 0;
|
|
2917
|
-
const files = [...result.touched.map((f) => ({ path: current.repoPath(f), content: result.files.get(f) })), ...changeset ? [changeset] : []];
|
|
2918
|
-
const commitMessage2 = `${p.kind === "revert" ? "revert(tokens)" : "fix(tokens)"}: ${p.title.replace(/^Revert: /, "")}
|
|
2919
|
-
|
|
2920
|
-
${p.source?.pr ? `Reverts #${p.source.pr.number}. ` : ""}Tier ${tier.tier}. Planned with ${p.tool ?? "Bridge"}.`;
|
|
2921
|
-
const blocked = newErrors.length || newFailures.length;
|
|
2922
|
-
return {
|
|
2923
|
-
...base,
|
|
2924
|
-
status: blocked ? "blocked" : "ready",
|
|
2925
|
-
...blocked ? { why: [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and ") } : {},
|
|
2926
|
-
changes,
|
|
2927
|
-
contrast,
|
|
2928
|
-
newFailures,
|
|
2929
|
-
newErrors,
|
|
2930
|
-
tier,
|
|
2931
|
-
files,
|
|
2932
|
-
...changeset ? { changeset } : {},
|
|
2933
|
-
commitMessage: commitMessage2,
|
|
2934
|
-
prBody,
|
|
2935
|
-
notes
|
|
2936
|
-
};
|
|
2937
|
-
}
|
|
2938
|
-
function planToJson(plan) {
|
|
2939
|
-
return {
|
|
2940
|
-
bridge: "change-plan",
|
|
2941
|
-
kind: plan.kind,
|
|
2942
|
-
status: plan.status,
|
|
2943
|
-
title: plan.prTitle,
|
|
2944
|
-
branch: plan.branch,
|
|
2945
|
-
tier: plan.tier.tier,
|
|
2946
|
-
bump: plan.tier.bump,
|
|
2947
|
-
edits: plan.edits,
|
|
2948
|
-
conflicts: plan.conflicts.map((c) => ({ key: c.key, reason: c.reason, message: c.message, resolution: c.resolution })),
|
|
2949
|
-
files: plan.files,
|
|
2950
|
-
commitMessage: plan.commitMessage,
|
|
2951
|
-
prBody: plan.prBody
|
|
2734
|
+
return { kind: "request", title: piece.title, request: request2, neededBy };
|
|
2735
|
+
}
|
|
2736
|
+
if (piece.kind === "variant" && piece.component) {
|
|
2737
|
+
const values = piece.values.map((v) => `${v.property ?? v.category} ${v.value}`).join(", ");
|
|
2738
|
+
const exploration = {
|
|
2739
|
+
name: piece.title,
|
|
2740
|
+
purpose: `${forLine(branch)}: ${/^[aeiou]/i.test(piece.name) ? "an" : "a"} ${piece.name} ${piece.aspect ?? "style"} on ${piece.component} (${values}).${near}`,
|
|
2741
|
+
useCase: "terminal",
|
|
2742
|
+
experimental: [piece.title],
|
|
2743
|
+
source: opts.source === "figma" ? "figma" : opts.source ?? "mcp"
|
|
2744
|
+
};
|
|
2745
|
+
const brief = [
|
|
2746
|
+
`Add the ${piece.name} ${piece.aspect ?? "style"} to ${piece.component} in the library, on this exploration branch.`,
|
|
2747
|
+
`The product branch writes it by hand: ${values}. Express it with tokens (a component or semantic token per value), add it to the component's props, docs and examples, and keep every existing ${piece.component} unchanged.`,
|
|
2748
|
+
piece.nearest ? `Say in the pull request why ${piece.nearest.label} is not enough, or recommend it instead.` : ""
|
|
2749
|
+
].filter(Boolean).join(" ");
|
|
2750
|
+
return { kind: "exploration", title: piece.title, exploration, neededBy, brief };
|
|
2751
|
+
}
|
|
2752
|
+
const name = piece.component ?? cap(piece.name);
|
|
2753
|
+
const request = {
|
|
2754
|
+
component: name,
|
|
2755
|
+
kind: "new-component",
|
|
2756
|
+
title: `Add ${name}`,
|
|
2757
|
+
summary: `${forLine(branch)}: ${piece.summary}${near}`,
|
|
2758
|
+
changes: piece.values.slice(0, 6).map((v) => ({ field: v.property ?? v.category, after: v.value })),
|
|
2759
|
+
source: opts.source ?? "mcp",
|
|
2760
|
+
...opts.requestedBy ? { requestedBy: opts.requestedBy } : {}
|
|
2952
2761
|
};
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
function buildReport(args) {
|
|
2969
|
-
const entries = assignVersions(args.entries, args.releases).sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
|
2970
|
-
return { ...args, entries, timeline: versionTimeline(entries, args.releases) };
|
|
2971
|
-
}
|
|
2972
|
-
const COMPONENT_SPECS_VERSION = 1;
|
|
2973
|
-
const DEFAULT_COMPONENT_SPECS_FILE = "packages/react/component-specs.json";
|
|
2974
|
-
const SPEC_FIELDS = [
|
|
2975
|
-
"fill",
|
|
2976
|
-
"stroke",
|
|
2977
|
-
"strokeWeight",
|
|
2978
|
-
"strokeTopWeight",
|
|
2979
|
-
"strokeRightWeight",
|
|
2980
|
-
"strokeBottomWeight",
|
|
2981
|
-
"strokeLeftWeight",
|
|
2982
|
-
"cornerRadius",
|
|
2983
|
-
"paddingTop",
|
|
2984
|
-
"paddingRight",
|
|
2985
|
-
"paddingBottom",
|
|
2986
|
-
"paddingLeft",
|
|
2987
|
-
"itemSpacing",
|
|
2988
|
-
"counterAxisSpacing",
|
|
2989
|
-
"width",
|
|
2990
|
-
"height",
|
|
2991
|
-
"minWidth",
|
|
2992
|
-
"minHeight",
|
|
2993
|
-
"opacity",
|
|
2994
|
-
"textFill",
|
|
2995
|
-
"fontFamily",
|
|
2996
|
-
"fontSize",
|
|
2997
|
-
"fontWeight",
|
|
2998
|
-
"lineHeight",
|
|
2999
|
-
"letterSpacing",
|
|
3000
|
-
"textDecoration"
|
|
3001
|
-
];
|
|
3002
|
-
const COLOR_FIELDS = /* @__PURE__ */ new Set(["fill", "stroke", "textFill"]);
|
|
3003
|
-
const TRIGGER_LABELS = {
|
|
3004
|
-
ON_HOVER: "While hovering",
|
|
3005
|
-
ON_PRESS: "While pressing",
|
|
3006
|
-
ON_CLICK: "On click",
|
|
3007
|
-
MOUSE_ENTER: "Mouse enter",
|
|
3008
|
-
MOUSE_LEAVE: "Mouse leave"
|
|
2762
|
+
return { kind: "request", title: `New component: ${name}`, request, neededBy };
|
|
2763
|
+
}
|
|
2764
|
+
const STATUS_LABEL = {
|
|
2765
|
+
draft: "Draft",
|
|
2766
|
+
"in-review": "In review",
|
|
2767
|
+
changes: "Changes requested",
|
|
2768
|
+
approved: "Approved",
|
|
2769
|
+
shipped: "Shipped",
|
|
2770
|
+
closed: "Closed",
|
|
2771
|
+
exploring: "Being built",
|
|
2772
|
+
requested: "Requested",
|
|
2773
|
+
"in-progress": "In progress",
|
|
2774
|
+
done: "Done",
|
|
2775
|
+
declined: "Declined",
|
|
2776
|
+
outdated: "Outdated"
|
|
3009
2777
|
};
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
}
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
}
|
|
3104
|
-
return
|
|
3105
|
-
}
|
|
3106
|
-
function
|
|
3107
|
-
if (
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
return
|
|
3112
|
-
}
|
|
3113
|
-
const
|
|
3114
|
-
function
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
const REDUCED_MOTION_NOTE = "In code, reduced motion turns this off.";
|
|
3129
|
-
function componentMotion(spec) {
|
|
3130
|
-
const keys = new Set(spec.variants.map((v) => variantKeyOf(v.props)));
|
|
3131
|
-
const reactions = [];
|
|
3132
|
-
const triggers = [];
|
|
3133
|
-
for (const x of spec.interactions ?? []) {
|
|
3134
|
-
const only = x.variants ? new Set(x.variants) : void 0;
|
|
3135
|
-
let count = 0;
|
|
3136
|
-
for (const v of spec.variants) {
|
|
3137
|
-
if (v.props[x.axis] !== x.from) continue;
|
|
3138
|
-
const fromKey = variantKeyOf(v.props);
|
|
3139
|
-
if (only && !only.has(fromKey)) continue;
|
|
3140
|
-
const to = { ...v.props, [x.axis]: x.to };
|
|
3141
|
-
const toKey = variantKeyOf(to);
|
|
3142
|
-
if (!keys.has(toKey)) continue;
|
|
3143
|
-
reactions.push({ trigger: x.trigger, label: TRIGGER_LABELS[x.trigger], from: { ...v.props }, to, fromKey, toKey, transition: x.transition });
|
|
3144
|
-
count++;
|
|
3145
|
-
}
|
|
3146
|
-
triggers.push({ ...x, label: TRIGGER_LABELS[x.trigger], count });
|
|
3147
|
-
}
|
|
3148
|
-
const transitions = (spec.interactions ?? []).map((x) => x.transition).filter((t) => !!t);
|
|
3149
|
-
const cannotPlay = [...spec.motion ?? []];
|
|
3150
|
-
const openNote = cannotPlay.find((n) => n.enter || n.exit);
|
|
3151
|
-
const opening = openNote ? { enter: openNote.enter ?? null, exit: openNote.exit ?? null } : void 0;
|
|
3152
|
-
const openTransitions = opening ? [opening.enter, opening.exit].filter((t) => !!t) : [];
|
|
3153
|
-
const tokens = [
|
|
3154
|
-
...new Set([...transitions, ...openTransitions].flatMap((t) => [t.durationToken, t.easingToken]).concat(cannotPlay.map((n) => n.durationToken)).filter((t) => !!t))
|
|
3155
|
-
].sort();
|
|
3156
|
-
const properties = [...new Set(transitions.flatMap((t) => t.properties))].sort();
|
|
3157
|
-
const approximations = [...new Set(transitions.map((t) => t.approximated).filter((t) => !!t))];
|
|
3158
|
-
const animates = transitions.length > 0 || openTransitions.length > 0 || cannotPlay.some((n) => /animation/i.test(n.what));
|
|
3159
|
-
return { name: spec.name, reactions, triggers, tokens, properties, approximations, cannotPlay, opening, reducedMotion: animates ? REDUCED_MOTION_NOTE : void 0 };
|
|
3160
|
-
}
|
|
3161
|
-
function componentsUsingMotionToken(specs, tokenPath, chainOf) {
|
|
3162
|
-
const out = /* @__PURE__ */ new Set();
|
|
3163
|
-
for (const c of specs.components) {
|
|
3164
|
-
for (const t of componentMotion(c).tokens) {
|
|
3165
|
-
if (t === tokenPath || (chainOf?.(t) ?? []).includes(tokenPath)) out.add(c.name);
|
|
3166
|
-
}
|
|
3167
|
-
}
|
|
3168
|
-
return [...out].sort();
|
|
3169
|
-
}
|
|
3170
|
-
function parseComponentSpecs(text) {
|
|
3171
|
-
let data;
|
|
3172
|
-
try {
|
|
3173
|
-
data = JSON.parse(text);
|
|
3174
|
-
} catch {
|
|
3175
|
-
throw new Error("component-specs.json is not valid JSON.");
|
|
3176
|
-
}
|
|
3177
|
-
const f = data;
|
|
3178
|
-
if (!f || typeof f !== "object" || !Array.isArray(f.components)) throw new Error('component-specs.json has no "components" list.');
|
|
3179
|
-
if (f.version !== COMPONENT_SPECS_VERSION) throw new Error(`component-specs.json is version ${String(f.version)}; this tool reads version ${COMPONENT_SPECS_VERSION}.`);
|
|
3180
|
-
return f;
|
|
3181
|
-
}
|
|
3182
|
-
const tokenCssVar = (path, prefix = "--", separator = "-") => `${prefix}${path.split(".").join(separator)}`;
|
|
3183
|
-
function figmaCell(entry) {
|
|
3184
|
-
if (!entry) return void 0;
|
|
3185
|
-
if (entry.status === "generated") return { label: "Yes" };
|
|
3186
|
-
if (entry.status === "drafted") return { label: "Drafted", title: "Drafted with Claude where the code cannot be read on its own: in Figma, check it against the code." };
|
|
3187
|
-
if (entry.status === "layout") return { label: "Auto layout", title: "A layout helper: nothing to draw, use auto layout in Figma." };
|
|
3188
|
-
return { label: "Not yet", ...entry.reason ? { title: `Not supported yet: ${entry.reason}` } : {} };
|
|
2778
|
+
const LINK_STATUS_TONE = {
|
|
2779
|
+
draft: "neutral",
|
|
2780
|
+
"in-review": "info",
|
|
2781
|
+
changes: "warning",
|
|
2782
|
+
approved: "success",
|
|
2783
|
+
shipped: "success",
|
|
2784
|
+
closed: "neutral",
|
|
2785
|
+
exploring: "neutral",
|
|
2786
|
+
requested: "neutral",
|
|
2787
|
+
"in-progress": "info",
|
|
2788
|
+
done: "success",
|
|
2789
|
+
declined: "neutral",
|
|
2790
|
+
outdated: "warning"
|
|
2791
|
+
};
|
|
2792
|
+
const names = (people2) => people2.length <= 2 ? people2.map((p) => `@${p}`).join(" and ") : `@${people2[0]} and ${people2.length - 1} more`;
|
|
2793
|
+
const SUGGESTION_LINK_STATUS = {
|
|
2794
|
+
"needs-review": "in-review",
|
|
2795
|
+
"changes-requested": "changes",
|
|
2796
|
+
approved: "approved",
|
|
2797
|
+
applying: "in-progress",
|
|
2798
|
+
applied: "shipped",
|
|
2799
|
+
declined: "declined",
|
|
2800
|
+
outdated: "outdated"
|
|
2801
|
+
};
|
|
2802
|
+
function linkStatus(x) {
|
|
2803
|
+
const make = (key, people2 = [], verb = "reviews") => {
|
|
2804
|
+
const label = STATUS_LABEL[key];
|
|
2805
|
+
const tail = people2.length ? ` · ${names(people2)} ${people2.length === 1 ? verb : verb.replace(/s$/, "")}` : "";
|
|
2806
|
+
return { key, label, people: people2, line: `${label}${tail}` };
|
|
2807
|
+
};
|
|
2808
|
+
if (x.kind === "suggestion") {
|
|
2809
|
+
const key = SUGGESTION_LINK_STATUS[x.status];
|
|
2810
|
+
return make(key, key === "in-progress" ? [...x.assignees ?? []] : [], "builds");
|
|
2811
|
+
}
|
|
2812
|
+
if (x.kind === "request") {
|
|
2813
|
+
if (x.state === "closed") return make(x.stateReason === "not_planned" ? "declined" : "done");
|
|
2814
|
+
return x.implemented ? make("in-progress", [...x.assignees ?? []], "builds") : make("requested", [...x.assignees ?? []], "builds");
|
|
2815
|
+
}
|
|
2816
|
+
if (x.state === "merged") return make("shipped");
|
|
2817
|
+
if (x.state !== "open") return make("closed");
|
|
2818
|
+
if ((x.labels ?? []).includes("exploration")) return make("exploring");
|
|
2819
|
+
if (x.draft) return make("draft");
|
|
2820
|
+
const latest = /* @__PURE__ */ new Map();
|
|
2821
|
+
for (const r of x.reviews ?? []) if (r.user && r.state !== "COMMENTED") latest.set(r.user, r.state);
|
|
2822
|
+
const states = [...latest.values()];
|
|
2823
|
+
if (states.includes("CHANGES_REQUESTED")) return make("changes", [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([u]) => u), "asks");
|
|
2824
|
+
const waiting = [...x.requestedReviewers ?? []];
|
|
2825
|
+
if (states.includes("APPROVED") && !waiting.length) return make("approved");
|
|
2826
|
+
return make("in-review", waiting);
|
|
2827
|
+
}
|
|
2828
|
+
function libraryPreviewState(x) {
|
|
2829
|
+
if (x.kind === "suggestion") {
|
|
2830
|
+
if (x.lifecycle === "closed") return { key: "declined", sentence: `The library declined this suggestion.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
|
|
2831
|
+
if (x.lifecycle === "merged") return x.behindMain === 0 ? { key: "switched", sentence: "Your branch uses the published version.", action: null } : { key: "published", sentence: "The library applied the suggestion. Switch your branch to the published version.", action: { kind: "switch", label: "Switch to the published version" } };
|
|
2832
|
+
return { key: "nothing-yet", sentence: "Nothing to preview yet: owners review the suggestion, then a developer applies it. Your branch keeps its own value until then.", action: null };
|
|
2833
|
+
}
|
|
2834
|
+
if (x.kind === "request") {
|
|
2835
|
+
if (x.lifecycle === "closed") return { key: "declined", sentence: `The library declined this request.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
|
|
2836
|
+
if (x.lifecycle === "merged") return x.behindMain === 0 ? { key: "switched", sentence: "Your branch uses the published version.", action: null } : { key: "published", sentence: "The library built it. Switch your branch to the published version, then ask Claude to use it.", action: { kind: "switch", label: "Switch to the published version" } };
|
|
2837
|
+
return { key: "nothing-yet", sentence: "Nothing to preview yet: the library builds it first. Your branch keeps its own version until then.", action: null };
|
|
2838
|
+
}
|
|
2839
|
+
if (x.lifecycle === "merged") {
|
|
2840
|
+
if (x.behindMain === 0) return { key: "switched", sentence: "Your branch uses the published version.", action: null };
|
|
2841
|
+
return { key: "published", sentence: "The library published it. Switch your branch to the published version.", action: { kind: "switch", label: "Switch to the published version" } };
|
|
2842
|
+
}
|
|
2843
|
+
if (x.lifecycle === "closed") return { key: "declined", sentence: `The library closed it without shipping.${x.nearest ? ` Ask Claude to use ${x.nearest} instead.` : ""}`, action: null };
|
|
2844
|
+
if (!x.hasChanges) return { key: "nothing-yet", sentence: "The library branch has no change yet. Claude adds it there, then your branch can use its preview.", action: null };
|
|
2845
|
+
if (x.missing === 0) return { key: "preview", sentence: "Your branch uses a preview of the library branch. When the library publishes it, switch to the real version here in one click.", action: null };
|
|
2846
|
+
if (x.missing !== null && x.total !== null && x.missing < x.total) return { key: "outdated", sentence: "The library branch changed since your branch took its preview.", action: { kind: "refresh-preview", label: "Get the latest preview" } };
|
|
2847
|
+
return { key: "not-used", sentence: "Your branch does not use the library branch yet.", action: { kind: "use-preview", label: "Use its preview in my branch" } };
|
|
2848
|
+
}
|
|
2849
|
+
const nPieces = (n) => `${n} ${n === 1 ? "piece" : "pieces"}`;
|
|
2850
|
+
function productBranchSentence(r, opts = {}) {
|
|
2851
|
+
if (!r) return { lead: "Not checked against the library yet.", strong: "", rest: " Ask Claude what is missing from the library." };
|
|
2852
|
+
const n = r.pieces.length;
|
|
2853
|
+
if (!n) return { lead: "Built with the library.", strong: " Nothing is missing", rest: ": every value and component comes from it." };
|
|
2854
|
+
const linked = r.pieces.filter((p) => r.links.some((l) => l.piece === p.id));
|
|
2855
|
+
const openers = [...new Set(linked.map((p) => r.links.find((l) => l.piece === p.id)).map((l) => l.agent ? opts.agentName ?? "Claude AI" : l.openedBy && l.openedBy === opts.viewer ? "you" : l.openedBy ? `@${l.openedBy}` : "someone"))];
|
|
2856
|
+
const by = openers.length === 1 ? openers[0] : "your team";
|
|
2857
|
+
const was = n === 1 ? "was" : "were";
|
|
2858
|
+
if (linked.length === n) {
|
|
2859
|
+
const what = n === 1 ? "a library branch for it" : "library branches for them";
|
|
2860
|
+
return { lead: "Built with the library. ", strong: `${nPieces(n)} ${was} missing`, rest: `, and ${by} opened ${what}.` };
|
|
2861
|
+
}
|
|
2862
|
+
if (!linked.length) return { lead: "Built with the library. ", strong: `${nPieces(n)} ${n === 1 ? "is" : "are"} missing`, rest: n === 1 ? ": open a library branch for it." : ": open a library branch for each." };
|
|
2863
|
+
return { lead: "Built with the library. ", strong: `${nPieces(n)} ${was} missing`, rest: `: ${linked.length} ${linked.length === 1 ? "has" : "have"} a library branch, ${n - linked.length} still ${n - linked.length === 1 ? "needs" : "need"} one.` };
|
|
2864
|
+
}
|
|
2865
|
+
function whyItExists(n, opts = {}) {
|
|
2866
|
+
const who = n.author ? opts.names?.[n.author] ?? `@${n.author}` : "A product team";
|
|
2867
|
+
return { who, title: n.title, product: n.productName };
|
|
2868
|
+
}
|
|
2869
|
+
function nearestQuestion(n, opts = {}) {
|
|
2870
|
+
if (!n.nearest) return null;
|
|
2871
|
+
const who = n.author ? opts.names?.[n.author] ?? `@${n.author}` : "the product team";
|
|
2872
|
+
return `It looks a lot like "${n.nearest}". Is a new one worth it, or should ${who} use ${n.nearest}?`;
|
|
2873
|
+
}
|
|
2874
|
+
function suggestNearestComment(n) {
|
|
2875
|
+
if (!n.nearest) return null;
|
|
2876
|
+
return `Let's not add it to the library: ${n.nearest}${n.nearestToken ? ` (\`${n.nearestToken}\`)` : ""} covers it. Use it on ${n.title} (#${n.pr}) instead, then close this branch.`;
|
|
2877
|
+
}
|
|
2878
|
+
function pieceId(parts) {
|
|
2879
|
+
return slugify(parts.filter(Boolean).join(" "), 48);
|
|
2880
|
+
}
|
|
2881
|
+
const TONE_WORDS = ["neutral", "success", "warning", "danger", "info", "error", "critical", "accent", "brand", "primary", "secondary"];
|
|
2882
|
+
function tokenLabel(path) {
|
|
2883
|
+
const parts = path.split(".");
|
|
2884
|
+
const tone = parts.find((p) => TONE_WORDS.includes(p));
|
|
2885
|
+
if (tone) return tone;
|
|
2886
|
+
const meaningful = parts.filter((p) => !/^(color|bg|fg|text|border|default|base|value|\d+)$/.test(p));
|
|
2887
|
+
return meaningful[meaningful.length - 1] ?? parts[parts.length - 1] ?? path;
|
|
2888
|
+
}
|
|
2889
|
+
function proposedTokenPath(nearest, name, category) {
|
|
2890
|
+
const slug = slugify(name, 32);
|
|
2891
|
+
if (!nearest) return `${category === "color" ? "color" : category}.${slug}`;
|
|
2892
|
+
const parts = nearest.split(".");
|
|
2893
|
+
const toneAt = parts.findIndex((p) => TONE_WORDS.includes(p));
|
|
2894
|
+
if (toneAt > 0) return [...parts.slice(0, toneAt), slug, ...parts.slice(toneAt + 1)].join(".");
|
|
2895
|
+
return [...parts.slice(0, -1), slug].join(".");
|
|
3189
2896
|
}
|
|
3190
2897
|
export {
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
2898
|
+
layoutOptions as $,
|
|
2899
|
+
formatValue as A,
|
|
2900
|
+
resolve as B,
|
|
2901
|
+
moveToken as C,
|
|
2902
|
+
effectiveScopes as D,
|
|
2903
|
+
EXT_ID as E,
|
|
2904
|
+
sameScopes as F,
|
|
2905
|
+
defaultScopes as G,
|
|
2906
|
+
readTokenAt as H,
|
|
2907
|
+
modeFile as I,
|
|
2908
|
+
defaultFile as J,
|
|
2909
|
+
selectTokenFiles as K,
|
|
2910
|
+
LayeredTokenSet as L,
|
|
2911
|
+
resolveBranchPreviews as M,
|
|
2912
|
+
loadConfig as N,
|
|
2913
|
+
tokensPath as O,
|
|
2914
|
+
toConfigJson as P,
|
|
2915
|
+
validateLayered as Q,
|
|
2916
|
+
parseContrastChecks as R,
|
|
3210
2917
|
SINGLE_MODE as S,
|
|
3211
2918
|
TIER_NAMES as T,
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
2919
|
+
classifyRisk as U,
|
|
2920
|
+
CONFIG_FILE as V,
|
|
2921
|
+
RISK_LABELS as W,
|
|
2922
|
+
previewLinks as X,
|
|
2923
|
+
findPreviewComment as Y,
|
|
2924
|
+
previewsSettled as Z,
|
|
3218
2925
|
DEFAULT_TOKEN_PATHS as _,
|
|
3219
|
-
|
|
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
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
packageDirCandidates as p,
|
|
3399
|
-
COLOR_FIELDS as q,
|
|
3400
|
-
releasesFromTags as r,
|
|
3401
|
-
specHash as s,
|
|
3402
|
-
tokenCssVar as t,
|
|
3403
|
-
modeDisplayName as u,
|
|
3404
|
-
validateComponentSpec as v,
|
|
3405
|
-
modesOf as w,
|
|
3406
|
-
isMultiMode as x,
|
|
3407
|
-
TIER_AXIS as y,
|
|
3408
|
-
findReferenceProblems as z
|
|
2926
|
+
proposedTokenPath as a,
|
|
2927
|
+
contextFor as a$,
|
|
2928
|
+
buildTokenSet as a0,
|
|
2929
|
+
readPreviewComment as a1,
|
|
2930
|
+
linkStatus as a2,
|
|
2931
|
+
libraryPreviewState as a3,
|
|
2932
|
+
parsePiecesRecord as a4,
|
|
2933
|
+
libraryBranchPlan as a5,
|
|
2934
|
+
NEEDED_BY_LABEL as a6,
|
|
2935
|
+
withNeededBy as a7,
|
|
2936
|
+
withLink as a8,
|
|
2937
|
+
withPiecesRecord as a9,
|
|
2938
|
+
RIGHTS as aA,
|
|
2939
|
+
RIGHT_LABEL as aB,
|
|
2940
|
+
RISK_TIER as aC,
|
|
2941
|
+
TIER_ORDER as aD,
|
|
2942
|
+
TeamDirectory as aE,
|
|
2943
|
+
VISUAL_CHECK_LABEL as aF,
|
|
2944
|
+
affectedContrast as aG,
|
|
2945
|
+
aliasOf as aH,
|
|
2946
|
+
appForProject as aI,
|
|
2947
|
+
appUrl as aJ,
|
|
2948
|
+
approverEntries as aK,
|
|
2949
|
+
approverLists as aL,
|
|
2950
|
+
baseBranchOf as aM,
|
|
2951
|
+
branchName as aN,
|
|
2952
|
+
branchSlug as aO,
|
|
2953
|
+
canShip as aP,
|
|
2954
|
+
changeKind as aQ,
|
|
2955
|
+
changedModes as aR,
|
|
2956
|
+
changesMode as aS,
|
|
2957
|
+
clearRawDeep as aT,
|
|
2958
|
+
colorToCss as aU,
|
|
2959
|
+
colorsEqual as aV,
|
|
2960
|
+
comboKey as aW,
|
|
2961
|
+
commitMessage as aX,
|
|
2962
|
+
compactJson as aY,
|
|
2963
|
+
composePrBody as aZ,
|
|
2964
|
+
composite as a_,
|
|
2965
|
+
productBranchSentence as aa,
|
|
2966
|
+
mergePiecesRecord as ab,
|
|
2967
|
+
productOfBranch as ac,
|
|
2968
|
+
defaultProduct as ad,
|
|
2969
|
+
slugify as ae,
|
|
2970
|
+
AAA_MIN as af,
|
|
2971
|
+
AA_MIN as ag,
|
|
2972
|
+
CHANGES_MODES as ah,
|
|
2973
|
+
CONTRAST_CELL_MAX as ai,
|
|
2974
|
+
CONTRAST_CHECKS_FILE as aj,
|
|
2975
|
+
ConfigError as ak,
|
|
2976
|
+
DEFAULT_CHANGES_MODE as al,
|
|
2977
|
+
DEFAULT_GOVERNANCE as am,
|
|
2978
|
+
DEFAULT_OVERRIDE as an,
|
|
2979
|
+
DEFAULT_PRODUCTS as ao,
|
|
2980
|
+
DEFAULT_RIGHTS as ap,
|
|
2981
|
+
DEFAULT_VISUAL_IMPACT as aq,
|
|
2982
|
+
DTCG_TYPES as ar,
|
|
2983
|
+
JsonParseError as as,
|
|
2984
|
+
LINK_STATUS_TONE as at,
|
|
2985
|
+
OWNER_GROUPS as au,
|
|
2986
|
+
PREVIEW_APP_LABELS as av,
|
|
2987
|
+
PREVIEW_COMMENT_MARKER as aw,
|
|
2988
|
+
PREVIEW_PLACEHOLDERS as ax,
|
|
2989
|
+
PRIMITIVE_MODE as ay,
|
|
2990
|
+
PRODUCT_LABEL_PREFIX as az,
|
|
2991
|
+
modesOf as b,
|
|
2992
|
+
sameProducts as b$,
|
|
2993
|
+
contrastLevel as b0,
|
|
2994
|
+
contrastPair as b1,
|
|
2995
|
+
contrastRatio as b2,
|
|
2996
|
+
defaultContext as b3,
|
|
2997
|
+
defaultTitle as b4,
|
|
2998
|
+
defineConfig as b5,
|
|
2999
|
+
detectLayout as b6,
|
|
3000
|
+
entriesFor as b7,
|
|
3001
|
+
entryLabel as b8,
|
|
3002
|
+
expandEntries as b9,
|
|
3003
|
+
nearestQuestion as bA,
|
|
3004
|
+
neededBySection as bB,
|
|
3005
|
+
nodesEqual as bC,
|
|
3006
|
+
normalizeRights as bD,
|
|
3007
|
+
overrideKey as bE,
|
|
3008
|
+
ownerEntryProblems as bF,
|
|
3009
|
+
pairKey as bG,
|
|
3010
|
+
parseDimension as bH,
|
|
3011
|
+
parseJson as bI,
|
|
3012
|
+
parseNeededBy as bJ,
|
|
3013
|
+
parseOwnerEntry as bK,
|
|
3014
|
+
parseVercelComment as bL,
|
|
3015
|
+
piecesSection as bM,
|
|
3016
|
+
plainEqual as bN,
|
|
3017
|
+
previewComment as bO,
|
|
3018
|
+
previewLinksOf as bP,
|
|
3019
|
+
previewUrl as bQ,
|
|
3020
|
+
productForPlace as bR,
|
|
3021
|
+
productLabel as bS,
|
|
3022
|
+
productMarks as bT,
|
|
3023
|
+
productProblems as bU,
|
|
3024
|
+
productsOf as bV,
|
|
3025
|
+
projectOfLabel as bW,
|
|
3026
|
+
protectedBranchesOf as bX,
|
|
3027
|
+
renderedColor as bY,
|
|
3028
|
+
rgbaToDtcg as bZ,
|
|
3029
|
+
rightsProblems as b_,
|
|
3030
|
+
expandOwners as ba,
|
|
3031
|
+
fieldChanges as bb,
|
|
3032
|
+
findProduct as bc,
|
|
3033
|
+
findToken as bd,
|
|
3034
|
+
findVercelComment as be,
|
|
3035
|
+
flattenLayerFile as bf,
|
|
3036
|
+
formatNumber as bg,
|
|
3037
|
+
formatOwnerEntry as bh,
|
|
3038
|
+
fromPlain as bi,
|
|
3039
|
+
getChild as bj,
|
|
3040
|
+
getEntry as bk,
|
|
3041
|
+
globToRegExp as bl,
|
|
3042
|
+
groupsPhrase as bm,
|
|
3043
|
+
hexToRgba as bn,
|
|
3044
|
+
holdersOf as bo,
|
|
3045
|
+
indexByPath as bp,
|
|
3046
|
+
inheritedTypeAt as bq,
|
|
3047
|
+
installCommand as br,
|
|
3048
|
+
installCommandFromPreviewComment as bs,
|
|
3049
|
+
isGuardrail as bt,
|
|
3050
|
+
isTeamEntry as bu,
|
|
3051
|
+
isTokenFile as bv,
|
|
3052
|
+
isVercelComment as bw,
|
|
3053
|
+
kindForMin as bx,
|
|
3054
|
+
layeredParityIssues as by,
|
|
3055
|
+
luminance as bz,
|
|
3056
|
+
TIER_AXIS as c,
|
|
3057
|
+
sameToken as c0,
|
|
3058
|
+
serializeJson as c1,
|
|
3059
|
+
shipPolicy as c2,
|
|
3060
|
+
snapshotUrl as c3,
|
|
3061
|
+
storybookUrl as c4,
|
|
3062
|
+
suggestNearestComment as c5,
|
|
3063
|
+
summarizeTokenChange as c6,
|
|
3064
|
+
tarballAssetName as c7,
|
|
3065
|
+
tarballUrl as c8,
|
|
3066
|
+
teamKey as c9,
|
|
3067
|
+
teamKeyOf as ca,
|
|
3068
|
+
teamUnreadableMessage as cb,
|
|
3069
|
+
teamsIn as cc,
|
|
3070
|
+
toAlias as cd,
|
|
3071
|
+
toMap as ce,
|
|
3072
|
+
toPlain as cf,
|
|
3073
|
+
tokenBump as cg,
|
|
3074
|
+
tokenNode as ch,
|
|
3075
|
+
unreadableShipTeams as ci,
|
|
3076
|
+
unreadableTeams as cj,
|
|
3077
|
+
valueIn as ck,
|
|
3078
|
+
valueToCss as cl,
|
|
3079
|
+
visualImpactProblems as cm,
|
|
3080
|
+
whoCanShipSentence as cn,
|
|
3081
|
+
whyItExists as co,
|
|
3082
|
+
collectRefs as d,
|
|
3083
|
+
evaluatePairs as e,
|
|
3084
|
+
findReferenceProblems as f,
|
|
3085
|
+
contrastScope as g,
|
|
3086
|
+
contextLabel as h,
|
|
3087
|
+
isMultiMode as i,
|
|
3088
|
+
isAlias as j,
|
|
3089
|
+
aliasTarget as k,
|
|
3090
|
+
pathId as l,
|
|
3091
|
+
modeDisplayName as m,
|
|
3092
|
+
flattenTokens as n,
|
|
3093
|
+
threeWayDiff as o,
|
|
3094
|
+
pieceId as p,
|
|
3095
|
+
emptyTokenFile as q,
|
|
3096
|
+
removeToken as r,
|
|
3097
|
+
serializeTokenFile as s,
|
|
3098
|
+
tokenLabel as t,
|
|
3099
|
+
upsertToken as u,
|
|
3100
|
+
valuesEqual as v,
|
|
3101
|
+
parseTokenFile as w,
|
|
3102
|
+
EXT_SCOPES as x,
|
|
3103
|
+
parseColor as y,
|
|
3104
|
+
rgbaToHex as z
|
|
3409
3105
|
};
|
|
3410
|
-
//# sourceMappingURL=
|
|
3106
|
+
//# sourceMappingURL=prod-branch-BdMPKffE.js.map
|