@bridge-ds/mcp 0.0.0-stage → 0.2.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 +130 -2
- package/dist/audit.d.ts +15 -0
- package/dist/brand.d.ts +263 -0
- package/dist/chunk-6JO2AG6X.js +7365 -0
- package/dist/chunk-FJL6UTZG.js +1317 -0
- package/dist/components.d.ts +55 -0
- package/dist/config.d.ts +13 -0
- package/dist/context.d.ts +42 -0
- package/dist/core-adapter.d.ts +48 -0
- package/dist/drafts.d.ts +46 -0
- package/dist/explorations.d.ts +168 -0
- package/dist/history.d.ts +431 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +33 -0
- package/dist/lib.d.ts +19 -0
- package/dist/lib.js +46 -0
- package/dist/markdown.d.ts +16 -0
- package/dist/ops.d.ts +721 -0
- package/dist/requests.d.ts +89 -0
- package/dist/server.d.ts +4 -0
- package/dist/source.d.ts +39 -0
- package/dist/src-ABJPOTKE.js +1125 -0
- package/package.json +65 -4
|
@@ -0,0 +1,1317 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// ../core/src/previews.ts
|
|
8
|
+
var PREVIEW_APP_LABELS = { docs: "Docs", storybook: "Storybook", admin: "Admin demo" };
|
|
9
|
+
function branchSlug(branch) {
|
|
10
|
+
return branch.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
11
|
+
}
|
|
12
|
+
function previewUrl(template, ctx) {
|
|
13
|
+
if (!template) return null;
|
|
14
|
+
const values = {
|
|
15
|
+
branch: branchSlug(ctx.branch),
|
|
16
|
+
rawBranch: encodeURIComponent(ctx.branch),
|
|
17
|
+
pr: ctx.pr === void 0 ? void 0 : String(ctx.pr),
|
|
18
|
+
sha: ctx.sha,
|
|
19
|
+
sha7: ctx.sha?.slice(0, 7)
|
|
20
|
+
};
|
|
21
|
+
let missing = false;
|
|
22
|
+
const filled = template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
23
|
+
const v = values[key];
|
|
24
|
+
if (!v) missing = true;
|
|
25
|
+
return v ?? "";
|
|
26
|
+
});
|
|
27
|
+
if (missing) return null;
|
|
28
|
+
let url;
|
|
29
|
+
try {
|
|
30
|
+
url = new URL(filled);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
35
|
+
if (url.hostname.split(".").some((label) => label.length > 63)) return null;
|
|
36
|
+
return url.href;
|
|
37
|
+
}
|
|
38
|
+
function previewLinks(apps, ctx) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const app of Object.keys(PREVIEW_APP_LABELS)) {
|
|
41
|
+
const url = previewUrl(apps[app], ctx);
|
|
42
|
+
if (url) out.push({ app, label: PREVIEW_APP_LABELS[app], url });
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function snapshotUrl(owner, repo, pkg, ref) {
|
|
47
|
+
return `https://pkg.pr.new/${owner}/${repo}/${pkg}@${ref}`;
|
|
48
|
+
}
|
|
49
|
+
function installCommand(urls, pm = "npm") {
|
|
50
|
+
const verb = pm === "npm" ? "npm i" : pm === "pnpm" ? "pnpm add" : "yarn add";
|
|
51
|
+
return `${verb} ${urls.join(" ")}`;
|
|
52
|
+
}
|
|
53
|
+
function tarballAssetName(pkg, sha) {
|
|
54
|
+
return `${pkg.replace(/^@/, "").replace(/\//g, "-")}-${sha.slice(0, 7)}.tgz`;
|
|
55
|
+
}
|
|
56
|
+
function tarballUrl(owner, repo, pr, pkg, sha) {
|
|
57
|
+
return `https://github.com/${owner}/${repo}/releases/download/preview-pr-${pr}/${tarballAssetName(pkg, sha)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ../core/src/config.ts
|
|
61
|
+
var CONFIG_FILE = "bridge.config.json";
|
|
62
|
+
var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
|
|
63
|
+
var DEFAULT_TOKEN_PATHS = {
|
|
64
|
+
root: "tokens",
|
|
65
|
+
primitives: "primitives",
|
|
66
|
+
brands: "brands",
|
|
67
|
+
semantic: "semantic",
|
|
68
|
+
base: "base",
|
|
69
|
+
density: "density",
|
|
70
|
+
components: "components",
|
|
71
|
+
contrastChecks: "contrast.checks.json"
|
|
72
|
+
};
|
|
73
|
+
var DEFAULT_GOVERNANCE = {
|
|
74
|
+
protectedBranches: ["main", "master"],
|
|
75
|
+
branchPrefix: "bridge/",
|
|
76
|
+
labels: ["source: mcp"],
|
|
77
|
+
guardrails: ["AGENTS.md", "CLAUDE.md", "docs/GOVERNANCE.md", ".stylelintrc.json", ".github/", CONFIG_FILE],
|
|
78
|
+
owners: { design: [], tech: [] }
|
|
79
|
+
};
|
|
80
|
+
var FLAT_TOKEN_KEYS = ["primitives", "brands", "semantic", "base", "density", "components", "contrastChecks"];
|
|
81
|
+
var CANONICAL_TOKEN_KEYS = ["root", "include", "exclude", "tiers", "layers"];
|
|
82
|
+
var FLAT_MODE_KEYS = ["brands", "themes", "densities", "defaults"];
|
|
83
|
+
var AXIS_OF = { brands: "brand", themes: "theme", densities: "density" };
|
|
84
|
+
var ConfigError = class extends Error {
|
|
85
|
+
constructor(problems) {
|
|
86
|
+
super(`${CONFIG_FILE} is invalid: ${problems.join("; ")}`);
|
|
87
|
+
this.problems = problems;
|
|
88
|
+
this.name = "ConfigError";
|
|
89
|
+
}
|
|
90
|
+
problems;
|
|
91
|
+
};
|
|
92
|
+
var trimSlashes = (p) => p.trim().replace(/^\.?\/+|\/+$/g, "");
|
|
93
|
+
var list = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
|
|
94
|
+
var isFlatModes = (m) => Object.keys(m).some((k) => FLAT_MODE_KEYS.includes(k)) || Object.keys(m).length === 0;
|
|
95
|
+
function parseRepoInput(repo, branch) {
|
|
96
|
+
if (!repo) return void 0;
|
|
97
|
+
if (typeof repo === "string") {
|
|
98
|
+
const [owner, name] = repo.split("/");
|
|
99
|
+
return { owner, name, defaultBranch: branch ?? "main" };
|
|
100
|
+
}
|
|
101
|
+
return { owner: repo.owner, name: repo.name, defaultBranch: repo.defaultBranch ?? branch ?? "main" };
|
|
102
|
+
}
|
|
103
|
+
function conventionalLayers(p) {
|
|
104
|
+
return [
|
|
105
|
+
{ files: `${p.primitives}/*.tokens.json`, optional: false },
|
|
106
|
+
{ files: `${p.semantic}/${p.base}.tokens.json`, optional: true },
|
|
107
|
+
{ files: `${p.brands}/{brand}.tokens.json`, optional: false },
|
|
108
|
+
{ files: `${p.semantic}/{theme}.tokens.json`, optional: false },
|
|
109
|
+
{ files: `${p.brands}/{brand}.{theme}.tokens.json`, optional: true },
|
|
110
|
+
{ files: `${p.density}/{density}.tokens.json`, optional: false },
|
|
111
|
+
{ files: `${p.components}/*.tokens.json`, optional: false }
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
function defineConfig(input) {
|
|
115
|
+
const t = input.tokens ?? {};
|
|
116
|
+
const root = trimSlashes(t.root ?? DEFAULT_TOKEN_PATHS.root) || ".";
|
|
117
|
+
const tiersIn = t.tiers ? Object.fromEntries(Object.entries(t.tiers).map(([k, v]) => [k, list(v).map(trimSlashes)])) : void 0;
|
|
118
|
+
const firstDir = (tier) => tiersIn?.[tier]?.find((d) => !d.endsWith(".json"));
|
|
119
|
+
const baseFile = tiersIn?.base?.find((d) => d.endsWith(".json"));
|
|
120
|
+
const contrastPath = input.contrast?.path ? trimSlashes(input.contrast.path) : void 0;
|
|
121
|
+
const underRoot = (p) => root === "." ? p : p.startsWith(`${root}/`) ? p.slice(root.length + 1) : void 0;
|
|
122
|
+
const paths = {
|
|
123
|
+
root,
|
|
124
|
+
primitives: trimSlashes(t.primitives ?? firstDir("primitive") ?? DEFAULT_TOKEN_PATHS.primitives),
|
|
125
|
+
brands: trimSlashes(t.brands ?? firstDir("brand") ?? DEFAULT_TOKEN_PATHS.brands),
|
|
126
|
+
semantic: trimSlashes(t.semantic ?? firstDir("semantic") ?? DEFAULT_TOKEN_PATHS.semantic),
|
|
127
|
+
base: trimSlashes(t.base ?? (baseFile ? baseFile.split("/").pop().replace(/(\.tokens)?\.json$/, "") : DEFAULT_TOKEN_PATHS.base)),
|
|
128
|
+
density: trimSlashes(t.density ?? firstDir("density") ?? DEFAULT_TOKEN_PATHS.density),
|
|
129
|
+
components: trimSlashes(t.components ?? firstDir("component") ?? DEFAULT_TOKEN_PATHS.components),
|
|
130
|
+
contrastChecks: trimSlashes(t.contrastChecks ?? (contrastPath ? underRoot(contrastPath) ?? contrastPath : DEFAULT_TOKEN_PATHS.contrastChecks))
|
|
131
|
+
};
|
|
132
|
+
const tiers = tiersIn ?? {
|
|
133
|
+
primitive: [paths.primitives],
|
|
134
|
+
brand: [paths.brands],
|
|
135
|
+
base: [`${paths.semantic}/${paths.base}.tokens.json`],
|
|
136
|
+
semantic: [paths.semantic],
|
|
137
|
+
density: [paths.density],
|
|
138
|
+
component: [paths.components]
|
|
139
|
+
};
|
|
140
|
+
const layers = t.layers?.length ? t.layers.map((l) => typeof l === "string" ? { files: l, optional: false } : { files: l.files, optional: l.optional ?? false }) : conventionalLayers(paths);
|
|
141
|
+
const axes = {};
|
|
142
|
+
const m = input.modes ?? {};
|
|
143
|
+
if (isFlatModes(m)) {
|
|
144
|
+
for (const key of ["brands", "themes", "densities"]) {
|
|
145
|
+
const values = m[key] ?? [];
|
|
146
|
+
if (!values.length) continue;
|
|
147
|
+
const axis = AXIS_OF[key];
|
|
148
|
+
const def = m.defaults?.[axis] ?? values[0];
|
|
149
|
+
axes[axis] = { values, default: def, parity: axis === "theme" };
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
for (const [name, a] of Object.entries(m)) axes[name] = { values: a.values, default: a.default ?? a.values[0], parity: a.parity ?? false };
|
|
153
|
+
}
|
|
154
|
+
const ordered = (axis) => {
|
|
155
|
+
const a = axes[axis];
|
|
156
|
+
return a ? [a.default, ...a.values.filter((v) => v !== a.default)] : [];
|
|
157
|
+
};
|
|
158
|
+
const modes = {
|
|
159
|
+
brands: ordered("brand"),
|
|
160
|
+
themes: ordered("theme"),
|
|
161
|
+
densities: ordered("density"),
|
|
162
|
+
defaults: { brand: axes.brand?.default, theme: axes.theme?.default, density: axes.density?.default }
|
|
163
|
+
};
|
|
164
|
+
const pkg = input.changesets?.package ?? input.changeset?.package;
|
|
165
|
+
const g = input.governance ?? {};
|
|
166
|
+
const contrastFile = contrastPath ?? [root === "." ? "" : root, paths.contrastChecks].filter(Boolean).join("/");
|
|
167
|
+
return {
|
|
168
|
+
repo: parseRepoInput(input.repo, input.branch),
|
|
169
|
+
tokens: { ...paths, include: input.tokens?.include ?? ["**/*.tokens.json"], exclude: input.tokens?.exclude ?? [], tiers, layers },
|
|
170
|
+
axes,
|
|
171
|
+
modes,
|
|
172
|
+
contrast: { path: contrastFile, modes: input.contrast?.modes },
|
|
173
|
+
components: input.components ? {
|
|
174
|
+
dir: trimSlashes(input.components.dir),
|
|
175
|
+
docsSuffix: input.components.docsSuffix ?? ".docs.json",
|
|
176
|
+
styleSuffix: input.components.styleSuffix ?? ".module.css",
|
|
177
|
+
reference: input.components.reference,
|
|
178
|
+
index: input.components.index,
|
|
179
|
+
specs: input.components.specs
|
|
180
|
+
} : void 0,
|
|
181
|
+
docs: { ...input.docs },
|
|
182
|
+
changesets: pkg ? { dir: trimSlashes(input.changesets?.dir ?? ".changeset"), package: pkg } : void 0,
|
|
183
|
+
changeset: pkg ? { package: pkg } : void 0,
|
|
184
|
+
cssVar: { prefix: input.cssVar?.prefix ?? "--", separator: input.cssVar?.separator ?? "-" },
|
|
185
|
+
governance: {
|
|
186
|
+
protectedBranches: g.protectedBranches ?? DEFAULT_GOVERNANCE.protectedBranches,
|
|
187
|
+
branchPrefix: g.branchPrefix ?? DEFAULT_GOVERNANCE.branchPrefix,
|
|
188
|
+
labels: g.labels ?? DEFAULT_GOVERNANCE.labels,
|
|
189
|
+
guardrails: g.guardrails ?? [...DEFAULT_GOVERNANCE.guardrails, contrastFile],
|
|
190
|
+
owners: { design: g.owners?.design ?? [], tech: g.owners?.tech ?? [] },
|
|
191
|
+
...g.override ? {
|
|
192
|
+
override: {
|
|
193
|
+
allowed: g.override.allowed ?? DEFAULT_OVERRIDE.allowed,
|
|
194
|
+
requireReason: g.override.requireReason ?? DEFAULT_OVERRIDE.requireReason,
|
|
195
|
+
minReasonLength: g.override.minReasonLength ?? DEFAULT_OVERRIDE.minReasonLength
|
|
196
|
+
}
|
|
197
|
+
} : {}
|
|
198
|
+
},
|
|
199
|
+
...input.audit ? { audit: { ...input.audit } } : {},
|
|
200
|
+
...input.integrations ? { integrations: { ...input.integrations } } : {},
|
|
201
|
+
...input.previews ? {
|
|
202
|
+
previews: {
|
|
203
|
+
packages: input.previews.packages ?? [],
|
|
204
|
+
apps: { ...input.previews.apps },
|
|
205
|
+
...input.previews.production ? { production: { ...input.previews.production } } : {}
|
|
206
|
+
}
|
|
207
|
+
} : {}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
var isStr = (x) => typeof x === "string" && x.trim().length > 0;
|
|
211
|
+
var isStrList = (x) => Array.isArray(x) && x.every(isStr);
|
|
212
|
+
var isObj = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
213
|
+
function loadConfig(source) {
|
|
214
|
+
let data = source;
|
|
215
|
+
if (typeof source === "string") {
|
|
216
|
+
try {
|
|
217
|
+
data = JSON.parse(source);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
throw new ConfigError([`not valid JSON (${err.message})`]);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!isObj(data)) throw new ConfigError(["top level must be an object"]);
|
|
223
|
+
const problems = [];
|
|
224
|
+
const d = data;
|
|
225
|
+
const repo = d.repo;
|
|
226
|
+
if (typeof repo === "string") {
|
|
227
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) problems.push('"repo" must be "owner/name" or { "owner", "name" }');
|
|
228
|
+
} else if (repo !== void 0) {
|
|
229
|
+
if (!isObj(repo)) problems.push('"repo" must be "owner/name" or { "owner", "name" }');
|
|
230
|
+
else {
|
|
231
|
+
if (!isStr(repo.owner)) problems.push('"repo.owner" must be a non-empty string');
|
|
232
|
+
if (!isStr(repo.name)) problems.push('"repo.name" must be a non-empty string');
|
|
233
|
+
if (repo.defaultBranch !== void 0 && !isStr(repo.defaultBranch)) problems.push('"repo.defaultBranch" must be a non-empty string');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (d.branch !== void 0 && !isStr(d.branch)) problems.push('"branch" must be a non-empty string');
|
|
237
|
+
const tokens = d.tokens;
|
|
238
|
+
if (tokens !== void 0) {
|
|
239
|
+
if (!isObj(tokens)) problems.push('"tokens" must be an object');
|
|
240
|
+
else
|
|
241
|
+
for (const [k, v] of Object.entries(tokens)) {
|
|
242
|
+
if (FLAT_TOKEN_KEYS.includes(k) || k === "root") {
|
|
243
|
+
if (!isStr(v)) problems.push(`"tokens.${k}" must be a non-empty string`);
|
|
244
|
+
} else if (k === "include" || k === "exclude") {
|
|
245
|
+
if (!isStrList(v)) problems.push(`"tokens.${k}" must be a list of globs`);
|
|
246
|
+
} else if (k === "tiers") {
|
|
247
|
+
if (!isObj(v) || !Object.values(v).every((x) => isStr(x) || isStrList(x))) problems.push('"tokens.tiers" must map tier names to directories');
|
|
248
|
+
} else if (k === "layers") {
|
|
249
|
+
if (!Array.isArray(v) || !v.length || !v.every((l) => isStr(l) || isObj(l) && isStr(l.files) && (l.optional === void 0 || typeof l.optional === "boolean")))
|
|
250
|
+
problems.push('"tokens.layers" must be a non-empty list of { "files", "optional"? }');
|
|
251
|
+
} else if (!CANONICAL_TOKEN_KEYS.includes(k)) problems.push(`"tokens.${k}" is not a known key`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const modes = d.modes;
|
|
255
|
+
let axisNames = [];
|
|
256
|
+
if (modes !== void 0) {
|
|
257
|
+
if (!isObj(modes)) problems.push('"modes" must be an object');
|
|
258
|
+
else if (isFlatModes(modes)) {
|
|
259
|
+
for (const k of ["brands", "themes", "densities"]) {
|
|
260
|
+
if (modes[k] !== void 0 && !isStrList(modes[k])) problems.push(`"modes.${k}" must be a list of names`);
|
|
261
|
+
}
|
|
262
|
+
const defs = modes.defaults;
|
|
263
|
+
const lists = { brand: "brands", theme: "themes", density: "densities" };
|
|
264
|
+
for (const [k, l] of Object.entries(lists)) {
|
|
265
|
+
const v = defs?.[k];
|
|
266
|
+
if (v === void 0) continue;
|
|
267
|
+
if (!isStr(v)) problems.push(`"modes.defaults.${k}" must be a string`);
|
|
268
|
+
else if (isStrList(modes[l]) && !modes[l].includes(v)) problems.push(`"modes.defaults.${k}" ("${v}") is not in "modes.${l}"`);
|
|
269
|
+
}
|
|
270
|
+
axisNames = Object.entries(lists).filter(([, l]) => isStrList(modes[l]) && modes[l].length).map(([k]) => k);
|
|
271
|
+
} else {
|
|
272
|
+
for (const [name, a] of Object.entries(modes)) {
|
|
273
|
+
if (!isObj(a) || !isStrList(a.values) || !a.values.length) {
|
|
274
|
+
problems.push(`"modes.${name}.values" must be a non-empty list of names`);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (a.default !== void 0 && (!isStr(a.default) || !a.values.includes(a.default))) problems.push(`"modes.${name}.default" ("${String(a.default)}") is not in "modes.${name}.values"`);
|
|
278
|
+
if (a.parity !== void 0 && typeof a.parity !== "boolean") problems.push(`"modes.${name}.parity" must be true or false`);
|
|
279
|
+
axisNames.push(name);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (isObj(tokens) && Array.isArray(tokens.layers)) {
|
|
284
|
+
for (const l of tokens.layers) {
|
|
285
|
+
const files = typeof l === "string" ? l : isObj(l) ? String(l.files) : "";
|
|
286
|
+
for (const ph of files.match(/\{([^}]+)\}/g) ?? []) {
|
|
287
|
+
if (!axisNames.includes(ph.slice(1, -1))) problems.push(`layer "${files}" uses ${ph} but "modes.${ph.slice(1, -1)}" is not defined`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const contrast = d.contrast;
|
|
292
|
+
if (contrast !== void 0 && (!isObj(contrast) || !isStr(contrast.path) || contrast.modes !== void 0 && !isStrList(contrast.modes)))
|
|
293
|
+
problems.push('"contrast" must be { "path", "modes"? }');
|
|
294
|
+
const comps = d.components;
|
|
295
|
+
if (comps !== void 0 && (!isObj(comps) || !isStr(comps.dir))) problems.push('"components.dir" must be a non-empty string');
|
|
296
|
+
const cs = d.changeset;
|
|
297
|
+
if (cs !== void 0 && (!isObj(cs) || !isStr(cs.package))) problems.push('"changeset.package" must be a non-empty string');
|
|
298
|
+
const css = d.changesets;
|
|
299
|
+
if (css !== void 0 && (!isObj(css) || !isStr(css.package))) problems.push('"changesets.package" must be a non-empty string');
|
|
300
|
+
const gov = d.governance;
|
|
301
|
+
if (gov !== void 0) {
|
|
302
|
+
if (!isObj(gov)) problems.push('"governance" must be an object');
|
|
303
|
+
else {
|
|
304
|
+
for (const k of ["protectedBranches", "labels", "guardrails"]) if (gov[k] !== void 0 && !(Array.isArray(gov[k]) && gov[k].every((x) => typeof x === "string"))) problems.push(`"governance.${k}" must be a list of strings`);
|
|
305
|
+
if (gov.branchPrefix !== void 0 && !isStr(gov.branchPrefix)) problems.push('"governance.branchPrefix" must be a non-empty string');
|
|
306
|
+
const o = gov.owners;
|
|
307
|
+
if (o !== void 0 && (!isObj(o) || ["design", "tech"].some((k) => o[k] !== void 0 && !(Array.isArray(o[k]) && o[k].every((x) => typeof x === "string")))))
|
|
308
|
+
problems.push('"governance.owners" must be { "design": [logins], "tech": [logins] }');
|
|
309
|
+
const ov = gov.override;
|
|
310
|
+
if (ov !== void 0) {
|
|
311
|
+
if (!isObj(ov)) problems.push('"governance.override" must be an object');
|
|
312
|
+
else {
|
|
313
|
+
if (ov.allowed !== void 0 && !isStrList(ov.allowed)) problems.push('"governance.override.allowed" must be a list of GitHub logins');
|
|
314
|
+
if (ov.requireReason !== void 0 && typeof ov.requireReason !== "boolean") problems.push('"governance.override.requireReason" must be true or false');
|
|
315
|
+
if (ov.minReasonLength !== void 0 && !(Number.isInteger(ov.minReasonLength) && ov.minReasonLength >= 0)) problems.push('"governance.override.minReasonLength" must be a whole number, 0 or more');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const audit = d.audit;
|
|
321
|
+
if (audit !== void 0) {
|
|
322
|
+
if (!isObj(audit)) problems.push('"audit" must be an object');
|
|
323
|
+
else for (const k of ["packages", "foreignLibraries", "nativeElements", "exclude"]) if (audit[k] !== void 0 && !isStrList(audit[k])) problems.push(`"audit.${k}" must be a list of strings`);
|
|
324
|
+
}
|
|
325
|
+
const pv = d.previews;
|
|
326
|
+
if (pv !== void 0) {
|
|
327
|
+
if (!isObj(pv)) problems.push('"previews" must be an object');
|
|
328
|
+
else {
|
|
329
|
+
if (pv.packages !== void 0 && !isStrList(pv.packages)) problems.push('"previews.packages" must be a list of package names');
|
|
330
|
+
for (const field of ["apps", "production"]) {
|
|
331
|
+
const apps = pv[field];
|
|
332
|
+
if (apps === void 0) continue;
|
|
333
|
+
if (!isObj(apps)) {
|
|
334
|
+
problems.push(`"previews.${field}" must be an object`);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
for (const [k, v] of Object.entries(apps)) {
|
|
338
|
+
if (!(k in PREVIEW_APP_LABELS)) problems.push(`"previews.${field}.${k}" is not a known app (${Object.keys(PREVIEW_APP_LABELS).join(", ")})`);
|
|
339
|
+
else if (!isStr(v) || !/^https?:\/\//.test(v)) problems.push(`"previews.${field}.${k}" must be an http(s) URL${field === "apps" ? " template" : ""}`);
|
|
340
|
+
else if (field === "production" && /\{\w+\}/.test(v)) problems.push(`"previews.production.${k}" is a fixed URL: placeholders such as {branch} belong in "previews.apps"`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (problems.length) throw new ConfigError(problems);
|
|
346
|
+
return defineConfig(d);
|
|
347
|
+
}
|
|
348
|
+
var tokensPath = (config, rel = "") => [config.tokens.root === "." ? "" : config.tokens.root, rel].filter(Boolean).join("/");
|
|
349
|
+
var protectedBranchesOf = (config) => [.../* @__PURE__ */ new Set([...config.governance.protectedBranches, ...config.repo ? [config.repo.defaultBranch] : []])];
|
|
350
|
+
var baseBranchOf = (config) => config.repo?.defaultBranch ?? "main";
|
|
351
|
+
function toConfigJson(config, schema = "./packages/core/bridge.config.schema.json") {
|
|
352
|
+
return {
|
|
353
|
+
$schema: schema,
|
|
354
|
+
version: 1,
|
|
355
|
+
...config.repo ? { repo: config.repo } : {},
|
|
356
|
+
tokens: { root: config.tokens.root, include: config.tokens.include, ...config.tokens.exclude.length ? { exclude: config.tokens.exclude } : {}, tiers: config.tokens.tiers, layers: config.tokens.layers.map((l) => l.optional ? l : { files: l.files }) },
|
|
357
|
+
modes: Object.fromEntries(Object.entries(config.axes).map(([k, a]) => [k, a.parity ? a : { values: a.values, default: a.default }])),
|
|
358
|
+
...config.contrast ? { contrast: config.contrast.modes ? config.contrast : { path: config.contrast.path } } : {},
|
|
359
|
+
...config.components ? { components: Object.fromEntries(Object.entries(config.components).filter(([, v]) => v !== void 0)) } : {},
|
|
360
|
+
docs: Object.fromEntries(Object.entries(config.docs).filter(([, v]) => v !== void 0)),
|
|
361
|
+
...config.changesets ? { changesets: config.changesets } : {},
|
|
362
|
+
cssVar: config.cssVar,
|
|
363
|
+
governance: config.governance,
|
|
364
|
+
...config.audit ? { audit: config.audit } : {},
|
|
365
|
+
...config.previews ? { previews: config.previews } : {}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ../core/src/values.ts
|
|
370
|
+
var ALIAS_RE = /^\{([^{}]+)\}$/;
|
|
371
|
+
var isAlias = (v) => typeof v === "string" && ALIAS_RE.test(v);
|
|
372
|
+
var aliasTarget = (v) => ALIAS_RE.exec(v)?.[1] ?? v;
|
|
373
|
+
var clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
374
|
+
var round = (n, digits) => {
|
|
375
|
+
const f = 10 ** digits;
|
|
376
|
+
return Math.round(n * f) / f;
|
|
377
|
+
};
|
|
378
|
+
function hexToRgba(hex) {
|
|
379
|
+
const m = /^#?([0-9a-f]{3,8})$/i.exec(hex.trim());
|
|
380
|
+
if (!m) return void 0;
|
|
381
|
+
let h = m[1];
|
|
382
|
+
if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
|
|
383
|
+
if (h.length !== 6 && h.length !== 8) return void 0;
|
|
384
|
+
const n = (idx) => parseInt(h.slice(idx, idx + 2), 16) / 255;
|
|
385
|
+
return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) : 1 };
|
|
386
|
+
}
|
|
387
|
+
var to255 = (n) => Math.round(clamp01(n) * 255);
|
|
388
|
+
var rgbaToHex = (c) => "#" + [c.r, c.g, c.b].map((x) => to255(x).toString(16).padStart(2, "0")).join("");
|
|
389
|
+
function parseColor(value) {
|
|
390
|
+
if (typeof value === "string") return isAlias(value) ? void 0 : hexToRgba(value);
|
|
391
|
+
if (!value || typeof value !== "object") return void 0;
|
|
392
|
+
const v = value;
|
|
393
|
+
const alpha = typeof v.alpha === "number" ? v.alpha : 1;
|
|
394
|
+
if (v.colorSpace === "srgb" && Array.isArray(v.components) && v.components.length === 3) {
|
|
395
|
+
const [r, g, b] = v.components;
|
|
396
|
+
if ([r, g, b].every((x) => typeof x === "number")) return { r, g, b, a: alpha };
|
|
397
|
+
}
|
|
398
|
+
if (typeof v.hex === "string") {
|
|
399
|
+
const c = hexToRgba(v.hex);
|
|
400
|
+
if (c) return { ...c, a: alpha };
|
|
401
|
+
}
|
|
402
|
+
return void 0;
|
|
403
|
+
}
|
|
404
|
+
function rgbaToDtcg(c) {
|
|
405
|
+
const out = {
|
|
406
|
+
colorSpace: "srgb",
|
|
407
|
+
components: [round(clamp01(c.r), 3), round(clamp01(c.g), 3), round(clamp01(c.b), 3)]
|
|
408
|
+
};
|
|
409
|
+
if (c.a < 0.9995) out.alpha = round(clamp01(c.a), 3);
|
|
410
|
+
out.hex = rgbaToHex(c);
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
function colorsEqual(a, b) {
|
|
414
|
+
return rgbaToHex(a) === rgbaToHex(b) && Math.abs(a.a - b.a) < 2e-3;
|
|
415
|
+
}
|
|
416
|
+
function parseDimension(value) {
|
|
417
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
418
|
+
const v = value;
|
|
419
|
+
if (typeof v.value === "number" && typeof v.unit === "string") return { value: v.value, unit: v.unit };
|
|
420
|
+
}
|
|
421
|
+
if (typeof value === "string") {
|
|
422
|
+
const m = /^(-?\d+(?:\.\d+)?)(px|rem)$/.exec(value.trim());
|
|
423
|
+
if (m) return { value: Number(m[1]), unit: m[2] };
|
|
424
|
+
}
|
|
425
|
+
if (typeof value === "number") return { value, unit: "px" };
|
|
426
|
+
return void 0;
|
|
427
|
+
}
|
|
428
|
+
var numbersEqual = (a, b) => Math.abs(a - b) < 1e-4;
|
|
429
|
+
function deepEqual(a, b) {
|
|
430
|
+
if (a === b) return true;
|
|
431
|
+
if (typeof a === "number" && typeof b === "number") return numbersEqual(a, b);
|
|
432
|
+
if (typeof a !== typeof b || !a || !b || typeof a !== "object") return false;
|
|
433
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
434
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, idx) => deepEqual(v, b[idx]));
|
|
435
|
+
const ka = Object.keys(a).filter((k) => a[k] !== void 0);
|
|
436
|
+
const kb = Object.keys(b).filter((k) => b[k] !== void 0);
|
|
437
|
+
if (ka.length !== kb.length) return false;
|
|
438
|
+
return ka.every((k) => deepEqual(a[k], b[k]));
|
|
439
|
+
}
|
|
440
|
+
function valuesEqual(type, a, b) {
|
|
441
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
442
|
+
if (isAlias(a) || isAlias(b)) return isAlias(a) && isAlias(b) && aliasTarget(a) === aliasTarget(b);
|
|
443
|
+
if (type === "color") {
|
|
444
|
+
const ca = parseColor(a);
|
|
445
|
+
const cb = parseColor(b);
|
|
446
|
+
if (ca && cb) return colorsEqual(ca, cb);
|
|
447
|
+
}
|
|
448
|
+
if (type === "dimension") {
|
|
449
|
+
const da = parseDimension(a);
|
|
450
|
+
const db = parseDimension(b);
|
|
451
|
+
if (da && db) return da.unit === db.unit && numbersEqual(da.value, db.value);
|
|
452
|
+
}
|
|
453
|
+
return deepEqual(a, b);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ../core/src/wcag.ts
|
|
457
|
+
var AA_MIN = { text: 4.5, "large-text": 3, ui: 3 };
|
|
458
|
+
var channel = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
459
|
+
function luminance(c) {
|
|
460
|
+
const q = (x) => Math.round(Math.min(1, Math.max(0, x)) * 255) / 255;
|
|
461
|
+
return 0.2126 * channel(q(c.r)) + 0.7152 * channel(q(c.g)) + 0.0722 * channel(q(c.b));
|
|
462
|
+
}
|
|
463
|
+
function composite(fg, bg) {
|
|
464
|
+
const a = fg.a;
|
|
465
|
+
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 };
|
|
466
|
+
}
|
|
467
|
+
function contrastRatio(fg, bg) {
|
|
468
|
+
const f = typeof fg === "string" ? hexToRgba(fg) : fg;
|
|
469
|
+
const b = typeof bg === "string" ? hexToRgba(bg) : bg;
|
|
470
|
+
if (!f || !b) throw new Error("Invalid color");
|
|
471
|
+
const opaqueBg = { ...b, a: 1 };
|
|
472
|
+
const l1 = luminance(composite(f, opaqueBg));
|
|
473
|
+
const l2 = luminance(opaqueBg);
|
|
474
|
+
const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
|
|
475
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
476
|
+
}
|
|
477
|
+
function renderedColor(value) {
|
|
478
|
+
const c = parseColor(value);
|
|
479
|
+
const hex = value && typeof value === "object" ? value.hex : void 0;
|
|
480
|
+
const fromHex = typeof hex === "string" ? hexToRgba(hex) : void 0;
|
|
481
|
+
return c && fromHex ? { ...fromHex, a: c.a } : c;
|
|
482
|
+
}
|
|
483
|
+
var kindForMin = (min) => min >= AA_MIN.text ? "text" : "ui";
|
|
484
|
+
var contrastPair = (foreground, background, min = AA_MIN.text) => ({ foreground, background, min, kind: kindForMin(min) });
|
|
485
|
+
function parseContrastChecks(text) {
|
|
486
|
+
if (!text) return [];
|
|
487
|
+
const data = JSON.parse(text);
|
|
488
|
+
const list2 = Array.isArray(data) ? data : Array.isArray(data.pairs) ? data.pairs : [];
|
|
489
|
+
const out = [];
|
|
490
|
+
for (const p of list2) {
|
|
491
|
+
let fg, bg, min;
|
|
492
|
+
if (Array.isArray(p)) [fg, bg, min] = p;
|
|
493
|
+
else if (p && typeof p === "object") {
|
|
494
|
+
const o = p;
|
|
495
|
+
fg = o.fg ?? o.foreground;
|
|
496
|
+
bg = o.bg ?? o.background;
|
|
497
|
+
min = o.min ?? o.ratio ?? AA_MIN.text;
|
|
498
|
+
}
|
|
499
|
+
if (typeof fg !== "string" || typeof bg !== "string" || typeof min !== "number" || !(min > 0)) continue;
|
|
500
|
+
out.push(contrastPair(fg, bg, min));
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ../core/src/layered.ts
|
|
506
|
+
var DTCG_TYPES = /* @__PURE__ */ new Set(["color", "dimension", "fontFamily", "fontWeight", "duration", "cubicBezier", "number", "strokeStyle", "border", "transition", "shadow", "gradient", "typography", "fontStyle", "string", "boolean"]);
|
|
507
|
+
var aliasOf = (v) => isAlias(v) ? aliasTarget(v) : null;
|
|
508
|
+
function globToRegExp(glob) {
|
|
509
|
+
let re = "";
|
|
510
|
+
for (let i = 0; i < glob.length; i++) {
|
|
511
|
+
const c = glob[i];
|
|
512
|
+
if (c === "*" && glob[i + 1] === "*") {
|
|
513
|
+
if (glob[i + 2] === "/") {
|
|
514
|
+
re += "(?:.*/)?";
|
|
515
|
+
i += 2;
|
|
516
|
+
} else {
|
|
517
|
+
re += ".*";
|
|
518
|
+
i += 1;
|
|
519
|
+
}
|
|
520
|
+
} else if (c === "*") re += "[^/]*";
|
|
521
|
+
else if (c === "?") re += "[^/]";
|
|
522
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
523
|
+
}
|
|
524
|
+
return new RegExp(`^${re}$`);
|
|
525
|
+
}
|
|
526
|
+
var comboKey = (c) => Object.entries(c).map(([k, v]) => `${k}=${v}`).join(",") || "default";
|
|
527
|
+
function flattenLayerFile(json, file, issues) {
|
|
528
|
+
const out = [];
|
|
529
|
+
const walk = (node, path, inheritedType) => {
|
|
530
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
|
531
|
+
const n = node;
|
|
532
|
+
const type = typeof n.$type === "string" ? n.$type : inheritedType;
|
|
533
|
+
if ("$value" in n) {
|
|
534
|
+
out.push({
|
|
535
|
+
path: path.join("."),
|
|
536
|
+
...type ? { type } : {},
|
|
537
|
+
value: n.$value,
|
|
538
|
+
...typeof n.$description === "string" ? { description: n.$description } : {},
|
|
539
|
+
...n.$extensions && typeof n.$extensions === "object" ? { extensions: n.$extensions } : {},
|
|
540
|
+
...n.$deprecated !== void 0 ? { deprecated: n.$deprecated } : {},
|
|
541
|
+
file
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
for (const [k, v] of Object.entries(n)) {
|
|
546
|
+
if (k.startsWith("$")) continue;
|
|
547
|
+
const p = [...path, k].join(".");
|
|
548
|
+
if (/[{}.]/.test(k)) issues.push({ level: "error", code: "invalid-name", message: `Name "${k}" contains {, } or . which DTCG forbids.`, path: p, file });
|
|
549
|
+
if (v && typeof v === "object" && !Array.isArray(v)) walk(v, [...path, k], type);
|
|
550
|
+
else issues.push({ level: "error", code: "not-a-token", message: `"${p}" is neither a token (no $value) nor a group.`, path: p, file });
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
walk(json, []);
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
var toHex2 = (n) => Math.round(Math.max(0, Math.min(1, n)) * 255).toString(16).padStart(2, "0");
|
|
557
|
+
function compactJson(v) {
|
|
558
|
+
if (Array.isArray(v)) return `[${v.map(compactJson).join(", ")}]`;
|
|
559
|
+
if (v && typeof v === "object") {
|
|
560
|
+
const e = Object.entries(v);
|
|
561
|
+
return e.length ? `{ ${e.map(([k, x]) => `${JSON.stringify(k)}: ${compactJson(x)}`).join(", ")} }` : "{}";
|
|
562
|
+
}
|
|
563
|
+
return JSON.stringify(v);
|
|
564
|
+
}
|
|
565
|
+
function colorToCss(c) {
|
|
566
|
+
if (typeof c === "string") return c;
|
|
567
|
+
if (!c || typeof c !== "object") return String(c);
|
|
568
|
+
const o = c;
|
|
569
|
+
const [r, g, b] = (o.components ?? []).map((x) => typeof x === "number" ? x : 0);
|
|
570
|
+
const alpha = o.alpha ?? 1;
|
|
571
|
+
if (alpha < 1) return `rgb(${[r, g, b].map((x) => Math.round((x ?? 0) * 255)).join(" ")} / ${alpha})`;
|
|
572
|
+
return o.hex ?? `#${[r, g, b].map((x) => toHex2(x ?? 0)).join("")}`;
|
|
573
|
+
}
|
|
574
|
+
var dimToCss = (d) => d && typeof d === "object" && "value" in d ? `${d.value}${d.unit}` : String(d);
|
|
575
|
+
function valueToCss(type, v, ext) {
|
|
576
|
+
if (v === void 0 || v === null) return "";
|
|
577
|
+
switch (type) {
|
|
578
|
+
case "color":
|
|
579
|
+
return colorToCss(v);
|
|
580
|
+
case "dimension":
|
|
581
|
+
case "duration":
|
|
582
|
+
return dimToCss(v);
|
|
583
|
+
case "cubicBezier":
|
|
584
|
+
return Array.isArray(v) ? `cubic-bezier(${v.join(", ")})` : String(v);
|
|
585
|
+
case "fontFamily":
|
|
586
|
+
return Array.isArray(v) ? v.map((n) => /\s/.test(String(n)) ? `"${n}"` : String(n)).join(", ") : String(v);
|
|
587
|
+
case "shadow": {
|
|
588
|
+
if (typeof v === "string") return v;
|
|
589
|
+
const layers = Array.isArray(v) ? v : [v];
|
|
590
|
+
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(", ");
|
|
591
|
+
}
|
|
592
|
+
default: {
|
|
593
|
+
const unit = ext?.["dev.bridge.unit"];
|
|
594
|
+
if (typeof v === "number" && typeof unit === "string") return `${v}${unit}`;
|
|
595
|
+
return typeof v === "object" ? compactJson(v) : String(v);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
var LayeredTokenSet = class {
|
|
600
|
+
/** `files`: token file path relative to tokens.root -> file text. */
|
|
601
|
+
constructor(config, files) {
|
|
602
|
+
this.config = config;
|
|
603
|
+
this.files = files;
|
|
604
|
+
this.fileNames = [...files.keys()].sort();
|
|
605
|
+
for (const f of this.fileNames) {
|
|
606
|
+
let json;
|
|
607
|
+
try {
|
|
608
|
+
json = JSON.parse(files.get(f));
|
|
609
|
+
} catch (e) {
|
|
610
|
+
this.issues.push({ level: "error", code: "invalid-json", message: `Invalid JSON: ${e.message}`, file: f });
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
this.defs.push(...flattenLayerFile(json, f, this.issues));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
config;
|
|
617
|
+
files;
|
|
618
|
+
defs = [];
|
|
619
|
+
issues = [];
|
|
620
|
+
fileNames;
|
|
621
|
+
views = /* @__PURE__ */ new Map();
|
|
622
|
+
get modeNames() {
|
|
623
|
+
return Object.keys(this.config.axes);
|
|
624
|
+
}
|
|
625
|
+
get defaultCombo() {
|
|
626
|
+
return Object.fromEntries(Object.entries(this.config.axes).map(([k, m]) => [k, m.default]));
|
|
627
|
+
}
|
|
628
|
+
/** Every combination of the given modes (all modes when omitted); other modes stay at default. */
|
|
629
|
+
combos(only) {
|
|
630
|
+
let out = [this.defaultCombo];
|
|
631
|
+
for (const [name, m] of Object.entries(this.config.axes)) {
|
|
632
|
+
if (only && !only.includes(name)) continue;
|
|
633
|
+
out = out.flatMap((c) => m.values.map((v) => ({ ...c, [name]: v })));
|
|
634
|
+
}
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
637
|
+
normalizeCombo(partial = {}) {
|
|
638
|
+
const c = { ...this.defaultCombo };
|
|
639
|
+
for (const [k, v] of Object.entries(partial)) {
|
|
640
|
+
if (v === void 0) continue;
|
|
641
|
+
const m = this.config.axes[k];
|
|
642
|
+
if (!m) throw new Error(`Unknown mode "${k}". Modes: ${this.modeNames.join(", ") || "none"}.`);
|
|
643
|
+
if (!m.values.includes(v)) throw new Error(`Unknown ${k} "${v}". Values: ${m.values.join(", ")}.`);
|
|
644
|
+
c[k] = v;
|
|
645
|
+
}
|
|
646
|
+
return c;
|
|
647
|
+
}
|
|
648
|
+
tierOfFile(file) {
|
|
649
|
+
for (const [tier, dirs] of Object.entries(this.config.tokens.tiers)) {
|
|
650
|
+
if (dirs.some((d) => file === d || file.startsWith(`${d.replace(/\/$/, "")}/`) || globToRegExp(d).test(file))) return tier;
|
|
651
|
+
}
|
|
652
|
+
return void 0;
|
|
653
|
+
}
|
|
654
|
+
/** Tier of a token: the lowest layer that defines it in the default mode (an override does not change its tier). */
|
|
655
|
+
tierOf(path) {
|
|
656
|
+
const d = this.view(this.defaultCombo).get(path)?.[0] ?? this.defs.find((x) => x.path === path);
|
|
657
|
+
return d ? this.tierOfFile(d.file) : void 0;
|
|
658
|
+
}
|
|
659
|
+
/** Files of each layer for a combo, in resolution order. */
|
|
660
|
+
layerFiles(combo) {
|
|
661
|
+
const files = [];
|
|
662
|
+
const missing = [];
|
|
663
|
+
for (const l of this.config.tokens.layers) {
|
|
664
|
+
const pattern = l.files.replace(/\{([^}]+)\}/g, (_, k) => combo[k] ?? `{${k}}`);
|
|
665
|
+
const re = globToRegExp(pattern);
|
|
666
|
+
const hit = this.fileNames.filter((f) => re.test(f));
|
|
667
|
+
if (!hit.length && !l.optional) missing.push(pattern);
|
|
668
|
+
for (const f of hit) if (!files.includes(f)) files.push(f);
|
|
669
|
+
}
|
|
670
|
+
return { files, missing };
|
|
671
|
+
}
|
|
672
|
+
/** path -> definitions in layer order (last one wins) for a combo. */
|
|
673
|
+
view(combo) {
|
|
674
|
+
const key = comboKey(combo);
|
|
675
|
+
let v = this.views.get(key);
|
|
676
|
+
if (!v) {
|
|
677
|
+
const { files, missing } = this.layerFiles(combo);
|
|
678
|
+
const map = /* @__PURE__ */ new Map();
|
|
679
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
680
|
+
for (const d of this.defs) (byFile.get(d.file) ?? byFile.set(d.file, []).get(d.file)).push(d);
|
|
681
|
+
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);
|
|
682
|
+
v = { map, missing };
|
|
683
|
+
this.views.set(key, v);
|
|
684
|
+
}
|
|
685
|
+
return v.map;
|
|
686
|
+
}
|
|
687
|
+
missingLayers(combo) {
|
|
688
|
+
this.view(combo);
|
|
689
|
+
return this.views.get(comboKey(combo)).missing;
|
|
690
|
+
}
|
|
691
|
+
/** Every token path defined in any combo. */
|
|
692
|
+
allPaths() {
|
|
693
|
+
return [...new Set(this.defs.map((d) => d.path))].sort();
|
|
694
|
+
}
|
|
695
|
+
defsOf(path) {
|
|
696
|
+
return this.defs.filter((d) => d.path === path);
|
|
697
|
+
}
|
|
698
|
+
/** First combo whose layers include `file`, preferring the default. */
|
|
699
|
+
comboForFile(file) {
|
|
700
|
+
const all = [this.defaultCombo, ...this.combos()];
|
|
701
|
+
return all.find((c) => this.layerFiles(c).files.includes(file)) ?? this.defaultCombo;
|
|
702
|
+
}
|
|
703
|
+
resolve(path, combo) {
|
|
704
|
+
const view = this.view(combo);
|
|
705
|
+
const chain = [path];
|
|
706
|
+
const top = view.get(path);
|
|
707
|
+
if (!top) return { path, value: void 0, css: "", chain, definedIn: [], error: `"${path}" is not defined for ${comboKey(combo)}` };
|
|
708
|
+
const def = top[top.length - 1];
|
|
709
|
+
let type = def.type;
|
|
710
|
+
let error;
|
|
711
|
+
const deref = (target, stack) => {
|
|
712
|
+
if (stack.includes(target)) {
|
|
713
|
+
error = `circular alias: ${[...stack, target].join(" -> ")}`;
|
|
714
|
+
return void 0;
|
|
715
|
+
}
|
|
716
|
+
const t = view.get(target);
|
|
717
|
+
if (!t) {
|
|
718
|
+
error = `broken alias {${target}}`;
|
|
719
|
+
return void 0;
|
|
720
|
+
}
|
|
721
|
+
const td = t[t.length - 1];
|
|
722
|
+
type ??= td.type;
|
|
723
|
+
return walk(td.value, [...stack, target]);
|
|
724
|
+
};
|
|
725
|
+
const walk = (v, stack) => {
|
|
726
|
+
const a = aliasOf(v);
|
|
727
|
+
if (a) return deref(a, stack);
|
|
728
|
+
if (Array.isArray(v)) return v.map((x) => walk(x, stack));
|
|
729
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x, stack)]));
|
|
730
|
+
return v;
|
|
731
|
+
};
|
|
732
|
+
let cur = def.value;
|
|
733
|
+
const seen = /* @__PURE__ */ new Set([path]);
|
|
734
|
+
for (let a = aliasOf(cur); a; a = aliasOf(cur)) {
|
|
735
|
+
chain.push(a);
|
|
736
|
+
if (seen.has(a)) break;
|
|
737
|
+
seen.add(a);
|
|
738
|
+
const t = view.get(a);
|
|
739
|
+
if (!t) break;
|
|
740
|
+
cur = t[t.length - 1].value;
|
|
741
|
+
}
|
|
742
|
+
const value = walk(def.value, [path]);
|
|
743
|
+
return { path, ...type ? { type } : {}, value, css: error ? "" : valueToCss(type, value, def.extensions), chain, definedIn: top.map((d) => d.file), ...error ? { error } : {} };
|
|
744
|
+
}
|
|
745
|
+
/** Tokens whose own value aliases `path` directly, across all files. */
|
|
746
|
+
referencedBy(path) {
|
|
747
|
+
const needle = `{${path}}`;
|
|
748
|
+
return this.defs.filter((d) => JSON.stringify(d.value).includes(needle)).map((d) => ({ path: d.path, file: d.file }));
|
|
749
|
+
}
|
|
750
|
+
cssVarName(path) {
|
|
751
|
+
return `${this.config.cssVar.prefix}${path.split(".").join(this.config.cssVar.separator)}`;
|
|
752
|
+
}
|
|
753
|
+
/** Repo-relative path of a token file. */
|
|
754
|
+
repoPath(file) {
|
|
755
|
+
const root = this.config.tokens.root;
|
|
756
|
+
return root === "." || !root ? file : `${root.replace(/\/$/, "")}/${file}`;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
function selectTokenFiles(config, repoFiles) {
|
|
760
|
+
const root = config.tokens.root.replace(/\/$/, "");
|
|
761
|
+
const rel = (f) => root === "." || !root ? f : f.startsWith(`${root}/`) ? f.slice(root.length + 1) : null;
|
|
762
|
+
const inc = config.tokens.include.map(globToRegExp);
|
|
763
|
+
const exc = config.tokens.exclude.map(globToRegExp);
|
|
764
|
+
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)));
|
|
765
|
+
}
|
|
766
|
+
function checkShape(type, v) {
|
|
767
|
+
if (aliasOf(v)) return null;
|
|
768
|
+
const isObj2 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
|
|
769
|
+
switch (type) {
|
|
770
|
+
case "color":
|
|
771
|
+
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)";
|
|
772
|
+
if (!isObj2(v) || typeof v.colorSpace !== "string" || !Array.isArray(v.components)) return "color must be { colorSpace, components, alpha?, hex? }";
|
|
773
|
+
if (v.components.length !== 3 || !v.components.every((c) => typeof c === "number" || c === "none" || aliasOf(c))) return "color components must be 3 numbers";
|
|
774
|
+
if (v.alpha !== void 0 && (typeof v.alpha !== "number" || v.alpha < 0 || v.alpha > 1)) return "color alpha must be between 0 and 1";
|
|
775
|
+
if (v.hex !== void 0 && !/^#[0-9a-f]{6}$/i.test(String(v.hex))) return "color hex must be #rrggbb";
|
|
776
|
+
return null;
|
|
777
|
+
case "dimension":
|
|
778
|
+
if (typeof v === "string" || typeof v === "number") return "warn:dimension as a plain value: DTCG 2025.10 uses { value, unit }";
|
|
779
|
+
return isObj2(v) && typeof v.value === "number" && ["px", "rem"].includes(String(v.unit)) ? null : 'dimension must be { value: number, unit: "px" | "rem" }';
|
|
780
|
+
case "duration":
|
|
781
|
+
if (typeof v === "string") return "warn:duration as a string: DTCG 2025.10 uses { value, unit }";
|
|
782
|
+
return isObj2(v) && typeof v.value === "number" && ["ms", "s"].includes(String(v.unit)) ? null : 'duration must be { value: number, unit: "ms" | "s" }';
|
|
783
|
+
case "cubicBezier":
|
|
784
|
+
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";
|
|
785
|
+
case "fontFamily":
|
|
786
|
+
return typeof v === "string" || Array.isArray(v) && v.every((x) => typeof x === "string") ? null : "fontFamily must be a string or an array of strings";
|
|
787
|
+
case "fontWeight":
|
|
788
|
+
return typeof v === "number" && v >= 1 && v <= 1e3 || typeof v === "string" ? null : "fontWeight must be 1..1000 or a keyword";
|
|
789
|
+
case "number":
|
|
790
|
+
return typeof v === "number" ? null : "number must be a number";
|
|
791
|
+
case "shadow": {
|
|
792
|
+
const layers = Array.isArray(v) ? v : [v];
|
|
793
|
+
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";
|
|
794
|
+
}
|
|
795
|
+
default:
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function validateLayered(set) {
|
|
800
|
+
const issues = [...set.issues];
|
|
801
|
+
for (const d of set.defs) {
|
|
802
|
+
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 });
|
|
803
|
+
if (d.type) {
|
|
804
|
+
const s = checkShape(d.type, d.value);
|
|
805
|
+
if (s) issues.push({ level: s.startsWith("warn:") ? "warning" : "error", code: "invalid-value", message: s.replace(/^warn:/, ""), path: d.path, file: d.file });
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const merged = /* @__PURE__ */ new Map();
|
|
809
|
+
const add = (i, combo) => {
|
|
810
|
+
const k = `${i.code}|${i.path}|${i.message}`;
|
|
811
|
+
const prev = merged.get(k);
|
|
812
|
+
if (prev) prev.modes.push(comboKey(combo));
|
|
813
|
+
else merged.set(k, { ...i, modes: [comboKey(combo)] });
|
|
814
|
+
};
|
|
815
|
+
for (const combo of set.combos()) {
|
|
816
|
+
for (const miss of set.missingLayers(combo)) add({ level: "error", code: "missing-layer", message: `No file matches layer "${miss}".` }, combo);
|
|
817
|
+
for (const [path, defs] of set.view(combo)) {
|
|
818
|
+
const r = set.resolve(path, combo);
|
|
819
|
+
const def = defs[defs.length - 1];
|
|
820
|
+
if (r.error) add({ level: "error", code: r.error.startsWith("circular") ? "circular-alias" : "broken-alias", message: r.error, path, file: def.file }, combo);
|
|
821
|
+
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);
|
|
822
|
+
else if (def.type && aliasOf(def.value)) {
|
|
823
|
+
const target = set.view(combo).get(aliasOf(def.value));
|
|
824
|
+
const tt = target?.[target.length - 1]?.type;
|
|
825
|
+
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);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const total = set.combos().length;
|
|
830
|
+
for (const i of merged.values()) {
|
|
831
|
+
if (i.modes.length === total) delete i.modes;
|
|
832
|
+
issues.push(i);
|
|
833
|
+
}
|
|
834
|
+
issues.push(...layeredParityIssues(set));
|
|
835
|
+
return issues;
|
|
836
|
+
}
|
|
837
|
+
function layeredParityIssues(set) {
|
|
838
|
+
const out = [];
|
|
839
|
+
const pathsOf = (file) => new Set(set.defs.filter((d) => d.file === file).map((d) => d.path));
|
|
840
|
+
for (const [mode, m] of Object.entries(set.config.axes)) {
|
|
841
|
+
if (!m.parity) continue;
|
|
842
|
+
for (const layer of set.config.tokens.layers) {
|
|
843
|
+
if (!layer.files.includes(`{${mode}}`) || layer.optional) continue;
|
|
844
|
+
const others = (layer.files.match(/\{([^}]+)\}/g) ?? []).map((p) => p.slice(1, -1)).filter((k) => k !== mode);
|
|
845
|
+
let fixed = [{}];
|
|
846
|
+
for (const o of others) fixed = fixed.flatMap((c) => (set.config.axes[o]?.values ?? []).map((v) => ({ ...c, [o]: v })));
|
|
847
|
+
for (const c of fixed) {
|
|
848
|
+
const filesByValue = m.values.map((v) => {
|
|
849
|
+
const re = globToRegExp(layer.files.replace(/\{([^}]+)\}/g, (_, k) => k === mode ? v : c[k]));
|
|
850
|
+
return { v, files: set.fileNames.filter((f) => re.test(f)) };
|
|
851
|
+
});
|
|
852
|
+
const present = filesByValue.filter((x) => x.files.length);
|
|
853
|
+
if (!present.length) continue;
|
|
854
|
+
for (const x of filesByValue) {
|
|
855
|
+
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.` });
|
|
856
|
+
}
|
|
857
|
+
const sets = present.map((x) => ({ v: x.v, files: x.files, paths: new Set(x.files.flatMap((f) => [...pathsOf(f)])) }));
|
|
858
|
+
const union = new Set(sets.flatMap((s) => [...s.paths]));
|
|
859
|
+
for (const p of union) {
|
|
860
|
+
const lacking = sets.filter((s) => !s.paths.has(p));
|
|
861
|
+
if (lacking.length) {
|
|
862
|
+
const having = sets.find((s) => s.paths.has(p));
|
|
863
|
+
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] });
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return out;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ../core/src/scopes.ts
|
|
873
|
+
var has = (segments, ...names) => names.some((n) => segments.includes(n));
|
|
874
|
+
function defaultScopes(path, type, tier) {
|
|
875
|
+
const s = path.split(".");
|
|
876
|
+
switch (type) {
|
|
877
|
+
case "color":
|
|
878
|
+
if (tier === "primitive" || tier === "brand" && s[0] === "palette") return [];
|
|
879
|
+
if (has(s, "text")) return ["TEXT_FILL"];
|
|
880
|
+
if (has(s, "border", "ring", "outline")) return ["STROKE_COLOR"];
|
|
881
|
+
if (has(s, "bg", "surface", "background")) return ["FRAME_FILL", "SHAPE_FILL"];
|
|
882
|
+
if (has(s, "shadow")) return ["EFFECT_COLOR"];
|
|
883
|
+
return ["FRAME_FILL", "SHAPE_FILL", "TEXT_FILL", "STROKE_COLOR"];
|
|
884
|
+
case "dimension":
|
|
885
|
+
if (has(s, "radius")) return ["CORNER_RADIUS"];
|
|
886
|
+
if (has(s, "border", "width", "stroke")) return ["STROKE_FLOAT"];
|
|
887
|
+
if (has(s, "space", "spacing", "gap")) return ["GAP", "WIDTH_HEIGHT"];
|
|
888
|
+
if (has(s, "size") && has(s, "font")) return ["FONT_SIZE"];
|
|
889
|
+
if (has(s, "letter-spacing")) return ["LETTER_SPACING"];
|
|
890
|
+
return ["WIDTH_HEIGHT", "GAP"];
|
|
891
|
+
case "fontWeight":
|
|
892
|
+
return ["FONT_WEIGHT"];
|
|
893
|
+
case "fontFamily":
|
|
894
|
+
return ["FONT_FAMILY"];
|
|
895
|
+
case "number":
|
|
896
|
+
if (has(s, "line-height", "lineHeight")) return ["LINE_HEIGHT"];
|
|
897
|
+
if (has(s, "opacity")) return ["OPACITY"];
|
|
898
|
+
return ["WIDTH_HEIGHT", "GAP"];
|
|
899
|
+
case "string":
|
|
900
|
+
return ["TEXT_CONTENT"];
|
|
901
|
+
default:
|
|
902
|
+
return [];
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
var sameScopes = (a, b) => {
|
|
906
|
+
const sa = [...new Set(a)].sort();
|
|
907
|
+
const sb = [...new Set(b)].sort();
|
|
908
|
+
return sa.length === sb.length && sa.every((x, i) => x === sb[i]);
|
|
909
|
+
};
|
|
910
|
+
var effectiveScopes = (t) => t.scopes ?? defaultScopes(t.path, t.type, t.tier);
|
|
911
|
+
|
|
912
|
+
// ../core/src/diff-engine.ts
|
|
913
|
+
var normDesc = (d) => (d ?? "").trim();
|
|
914
|
+
function fieldChanges(a, b) {
|
|
915
|
+
const out = [];
|
|
916
|
+
if (a.path !== b.path) out.push({ field: "path", before: a.path, after: b.path });
|
|
917
|
+
if (a.type !== b.type) out.push({ field: "type", before: a.type, after: b.type });
|
|
918
|
+
const modes = [.../* @__PURE__ */ new Set([...Object.keys(a.values), ...Object.keys(b.values)])];
|
|
919
|
+
for (const mode of modes) {
|
|
920
|
+
if (!valuesEqual(b.type, a.values[mode], b.values[mode])) out.push({ field: "value", mode, before: a.values[mode], after: b.values[mode] });
|
|
921
|
+
}
|
|
922
|
+
if (normDesc(a.description) !== normDesc(b.description)) out.push({ field: "description", before: a.description, after: b.description });
|
|
923
|
+
const sa = effectiveScopes(a);
|
|
924
|
+
const sb = effectiveScopes(b);
|
|
925
|
+
if (!sameScopes(sa, sb)) out.push({ field: "scopes", before: sa, after: sb });
|
|
926
|
+
return out;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// ../core/src/risk.ts
|
|
930
|
+
var RISK_LABELS = {
|
|
931
|
+
1: "Tier 1: low risk, one code owner",
|
|
932
|
+
2: "Tier 2: medium risk, design review then design + tech approval",
|
|
933
|
+
3: "Tier 3: token change, needs design + tech approval"
|
|
934
|
+
};
|
|
935
|
+
var BUMP_RANK = { patch: 0, minor: 1, major: 2 };
|
|
936
|
+
var LOW = [/\.md$/, /^docs\//, /\.stories\.tsx?$/, /\.docs\.json$/, /\.test\.tsx?$/, /^\.changeset\//];
|
|
937
|
+
var isSummary = (c) => "kind" in c;
|
|
938
|
+
function summarizeTokenChange(c) {
|
|
939
|
+
if (isSummary(c)) return c;
|
|
940
|
+
const path = (c.target ?? c.current).path;
|
|
941
|
+
if (!c.target) return { kind: "removed", path };
|
|
942
|
+
if (!c.current) return { kind: "added", path };
|
|
943
|
+
if (c.current.path !== c.target.path) return { kind: "renamed", path };
|
|
944
|
+
const fields = fieldChanges(c.current, c.target);
|
|
945
|
+
return { kind: "changed", path, descriptionOnly: fields.length > 0 && fields.every((f) => f.field === "description") };
|
|
946
|
+
}
|
|
947
|
+
function tokenBump(changes) {
|
|
948
|
+
let bump;
|
|
949
|
+
for (const c of changes.map(summarizeTokenChange)) {
|
|
950
|
+
const b = c.kind === "removed" || c.kind === "renamed" ? "major" : c.kind === "changed" && c.descriptionOnly ? "patch" : "minor";
|
|
951
|
+
if (!bump || BUMP_RANK[b] > BUMP_RANK[bump]) bump = b;
|
|
952
|
+
}
|
|
953
|
+
return bump;
|
|
954
|
+
}
|
|
955
|
+
function isGuardrail(path, config) {
|
|
956
|
+
const list2 = config ? [...config.governance.guardrails, CONFIG_FILE, ...config.contrast ? [config.contrast.path] : []] : DEFAULT_GOVERNANCE.guardrails;
|
|
957
|
+
return list2.some((g) => g.endsWith("/") ? path.startsWith(g) : path === g) || path.startsWith(".github/") || path.endsWith("/contrast.checks.json") || path === "contrast.checks.json";
|
|
958
|
+
}
|
|
959
|
+
function classifyRisk(input) {
|
|
960
|
+
const reasons = [];
|
|
961
|
+
const tokenChanges = (input.tokenChanges ?? []).map(summarizeTokenChange);
|
|
962
|
+
for (const c of tokenChanges) {
|
|
963
|
+
if (c.kind === "changed" && c.descriptionOnly) reasons.push({ tier: 1, text: `Token description changed: ${c.path}` });
|
|
964
|
+
else reasons.push({ tier: 3, text: `Token ${c.kind}: ${c.path}` });
|
|
965
|
+
}
|
|
966
|
+
const bump = tokenBump(tokenChanges);
|
|
967
|
+
const componentsDir = input.config?.components?.dir;
|
|
968
|
+
for (const f of input.files ?? []) {
|
|
969
|
+
const p = f.path.replace(/^\/+/, "");
|
|
970
|
+
if (p.endsWith(".tokens.json")) {
|
|
971
|
+
if (!input.tokenChanges) reasons.push({ tier: 3, text: `Token file ${f.status ?? "changed"}: ${p}` });
|
|
972
|
+
} else if (isGuardrail(p, input.config)) reasons.push({ tier: 3, text: `Guardrail file changed: ${p}` });
|
|
973
|
+
else if (LOW.some((r) => r.test(p))) reasons.push({ tier: 1, text: `Docs, story or test: ${p}` });
|
|
974
|
+
else if (componentsDir && p.startsWith(`${componentsDir}/`)) reasons.push({ tier: 2, text: `Component source ${f.status ?? "changed"}: ${p}` });
|
|
975
|
+
else reasons.push({ tier: 2, text: `Source file changed: ${p}` });
|
|
976
|
+
}
|
|
977
|
+
reasons.sort((a, b) => b.tier - a.tier);
|
|
978
|
+
const tier = reasons[0]?.tier ?? 1;
|
|
979
|
+
return { tier, label: RISK_LABELS[tier], reasons: reasons.map((r) => r.text), bump };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// ../core/src/layered-review.ts
|
|
983
|
+
function checkLayeredPair(set, pair, combo) {
|
|
984
|
+
const base = { fg: pair.foreground, bg: pair.background, min: pair.min, modes: comboKey(combo) };
|
|
985
|
+
const f = set.resolve(pair.foreground, combo);
|
|
986
|
+
const b = set.resolve(pair.background, combo);
|
|
987
|
+
if (f.error || b.error) return { ...base, ratio: null, pass: false, note: f.error ?? b.error };
|
|
988
|
+
const fc = renderedColor(f.value);
|
|
989
|
+
const bc = renderedColor(b.value);
|
|
990
|
+
if (!fc || !bc) return { ...base, ratio: null, pass: false, note: "not a color Bridge can measure (needs srgb or hex)" };
|
|
991
|
+
if (bc.a < 1) return { ...base, ratio: null, pass: false, note: "background is translucent: contrast depends on what is behind it" };
|
|
992
|
+
const ratio = Math.round(contrastRatio(fc, bc) * 100) / 100;
|
|
993
|
+
return { ...base, ratio, pass: ratio >= pair.min, fgValue: rgbaToHex(fc), bgValue: rgbaToHex(bc), ...fc.a < 1 ? { note: "foreground alpha composited over background" } : {} };
|
|
994
|
+
}
|
|
995
|
+
function runLayeredContrast(set, pairs, modes) {
|
|
996
|
+
const combos = set.combos(modes ?? set.config.contrast?.modes);
|
|
997
|
+
return combos.flatMap((c) => pairs.map((p) => checkLayeredPair(set, p, c)));
|
|
998
|
+
}
|
|
999
|
+
function contrastDelta(before, after) {
|
|
1000
|
+
const key = (r) => `${r.fg}|${r.bg}|${r.modes}`;
|
|
1001
|
+
const prev = new Map(before.map((r) => [key(r), r]));
|
|
1002
|
+
return after.map((a) => {
|
|
1003
|
+
const b = prev.get(key(a));
|
|
1004
|
+
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 };
|
|
1005
|
+
}).filter((d) => d.before !== d.after || !d.passAfter);
|
|
1006
|
+
}
|
|
1007
|
+
var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1008
|
+
function diffLayered(before, after) {
|
|
1009
|
+
const key = (d) => `${d.file}\0${d.path}`;
|
|
1010
|
+
const b = new Map(before.defs.map((d) => [key(d), d]));
|
|
1011
|
+
const a = new Map(after.defs.map((d) => [key(d), d]));
|
|
1012
|
+
const css = (set, d) => set.resolve(d.path, set.comboForFile(d.file)).css;
|
|
1013
|
+
const tierOf = (set, f) => {
|
|
1014
|
+
const t = set.tierOfFile(f);
|
|
1015
|
+
return t ? { tier: t } : {};
|
|
1016
|
+
};
|
|
1017
|
+
const changes = [];
|
|
1018
|
+
const removed = [];
|
|
1019
|
+
const added = [];
|
|
1020
|
+
for (const [k, d] of b) {
|
|
1021
|
+
const n = a.get(k);
|
|
1022
|
+
if (!n) {
|
|
1023
|
+
removed.push(d);
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const fields = [];
|
|
1027
|
+
if (!same(d.value, n.value)) fields.push("value");
|
|
1028
|
+
if (d.type !== n.type) fields.push("type");
|
|
1029
|
+
if (d.description !== n.description) fields.push("description");
|
|
1030
|
+
if (!same(d.extensions, n.extensions)) fields.push("extensions");
|
|
1031
|
+
if (!fields.length) continue;
|
|
1032
|
+
changes.push({
|
|
1033
|
+
kind: "changed",
|
|
1034
|
+
path: d.path,
|
|
1035
|
+
file: d.file,
|
|
1036
|
+
...tierOf(after, d.file),
|
|
1037
|
+
...n.type ? { type: n.type } : {},
|
|
1038
|
+
fields,
|
|
1039
|
+
before: d.value,
|
|
1040
|
+
after: n.value,
|
|
1041
|
+
beforeCss: css(before, d),
|
|
1042
|
+
afterCss: css(after, n),
|
|
1043
|
+
...d.description !== n.description ? { descriptionBefore: d.description ?? "", descriptionAfter: n.description ?? "" } : {}
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
for (const [k, d] of a) if (!b.has(k)) added.push(d);
|
|
1047
|
+
for (const r of [...removed]) {
|
|
1048
|
+
const i = added.findIndex((x) => x.file === r.file && same(x.value, r.value) && x.type === r.type);
|
|
1049
|
+
if (i < 0) continue;
|
|
1050
|
+
const n = added.splice(i, 1)[0];
|
|
1051
|
+
removed.splice(removed.indexOf(r), 1);
|
|
1052
|
+
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) });
|
|
1053
|
+
}
|
|
1054
|
+
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) });
|
|
1055
|
+
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) });
|
|
1056
|
+
return changes.sort((x, y) => x.path.localeCompare(y.path) || x.file.localeCompare(y.file));
|
|
1057
|
+
}
|
|
1058
|
+
function layeredImpact(before, after, direct, limit = 60) {
|
|
1059
|
+
const skip = new Set(direct.map((c) => c.path));
|
|
1060
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1061
|
+
for (const combo of after.combos()) {
|
|
1062
|
+
const paths = /* @__PURE__ */ new Set([...before.view(combo).keys(), ...after.view(combo).keys()]);
|
|
1063
|
+
for (const p of paths) {
|
|
1064
|
+
if (skip.has(p)) continue;
|
|
1065
|
+
const x = before.resolve(p, combo).css;
|
|
1066
|
+
const y = after.resolve(p, combo).css;
|
|
1067
|
+
if (x === y) continue;
|
|
1068
|
+
const e = byPath.get(p);
|
|
1069
|
+
if (e) e.modes.push(comboKey(combo));
|
|
1070
|
+
else byPath.set(p, { path: p, modes: [comboKey(combo)], before: x, after: y });
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const all = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
1074
|
+
return { total: all.length, entries: all.slice(0, limit) };
|
|
1075
|
+
}
|
|
1076
|
+
var toChangeSummary = (c) => ({
|
|
1077
|
+
kind: c.kind,
|
|
1078
|
+
path: c.path,
|
|
1079
|
+
...c.kind === "changed" ? { descriptionOnly: c.fields.length > 0 && c.fields.every((f) => f === "description") } : {}
|
|
1080
|
+
});
|
|
1081
|
+
function classifyLayered(changes, otherFiles = [], config) {
|
|
1082
|
+
const r = classifyRisk({ tokenChanges: changes.map(toChangeSummary), files: otherFiles.map((path) => ({ path })), config });
|
|
1083
|
+
return { tier: r.tier, bump: r.bump ?? "patch", reasons: r.reasons.length ? r.reasons : ["No token or guardrail change"], label: r.label };
|
|
1084
|
+
}
|
|
1085
|
+
var short = (v) => {
|
|
1086
|
+
if (v === void 0) return "";
|
|
1087
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
1088
|
+
return s.length > 60 ? `${s.slice(0, 57)}...` : s;
|
|
1089
|
+
};
|
|
1090
|
+
var cell = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1091
|
+
var swatch = (css) => css ? `\`${cell(css)}\`` : "";
|
|
1092
|
+
var aliasNote = (v) => typeof v === "string" && v.startsWith("{") ? ` (${cell(short(v))})` : "";
|
|
1093
|
+
function changeTable(changes) {
|
|
1094
|
+
if (!changes.length) return "_No token changes._";
|
|
1095
|
+
const rows = changes.map((c) => {
|
|
1096
|
+
const what = c.kind === "renamed" ? `renamed from \`${c.from}\`` : c.kind === "changed" ? c.fields.join(", ") : c.kind;
|
|
1097
|
+
const before = c.kind === "added" || c.kind === "renamed" ? "" : `${swatch(c.beforeCss)}${aliasNote(c.before)}`;
|
|
1098
|
+
const after = c.kind === "removed" ? "" : `${swatch(c.afterCss)}${aliasNote(c.after)}`;
|
|
1099
|
+
return `| \`${c.path}\` | ${what} | ${before} | ${after} | ${c.file} |`;
|
|
1100
|
+
});
|
|
1101
|
+
return ["| Token | Change | Before | After | File |", "| --- | --- | --- | --- | --- |", ...rows].join("\n");
|
|
1102
|
+
}
|
|
1103
|
+
function contrastDeltaTable(deltas, limit = 40) {
|
|
1104
|
+
if (!deltas.length) return "_No contrast pair moved._";
|
|
1105
|
+
const rows = deltas.slice(0, limit).map((d) => `| \`${d.fg}\` on \`${d.bg}\` | ${d.modes} | ${d.before ?? "n/a"} | ${d.after ?? "n/a"} | ${d.min} | ${d.passAfter ? "pass" : "**fail**"} |`);
|
|
1106
|
+
const more = deltas.length > limit ? `
|
|
1107
|
+
|
|
1108
|
+
_${deltas.length - limit} more pair(s) not shown._` : "";
|
|
1109
|
+
return ["| Pair | Mode | Before | After | Min | Result |", "| --- | --- | --- | --- | --- | --- |", ...rows].join("\n") + more;
|
|
1110
|
+
}
|
|
1111
|
+
function composeProposalBody(args) {
|
|
1112
|
+
const t = args.tier.tier;
|
|
1113
|
+
const box = (n) => `- ${n === t ? "[x]" : "[ ]"} ${RISK_LABELS[n]}`;
|
|
1114
|
+
const impactList = args.impact.entries.slice(0, 15).map((e) => `- \`${e.path}\`: ${swatch(e.before)} -> ${swatch(e.after)} (${e.modes.length} mode${e.modes.length > 1 ? "s" : ""})`);
|
|
1115
|
+
return [
|
|
1116
|
+
"## What changed and why",
|
|
1117
|
+
"",
|
|
1118
|
+
args.summary.trim(),
|
|
1119
|
+
"",
|
|
1120
|
+
`Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
|
|
1121
|
+
"",
|
|
1122
|
+
"## Risk tier",
|
|
1123
|
+
"",
|
|
1124
|
+
box(1),
|
|
1125
|
+
box(2),
|
|
1126
|
+
box(3),
|
|
1127
|
+
"",
|
|
1128
|
+
...args.tier.reasons.map((r) => `- ${r}`),
|
|
1129
|
+
`- Semver bump: \`${args.tier.bump}\``,
|
|
1130
|
+
"",
|
|
1131
|
+
"## Token changes",
|
|
1132
|
+
"",
|
|
1133
|
+
changeTable(args.changes),
|
|
1134
|
+
"",
|
|
1135
|
+
"## Contrast before and after",
|
|
1136
|
+
"",
|
|
1137
|
+
contrastDeltaTable(args.contrast),
|
|
1138
|
+
"",
|
|
1139
|
+
"## Downstream impact",
|
|
1140
|
+
"",
|
|
1141
|
+
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._",
|
|
1142
|
+
"",
|
|
1143
|
+
"## Checks",
|
|
1144
|
+
"",
|
|
1145
|
+
`- Token validation: ${args.validationErrors ? `**${args.validationErrors} error(s)**` : "no errors"} (DTCG schema, aliases, light and dark parity)`,
|
|
1146
|
+
`- Contrast gate: ${args.contrast.some((d) => !d.passAfter) ? "**failing pairs, see table**" : "all pairs pass"}`,
|
|
1147
|
+
"",
|
|
1148
|
+
"## Authored by",
|
|
1149
|
+
"",
|
|
1150
|
+
"- [ ] Human",
|
|
1151
|
+
"- [x] AI agent (Claude), reviewed by a human before merge",
|
|
1152
|
+
"",
|
|
1153
|
+
"## Design review checklist",
|
|
1154
|
+
"",
|
|
1155
|
+
"- [ ] Checked in light and dark themes and in every brand",
|
|
1156
|
+
"- [ ] Contrast table reviewed, no pair loosened",
|
|
1157
|
+
"- [ ] Changeset bump matches the tier",
|
|
1158
|
+
""
|
|
1159
|
+
].join("\n");
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// ../core/src/node/git-local.ts
|
|
1163
|
+
import { execFile } from "node:child_process";
|
|
1164
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
1165
|
+
import { tmpdir } from "node:os";
|
|
1166
|
+
import { join } from "node:path";
|
|
1167
|
+
|
|
1168
|
+
// ../core/src/governance.ts
|
|
1169
|
+
var DEFAULT_PROTECTED_BRANCHES = ["main", "master"];
|
|
1170
|
+
var ProtectedBranchError = class extends Error {
|
|
1171
|
+
constructor(message) {
|
|
1172
|
+
super(message);
|
|
1173
|
+
this.name = "ProtectedBranchError";
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
function assertSafeBranch(branch, rules = {}) {
|
|
1177
|
+
const name = branch.replace(/^refs\/heads\//, "").trim();
|
|
1178
|
+
const blocked = /* @__PURE__ */ new Set([...rules.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES, ...rules.base ? [rules.base] : []]);
|
|
1179
|
+
if (!name || blocked.has(name)) {
|
|
1180
|
+
throw new ProtectedBranchError(`Refusing to write to protected branch "${name}". Bridge only creates new branches and pull requests.`);
|
|
1181
|
+
}
|
|
1182
|
+
if (rules.branchPrefix && !name.startsWith(rules.branchPrefix)) {
|
|
1183
|
+
throw new ProtectedBranchError(`Branch "${name}" must start with "${rules.branchPrefix}".`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// ../core/src/node/git-local.ts
|
|
1188
|
+
function git(root, args, opts = {}) {
|
|
1189
|
+
return new Promise((resolve, reject) => {
|
|
1190
|
+
const child = execFile("git", args, { cwd: root, env: { ...process.env, ...opts.env }, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
1191
|
+
if (err) reject(new Error(`git ${args.join(" ")}: ${stderr || err.message}`.trim()));
|
|
1192
|
+
else resolve(stdout);
|
|
1193
|
+
});
|
|
1194
|
+
if (opts.input !== void 0) child.stdin.end(opts.input);
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
async function isGitRepo(root) {
|
|
1198
|
+
try {
|
|
1199
|
+
return (await git(root, ["rev-parse", "--is-inside-work-tree"])).trim() === "true";
|
|
1200
|
+
} catch {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
async function currentBranch(root) {
|
|
1205
|
+
return (await git(root, ["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
1206
|
+
}
|
|
1207
|
+
async function branchExists(root, branch) {
|
|
1208
|
+
try {
|
|
1209
|
+
await git(root, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
1210
|
+
return true;
|
|
1211
|
+
} catch {
|
|
1212
|
+
return false;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async function showFile(root, ref, path) {
|
|
1216
|
+
try {
|
|
1217
|
+
return await git(root, ["show", `${ref}:${path}`]);
|
|
1218
|
+
} catch {
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function listFilesAt(root, ref) {
|
|
1223
|
+
return (await git(root, ["ls-tree", "-r", "--name-only", ref])).split("\n").filter(Boolean);
|
|
1224
|
+
}
|
|
1225
|
+
async function originRepo(root) {
|
|
1226
|
+
try {
|
|
1227
|
+
const url = (await git(root, ["remote", "get-url", "origin"])).trim();
|
|
1228
|
+
return /github\.com[:/]([\w.-]+\/[\w.-]+?)(\.git)?$/.exec(url)?.[1];
|
|
1229
|
+
} catch {
|
|
1230
|
+
return void 0;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
async function commitToNewLocalBranch(root, args) {
|
|
1234
|
+
try {
|
|
1235
|
+
assertSafeBranch(args.branch, args);
|
|
1236
|
+
} catch (err) {
|
|
1237
|
+
throw new Error(`Refusing to write branch "${args.branch}": ${err.message}`);
|
|
1238
|
+
}
|
|
1239
|
+
if (await branchExists(root, args.branch)) throw new Error(`Branch "${args.branch}" already exists. Pick another title or delete it first.`);
|
|
1240
|
+
const baseSha = (await git(root, ["rev-parse", "--verify", `${args.base}^{commit}`])).trim();
|
|
1241
|
+
const dir = await mkdtemp(join(tmpdir(), "bridge-index-"));
|
|
1242
|
+
const env = { GIT_INDEX_FILE: join(dir, "index") };
|
|
1243
|
+
try {
|
|
1244
|
+
await git(root, ["read-tree", baseSha], { env });
|
|
1245
|
+
for (const f of args.files) {
|
|
1246
|
+
if (f.content === null) {
|
|
1247
|
+
await git(root, ["update-index", "--force-remove", f.path], { env });
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
const blob = (await git(root, ["hash-object", "-w", "--stdin"], { input: f.content })).trim();
|
|
1251
|
+
await git(root, ["update-index", "--add", "--cacheinfo", `100644,${blob},${f.path}`], { env });
|
|
1252
|
+
}
|
|
1253
|
+
const tree = (await git(root, ["write-tree"], { env })).trim();
|
|
1254
|
+
const ident = await identityEnv(root);
|
|
1255
|
+
const sha = (await git(root, ["commit-tree", tree, "-p", baseSha, "-F", "-"], { input: args.message, env: ident })).trim();
|
|
1256
|
+
await git(root, ["update-ref", `refs/heads/${args.branch}`, sha, ""]);
|
|
1257
|
+
const diff = await git(root, ["diff", baseSha, sha]);
|
|
1258
|
+
return { sha, diff };
|
|
1259
|
+
} finally {
|
|
1260
|
+
await rm(dir, { recursive: true, force: true });
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
async function identityEnv(root) {
|
|
1264
|
+
const get = async (k) => (await git(root, ["config", k]).catch(() => "")).trim();
|
|
1265
|
+
const name = await get("user.name") || "Bridge";
|
|
1266
|
+
const email = await get("user.email") || "bridge@users.noreply.github.com";
|
|
1267
|
+
return { GIT_AUTHOR_NAME: name, GIT_AUTHOR_EMAIL: email, GIT_COMMITTER_NAME: name, GIT_COMMITTER_EMAIL: email };
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
export {
|
|
1271
|
+
__export,
|
|
1272
|
+
previewLinks,
|
|
1273
|
+
snapshotUrl,
|
|
1274
|
+
installCommand,
|
|
1275
|
+
tarballUrl,
|
|
1276
|
+
CONFIG_FILE,
|
|
1277
|
+
loadConfig,
|
|
1278
|
+
tokensPath,
|
|
1279
|
+
protectedBranchesOf,
|
|
1280
|
+
baseBranchOf,
|
|
1281
|
+
toConfigJson,
|
|
1282
|
+
hexToRgba,
|
|
1283
|
+
rgbaToHex,
|
|
1284
|
+
rgbaToDtcg,
|
|
1285
|
+
renderedColor,
|
|
1286
|
+
contrastPair,
|
|
1287
|
+
parseContrastChecks,
|
|
1288
|
+
RISK_LABELS,
|
|
1289
|
+
classifyRisk,
|
|
1290
|
+
aliasOf,
|
|
1291
|
+
globToRegExp,
|
|
1292
|
+
comboKey,
|
|
1293
|
+
LayeredTokenSet,
|
|
1294
|
+
selectTokenFiles,
|
|
1295
|
+
validateLayered,
|
|
1296
|
+
layeredParityIssues,
|
|
1297
|
+
checkLayeredPair,
|
|
1298
|
+
runLayeredContrast,
|
|
1299
|
+
contrastDelta,
|
|
1300
|
+
diffLayered,
|
|
1301
|
+
layeredImpact,
|
|
1302
|
+
classifyLayered,
|
|
1303
|
+
changeTable,
|
|
1304
|
+
contrastDeltaTable,
|
|
1305
|
+
composeProposalBody,
|
|
1306
|
+
DEFAULT_PROTECTED_BRANCHES,
|
|
1307
|
+
ProtectedBranchError,
|
|
1308
|
+
assertSafeBranch,
|
|
1309
|
+
git,
|
|
1310
|
+
isGitRepo,
|
|
1311
|
+
currentBranch,
|
|
1312
|
+
branchExists,
|
|
1313
|
+
showFile,
|
|
1314
|
+
listFilesAt,
|
|
1315
|
+
originRepo,
|
|
1316
|
+
commitToNewLocalBranch
|
|
1317
|
+
};
|