@bridge-ds/core 0.17.0 → 0.19.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/bridge.config.schema.json +16 -0
- package/dist/activity.d.ts +207 -0
- package/dist/audit/index.js +3 -3
- package/dist/audit/report.d.ts +17 -2
- package/dist/branch-actions.d.ts +7 -1
- package/dist/build-requests-github.d.ts +25 -0
- package/dist/build-requests.d.ts +403 -0
- package/dist/changelog-model.d.ts +102 -0
- package/dist/component-spec.d.ts +3 -1
- package/dist/config.d.ts +21 -0
- package/dist/css-color.d.ts +25 -0
- package/dist/docs-proposal.d.ts +1 -0
- package/dist/explorations.d.ts +1 -0
- package/dist/fidelity-references.d.ts +12 -0
- package/dist/figma-fix-loop.d.ts +153 -0
- package/dist/figma-fixes.d.ts +169 -0
- package/dist/figma-push-github.d.ts +7 -2
- package/dist/figma-push.d.ts +15 -2
- package/dist/gap-case.d.ts +144 -0
- package/dist/github-client.d.ts +31 -0
- package/dist/id-map.d.ts +10 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +3944 -774
- package/dist/index.js.map +1 -1
- package/dist/layered-review.d.ts +2 -0
- package/dist/measure-status.d.ts +127 -0
- package/dist/measured-only.d.ts +28 -0
- package/dist/measured-spec.d.ts +349 -0
- package/dist/measured-tokens.d.ts +25 -0
- package/dist/motion-docs.d.ts +51 -0
- package/dist/node/audit.d.ts +11 -3
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +1637 -24
- package/dist/node/index.js.map +1 -1
- package/dist/node/measure-script.d.ts +1 -0
- package/dist/node/measure.d.ts +212 -0
- package/dist/node/spec-css.d.ts +9 -1
- package/dist/node/tokens-script.d.ts +3 -0
- package/dist/pr-composer.d.ts +5 -0
- package/dist/previews.d.ts +4 -1
- package/dist/{prod-branch-DenQkr_e.js → prod-branch-Bdx_2bRQ.js} +197 -159
- package/dist/prod-branch-Bdx_2bRQ.js.map +1 -0
- package/dist/prod-branch.d.ts +1 -1
- package/dist/repo-model.d.ts +5 -1
- package/dist/repo-source-github.d.ts +8 -0
- package/dist/{repo-source-B4jNZhyC.js → repo-source-m0Tc603o.js} +1406 -629
- package/dist/repo-source-m0Tc603o.js.map +1 -0
- package/dist/repo-source.d.ts +6 -0
- package/dist/{report-DVEmUr0p.js → report-DFPKygfk.js} +17 -6
- package/dist/{report-DVEmUr0p.js.map → report-DFPKygfk.js.map} +1 -1
- package/dist/requester.d.ts +14 -0
- package/dist/requests.d.ts +2 -0
- package/dist/review-gate.d.ts +183 -2
- package/dist/risk.d.ts +12 -0
- package/dist/share-branch.d.ts +1 -1
- package/dist/snapshot.d.ts +34 -0
- package/dist/suggestion-review.d.ts +2 -0
- package/dist/suggestions-github.d.ts +21 -0
- package/dist/suggestions.d.ts +6 -0
- package/dist/token-proposal.d.ts +7 -0
- package/dist/try-in-product.d.ts +9 -0
- package/package.json +1 -1
- package/dist/prod-branch-DenQkr_e.js.map +0 -1
- package/dist/repo-source-B4jNZhyC.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cr as tokenBump, aW as changeKind, L as LayeredTokenSet, aN as aliasOf, c4 as rgbaToDtcg, bu as hexToRgba, w as parseTokenFile, q as emptyTokenFile, s as serializeTokenFile, C as moveToken, r as removeToken, bx as inheritedTypeAt, u as upsertToken, H as readTokenAt, b0 as comboKey, c3 as renderedColor, b8 as contrastRatio, z as rgbaToHex, U as classifyRisk, bh as figmaPushNextLine, W as RISK_LABELS, bi as figmaPushTierLabel, V as CONFIG_FILE, N as loadConfig, ah as ConfigError, _ as DEFAULT_TOKEN_PATHS, ag as globToRegExp, ae as slugify, Q as validateLayered, bP as parseJson, c8 as serializeJson, bU as plainEqual, cq as toPlain, br as getEntry, bp as fromPlain, m as modeDisplayName, A as formatValue, X as previewLinks, a1 as readPreviewComment, aV as canShip, cz as whoCanShipSentence, ct as unreadableShipTeams, cb as soloApprover, aR as approverLists, O as tokensPath, a6 as NEEDED_BY_LABEL, K as selectTokenFiles, R as parseContrastChecks } from "./prod-branch-Bdx_2bRQ.js";
|
|
2
2
|
function changesetBump(changes) {
|
|
3
3
|
return tokenBump(changes) ?? "patch";
|
|
4
4
|
}
|
|
@@ -177,9 +177,9 @@ function checkLayeredPair(set, pair, combo) {
|
|
|
177
177
|
const ratio = Math.round(contrastRatio(fc, bc) * 100) / 100;
|
|
178
178
|
return { ...base, ratio, pass: ratio >= pair.min, fgValue: rgbaToHex(fc), bgValue: rgbaToHex(bc), ...fc.a < 1 ? { note: "foreground alpha composited over background" } : {} };
|
|
179
179
|
}
|
|
180
|
-
function runLayeredContrast(set,
|
|
180
|
+
function runLayeredContrast(set, pairs2, modes) {
|
|
181
181
|
const combos = set.combos(modes ?? set.config.contrast?.modes);
|
|
182
|
-
return combos.flatMap((c) =>
|
|
182
|
+
return combos.flatMap((c) => pairs2.map((p) => checkLayeredPair(set, p, c)));
|
|
183
183
|
}
|
|
184
184
|
function contrastDelta(before, after) {
|
|
185
185
|
const key = (r) => `${r.fg}|${r.bg}|${r.modes}`;
|
|
@@ -295,14 +295,15 @@ _${deltas.length - limit} more pair(s) not shown._` : "";
|
|
|
295
295
|
}
|
|
296
296
|
function composeProposalBody(args) {
|
|
297
297
|
const t = args.tier.tier;
|
|
298
|
-
const
|
|
298
|
+
const figma = Boolean(args.fromFigma);
|
|
299
|
+
const box = (n) => `- ${n === t ? "[x]" : "[ ]"} ${figma && n === t ? figmaPushTierLabel(n, args.changes) : RISK_LABELS[n]}`;
|
|
299
300
|
const impactList = args.impact.entries.slice(0, 15).map((e) => `- \`${e.path}\`: ${swatch(e.before)} -> ${swatch(e.after)} (${e.modes.length} mode${e.modes.length > 1 ? "s" : ""})`);
|
|
300
301
|
return [
|
|
301
302
|
"## What changed and why",
|
|
302
303
|
"",
|
|
303
304
|
args.summary.trim(),
|
|
304
305
|
"",
|
|
305
|
-
`Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
|
|
306
|
+
figma ? `Pushed from Figma through ${args.tool ?? "the Bridge Figma plugin"}${args.author ? ` by ${args.author}` : ""}. ${figmaPushNextLine(args.changes)}` : `Proposed through ${args.tool ?? "Bridge MCP"}${args.author ? ` by ${args.author}` : ""}. Agent-authored: a human design owner and tech owner review and merge.`,
|
|
306
307
|
"",
|
|
307
308
|
"## Risk tier",
|
|
308
309
|
"",
|
|
@@ -332,8 +333,8 @@ function composeProposalBody(args) {
|
|
|
332
333
|
"",
|
|
333
334
|
"## Authored by",
|
|
334
335
|
"",
|
|
335
|
-
|
|
336
|
-
"
|
|
336
|
+
`- [${figma ? "x" : " "}] Human`,
|
|
337
|
+
`- [${figma ? " " : "x"}] AI agent (Claude), reviewed by a human before merge`,
|
|
337
338
|
"",
|
|
338
339
|
"## Design review checklist",
|
|
339
340
|
"",
|
|
@@ -368,6 +369,7 @@ class GitHubError extends Error {
|
|
|
368
369
|
this.name = "GitHubError";
|
|
369
370
|
this.auth = info.auth;
|
|
370
371
|
this.permission = info.permission;
|
|
372
|
+
this.resetsAt = info.resetsAt;
|
|
371
373
|
}
|
|
372
374
|
status;
|
|
373
375
|
detail;
|
|
@@ -375,6 +377,8 @@ class GitHubError extends Error {
|
|
|
375
377
|
auth;
|
|
376
378
|
/** With auth "missing-permission": the permission the call needed, when known. */
|
|
377
379
|
permission;
|
|
380
|
+
/** For a rate limit (status 429): when the limit resets, when known. */
|
|
381
|
+
resetsAt;
|
|
378
382
|
}
|
|
379
383
|
const isTokenRejected = (err) => err instanceof GitHubError && err.auth === "rejected";
|
|
380
384
|
const isMissingPermission = (err) => err instanceof GitHubError && err.auth === "missing-permission";
|
|
@@ -656,7 +660,7 @@ class GitHubClient {
|
|
|
656
660
|
return copy(cached.body);
|
|
657
661
|
}
|
|
658
662
|
const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
|
|
659
|
-
const limited = new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
|
|
663
|
+
const limited = new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail, { resetsAt });
|
|
660
664
|
hourlyLimitErrors.add(limited);
|
|
661
665
|
throw limited;
|
|
662
666
|
}
|
|
@@ -693,6 +697,31 @@ class GitHubClient {
|
|
|
693
697
|
);
|
|
694
698
|
return { fullName: r.full_name, defaultBranch: r.default_branch, private: r.private, permissions: r.permissions };
|
|
695
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* What the token may write here, found without writing anything: each probe is invalid on purpose (a zero sha, a
|
|
702
|
+
* missing head branch, an empty title), so GitHub answers 422 when the token has the permission and 403 or 404 when
|
|
703
|
+
* it does not. repo.permissions.push says what the user may do, not what a fine-grained token may do.
|
|
704
|
+
*/
|
|
705
|
+
async writeAccess(base) {
|
|
706
|
+
this.assertToken("Checking the token");
|
|
707
|
+
const probe = async (path, body) => {
|
|
708
|
+
try {
|
|
709
|
+
await this.request("POST", `${this.repoPath}${path}`, body);
|
|
710
|
+
return true;
|
|
711
|
+
} catch (err) {
|
|
712
|
+
if (!(err instanceof GitHubError)) throw err;
|
|
713
|
+
if (err.status === 422) return true;
|
|
714
|
+
if (err.auth === "missing-permission" || err.status === 404 || err.status === 410) return false;
|
|
715
|
+
throw err;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
const [contents, pullRequests, issues] = await Promise.all([
|
|
719
|
+
probe("/git/refs", { ref: "refs/heads/bridge-permission-probe", sha: "0".repeat(40) }),
|
|
720
|
+
probe("/pulls", { title: "Bridge permission probe", head: "bridge-permission-probe-missing-branch", base }),
|
|
721
|
+
probe("/issues", { title: "" })
|
|
722
|
+
]);
|
|
723
|
+
return { contents, pullRequests, issues };
|
|
724
|
+
}
|
|
696
725
|
/** Commit sha a branch points to, or undefined when the branch does not exist. */
|
|
697
726
|
async getBranchSha(branch) {
|
|
698
727
|
try {
|
|
@@ -703,6 +732,14 @@ class GitHubClient {
|
|
|
703
732
|
throw err;
|
|
704
733
|
}
|
|
705
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Whether a branch exists, through the matching-refs list: it answers 200 with an empty list when the branch is
|
|
737
|
+
* missing, so asking never leaves a 404 in the browser console (an optional branch like bridge-data).
|
|
738
|
+
*/
|
|
739
|
+
async hasBranch(branch) {
|
|
740
|
+
const refs = await this.request("GET", `${this.repoPath}/git/matching-refs/heads/${encodeRef(branch)}`);
|
|
741
|
+
return Array.isArray(refs) && refs.some((r) => r.ref === `refs/heads/${branch}`);
|
|
742
|
+
}
|
|
706
743
|
async getCommitTree(commitSha) {
|
|
707
744
|
const r = await this.request("GET", `${this.repoPath}/git/commits/${commitSha}`);
|
|
708
745
|
return r.tree.sha;
|
|
@@ -1208,7 +1245,7 @@ class GitHubClient {
|
|
|
1208
1245
|
}
|
|
1209
1246
|
async getPullRequest(n) {
|
|
1210
1247
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}`);
|
|
1211
|
-
return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null, requestedReviewers: (r.requested_reviewers ?? []).map((x) => x.login) };
|
|
1248
|
+
return { ...toSummary(r), body: r.body ?? "", mergeable: r.mergeable ?? null, requestedReviewers: (r.requested_reviewers ?? []).map((x) => x.login), autoMerge: Boolean(r.auto_merge) };
|
|
1212
1249
|
}
|
|
1213
1250
|
async listPullRequestFiles(n) {
|
|
1214
1251
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}/files?per_page=100`);
|
|
@@ -1219,7 +1256,7 @@ class GitHubClient {
|
|
|
1219
1256
|
}
|
|
1220
1257
|
async listReviews(n) {
|
|
1221
1258
|
const r = await this.request("GET", `${this.repoPath}/pulls/${n}/reviews`);
|
|
1222
|
-
return r.map((x) => ({ id: x.id, user: x.user?.login, state: x.state, body: x.body ?? "", ...x.submitted_at ? { submittedAt: x.submitted_at } : {} }));
|
|
1259
|
+
return r.map((x) => ({ id: x.id, user: x.user?.login, state: x.state, body: x.body ?? "", ...x.submitted_at ? { submittedAt: x.submitted_at } : {}, ...x.commit_id ? { commitId: x.commit_id } : {} }));
|
|
1223
1260
|
}
|
|
1224
1261
|
/** Submit a review. `COMMENT` works for any collaborator; approving your own PR is refused by GitHub. */
|
|
1225
1262
|
async createReview(n, input) {
|
|
@@ -1413,7 +1450,10 @@ function toSummary(r) {
|
|
|
1413
1450
|
labels: (r.labels ?? []).map((l) => l.name),
|
|
1414
1451
|
url: r.html_url,
|
|
1415
1452
|
updatedAt: r.updated_at,
|
|
1416
|
-
...typeof r.body === "string" ? { body: r.body } : {}
|
|
1453
|
+
...typeof r.body === "string" ? { body: r.body } : {},
|
|
1454
|
+
...r.created_at ? { createdAt: r.created_at } : {},
|
|
1455
|
+
...r.merged_at ? { mergedAt: r.merged_at } : {},
|
|
1456
|
+
...r.assignees?.length ? { assignees: r.assignees.map((a) => a.login ?? "").filter(Boolean) } : {}
|
|
1417
1457
|
};
|
|
1418
1458
|
}
|
|
1419
1459
|
function friendlyMessage(status, detail, info = {}) {
|
|
@@ -1490,9 +1530,37 @@ function toIssue(r) {
|
|
|
1490
1530
|
labels: r.labels.map((l) => typeof l === "string" ? l : l.name ?? "").filter(Boolean),
|
|
1491
1531
|
author: r.user?.login ?? "",
|
|
1492
1532
|
createdAt: r.created_at,
|
|
1493
|
-
updatedAt: r.updated_at
|
|
1533
|
+
updatedAt: r.updated_at,
|
|
1534
|
+
...r.assignees?.length ? { assignees: r.assignees.map((a) => a.login ?? "").filter(Boolean) } : {},
|
|
1535
|
+
...r.closed_at ? { closedAt: r.closed_at } : {}
|
|
1494
1536
|
};
|
|
1495
1537
|
}
|
|
1538
|
+
const FORMER_MEMBER = "A former member";
|
|
1539
|
+
const requesterFirstName = (name) => name.trim() === FORMER_MEMBER ? FORMER_MEMBER : name.trim().split(/\s+/)[0] ?? name;
|
|
1540
|
+
const LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
|
1541
|
+
const LINE = /(?:^|\n)[ \t>*_-]*(?:<sub>)?\s*Requested (?:from [^\n<]{1,40}? )?by ([^\n<]{1,120}?)(?:\s+with Bridge)?\.?\s*(?:<\/sub>)?[ \t]*(?=\n|$)/i;
|
|
1542
|
+
const SUGGESTED = new RegExp(LINE.source.replace("Requested (?:from", "Suggested (?:from"), "i");
|
|
1543
|
+
function requesterOf(body) {
|
|
1544
|
+
const m = LINE.exec(body ?? "") ?? SUGGESTED.exec(body ?? "");
|
|
1545
|
+
if (!m) return null;
|
|
1546
|
+
const who = m[1].trim().replace(/\.$/, "").replace(/\s+through Bridge$/i, "").replace(/\s*\([^()\s@]+@[^()\s]+\)$/, "");
|
|
1547
|
+
const both = /^(.+?)\s*\(@([^)\s]+)\)$/.exec(who);
|
|
1548
|
+
if (both) return LOGIN.test(both[2]) ? { name: both[1].trim(), login: both[2] } : null;
|
|
1549
|
+
if (who.startsWith("@")) return LOGIN.test(who.slice(1)) ? { name: null, login: who.slice(1) } : null;
|
|
1550
|
+
return /^[\p{L}][\p{L}'’. -]{0,60}$/u.test(who) && who.split(/\s+/).length <= 5 ? { name: who, login: null } : null;
|
|
1551
|
+
}
|
|
1552
|
+
function requesterLine(r) {
|
|
1553
|
+
const name = r.name?.trim();
|
|
1554
|
+
const login = r.login?.replace(/^@/, "").trim();
|
|
1555
|
+
if (name && login) return `Requested by ${name} (@${login}).`;
|
|
1556
|
+
if (login) return `Requested by @${login}.`;
|
|
1557
|
+
if (name) return `Requested by ${name}.`;
|
|
1558
|
+
throw new Error("A requester needs a name or a login.");
|
|
1559
|
+
}
|
|
1560
|
+
function requesterName(body) {
|
|
1561
|
+
const r = requesterOf(body);
|
|
1562
|
+
return r ? r.name ?? r.login : null;
|
|
1563
|
+
}
|
|
1496
1564
|
const BULK_LIMIT = 60;
|
|
1497
1565
|
const UNRELEASED = "Unreleased";
|
|
1498
1566
|
const SUBJECT_PR = [/^Merge pull request #(\d+)/, /\(#(\d+)\)\s*$/];
|
|
@@ -1557,7 +1625,9 @@ function buildHistoryEntry(input) {
|
|
|
1557
1625
|
sha: commit.sha,
|
|
1558
1626
|
...commit.parents[0] ? { baseSha: commit.parents[0] } : {},
|
|
1559
1627
|
date: commit.date,
|
|
1560
|
-
|
|
1628
|
+
// Who asked, as Bridge recorded it in the pull request ("Requested by Priya Shah ... through Bridge."), before the
|
|
1629
|
+
// account that opened it (the Bridge app for an email member, an agent): History names the person.
|
|
1630
|
+
author: requesterName(commit.pr?.body) ?? commit.pr?.author ?? commit.author,
|
|
1561
1631
|
title,
|
|
1562
1632
|
why,
|
|
1563
1633
|
...pr ? { pr } : {},
|
|
@@ -1910,9 +1980,9 @@ function finishPlan(p) {
|
|
|
1910
1980
|
if (!changes.length) return nothing("The edits leave every token as it is now.");
|
|
1911
1981
|
const prev = new Set(validateLayered(current).map(issueKey));
|
|
1912
1982
|
const newErrors = validateLayered(after).filter((i) => i.level === "error" && !prev.has(issueKey(i)));
|
|
1913
|
-
const
|
|
1983
|
+
const pairs2 = p.pairs ?? [];
|
|
1914
1984
|
const modes = config.contrast?.modes;
|
|
1915
|
-
const contrast =
|
|
1985
|
+
const contrast = pairs2.length ? contrastDelta(runLayeredContrast(current, pairs2, modes), runLayeredContrast(after, pairs2, modes)) : [];
|
|
1916
1986
|
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
1917
1987
|
const tier = classifyLayered(changes, [], config);
|
|
1918
1988
|
const impact = layeredImpact(current, after, changes);
|
|
@@ -2177,11 +2247,11 @@ function propShape(s) {
|
|
|
2177
2247
|
}
|
|
2178
2248
|
const VARIANT_FIRST = ["variant", "tone", "intent", "appearance", "kind", "type", "style"];
|
|
2179
2249
|
function shortVariant(key) {
|
|
2180
|
-
const
|
|
2250
|
+
const pairs2 = key.split(", ").map((p) => {
|
|
2181
2251
|
const at = p.indexOf("=");
|
|
2182
2252
|
return at < 0 ? { k: "", v: p.trim() } : { k: p.slice(0, at).trim(), v: p.slice(at + 1).trim() };
|
|
2183
2253
|
});
|
|
2184
|
-
const kept =
|
|
2254
|
+
const kept = pairs2.filter((p) => p.v && !/^default$/i.test(p.v));
|
|
2185
2255
|
if (!kept.length) return key;
|
|
2186
2256
|
const rank = (k) => {
|
|
2187
2257
|
const i = VARIANT_FIRST.indexOf(k.toLowerCase());
|
|
@@ -2222,6 +2292,14 @@ function firstChange(component, kind, c, all) {
|
|
|
2222
2292
|
}
|
|
2223
2293
|
case "new-variant": {
|
|
2224
2294
|
if (!c) return { title: `Update the ${component} variants`, summary: `The variants of ${component} changed in Figma.` };
|
|
2295
|
+
if (c.field === "Variants") {
|
|
2296
|
+
const gone = (c.before ?? "").split("; ").filter(Boolean);
|
|
2297
|
+
const born = (c.after ?? "").split("; ").filter(Boolean);
|
|
2298
|
+
if (gone.length === 1 && !born.length) return { title: `Remove the ${shortVariant(gone[0])} variant`, summary: `${component} no longer has the ${shortVariant(gone[0])} variant (${gone[0]}).` };
|
|
2299
|
+
if (born.length === 1 && !gone.length) return { title: `Add the ${shortVariant(born[0])} variant`, summary: `${component} gets a new variant, ${shortVariant(born[0])} (${born[0]}).` };
|
|
2300
|
+
const parts = [born.length ? `gets ${plural(born.length, "new variant")}: ${andList(born.map(shortVariant))}` : "", gone.length ? `no longer has ${andList(gone.map(shortVariant))}` : ""].filter(Boolean);
|
|
2301
|
+
return { title: `Update the ${component} variants`, summary: `${component} ${parts.join(", and ")}.` };
|
|
2302
|
+
}
|
|
2225
2303
|
const axis = c.field.replace(/^Variant /, "");
|
|
2226
2304
|
const before = values(c.before);
|
|
2227
2305
|
const after = values(c.after);
|
|
@@ -2250,7 +2328,7 @@ function firstChange(component, kind, c, all) {
|
|
|
2250
2328
|
if (!c) return { title: "Use another token", summary: `A part of ${component} uses another token in Figma.` };
|
|
2251
2329
|
const m = /^(.*?)(?: \((.*)\))?$/.exec(c.field);
|
|
2252
2330
|
const part = lower(m[1]);
|
|
2253
|
-
const where2 = m[2] ? ` in ${m[2]}` : "";
|
|
2331
|
+
const where2 = m[2] ? ` in ${shortVariants(m[2])}` : "";
|
|
2254
2332
|
const after = c.after && c.after !== "none" ? c.after : "";
|
|
2255
2333
|
const before = c.before && c.before !== "none" ? c.before : "";
|
|
2256
2334
|
if (!after) return { title: `Unbind the ${part}`, summary: `The ${part} of ${component}${where2} no longer uses ${before || "a token"}.` };
|
|
@@ -2300,6 +2378,11 @@ function sanitizeDraftLine(raw) {
|
|
|
2300
2378
|
}
|
|
2301
2379
|
function changePhrase(c) {
|
|
2302
2380
|
let m;
|
|
2381
|
+
if (c.field === "Variants") {
|
|
2382
|
+
const gone = (c.before ?? "").split("; ").filter(Boolean).map(shortVariant);
|
|
2383
|
+
const born = (c.after ?? "").split("; ").filter(Boolean).map(shortVariant);
|
|
2384
|
+
return [born.length ? `New variant${born.length === 1 ? "" : "s"} ${andList(born)}` : "", gone.length ? `${andList(gone)} variant${gone.length === 1 ? "" : "s"} removed` : ""].filter(Boolean).join("; ");
|
|
2385
|
+
}
|
|
2303
2386
|
if (m = /^Variant (.+)$/.exec(c.field)) {
|
|
2304
2387
|
const axis = m[1];
|
|
2305
2388
|
const before2 = values(c.before);
|
|
@@ -2325,7 +2408,7 @@ function changePhrase(c) {
|
|
|
2325
2408
|
const layer = parseStructureChange(c);
|
|
2326
2409
|
if (layer) return structurePhrase(layer);
|
|
2327
2410
|
m = /^(.*?)(?: \((.*)\))?$/.exec(c.field);
|
|
2328
|
-
const where2 = m[2] ? ` in ${m[2]}` : "";
|
|
2411
|
+
const where2 = m[2] ? ` in ${shortVariants(m[2])}` : "";
|
|
2329
2412
|
const before = c.before && c.before !== "none" ? c.before : "";
|
|
2330
2413
|
const after = c.after && c.after !== "none" ? c.after : "";
|
|
2331
2414
|
if (!after) return `${m[1]} no longer uses ${before || "a token"}${where2}`;
|
|
@@ -2403,10 +2486,10 @@ const EXPLORATION_NOTES_DIR = ".bridge/explorations";
|
|
|
2403
2486
|
const EXPLORATION_TIER_LABEL = "Exploration (not mergeable)";
|
|
2404
2487
|
const EXPLORATION_CHECK = "Exploration not merged as is";
|
|
2405
2488
|
const EXPLORATION_USE_CASES = [
|
|
2406
|
-
{ value: "user-test", label: "Prototype for a user test", hint: "A coded prototype
|
|
2407
|
-
{ value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone
|
|
2408
|
-
{ value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a
|
|
2409
|
-
{ value: "terminal", label: "Exploring with Claude Code", hint: "A designer works from the terminal with Claude and the Bridge MCP server
|
|
2489
|
+
{ value: "user-test", label: "Prototype for a user test", hint: "A coded prototype in a test version of a product, with the real components and tokens." },
|
|
2490
|
+
{ value: "stakeholder-demo", label: "Stakeholder demo", hint: "Show an idea on real screens before anyone signs off on it." },
|
|
2491
|
+
{ value: "ab-idea", label: "A/B idea", hint: "Ship a variant to a test version of a product and compare it with what is live." },
|
|
2492
|
+
{ value: "terminal", label: "Exploring with Claude Code", hint: "A designer works from the terminal with Claude and the Bridge MCP server inside this exploration, never in the library." },
|
|
2410
2493
|
{ value: "other", label: "Other", hint: "Anything else worth trying before it becomes a change." }
|
|
2411
2494
|
];
|
|
2412
2495
|
const EXPLORATION_STATUS_LABEL = { active: "Running", promoted: "Promoted to review", closed: "Closed" };
|
|
@@ -2433,8 +2516,8 @@ function explorationProblems(e) {
|
|
|
2433
2516
|
const name = e.name.trim();
|
|
2434
2517
|
const slug = e.slug?.trim() || explorationSlug(name);
|
|
2435
2518
|
if (!name) out.push('Name the exploration, for example "Denser data tables".');
|
|
2436
|
-
else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the
|
|
2437
|
-
else if (slug.length < 2 || !SLUG_RE.test(slug)) out.push("Use letters or digits in the name: it becomes the
|
|
2519
|
+
else if (name.length > 60) out.push("Keep the name under 60 characters: it becomes the exploration's link name.");
|
|
2520
|
+
else if (slug.length < 2 || !SLUG_RE.test(slug)) out.push("Use letters or digits in the name: it becomes the exploration's link name.");
|
|
2438
2521
|
if (!e.purpose.trim()) out.push("Say what you want to learn, in one sentence.");
|
|
2439
2522
|
return out;
|
|
2440
2523
|
}
|
|
@@ -2565,7 +2648,8 @@ function explorationShareMessage(input) {
|
|
|
2565
2648
|
const name = input.name.trim().replace(/"/g, "'");
|
|
2566
2649
|
const head = input.command ? `Exploration "${name}" (${input.branch}): try it in a staging branch of your product with: ${input.command}.` : `Exploration "${name}" (${input.branch}): its test build is being prepared, the install command for a staging branch of your product will be on the pull request in a few minutes.`;
|
|
2567
2650
|
const previews = (input.previews ?? []).map((p) => `${p.label} ${p.url}`);
|
|
2568
|
-
|
|
2651
|
+
const local = !previews.length && input.local ? `Try it locally: ${input.local}, then start Storybook or the app with its dev command.` : "";
|
|
2652
|
+
return [head, previews.length ? `Previews: ${previews.join(", ")}.` : local, `Pull request: ${input.url}.`].filter(Boolean).join(" ");
|
|
2569
2653
|
}
|
|
2570
2654
|
function explorationRules(branch) {
|
|
2571
2655
|
return [
|
|
@@ -2800,6 +2884,7 @@ const SPEC_FIELDS = [
|
|
|
2800
2884
|
"minWidth",
|
|
2801
2885
|
"minHeight",
|
|
2802
2886
|
"opacity",
|
|
2887
|
+
"shadow",
|
|
2803
2888
|
"textFill",
|
|
2804
2889
|
"fontFamily",
|
|
2805
2890
|
"fontSize",
|
|
@@ -2996,142 +3081,835 @@ function figmaCell(entry) {
|
|
|
2996
3081
|
if (entry.status === "layout") return { label: "Auto layout", title: "A layout helper: nothing to draw, use auto layout in Figma." };
|
|
2997
3082
|
return { label: "Not yet", ...entry.reason ? { title: `Not supported yet: ${entry.reason}` } : {} };
|
|
2998
3083
|
}
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
if (m) found.push(Number(m[1]));
|
|
3004
|
-
}
|
|
3005
|
-
for (const m of (body ?? "").matchAll(/-\s*\[[xX]\]\s*\**\s*Tier\s*([123])\b/g)) found.push(Number(m[1]));
|
|
3006
|
-
for (const m of (body ?? "").matchAll(/risk tier\W{0,6}(?:tier\s*)?([123])\b/gi)) found.push(Number(m[1]));
|
|
3007
|
-
return found.length ? Math.max(...found) : null;
|
|
3008
|
-
}
|
|
3009
|
-
function pullState(p) {
|
|
3010
|
-
if (p.merged_at || p.merged) return "merged";
|
|
3011
|
-
if (p.state && p.state !== "open") return "closed";
|
|
3012
|
-
if (p.draft) return "draft";
|
|
3013
|
-
return "open";
|
|
3014
|
-
}
|
|
3015
|
-
const PULL_STATE_PILL = {
|
|
3016
|
-
merged: { label: "Merged", cls: "" },
|
|
3017
|
-
closed: { label: "Closed", cls: "" },
|
|
3018
|
-
draft: { label: "Draft", cls: "" },
|
|
3019
|
-
open: { label: "Open", cls: "" }
|
|
3084
|
+
const px = (v) => {
|
|
3085
|
+
if (!v) return void 0;
|
|
3086
|
+
const m = /^(-?[\d.]+)px$/.exec(v.trim());
|
|
3087
|
+
return m ? Math.round(Number(m[1]) * 100) / 100 : void 0;
|
|
3020
3088
|
};
|
|
3021
|
-
function
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3089
|
+
function evalCalc(expr, resolve) {
|
|
3090
|
+
const body = /^calc\((.*)\)$/s.exec(expr.trim())?.[1];
|
|
3091
|
+
if (!body) return void 0;
|
|
3092
|
+
let missing = false;
|
|
3093
|
+
const text = body.replace(/[A-Za-z_][\w-]*(?:\.[\w-]+)*/g, (path) => {
|
|
3094
|
+
const v2 = resolve(path);
|
|
3095
|
+
const n = typeof v2 === "number" ? v2 : v2 === void 0 ? void 0 : Number.parseFloat(v2);
|
|
3096
|
+
if (n === void 0 || Number.isNaN(n)) missing = true;
|
|
3097
|
+
return String(n);
|
|
3025
3098
|
});
|
|
3099
|
+
if (missing || !/^[\d.\s+\-*/()]+$/.test(text)) return void 0;
|
|
3100
|
+
const tokens = text.match(/\d*\.?\d+|[+\-*/()]/g) ?? [];
|
|
3101
|
+
let i = 0;
|
|
3102
|
+
const factor = () => {
|
|
3103
|
+
const t = tokens[i++];
|
|
3104
|
+
if (t === "(") {
|
|
3105
|
+
const v2 = sum();
|
|
3106
|
+
i++;
|
|
3107
|
+
return v2;
|
|
3108
|
+
}
|
|
3109
|
+
if (t === "-") return -factor();
|
|
3110
|
+
return Number(t);
|
|
3111
|
+
};
|
|
3112
|
+
const product = () => {
|
|
3113
|
+
let v2 = factor();
|
|
3114
|
+
while (tokens[i] === "*" || tokens[i] === "/") v2 = tokens[i++] === "*" ? v2 * factor() : v2 / factor();
|
|
3115
|
+
return v2;
|
|
3116
|
+
};
|
|
3117
|
+
const sum = () => {
|
|
3118
|
+
let v2 = product();
|
|
3119
|
+
while (tokens[i] === "+" || tokens[i] === "-") v2 = tokens[i++] === "+" ? v2 + product() : v2 - product();
|
|
3120
|
+
return v2;
|
|
3121
|
+
};
|
|
3122
|
+
const v = sum();
|
|
3123
|
+
return Number.isFinite(v) ? Math.round(v * 100) / 100 : void 0;
|
|
3124
|
+
}
|
|
3125
|
+
const sameName = (cssVar, path) => cssVar.replace(/^--/, "").replace(/[^A-Za-z0-9]/g, "").toLowerCase() === path.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
|
|
3126
|
+
function normColor(v) {
|
|
3127
|
+
const t = v.trim().toLowerCase();
|
|
3128
|
+
const hex2 = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(t)?.[1];
|
|
3129
|
+
if (hex2) {
|
|
3130
|
+
const full = hex2.length <= 4 ? hex2.split("").map((c) => c + c).join("") : hex2;
|
|
3131
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
|
|
3132
|
+
const a = full.length === 8 ? Math.round(parseInt(full.slice(6, 8), 16) / 255 * 100) / 100 : 1;
|
|
3133
|
+
return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a})`;
|
|
3134
|
+
}
|
|
3135
|
+
const m = /^rgba?\(([^)]*)\)$/.exec(t);
|
|
3136
|
+
if (m) {
|
|
3137
|
+
const parts = m[1].split(/[\s,/]+/).filter(Boolean).map(Number);
|
|
3138
|
+
const [r, g, b] = parts;
|
|
3139
|
+
const a = parts[3] ?? 1;
|
|
3140
|
+
return a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${Math.round(a * 100) / 100})`;
|
|
3141
|
+
}
|
|
3142
|
+
return t.replace(/\s+/g, "");
|
|
3143
|
+
}
|
|
3144
|
+
const normText = (t) => (t ?? "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
3145
|
+
const graphic = (k) => k === "icon" || k === "vector" ? "graphic" : k;
|
|
3146
|
+
function withoutAddedWrappers(parts, measured) {
|
|
3147
|
+
const real = new Set(measured.parts.map((m) => m.id));
|
|
3148
|
+
const added = /* @__PURE__ */ new Map();
|
|
3149
|
+
for (const p of parts) {
|
|
3150
|
+
if (p.kind !== "frame" || !p.parent || real.has(p.id)) continue;
|
|
3151
|
+
const kids = parts.filter((c) => c.parent === p.id);
|
|
3152
|
+
if (kids.length === 1 && `${kids[0].id}-wrapper` === p.id) added.set(p.id, p.parent);
|
|
3153
|
+
}
|
|
3154
|
+
if (!added.size) return parts;
|
|
3155
|
+
const up = (id) => id && added.has(id) ? up(added.get(id)) : id;
|
|
3156
|
+
return parts.filter((p) => !added.has(p.id)).map((p) => p.parent && added.has(p.parent) ? { ...p, parent: up(p.parent) } : p);
|
|
3157
|
+
}
|
|
3158
|
+
function pairParts(staticParts, measured, o = {}) {
|
|
3159
|
+
const pairs2 = /* @__PURE__ */ new Map();
|
|
3160
|
+
const taken = /* @__PURE__ */ new Set();
|
|
3161
|
+
const mParts = new Map(measured.parts.map((p) => [p.id, p]));
|
|
3162
|
+
const sParts = new Map(staticParts.map((p) => [p.id, p]));
|
|
3163
|
+
const take = (s, m) => {
|
|
3164
|
+
pairs2.set(s, m);
|
|
3165
|
+
taken.add(m);
|
|
3166
|
+
};
|
|
3167
|
+
const ancestors = (id) => {
|
|
3168
|
+
const out = [];
|
|
3169
|
+
for (let p = mParts.get(id)?.parent; p; p = mParts.get(p)?.parent) out.push(p);
|
|
3170
|
+
return out;
|
|
3171
|
+
};
|
|
3172
|
+
const pairedAncestor = (p) => {
|
|
3173
|
+
for (let a = p.parent; a; a = sParts.get(a)?.parent) if (pairs2.has(a)) return pairs2.get(a);
|
|
3174
|
+
return void 0;
|
|
3175
|
+
};
|
|
3176
|
+
const best = (p, cands) => {
|
|
3177
|
+
if (!cands.length) return void 0;
|
|
3178
|
+
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3179
|
+
if (parent) {
|
|
3180
|
+
const direct = cands.find((c) => c.parent === parent);
|
|
3181
|
+
if (direct) return direct;
|
|
3182
|
+
}
|
|
3183
|
+
const anc = pairedAncestor(p);
|
|
3184
|
+
if (anc) {
|
|
3185
|
+
const under2 = cands.find((c) => ancestors(c.id).includes(anc));
|
|
3186
|
+
if (under2) return under2;
|
|
3187
|
+
}
|
|
3188
|
+
return cands[0];
|
|
3189
|
+
};
|
|
3190
|
+
const root = staticParts.find((p) => !p.parent);
|
|
3191
|
+
if (root) take(root.id, "root");
|
|
3192
|
+
const textOf = (p) => {
|
|
3193
|
+
if (p.characters) return normText(p.characters);
|
|
3194
|
+
const prop = p.text ? o.properties?.find((x) => x.name === p.text && x.type === "TEXT") : void 0;
|
|
3195
|
+
return typeof prop?.default === "string" ? normText(prop.default) : "";
|
|
3196
|
+
};
|
|
3197
|
+
const classesOf = (p) => [...(p?.source ?? "").matchAll(/(?:^|[\s,])\.([\w-]+)/g)].map((x) => x[1]);
|
|
3198
|
+
const mClasses = (m) => [...new Set((m.source ?? "").split(" ").filter(Boolean))];
|
|
3199
|
+
const shared = (p, m) => {
|
|
3200
|
+
const mine = new Set(classesOf(p));
|
|
3201
|
+
return mClasses(m).filter((c) => mine.has(c)).length;
|
|
3202
|
+
};
|
|
3203
|
+
const staticClasses = new Set(staticParts.flatMap((p) => classesOf(p)));
|
|
3204
|
+
const claimedByOther = (p, m) => !shared(p, m) && mClasses(m).length > 0 && (classesOf(p).length > 0 || mClasses(m).some((c) => staticClasses.has(c)));
|
|
3205
|
+
for (const p of staticParts) {
|
|
3206
|
+
if (p.kind !== "text" || pairs2.has(p.id)) continue;
|
|
3207
|
+
const t = textOf(p);
|
|
3208
|
+
if (!t) continue;
|
|
3209
|
+
const hit = best(p, measured.parts.filter((m) => m.kind === "text" && !taken.has(m.id) && normText(m.characters) === t && !(classesOf(p).length && mClasses(m).length && !shared(p, m))));
|
|
3210
|
+
if (hit) take(p.id, hit.id);
|
|
3211
|
+
}
|
|
3212
|
+
const classOf = (p) => /\.([\w-]+)\s*$/.exec(p?.source ?? "")?.[1];
|
|
3213
|
+
for (const p of staticParts) {
|
|
3214
|
+
if (p.kind !== "text" || !pairs2.has(p.id) || !p.parent || pairs2.has(p.parent)) continue;
|
|
3215
|
+
const sp = sParts.get(p.parent);
|
|
3216
|
+
if (!sp || sp.kind !== "frame" || !sp.parent) continue;
|
|
3217
|
+
const cls = classOf(sp);
|
|
3218
|
+
const mp = cls ? ancestors(pairs2.get(p.id)).find((a) => (mParts.get(a)?.source ?? "").split(" ").includes(cls)) : mParts.get(pairs2.get(p.id))?.parent;
|
|
3219
|
+
if (mp && mp !== "root" && !taken.has(mp) && mParts.get(mp)?.kind === "frame") take(sp.id, mp);
|
|
3220
|
+
}
|
|
3221
|
+
for (const p of staticParts) {
|
|
3222
|
+
if (pairs2.has(p.id) || !classesOf(p).length) continue;
|
|
3223
|
+
if (p.parent && !pairs2.has(p.parent) && classesOf(sParts.get(p.parent)).length) continue;
|
|
3224
|
+
const all = measured.parts.filter((m) => !taken.has(m.id) && shared(p, m) > 0);
|
|
3225
|
+
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3226
|
+
const under2 = parent ? all.filter((m) => m.parent === parent) : [];
|
|
3227
|
+
const cands = under2.length ? under2 : all;
|
|
3228
|
+
const most = Math.max(0, ...cands.map((m) => shared(p, m)));
|
|
3229
|
+
const hit = best(p, cands.filter((m) => shared(p, m) === most));
|
|
3230
|
+
if (hit) take(p.id, hit.id);
|
|
3231
|
+
}
|
|
3232
|
+
for (const p of staticParts) {
|
|
3233
|
+
if (pairs2.has(p.id)) continue;
|
|
3234
|
+
if (p.parent && !pairs2.has(p.parent)) continue;
|
|
3235
|
+
const parent = p.parent ? pairs2.get(p.parent) : void 0;
|
|
3236
|
+
const known = p.kind === "text" && !!textOf(p);
|
|
3237
|
+
const free = measured.parts.filter((m) => !taken.has(m.id) && graphic(m.kind) === graphic(p.kind) && !claimedByOther(p, m));
|
|
3238
|
+
const hit = free.find((m) => !parent || m.parent === parent) ?? (p.kind === "text" && parent && !known ? free.find((m) => ancestors(m.id).includes(parent)) : void 0);
|
|
3239
|
+
if (hit) take(p.id, hit.id);
|
|
3240
|
+
}
|
|
3241
|
+
for (const p of staticParts) {
|
|
3242
|
+
if (pairs2.has(p.id) || p.kind !== "text" || !p.parent) continue;
|
|
3243
|
+
const mp = pairs2.get(p.parent);
|
|
3244
|
+
const t = textOf(p);
|
|
3245
|
+
if (mp && mParts.get(mp)?.kind === "text" && staticParts.filter((x) => x.parent === p.parent).length === 1 && (!t || normText(mParts.get(mp).characters) === t)) pairs2.set(p.id, mp);
|
|
3246
|
+
}
|
|
3247
|
+
return pairs2;
|
|
3248
|
+
}
|
|
3249
|
+
function compareSpecToMeasured(staticParts, staticValues, measured, resolve, o = {}) {
|
|
3250
|
+
const pairs2 = pairParts(staticParts, measured, o);
|
|
3251
|
+
const out = { partsMatched: pairs2.size, staticParts: staticParts.length, measuredParts: measured.parts.length, compared: 0, same: 0, sameToken: 0, diffs: [], unmatchedParts: [], staticOnly: 0, measuredOnly: 0 };
|
|
3252
|
+
const num2 = (v) => typeof v === "number" ? v : v === void 0 ? void 0 : /^-?[\d.]+$/.test(v.trim()) ? Number(v) : px(v);
|
|
3253
|
+
const near = (a, b) => {
|
|
3254
|
+
if (a === void 0 || b === void 0) return false;
|
|
3255
|
+
const na0 = num2(a);
|
|
3256
|
+
const nb0 = num2(b);
|
|
3257
|
+
if (na0 !== void 0 && nb0 !== void 0) return Math.abs(na0 - nb0) <= 0.5;
|
|
3258
|
+
if (typeof a === "number" && typeof b === "number") return Math.abs(a - b) <= 0.5;
|
|
3259
|
+
const na = typeof a === "string" ? px(a) ?? normColor(a) : a;
|
|
3260
|
+
const nb = typeof b === "string" ? px(b) ?? normColor(b) : b;
|
|
3261
|
+
return typeof na === "number" && typeof nb === "number" ? Math.abs(na - nb) <= 0.5 : na === nb;
|
|
3262
|
+
};
|
|
3263
|
+
for (const sp of staticParts) {
|
|
3264
|
+
const mid = pairs2.get(sp.id);
|
|
3265
|
+
if (!mid) {
|
|
3266
|
+
out.unmatchedParts.push(sp.id);
|
|
3267
|
+
continue;
|
|
3268
|
+
}
|
|
3269
|
+
const sv = staticValues[sp.id] ?? {};
|
|
3270
|
+
const mv = mid ? measured.values[mid] ?? {} : {};
|
|
3271
|
+
const mr = mid ? measured.raw[mid] ?? {} : {};
|
|
3272
|
+
const fields = /* @__PURE__ */ new Set([...Object.keys(sv), ...Object.keys(mv)]);
|
|
3273
|
+
for (const f of fields) {
|
|
3274
|
+
if ((f === "width" || f === "height") && sv[f] === void 0) continue;
|
|
3275
|
+
const s = sv[f];
|
|
3276
|
+
const m = mv[f] !== void 0 ? mv[f] : mr[f];
|
|
3277
|
+
if (s === void 0) {
|
|
3278
|
+
if (m !== void 0 && m !== 0) out.measuredOnly++;
|
|
3279
|
+
continue;
|
|
3280
|
+
}
|
|
3281
|
+
if (m === void 0 && !(f in mr)) {
|
|
3282
|
+
if (s === 0 || s === null) continue;
|
|
3283
|
+
const r = typeof s === "string" && resolve ? resolve(s) : s;
|
|
3284
|
+
if (r === 0 || r === "0px" || r === null) continue;
|
|
3285
|
+
out.staticOnly++;
|
|
3286
|
+
out.diffs.push({ part: sp.id, field: f, static: s, same: false });
|
|
3287
|
+
continue;
|
|
3288
|
+
}
|
|
3289
|
+
out.compared++;
|
|
3290
|
+
let same2 = s === m;
|
|
3291
|
+
if (!same2 && typeof s === "string" && !/^calc\(/.test(s)) same2 = (measured.tokens[mid]?.[f]?.chain ?? []).some((v) => sameName(v, s));
|
|
3292
|
+
if (same2 && typeof s === "string") out.sameToken++;
|
|
3293
|
+
if (!same2 && resolve) {
|
|
3294
|
+
let rs = typeof s === "string" ? /^calc\(/.test(s) ? evalCalc(s, resolve) : resolve(s) : typeof s === "number" ? s : void 0;
|
|
3295
|
+
const size = typeof mr.fontSize === "number" ? mr.fontSize : void 0;
|
|
3296
|
+
if (f === "lineHeight" && size && rs !== void 0 && /^[\d.]+$/.test(String(rs).trim()) && Number(rs) < 4) rs = Number(rs) * size;
|
|
3297
|
+
same2 = near(rs, mr[f]);
|
|
3298
|
+
}
|
|
3299
|
+
if (same2) out.same++;
|
|
3300
|
+
else out.diffs.push({ part: sp.id, field: f, static: s, measured: m ?? null, same: same2 });
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
const root = staticValues[staticParts.find((p) => !p.parent)?.id ?? "root"] ?? {};
|
|
3304
|
+
const box = measured.boxes.root;
|
|
3305
|
+
if (box) out.rootSize = { static: { width: typeof root.width === "number" ? root.width : void 0, height: typeof root.height === "number" ? root.height : void 0 }, measured: { width: box.width, height: box.height } };
|
|
3306
|
+
return out;
|
|
3026
3307
|
}
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3308
|
+
function tokenVariablePath(name, prefix) {
|
|
3309
|
+
let parts = name.replace(/^--/, "").split(/-+/).flatMap((x) => x.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")).map((x) => x.toLowerCase()).filter(Boolean);
|
|
3310
|
+
if (prefix && parts[0] === prefix.toLowerCase() && parts.length > 1) parts = parts.slice(1);
|
|
3311
|
+
return parts.join("/");
|
|
3312
|
+
}
|
|
3313
|
+
function sharedTokenPrefix(names2) {
|
|
3314
|
+
if (names2.length < 20) return void 0;
|
|
3315
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3316
|
+
for (const n2 of names2) {
|
|
3317
|
+
const first = n2.replace(/^--/, "").split("-")[0];
|
|
3318
|
+
counts.set(first, (counts.get(first) ?? 0) + 1);
|
|
3036
3319
|
}
|
|
3320
|
+
const [top, n] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
|
|
3321
|
+
return top && n / names2.length >= 0.6 ? top : void 0;
|
|
3322
|
+
}
|
|
3323
|
+
function markUsedTokens(tokens, stories) {
|
|
3324
|
+
const byName = new Map(tokens.properties.map((p) => [p.name, p]));
|
|
3037
3325
|
const seen = /* @__PURE__ */ new Set();
|
|
3038
|
-
|
|
3039
|
-
if (seen.has(
|
|
3040
|
-
seen.add(
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3326
|
+
const use = (name) => {
|
|
3327
|
+
if (seen.has(name)) return;
|
|
3328
|
+
seen.add(name);
|
|
3329
|
+
const p = byName.get(name);
|
|
3330
|
+
if (!p) return;
|
|
3331
|
+
p.used = true;
|
|
3332
|
+
for (const target of [p.aliasOf, ...Object.values(p.aliasOfByMode ?? {})]) if (target) use(target);
|
|
3333
|
+
if (p.kind === "expression") for (const m of p.raw.matchAll(/var\(\s*(--[\w-]+)/g)) use(m[1]);
|
|
3334
|
+
};
|
|
3335
|
+
for (const s of stories)
|
|
3336
|
+
for (const spec of [s.spec, ...s.instances ?? []])
|
|
3337
|
+
for (const part of Object.values(spec?.tokens ?? {}))
|
|
3338
|
+
for (const t of Object.values(part ?? {})) for (const name of t?.chain ?? []) use(name);
|
|
3339
|
+
const used = tokens.properties.filter((p) => p.used).length;
|
|
3340
|
+
tokens.counts.used = used;
|
|
3341
|
+
return used;
|
|
3342
|
+
}
|
|
3343
|
+
class MeasuredSpecsError extends Error {
|
|
3344
|
+
}
|
|
3345
|
+
const isObj$1 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3346
|
+
function parseMeasuredSpecsFile(input) {
|
|
3347
|
+
let data = input;
|
|
3348
|
+
if (typeof input === "string") {
|
|
3349
|
+
try {
|
|
3350
|
+
data = JSON.parse(input);
|
|
3351
|
+
} catch (e) {
|
|
3352
|
+
throw new MeasuredSpecsError(`measured specs are not JSON: ${e.message}`);
|
|
3353
|
+
}
|
|
3044
3354
|
}
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3355
|
+
if (!isObj$1(data)) throw new MeasuredSpecsError("measured specs: expected an object");
|
|
3356
|
+
if (data.version !== 1) throw new MeasuredSpecsError(`measured specs: version ${String(data.version)} is not supported (expected 1)`);
|
|
3357
|
+
if (typeof data.storybook !== "string") throw new MeasuredSpecsError("measured specs: storybook URL missing");
|
|
3358
|
+
if (!Array.isArray(data.stories)) throw new MeasuredSpecsError("measured specs: stories missing");
|
|
3359
|
+
data.stories.forEach((s, i) => {
|
|
3360
|
+
if (!isObj$1(s) || typeof s.id !== "string") throw new MeasuredSpecsError(`measured specs: story ${i} has no id`);
|
|
3361
|
+
const specs = [s.spec, ...Array.isArray(s.instances) ? s.instances : []].filter((x) => x !== void 0);
|
|
3362
|
+
for (const sp of specs) {
|
|
3363
|
+
if (!isObj$1(sp) || !Array.isArray(sp.parts) || !isObj$1(sp.values)) throw new MeasuredSpecsError(`measured specs: story ${s.id} has a spec without parts and values`);
|
|
3364
|
+
for (const p of sp.parts) if (!isObj$1(p) || typeof p.id !== "string" || typeof p.kind !== "string") throw new MeasuredSpecsError(`measured specs: story ${s.id} has a part without id or kind`);
|
|
3365
|
+
}
|
|
3366
|
+
if (s.props !== void 0 && !isObj$1(s.props)) throw new MeasuredSpecsError(`measured specs: story ${s.id} props is not an object`);
|
|
3367
|
+
});
|
|
3368
|
+
if (data.components !== void 0 && !Array.isArray(data.components)) throw new MeasuredSpecsError("measured specs: components is not a list");
|
|
3369
|
+
if (data.tokens !== void 0) {
|
|
3370
|
+
const t = data.tokens;
|
|
3371
|
+
if (!isObj$1(t) || !Array.isArray(t.properties) || !Array.isArray(t.modes)) throw new MeasuredSpecsError("measured specs: tokens needs properties and modes");
|
|
3372
|
+
for (const p of t.properties) if (!isObj$1(p) || typeof p.name !== "string" || typeof p.path !== "string" || p.tier !== "primitive" && p.tier !== "semantic") throw new MeasuredSpecsError("measured specs: a token has no name, path or tier");
|
|
3373
|
+
}
|
|
3374
|
+
return data;
|
|
3375
|
+
}
|
|
3376
|
+
const mul = (m, v) => [0, 1, 2].map((i) => m[i][0] * v[0] + m[i][1] * v[1] + m[i][2] * v[2]);
|
|
3377
|
+
const XYZ_D65_TO_LINEAR_SRGB = [
|
|
3378
|
+
[3.2409699419045226, -1.537383177570094, -0.4986107602930034],
|
|
3379
|
+
[-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
|
|
3380
|
+
[0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
|
|
3381
|
+
];
|
|
3382
|
+
const D50_TO_D65 = [
|
|
3383
|
+
[0.9554734527042182, -0.023098536874261423, 0.0632593086610217],
|
|
3384
|
+
[-0.028369706963208136, 1.0099954580058226, 0.021041398966943008],
|
|
3385
|
+
[0.012314001688319899, -0.020507696433477912, 1.3303659366080753]
|
|
3386
|
+
];
|
|
3387
|
+
const LINEAR_P3_TO_XYZ_D65 = [
|
|
3388
|
+
[0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
|
|
3389
|
+
[0.2289745640697488, 0.6917385218365064, 0.079286914093745],
|
|
3390
|
+
[0, 0.04511338185890264, 1.043944368900976]
|
|
3391
|
+
];
|
|
3392
|
+
const D50_WHITE = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
|
|
3393
|
+
const toLinear = (c) => Math.abs(c) <= 0.04045 ? c / 12.92 : Math.sign(c) * ((Math.abs(c) + 0.055) / 1.055) ** 2.4;
|
|
3394
|
+
const toGamma = (c) => Math.abs(c) <= 31308e-7 ? 12.92 * c : Math.sign(c) * (1.055 * Math.abs(c) ** (1 / 2.4) - 0.055);
|
|
3395
|
+
function oklabToLinearSrgb(L, a, b) {
|
|
3396
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
3397
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
3398
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
3399
|
+
return [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s];
|
|
3400
|
+
}
|
|
3401
|
+
function labToXyzD50(L, a, b) {
|
|
3402
|
+
const e = 216 / 24389;
|
|
3403
|
+
const k = 24389 / 27;
|
|
3404
|
+
const fy = (L + 16) / 116;
|
|
3405
|
+
const fx = a / 500 + fy;
|
|
3406
|
+
const fz = fy - b / 200;
|
|
3407
|
+
const x = fx ** 3 > e ? fx ** 3 : (116 * fx - 16) / k;
|
|
3408
|
+
const y = L > k * e ? fy ** 3 : L / k;
|
|
3409
|
+
const z = fz ** 3 > e ? fz ** 3 : (116 * fz - 16) / k;
|
|
3410
|
+
return [x * D50_WHITE[0], y * D50_WHITE[1], z * D50_WHITE[2]];
|
|
3411
|
+
}
|
|
3412
|
+
function num(s, full = 1) {
|
|
3413
|
+
if (s === void 0 || s === "none") return 0;
|
|
3414
|
+
if (s.endsWith("%")) return parseFloat(s) / 100 * full;
|
|
3415
|
+
if (/deg$/.test(s)) return parseFloat(s);
|
|
3416
|
+
if (/turn$/.test(s)) return parseFloat(s) * 360;
|
|
3417
|
+
if (/rad$/.test(s)) return parseFloat(s) * 180 / Math.PI;
|
|
3418
|
+
return parseFloat(s);
|
|
3419
|
+
}
|
|
3420
|
+
function linearSrgb(fn, args) {
|
|
3421
|
+
switch (fn) {
|
|
3422
|
+
case "oklab":
|
|
3423
|
+
return oklabToLinearSrgb(num(args[0]), num(args[1], 0.4), num(args[2], 0.4));
|
|
3424
|
+
case "oklch": {
|
|
3425
|
+
const c = num(args[1], 0.4);
|
|
3426
|
+
const h = num(args[2]) * Math.PI / 180;
|
|
3427
|
+
return oklabToLinearSrgb(num(args[0]), c * Math.cos(h), c * Math.sin(h));
|
|
3428
|
+
}
|
|
3429
|
+
case "lab":
|
|
3430
|
+
return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), num(args[1], 125), num(args[2], 125))));
|
|
3431
|
+
case "lch": {
|
|
3432
|
+
const c = num(args[1], 150);
|
|
3433
|
+
const h = num(args[2]) * Math.PI / 180;
|
|
3434
|
+
return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, labToXyzD50(num(args[0], 100), c * Math.cos(h), c * Math.sin(h))));
|
|
3435
|
+
}
|
|
3436
|
+
case "color": {
|
|
3437
|
+
const [space, ...c] = args;
|
|
3438
|
+
const v = [num(c[0]), num(c[1]), num(c[2])];
|
|
3439
|
+
if (space === "srgb") return v.map(toLinear);
|
|
3440
|
+
if (space === "srgb-linear") return v;
|
|
3441
|
+
if (space === "display-p3") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(LINEAR_P3_TO_XYZ_D65, v.map(toLinear)));
|
|
3442
|
+
if (space === "xyz" || space === "xyz-d65") return mul(XYZ_D65_TO_LINEAR_SRGB, v);
|
|
3443
|
+
if (space === "xyz-d50") return mul(XYZ_D65_TO_LINEAR_SRGB, mul(D50_TO_D65, v));
|
|
3444
|
+
return void 0;
|
|
3445
|
+
}
|
|
3056
3446
|
}
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
const
|
|
3062
|
-
const
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3447
|
+
return void 0;
|
|
3448
|
+
}
|
|
3449
|
+
const COLOR_FN = /\b(oklch|oklab|lab|lch|color)\(([^()]*)\)/gi;
|
|
3450
|
+
function cssColorToRgb(fn, inner) {
|
|
3451
|
+
const [main, alpha] = inner.split("/");
|
|
3452
|
+
const args = main.trim().split(/[\s,]+/).filter(Boolean);
|
|
3453
|
+
const lin = linearSrgb(fn.toLowerCase(), args);
|
|
3454
|
+
if (!lin || lin.some((x) => !Number.isFinite(x))) return void 0;
|
|
3455
|
+
const [r, g, b] = lin.map((x) => Math.round(Math.min(1, Math.max(0, toGamma(x))) * 255));
|
|
3456
|
+
const a = alpha === void 0 ? 1 : Math.min(1, Math.max(0, num(alpha.trim())));
|
|
3457
|
+
return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${Math.round(a * 1e3) / 1e3})`;
|
|
3458
|
+
}
|
|
3459
|
+
function cssColorsToRgb(text) {
|
|
3460
|
+
if (!/(oklch|oklab|lab|lch|color)\(/i.test(text)) return text;
|
|
3461
|
+
return text.replace(COLOR_FN, (whole, fn, inner) => cssColorToRgb(fn, inner) ?? whole);
|
|
3462
|
+
}
|
|
3463
|
+
function withSrgbColors(value) {
|
|
3464
|
+
if (typeof value === "string") return cssColorsToRgb(value);
|
|
3465
|
+
if (Array.isArray(value)) return value.map((v) => withSrgbColors(v));
|
|
3466
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, withSrgbColors(v)]));
|
|
3467
|
+
return value;
|
|
3468
|
+
}
|
|
3469
|
+
function parseBoxShadow(css) {
|
|
3470
|
+
if (!css || css.trim() === "none") return [];
|
|
3471
|
+
const out = [];
|
|
3472
|
+
for (const one of cssColorsToRgb(css).split(/,(?![^(]*\))/)) {
|
|
3473
|
+
const color = /rgba?\(([^)]*)\)/i.exec(one);
|
|
3474
|
+
const c = color ? color[1].split(/[\s,/]+/).filter(Boolean).map(Number) : [0, 0, 0, 1];
|
|
3475
|
+
const a = c[3] ?? 1;
|
|
3476
|
+
const lengths = one.replace(/rgba?\([^)]*\)/i, "").replace(/inset/i, "").trim().split(/\s+/).filter(Boolean).map((x) => Number.parseFloat(x));
|
|
3477
|
+
if (lengths.length < 2 || lengths.some((x) => !Number.isFinite(x)) || !(a > 0)) continue;
|
|
3478
|
+
out.push({ inset: /\binset\b/i.test(one), x: lengths[0], y: lengths[1], blur: lengths[2] ?? 0, spread: lengths[3] ?? 0, color: { r: c[0] / 255, g: c[1] / 255, b: c[2] / 255, a } });
|
|
3073
3479
|
}
|
|
3074
|
-
return
|
|
3480
|
+
return out;
|
|
3075
3481
|
}
|
|
3076
|
-
const
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3482
|
+
const where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
|
|
3483
|
+
const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
3484
|
+
const and = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs.at(-1)}`;
|
|
3485
|
+
function rawIssue(label, r, values2) {
|
|
3486
|
+
const matches = [...new Set(values2.flatMap((v) => v.matches ?? []))];
|
|
3487
|
+
const todo = !matches.length ? "No variable has that value: bind it to one so it follows the library, or create the variable and send it with the variables." : matches.length === 1 ? `${matches[0]} has that value: bind it so it follows the library.` : `${and(matches)} have that value: bind one so it follows the library.`;
|
|
3488
|
+
return { what: `${label} is ${and(values2.map((v) => v.value))}, not a variable.`, todo, raw: { kind: r.kind, variants: r.variants, values: values2 } };
|
|
3080
3489
|
}
|
|
3081
|
-
function
|
|
3082
|
-
const
|
|
3083
|
-
const
|
|
3084
|
-
const
|
|
3085
|
-
const
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
if (input.ci.state === "pending") blocking.push("Tests are still running.");
|
|
3092
|
-
if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
|
|
3093
|
-
const tier = input.tier ?? 3;
|
|
3094
|
-
if (tier >= 2) {
|
|
3095
|
-
if (!input.approvals.design) {
|
|
3096
|
-
approvalReasons.push("It needs a design owner approval.");
|
|
3097
|
-
missing.push("design approval");
|
|
3490
|
+
function planComponentPush(d, ctx) {
|
|
3491
|
+
const issues = [...d.unknown];
|
|
3492
|
+
const reasons = [];
|
|
3493
|
+
const edits = /* @__PURE__ */ new Map();
|
|
3494
|
+
const also = /* @__PURE__ */ new Set();
|
|
3495
|
+
for (const r of d.rebinds) {
|
|
3496
|
+
const label = `${cap(r.kind)} in ${where(r)}`;
|
|
3497
|
+
if (r.raw?.length) {
|
|
3498
|
+
issues.push(rawIssue(label, r, r.raw));
|
|
3499
|
+
continue;
|
|
3098
3500
|
}
|
|
3099
|
-
if (!
|
|
3100
|
-
|
|
3101
|
-
|
|
3501
|
+
if (!r.after.length) {
|
|
3502
|
+
issues.push({ what: `${label} is no longer bound to a variable: it uses a raw value.`, todo: "Bind it to a variable, or create the variable and send it with the variables, then scan again." });
|
|
3503
|
+
continue;
|
|
3102
3504
|
}
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3505
|
+
const missing = r.after.filter((p) => !ctx.tierOf(p));
|
|
3506
|
+
if (missing.length) {
|
|
3507
|
+
issues.push({ what: `${label} uses ${missing.join(", ")}, a variable the library does not have yet.`, todo: "Send the variable first (Variables, above), then scan again." });
|
|
3508
|
+
continue;
|
|
3509
|
+
}
|
|
3510
|
+
if (!r.before.length) {
|
|
3511
|
+
reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: the CSS needs it added.`);
|
|
3512
|
+
continue;
|
|
3513
|
+
}
|
|
3514
|
+
if (r.before.length !== 1 || r.after.length !== 1) {
|
|
3515
|
+
reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so the CSS needs writing.`);
|
|
3516
|
+
continue;
|
|
3517
|
+
}
|
|
3518
|
+
const [from, to] = [r.before[0], r.after[0]];
|
|
3519
|
+
if (ctx.tierOf(from) !== "component") {
|
|
3520
|
+
reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: the CSS needs to change to ${to}.`);
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
const elsewhere = ctx.usedElsewhere(from, r);
|
|
3524
|
+
if (elsewhere.variants.length) {
|
|
3525
|
+
const v = elsewhere.variants;
|
|
3526
|
+
const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
|
|
3527
|
+
reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so it needs a new token for ${where(r)}.`);
|
|
3528
|
+
continue;
|
|
3529
|
+
}
|
|
3530
|
+
for (const c of elsewhere.components) also.add(c);
|
|
3531
|
+
const prev = edits.get(from);
|
|
3532
|
+
if (prev && prev.value !== `{${to}}`) {
|
|
3533
|
+
reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): it needs splitting in two.`);
|
|
3534
|
+
edits.delete(from);
|
|
3535
|
+
continue;
|
|
3536
|
+
}
|
|
3537
|
+
edits.set(from, { op: "set", path: from, value: `{${to}}` });
|
|
3106
3538
|
}
|
|
3107
|
-
const
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
if (
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3539
|
+
const list = [...edits.values()];
|
|
3540
|
+
if (d.needsCode || reasons.length) {
|
|
3541
|
+
return {
|
|
3542
|
+
route: "claude-draft",
|
|
3543
|
+
said: `A developer builds it with Claude from what you changed, then a person reviews it.${list.length ? ` ${list.length === 1 ? "Its token swap goes" : "Its token swaps go"} with it.` : ""}`,
|
|
3544
|
+
edits: list,
|
|
3545
|
+
reasons,
|
|
3546
|
+
issues,
|
|
3547
|
+
alsoChanges: [...also]
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
if (list.length) {
|
|
3551
|
+
const n = list.length;
|
|
3552
|
+
const others = [...also];
|
|
3553
|
+
const names2 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
|
|
3554
|
+
const alsoLine = names2.length ? ` It also repaints ${names2.length > 1 ? `${names2.slice(0, -1).join(", ")} and ${names2[names2.length - 1]}` : names2[0]}: they use the same token${n === 1 ? "" : "s"}.` : "";
|
|
3555
|
+
return { route: "token-change", said: `Exact, no AI: ${n === 1 ? `${list[0].path} points` : `${n} ${d.component} tokens point`} to the new variable${n === 1 ? "" : "s"}, checked for contrast and reviewed like any token change.${alsoLine}`, edits: list, reasons, issues, alsoChanges: others };
|
|
3556
|
+
}
|
|
3557
|
+
return { route: "manual", said: issues.length ? "Fix it in Figma first: nothing is sent until Bridge can read it." : "Nothing to send.", edits: [], reasons, issues, alsoChanges: [] };
|
|
3123
3558
|
}
|
|
3124
|
-
function
|
|
3125
|
-
const
|
|
3126
|
-
return
|
|
3127
|
-
|
|
3128
|
-
|
|
3559
|
+
function specsUsage(specs, component) {
|
|
3560
|
+
const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
|
|
3561
|
+
return (path, r) => {
|
|
3562
|
+
const variants = [];
|
|
3563
|
+
const components = [];
|
|
3564
|
+
for (const c of specs.components) {
|
|
3565
|
+
for (const v of c.variants) {
|
|
3566
|
+
if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
|
|
3567
|
+
const key = variantKeyOf(v.props);
|
|
3568
|
+
if (c === own) {
|
|
3569
|
+
if (!r.variants.includes(key)) variants.push(key);
|
|
3570
|
+
} else if (!components.includes(c.name)) components.push(c.name);
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
return { variants, components };
|
|
3574
|
+
};
|
|
3129
3575
|
}
|
|
3130
|
-
function
|
|
3131
|
-
|
|
3576
|
+
function componentContrastPairs(edits, pairs2, tokens) {
|
|
3577
|
+
const out = /* @__PURE__ */ new Map();
|
|
3578
|
+
for (const e of edits) {
|
|
3579
|
+
const target = tokens.aliasOf(e.path);
|
|
3580
|
+
if (!target) continue;
|
|
3581
|
+
const group = e.path.split(".")[0];
|
|
3582
|
+
const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
|
|
3583
|
+
for (const p of pairs2) {
|
|
3584
|
+
if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
|
|
3585
|
+
if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
return [...out.values()];
|
|
3132
3589
|
}
|
|
3133
|
-
|
|
3134
|
-
|
|
3590
|
+
const FROM_FIGMA_LABEL = "from-figma";
|
|
3591
|
+
const WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
|
|
3592
|
+
const DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
|
|
3593
|
+
const FIGMA_SPEC_DIR = ".bridge/figma";
|
|
3594
|
+
const ORIGIN_MARK = "bridge-from-figma";
|
|
3595
|
+
const hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
3596
|
+
function parseFigmaOrigin(body) {
|
|
3597
|
+
if (!body) return void 0;
|
|
3598
|
+
const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
3599
|
+
if (!m) return void 0;
|
|
3600
|
+
try {
|
|
3601
|
+
const o = JSON.parse(m[1]);
|
|
3602
|
+
return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
|
|
3603
|
+
} catch {
|
|
3604
|
+
return void 0;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
function figmaOriginSection(o) {
|
|
3608
|
+
const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
|
|
3609
|
+
const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
|
|
3610
|
+
const status = o.route === "token-change" ? "Exact token change, no AI: Bridge read the token swap from the Figma component." : o.status === "drafted" ? "**Drafted with Claude** from the Figma spec. A person reviews it before it ships." : `**Waiting for a developer:** this branch carries the spec only, as a request. A developer builds the code here with Claude (\`bridge_get_figma_change\`, or \`bridge figma show <number>\`), then it is labelled ${DRAFTED_BY_CLAUDE_LABEL}.`;
|
|
3611
|
+
return [`**From Figma:** ${design}${who}. Once it ships, a pull in the Bridge Figma plugin rebuilds the component from this code.`, "", status, "", hidden(o)].join("\n");
|
|
3612
|
+
}
|
|
3613
|
+
function withFigmaOrigin(body, o) {
|
|
3614
|
+
let b = (body ?? "").trim();
|
|
3615
|
+
const at = b.indexOf("**From Figma:**");
|
|
3616
|
+
const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
|
|
3617
|
+
if (at >= 0 && mark > at) {
|
|
3618
|
+
const end = b.indexOf("-->", mark);
|
|
3619
|
+
b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
|
|
3620
|
+
}
|
|
3621
|
+
return `${figmaOriginSection(o)}${b ? `
|
|
3622
|
+
|
|
3623
|
+
${b}` : ""}`;
|
|
3624
|
+
}
|
|
3625
|
+
const figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48);
|
|
3626
|
+
const figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
|
|
3627
|
+
const kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
|
|
3628
|
+
function formatFigmaSpec(d, plan) {
|
|
3629
|
+
return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
|
|
3630
|
+
`;
|
|
3631
|
+
}
|
|
3632
|
+
function parseFigmaSpec(text) {
|
|
3633
|
+
if (!text) return void 0;
|
|
3634
|
+
try {
|
|
3635
|
+
const s = JSON.parse(text);
|
|
3636
|
+
return s && s.v === 1 && typeof s.component === "string" && Array.isArray(s.changes) ? s : void 0;
|
|
3637
|
+
} catch {
|
|
3638
|
+
return void 0;
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
function figmaDraftBrief(d, ctx) {
|
|
3642
|
+
const dir = `${(ctx.componentsDir ?? "packages/react/src/components").replace(/\/+$/, "")}/${d.component}`;
|
|
3643
|
+
const suggested = "suggestion" in ctx;
|
|
3644
|
+
const variants = Object.entries(d.variantAxes).map(([axis, values2]) => `${axis}: ${values2.join(", ")}`);
|
|
3645
|
+
const props = d.properties.filter((p) => p.type !== "VARIANT").map((p) => `${p.name} (${p.type.toLowerCase().replace("_", " ")}, default ${p.default || "empty"})`);
|
|
3646
|
+
return [
|
|
3647
|
+
suggested ? "Draft the code for a component change made in Figma, sent as a Bridge suggestion (this issue)." : `Draft the code for a component change made in Figma, on the library branch ${ctx.branch} (draft pull request #${ctx.pr}).`,
|
|
3648
|
+
"",
|
|
3649
|
+
d.coded ? `Component: ${d.component} (${dir})` : `New component: ${d.component} (create ${dir})`,
|
|
3650
|
+
`Kind: ${kindLabel(d.kind)}`,
|
|
3651
|
+
`Change: ${d.title}`,
|
|
3652
|
+
...d.line?.trim() ? [`What changes: ${d.line.trim()}`] : [],
|
|
3653
|
+
"",
|
|
3654
|
+
d.summary.trim(),
|
|
3655
|
+
"",
|
|
3656
|
+
...d.changes.length ? ["Changes in Figma:", ...d.changes.map((c) => `- ${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`), ""] : [],
|
|
3657
|
+
...variants.length ? ["Variant matrix in Figma:", ...variants.map((v) => `- ${v}`), ""] : [],
|
|
3658
|
+
...props.length ? ["Properties in Figma:", ...props.map((p) => `- ${p}`), ""] : [],
|
|
3659
|
+
...d.tokenEdits?.length ? ["Token swaps to make too (exact, with bridge_propose_token_change on this branch or by hand in the token files):", ...d.tokenEdits.map((e) => `- ${e.path} -> ${String(e.value)}`), ""] : [],
|
|
3660
|
+
...d.reasons?.length ? ["Why this needs code:", ...d.reasons.map((r) => `- ${r}`), ""] : [],
|
|
3661
|
+
`Spec: ${ctx.specPath} ${suggested ? "in the suggestion" : "on the branch"} has the bound variables of every variant (token paths), the layers of every variant and what code gave Figma before.`,
|
|
3662
|
+
...d.figma?.url ? [`Design: ${d.figma.url}`] : [],
|
|
3663
|
+
...suggested && d.figma?.fileKey && d.figma.nodeId ? [`Read the design straight from Figma: Figma MCP get_design_context { fileKey: "${d.figma.fileKey}", nodeId: "${d.figma.nodeId}" }${d.figma.before?.nodeId ? ` (before: node ${d.figma.before.nodeId})` : ""}, or the REST API GET https://api.figma.com/v1/files/${d.figma.fileKey}/nodes?ids=${encodeURIComponent(d.figma.nodeId)}.`] : [],
|
|
3664
|
+
...d.images?.length ? ["Screenshot:", ...d.images.map((u) => `- ${u}`)] : [],
|
|
3665
|
+
"",
|
|
3666
|
+
"Steps:",
|
|
3667
|
+
"suggestion" in ctx ? "1. Apply the suggestion on a new branch: bridge apply <this issue number> (it writes the spec file), or start a branch from the default branch." : `1. git fetch origin && git checkout ${ctx.branch}`,
|
|
3668
|
+
`2. Write the change in ${dir}: the TSX on Base UI, the CSS module with tokens only (the bound variables of the spec, as token paths), the stories (examples), docs.json and a changeset. Follow ${ctx.agentsFile ?? "AGENTS.md"}.`,
|
|
3669
|
+
"3. Run npm run specs so component-specs.json matches the code, then npm test.",
|
|
3670
|
+
"suggestion" in ctx ? '4. Commit, push and open a pull request that says "Closes #<this issue number>", so the suggestion closes when it ships. A person reviews it.' : `4. Commit and push to ${ctx.branch}, then call bridge_mark_figma_change_drafted { pr: ${ctx.pr} } (CLI: bridge figma drafted ${ctx.pr}). The pull request is then ready for review, labelled ${DRAFTED_BY_CLAUDE_LABEL}, and a person reviews it.`,
|
|
3671
|
+
"",
|
|
3672
|
+
"Rules:",
|
|
3673
|
+
"- Never invent a token or a value: use the variables the spec lists. A value the library lacks is a token change, said in the pull request.",
|
|
3674
|
+
"- Keep every existing variant and prop unchanged unless the spec changes it.",
|
|
3675
|
+
`- If the spec is unclear, comment on the ${suggested ? "suggestion" : "pull request"} instead of guessing.`
|
|
3676
|
+
].join("\n");
|
|
3677
|
+
}
|
|
3678
|
+
function figmaDraftBody(d, plan, o) {
|
|
3679
|
+
const cell2 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
3680
|
+
const rows = d.changes.length ? ["| What | Code now | In Figma |", "| --- | --- | --- |", ...d.changes.map((c) => `| ${cell2(c.field)} | ${cell2(c.before ?? "")} | ${cell2(c.after ?? "")} |`), ""] : [];
|
|
3681
|
+
return withFigmaOrigin(
|
|
3682
|
+
[
|
|
3683
|
+
`## ${d.coded ? `${kindLabel(d.kind)} for ${d.component}` : `New component: ${d.component}`}`,
|
|
3684
|
+
"",
|
|
3685
|
+
d.summary.trim(),
|
|
3686
|
+
"",
|
|
3687
|
+
...d.line?.trim() ? [`**What changes:** ${d.line.trim()}`, ""] : [],
|
|
3688
|
+
...rows,
|
|
3689
|
+
...d.images?.length ? [...d.images.map((u, i) => ``), ""] : [],
|
|
3690
|
+
...plan.edits.length ? ["**Token swaps to make too:**", ...plan.edits.map((e) => `- \`${e.path}\` -> \`${String(e.value)}\``), ""] : [],
|
|
3691
|
+
...plan.reasons.length ? ["**Why this needs code:**", ...plan.reasons.map((r) => `- ${r}`), ""] : [],
|
|
3692
|
+
...plan.issues.length ? ["**Left in Figma (the designer fixes these first):**", ...plan.issues.map((i) => `- ${i.what} ${i.todo}`), ""] : [],
|
|
3693
|
+
`**Spec:** \`${o.spec ?? figmaSpecPath(d)}\` (variant matrix, properties, bound variables and layers of every variant).`
|
|
3694
|
+
].join("\n"),
|
|
3695
|
+
o
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
const figmaBranchTitle = (d) => requestIssueTitle(d);
|
|
3699
|
+
function figmaPushStatus(x) {
|
|
3700
|
+
const labels = x.labels ?? [];
|
|
3701
|
+
if (x.state === "merged") return { key: "shipped", label: "Shipped", sentence: "It shipped. Pull to rebuild the component from the reviewed code.", rebuild: true };
|
|
3702
|
+
if (x.state !== "open") return { key: "closed", label: "Closed", sentence: "The library closed it without shipping. Pull to bring the component back to the code.", rebuild: true };
|
|
3703
|
+
if (labels.includes(WAITING_FOR_CLAUDE_LABEL)) return { key: "waiting-for-claude", label: "Waiting for a developer", sentence: "A developer builds it with Claude from its spec, then a person reviews it.", rebuild: false };
|
|
3704
|
+
if (labels.includes(DRAFTED_BY_CLAUDE_LABEL)) return { key: "drafted", label: "Drafted with Claude", sentence: "The code is drafted. A person reviews it before it ships.", rebuild: false };
|
|
3705
|
+
return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
|
|
3706
|
+
}
|
|
3707
|
+
function parseTier(labels, body) {
|
|
3708
|
+
const found = [];
|
|
3709
|
+
for (const l of labels) {
|
|
3710
|
+
const m = l.name.match(/^(?:risk[\s:-]*)?tier[\s:_-]*([123])\b/i) ?? l.name.match(/^risk[\s:_-]*([123])$/i);
|
|
3711
|
+
if (m) found.push(Number(m[1]));
|
|
3712
|
+
}
|
|
3713
|
+
for (const m of (body ?? "").matchAll(/-\s*\[[xX]\]\s*\**\s*Tier\s*([123])\b/g)) found.push(Number(m[1]));
|
|
3714
|
+
for (const m of (body ?? "").matchAll(/risk tier\W{0,6}(?:tier\s*)?([123])\b/gi)) found.push(Number(m[1]));
|
|
3715
|
+
return found.length ? Math.max(...found) : null;
|
|
3716
|
+
}
|
|
3717
|
+
function claudeStage(labels) {
|
|
3718
|
+
const names2 = (labels ?? []).map((l) => (typeof l === "string" ? l : l.name ?? "").toLowerCase());
|
|
3719
|
+
if (names2.includes(WAITING_FOR_CLAUDE_LABEL)) return "waiting";
|
|
3720
|
+
if (names2.includes(DRAFTED_BY_CLAUDE_LABEL)) return "drafted";
|
|
3721
|
+
return null;
|
|
3722
|
+
}
|
|
3723
|
+
const isDraft = (p) => Boolean(p.draft) && claudeStage(p.labels) !== "drafted";
|
|
3724
|
+
function pullState(p) {
|
|
3725
|
+
if (p.merged_at || p.merged) return "merged";
|
|
3726
|
+
if (p.state && p.state !== "open") return "closed";
|
|
3727
|
+
if (isDraft(p)) return "draft";
|
|
3728
|
+
return "open";
|
|
3729
|
+
}
|
|
3730
|
+
const PULL_STATE_PILL = {
|
|
3731
|
+
merged: { label: "Merged", cls: "" },
|
|
3732
|
+
closed: { label: "Closed", cls: "" },
|
|
3733
|
+
draft: { label: "Draft", cls: "" },
|
|
3734
|
+
open: { label: "Open", cls: "" }
|
|
3735
|
+
};
|
|
3736
|
+
function openPulls(pulls) {
|
|
3737
|
+
return pulls.filter((p) => {
|
|
3738
|
+
const s = pullState(p);
|
|
3739
|
+
return s === "open" || s === "draft";
|
|
3740
|
+
});
|
|
3741
|
+
}
|
|
3742
|
+
function isPlatformRefusal(s) {
|
|
3743
|
+
return s.state !== "success" && s.state !== "pending" && /rate.limited/i.test(s.description ?? "");
|
|
3744
|
+
}
|
|
3745
|
+
const OK = /* @__PURE__ */ new Set(["success", "neutral", "skipped"]);
|
|
3746
|
+
function summarizeCi(runs, statuses = []) {
|
|
3747
|
+
const failed = [];
|
|
3748
|
+
const pending = [];
|
|
3749
|
+
let passed = 0;
|
|
3750
|
+
for (const r of runs) {
|
|
3751
|
+
if (r.status !== "completed") pending.push({ name: r.name, url: r.html_url });
|
|
3752
|
+
else if (r.conclusion && OK.has(r.conclusion)) passed++;
|
|
3753
|
+
else failed.push({ name: r.name, url: r.html_url });
|
|
3754
|
+
}
|
|
3755
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3756
|
+
for (const s of statuses) {
|
|
3757
|
+
if (seen.has(s.context)) continue;
|
|
3758
|
+
seen.add(s.context);
|
|
3759
|
+
if (isPlatformRefusal(s)) continue;
|
|
3760
|
+
if (s.state === "success") passed++;
|
|
3761
|
+
else if (s.state === "pending") pending.push({ name: s.context, url: s.target_url ?? void 0 });
|
|
3762
|
+
else failed.push({ name: s.context, url: s.target_url ?? void 0 });
|
|
3763
|
+
}
|
|
3764
|
+
const total = passed + failed.length + pending.length;
|
|
3765
|
+
const state = total === 0 ? "none" : failed.length ? "fail" : pending.length ? "pending" : "pass";
|
|
3766
|
+
return { state, total, passed, failed, pending };
|
|
3767
|
+
}
|
|
3768
|
+
const hasOwnerApproval = (a) => a.owner ?? a.approvers.length > 0;
|
|
3769
|
+
function summarizeApprovals(reviews, owners, author, rules = {}) {
|
|
3770
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3771
|
+
const sorted = [...reviews].sort((a2, b) => String(a2.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
|
|
3772
|
+
for (const r of sorted) {
|
|
3773
|
+
const login = r.user?.login;
|
|
3774
|
+
if (!login || login === author) continue;
|
|
3775
|
+
if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r.state);
|
|
3776
|
+
}
|
|
3777
|
+
const approvers = [...latest].filter(([, s]) => s === "APPROVED").map(([l]) => l);
|
|
3778
|
+
const changesRequested = [...latest].filter(([, s]) => s === "CHANGES_REQUESTED").map(([l]) => l);
|
|
3779
|
+
const lower2 = (xs) => xs.map((x) => x.toLowerCase().replace(/^@/, ""));
|
|
3780
|
+
const d = lower2(owners.design);
|
|
3781
|
+
const t = lower2(owners.tech);
|
|
3782
|
+
const a = lower2(approvers);
|
|
3783
|
+
let design;
|
|
3784
|
+
let tech;
|
|
3785
|
+
const owner = d.length || t.length ? a.some((x) => d.includes(x) || t.includes(x)) : a.length > 0;
|
|
3786
|
+
if (d.length || t.length) {
|
|
3787
|
+
design = d.length ? a.some((x) => d.includes(x)) : a.length > 0;
|
|
3788
|
+
const solo = rules.soloOwnerCanApproveBoth ? soloApprover(owners) : null;
|
|
3789
|
+
const soloBoth = (x) => a.length === 1 && d.includes(x) && x !== solo;
|
|
3790
|
+
tech = t.length ? a.some((x) => t.includes(x) && !soloBoth(x)) : a.length > 1;
|
|
3791
|
+
} else {
|
|
3792
|
+
design = a.length >= 1;
|
|
3793
|
+
tech = a.length >= 2;
|
|
3794
|
+
}
|
|
3795
|
+
return { approvers, changesRequested, design, tech, owner };
|
|
3796
|
+
}
|
|
3797
|
+
function lookAgainReviewers(reviews, headSha, author) {
|
|
3798
|
+
if (!headSha) return [];
|
|
3799
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3800
|
+
const sorted = [...reviews].sort((a, b) => String(a.submitted_at ?? "").localeCompare(String(b.submitted_at ?? "")));
|
|
3801
|
+
for (const r of sorted) {
|
|
3802
|
+
const login = r.user?.login;
|
|
3803
|
+
if (!login || login === author) continue;
|
|
3804
|
+
if (r.state === "APPROVED" || r.state === "CHANGES_REQUESTED" || r.state === "DISMISSED") latest.set(login, r);
|
|
3805
|
+
}
|
|
3806
|
+
const asked = [...latest.values()].filter((r) => r.state === "CHANGES_REQUESTED");
|
|
3807
|
+
if (!asked.length) return [];
|
|
3808
|
+
return asked.every((r) => r.commit_id && r.commit_id !== headSha) ? asked.map((r) => r.user.login) : [];
|
|
3809
|
+
}
|
|
3810
|
+
function withSuggestionApprovals(pr, suggestion, prTier) {
|
|
3811
|
+
if (!suggestion) return pr;
|
|
3812
|
+
const s = suggestion.approvals;
|
|
3813
|
+
if (s.changesRequested.length || (prTier ?? 3) > suggestion.tier) return pr;
|
|
3814
|
+
const covered = suggestion.tier >= 2 ? s.design && s.tech : hasOwnerApproval(s);
|
|
3815
|
+
if (!covered) return pr;
|
|
3816
|
+
const approvers = [...pr.approvers, ...s.approvers.filter((l) => !pr.approvers.some((x) => normLogin(x) === normLogin(l)))];
|
|
3817
|
+
return { ...pr, approvers, design: pr.design || s.design, tech: pr.tech || s.tech, owner: hasOwnerApproval(pr) || hasOwnerApproval(s) };
|
|
3818
|
+
}
|
|
3819
|
+
function withFigmaPushApprovals(pr, push) {
|
|
3820
|
+
if (!push || push.scope === "outside") return pr;
|
|
3821
|
+
const tech = pr.tech || push.scope === "token-values";
|
|
3822
|
+
return { ...pr, design: true, tech, owner: true };
|
|
3823
|
+
}
|
|
3824
|
+
function withTokenValueApprovals(pr, valuesOnly) {
|
|
3825
|
+
if (!valuesOnly) return pr;
|
|
3826
|
+
return { ...pr, tech: pr.tech || pr.design, valuesOnly: true };
|
|
3827
|
+
}
|
|
3828
|
+
const normLogin = (x) => x.trim().toLowerCase().replace(/^@/, "");
|
|
3829
|
+
function canOverride(login, policy) {
|
|
3830
|
+
if (!login || !policy?.allowed.length) return false;
|
|
3831
|
+
return policy.allowed.map(normLogin).includes(normLogin(login));
|
|
3832
|
+
}
|
|
3833
|
+
function overrideHolders(policy, owners) {
|
|
3834
|
+
const allowed = policy?.allowed ?? [];
|
|
3835
|
+
const all = [...owners.design, ...owners.tech];
|
|
3836
|
+
if (!all.length || all.some((o) => o.includes("/"))) return allowed;
|
|
3837
|
+
const named = all.map(normLogin);
|
|
3838
|
+
return allowed.filter((l) => named.includes(normLogin(l)));
|
|
3839
|
+
}
|
|
3840
|
+
const ownerOverridePolicy = (policy, owners) => policy ? { ...policy, allowed: overrideHolders(policy, owners) } : null;
|
|
3841
|
+
function mergeGate(input) {
|
|
3842
|
+
const blocking = [];
|
|
3843
|
+
const missing = [];
|
|
3844
|
+
const approvalReasons = [];
|
|
3845
|
+
const lifecycle = pullState(input);
|
|
3846
|
+
if (lifecycle === "merged" || lifecycle === "closed") {
|
|
3847
|
+
return { allowed: false, reasons: [lifecycle === "merged" ? "This branch is already merged." : "This branch is closed."], overridable: false, missing: [], canShip: !input.viewer || canShip(input.viewer, input.ship) };
|
|
3848
|
+
}
|
|
3849
|
+
if (input.exploration) blocking.push("It is an exploration: promote it to review first.");
|
|
3850
|
+
if (input.draft) blocking.push("It is a draft. Mark it ready for review on GitHub first.");
|
|
3851
|
+
if (input.ci.state === "fail") blocking.push(`Tests fail: ${input.ci.failed.map((f) => f.name).join(", ")}.`);
|
|
3852
|
+
if (input.ci.state === "pending") blocking.push("Tests are still running.");
|
|
3853
|
+
if (input.approvals.changesRequested.length) blocking.push(`Changes requested by @${input.approvals.changesRequested.join(", @")}.`);
|
|
3854
|
+
const tier = input.tier ?? 3;
|
|
3855
|
+
if (tier >= 2) {
|
|
3856
|
+
if (!input.approvals.design) {
|
|
3857
|
+
approvalReasons.push("It needs a design owner approval.");
|
|
3858
|
+
missing.push("design approval");
|
|
3859
|
+
}
|
|
3860
|
+
if (!input.approvals.tech && !input.approvals.valuesOnly) {
|
|
3861
|
+
approvalReasons.push("It needs a tech owner approval.");
|
|
3862
|
+
missing.push("tech approval");
|
|
3863
|
+
}
|
|
3864
|
+
} else if (!hasOwnerApproval(input.approvals)) {
|
|
3865
|
+
approvalReasons.push("It needs one owner approval.");
|
|
3866
|
+
missing.push("approval");
|
|
3867
|
+
}
|
|
3868
|
+
const conflict = input.mergeable === false ? [CONFLICT_REASON] : [];
|
|
3869
|
+
const shipper = !input.viewer || canShip(input.viewer, input.ship);
|
|
3870
|
+
const ship = !shipper ? [whoCanShipSentence(input.ship), ...unreadableShipTeams(input.ship).map((t) => t.status === "unreadable" ? t.message : "")] : [];
|
|
3871
|
+
const reasons = [...blocking, ...approvalReasons, ...ship, ...conflict];
|
|
3872
|
+
const allowed = reasons.length === 0;
|
|
3873
|
+
const overridable = !allowed && blocking.length + conflict.length + ship.length === 0 && missing.length > 0 && canOverride(input.viewer, input.override);
|
|
3874
|
+
return { allowed, reasons, overridable, missing, canShip: shipper };
|
|
3875
|
+
}
|
|
3876
|
+
function branchVerdict(input) {
|
|
3877
|
+
const own = input.approvals ? withFigmaPushApprovals(input.approvals, input.figmaPush) : null;
|
|
3878
|
+
const carried = own ? withSuggestionApprovals(own, input.suggestion, input.tier) : null;
|
|
3879
|
+
const approvals = carried ? withTokenValueApprovals(carried, input.tokenValues || input.figmaPush?.scope === "token-values") : null;
|
|
3880
|
+
const override = ownerOverridePolicy(input.override, input.owners);
|
|
3881
|
+
const state = input.state ?? "open";
|
|
3882
|
+
const draft = isDraft(input);
|
|
3883
|
+
const status = reviewStatusKind({ state, draft, merged_at: input.merged_at, ci: input.ci, approvals, tier: input.tier, mergeable: input.mergeable });
|
|
3884
|
+
const gate = input.ci && approvals ? mergeGate({ tier: input.tier, ci: input.ci, approvals, draft, mergeable: input.mergeable, state, merged_at: input.merged_at, viewer: input.viewer, override, exploration: input.exploration, ship: input.ship }) : null;
|
|
3885
|
+
const valuesArmed = Boolean(input.autoMerge && approvals?.valuesOnly && approvals.design);
|
|
3886
|
+
const autoShip = (input.figmaPush?.scope === "token-values" || valuesArmed) && state === "open" && !input.merged_at && !input.exploration;
|
|
3887
|
+
const lookAgain = status === "changes-requested" ? [...input.lookAgain ?? []] : [];
|
|
3888
|
+
return { approvals, override, gate, status, label: reviewStatusLabel(status, lookAgain), lookAgain, autoShip };
|
|
3889
|
+
}
|
|
3890
|
+
const CONFLICT_REASON = "It conflicts with main: resolve it with Update from main on the branch.";
|
|
3891
|
+
const OVERRIDE_HEADING = "**Bridge owner override**";
|
|
3892
|
+
function overrideReasonMin(policy) {
|
|
3893
|
+
return policy.minReasonLength > 0 ? policy.minReasonLength : 20;
|
|
3894
|
+
}
|
|
3895
|
+
function overrideReasonProblem(reason, policy) {
|
|
3896
|
+
const min = overrideReasonMin(policy);
|
|
3897
|
+
const n = reason.trim().length;
|
|
3898
|
+
if (n === 0) return `Write why this merges without the missing approvals, in at least ${min} characters.`;
|
|
3899
|
+
if (n < min) return `Write at least ${min} characters (${n} so far), so the history says why.`;
|
|
3900
|
+
return null;
|
|
3901
|
+
}
|
|
3902
|
+
function overrideComment(login, reason, missing) {
|
|
3903
|
+
const why = reason.trim().replace(/\s+/g, " ").replace(/[.\s]+$/, "");
|
|
3904
|
+
return `${OVERRIDE_HEADING}
|
|
3905
|
+
|
|
3906
|
+
Owner override by @${normLogin(login)}: ${why || "no reason given"}. Missing: ${missing.join(", ")}. Merged from Bridge.`;
|
|
3907
|
+
}
|
|
3908
|
+
function overrideRefusedComment(body, message) {
|
|
3909
|
+
return body.replace(/ Merged from Bridge\.$/, ` Merge refused by GitHub: ${message.trim().replace(/[.\s]+$/, "")}. Not merged.`);
|
|
3910
|
+
}
|
|
3911
|
+
function parseOverrideComment(body) {
|
|
3912
|
+
const text = (body ?? "").trim();
|
|
3135
3913
|
if (!text.startsWith(OVERRIDE_HEADING)) return null;
|
|
3136
3914
|
const m = text.match(/Owner override by @([\w-]+): ([\s\S]*?)\. Missing: ([^.]*)\. (Merged from Bridge\.|Merge refused by GitHub)/);
|
|
3137
3915
|
if (!m) return null;
|
|
@@ -3177,9 +3955,9 @@ function reviewStatusKind(input) {
|
|
|
3177
3955
|
if (tier >= 2) {
|
|
3178
3956
|
if (!input.approvals.design) return out("needs-design");
|
|
3179
3957
|
if (!input.approvals.tech) return out("needs-tech");
|
|
3180
|
-
} else if (!input.approvals
|
|
3958
|
+
} else if (!hasOwnerApproval(input.approvals)) return out("needs-approval");
|
|
3181
3959
|
if (input.mergeable === null) return out("checking");
|
|
3182
|
-
const gate = mergeGate({ tier: input.tier, ci: input.ci, approvals: input.approvals, draft:
|
|
3960
|
+
const gate = mergeGate({ tier: input.tier, ci: input.ci, approvals: input.approvals, draft: isDraft(input), mergeable: input.mergeable, state: input.state ?? "open", merged_at: input.merged_at });
|
|
3183
3961
|
return gate.allowed ? out("ready") : out("unknown");
|
|
3184
3962
|
}
|
|
3185
3963
|
const REVIEW_STATUS_LABEL = {
|
|
@@ -3197,6 +3975,10 @@ const REVIEW_STATUS_LABEL = {
|
|
|
3197
3975
|
ready: "Ready to merge",
|
|
3198
3976
|
unknown: "Status unknown"
|
|
3199
3977
|
};
|
|
3978
|
+
const LOOK_AGAIN_STATUS_LABEL = "Updated, look again";
|
|
3979
|
+
function reviewStatusLabel(kind, lookAgain) {
|
|
3980
|
+
return kind === "changes-requested" && (lookAgain?.length ?? 0) > 0 ? LOOK_AGAIN_STATUS_LABEL : REVIEW_STATUS_LABEL[kind];
|
|
3981
|
+
}
|
|
3200
3982
|
const CI_STATE_LABEL = {
|
|
3201
3983
|
pass: "Tests pass",
|
|
3202
3984
|
fail: "Tests fail",
|
|
@@ -3234,8 +4016,8 @@ function reopenEligibility(x) {
|
|
|
3234
4016
|
}
|
|
3235
4017
|
const NO_CI = { state: "pass", total: 0, passed: 0, failed: [], pending: [] };
|
|
3236
4018
|
const ROLE_OF_MISSING = { "design approval": "design", "tech approval": "tech", approval: "approval" };
|
|
3237
|
-
function requiredApprovals(tier) {
|
|
3238
|
-
return mergeGate({ tier, ci: NO_CI, approvals: { approvers: [], changesRequested: [], design: false, tech: false }, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
|
|
4019
|
+
function requiredApprovals(tier, valuesOnly) {
|
|
4020
|
+
return mergeGate({ tier, ci: NO_CI, approvals: { approvers: [], changesRequested: [], design: false, tech: false, ...valuesOnly ? { valuesOnly: true } : {} }, draft: false, state: "open" }).missing.map((m) => ROLE_OF_MISSING[m]);
|
|
3239
4021
|
}
|
|
3240
4022
|
function pendingApprovals(tier, approvals) {
|
|
3241
4023
|
if (tier == null || !approvals) return [];
|
|
@@ -3352,7 +4134,7 @@ function summarizeSuggestionApprovals(comments, gov, opts) {
|
|
|
3352
4134
|
latestOld.delete(c.author);
|
|
3353
4135
|
reviews.push({ user: { login: c.author }, state: r.event === "approve" ? "APPROVED" : "CHANGES_REQUESTED", submitted_at: at });
|
|
3354
4136
|
});
|
|
3355
|
-
const summary = summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth });
|
|
4137
|
+
const summary = withTokenValueApprovals(summarizeApprovals(reviews, lists, opts.author ?? void 0, { soloOwnerCanApproveBoth: gov.soloOwnerCanApproveBoth }), opts.valuesOnly);
|
|
3356
4138
|
const stale = [...latestOld.keys()].filter((l) => !summary.approvers.includes(l) && !summary.changesRequested.includes(l));
|
|
3357
4139
|
return { ...summary, stale, ignored: [...ignored].filter((l) => !summary.approvers.includes(l)) };
|
|
3358
4140
|
}
|
|
@@ -3360,221 +4142,6 @@ function suggestionReviewState(tier, approvals) {
|
|
|
3360
4142
|
if (approvals.changesRequested.length) return "changes-requested";
|
|
3361
4143
|
return pendingApprovals(tier, approvals).length ? "needs-review" : "approved";
|
|
3362
4144
|
}
|
|
3363
|
-
const where = (r) => r.allVariants ? "every variant" : shortVariants(r.variants.join("; "));
|
|
3364
|
-
const cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
3365
|
-
function planComponentPush(d, ctx) {
|
|
3366
|
-
const issues = [...d.unknown];
|
|
3367
|
-
const reasons = [];
|
|
3368
|
-
const edits = /* @__PURE__ */ new Map();
|
|
3369
|
-
const also = /* @__PURE__ */ new Set();
|
|
3370
|
-
for (const r of d.rebinds) {
|
|
3371
|
-
const label = `${cap(r.kind)} in ${where(r)}`;
|
|
3372
|
-
if (!r.after.length) {
|
|
3373
|
-
issues.push({ what: `${label} is no longer bound to a variable: it uses a raw value.`, todo: "Bind it to a variable, or create the variable and push it with the variables, then push again." });
|
|
3374
|
-
continue;
|
|
3375
|
-
}
|
|
3376
|
-
const missing = r.after.filter((p) => !ctx.tierOf(p));
|
|
3377
|
-
if (missing.length) {
|
|
3378
|
-
issues.push({ what: `${label} uses ${missing.join(", ")}, a variable the library does not have yet.`, todo: "Push the variable first (Variables, above), then push this component again." });
|
|
3379
|
-
continue;
|
|
3380
|
-
}
|
|
3381
|
-
if (!r.before.length) {
|
|
3382
|
-
reasons.push(`${label} is bound to ${r.after.join(", ")} where code paints nothing: Claude AI adds it to the CSS.`);
|
|
3383
|
-
continue;
|
|
3384
|
-
}
|
|
3385
|
-
if (r.before.length !== 1 || r.after.length !== 1) {
|
|
3386
|
-
reasons.push(`${label} moved from ${r.before.join(", ")} to ${r.after.join(", ")}: several variables on one property, so Claude AI writes the CSS.`);
|
|
3387
|
-
continue;
|
|
3388
|
-
}
|
|
3389
|
-
const [from, to] = [r.before[0], r.after[0]];
|
|
3390
|
-
if (ctx.tierOf(from) !== "component") {
|
|
3391
|
-
reasons.push(`Code paints ${r.kind} with ${from} directly, not through a ${d.component} token: Claude AI changes the CSS to ${to}.`);
|
|
3392
|
-
continue;
|
|
3393
|
-
}
|
|
3394
|
-
const elsewhere = ctx.usedElsewhere(from, r);
|
|
3395
|
-
if (elsewhere.variants.length) {
|
|
3396
|
-
const v = elsewhere.variants;
|
|
3397
|
-
const list2 = v.length > 3 ? `${shortVariants(v.slice(0, 3).join("; ")).replace(/ and /, ", ")} and ${v.length - 3} more` : shortVariants(v.join("; "));
|
|
3398
|
-
reasons.push(`${from} also paints ${list2}: pointing it to ${to} would change them too, so Claude AI adds a token for ${where(r)}.`);
|
|
3399
|
-
continue;
|
|
3400
|
-
}
|
|
3401
|
-
for (const c of elsewhere.components) also.add(c);
|
|
3402
|
-
const prev = edits.get(from);
|
|
3403
|
-
if (prev && prev.value !== `{${to}}`) {
|
|
3404
|
-
reasons.push(`${from} moved to two different variables (${String(prev.value).slice(1, -1)} and ${to}): Claude AI splits it.`);
|
|
3405
|
-
edits.delete(from);
|
|
3406
|
-
continue;
|
|
3407
|
-
}
|
|
3408
|
-
edits.set(from, { op: "set", path: from, value: `{${to}}` });
|
|
3409
|
-
}
|
|
3410
|
-
const list = [...edits.values()];
|
|
3411
|
-
if (d.needsCode || reasons.length) {
|
|
3412
|
-
return {
|
|
3413
|
-
route: "claude-draft",
|
|
3414
|
-
said: `Claude AI drafts the code from what you changed, then a person reviews it.${list.length ? ` ${list.length === 1 ? "Its token swap goes" : "Its token swaps go"} with it.` : ""}`,
|
|
3415
|
-
edits: list,
|
|
3416
|
-
reasons,
|
|
3417
|
-
issues,
|
|
3418
|
-
alsoChanges: [...also]
|
|
3419
|
-
};
|
|
3420
|
-
}
|
|
3421
|
-
if (list.length) {
|
|
3422
|
-
const n = list.length;
|
|
3423
|
-
const others = [...also];
|
|
3424
|
-
const names2 = others.length > 3 ? [...others.slice(0, 3), `${others.length - 3} more`] : others;
|
|
3425
|
-
const alsoLine = names2.length ? ` It also repaints ${names2.length > 1 ? `${names2.slice(0, -1).join(", ")} and ${names2[names2.length - 1]}` : names2[0]}: they use the same token${n === 1 ? "" : "s"}.` : "";
|
|
3426
|
-
return { route: "token-change", said: `Exact, no AI: ${n === 1 ? `${list[0].path} points` : `${n} ${d.component} tokens point`} to the new variable${n === 1 ? "" : "s"}, checked for contrast and reviewed like any token change.${alsoLine}`, edits: list, reasons, issues, alsoChanges: others };
|
|
3427
|
-
}
|
|
3428
|
-
return { route: "manual", said: issues.length ? "Fix it in Figma first: nothing is sent until Bridge can read it." : "Nothing to send.", edits: [], reasons, issues, alsoChanges: [] };
|
|
3429
|
-
}
|
|
3430
|
-
function specsUsage(specs, component) {
|
|
3431
|
-
const own = specs.components.find((x) => x.name === component) ?? specs.components.find((x) => x.component === component);
|
|
3432
|
-
return (path, r) => {
|
|
3433
|
-
const variants = [];
|
|
3434
|
-
const components = [];
|
|
3435
|
-
for (const c of specs.components) {
|
|
3436
|
-
for (const v of c.variants) {
|
|
3437
|
-
if (!Object.values(v.values).some((fields) => Object.values(fields ?? {}).includes(path))) continue;
|
|
3438
|
-
const key = variantKeyOf(v.props);
|
|
3439
|
-
if (c === own) {
|
|
3440
|
-
if (!r.variants.includes(key)) variants.push(key);
|
|
3441
|
-
} else if (!components.includes(c.name)) components.push(c.name);
|
|
3442
|
-
}
|
|
3443
|
-
}
|
|
3444
|
-
return { variants, components };
|
|
3445
|
-
};
|
|
3446
|
-
}
|
|
3447
|
-
function componentContrastPairs(edits, pairs, tokens) {
|
|
3448
|
-
const out = /* @__PURE__ */ new Map();
|
|
3449
|
-
for (const e of edits) {
|
|
3450
|
-
const target = tokens.aliasOf(e.path);
|
|
3451
|
-
if (!target) continue;
|
|
3452
|
-
const group = e.path.split(".")[0];
|
|
3453
|
-
const sibling = (other) => tokens.paths.find((p) => p !== e.path && p.split(".")[0] === group && tokens.aliasOf(p) === other) ?? other;
|
|
3454
|
-
for (const p of pairs) {
|
|
3455
|
-
if (p.foreground === target) out.set(`${e.path}|${sibling(p.background)}`, { ...p, foreground: e.path, background: sibling(p.background) });
|
|
3456
|
-
if (p.background === target) out.set(`${sibling(p.foreground)}|${e.path}`, { ...p, foreground: sibling(p.foreground), background: e.path });
|
|
3457
|
-
}
|
|
3458
|
-
}
|
|
3459
|
-
return [...out.values()];
|
|
3460
|
-
}
|
|
3461
|
-
const FROM_FIGMA_LABEL = "from-figma";
|
|
3462
|
-
const WAITING_FOR_CLAUDE_LABEL = "waiting-for-claude";
|
|
3463
|
-
const DRAFTED_BY_CLAUDE_LABEL = "drafted-by-claude";
|
|
3464
|
-
const FIGMA_SPEC_DIR = ".bridge/figma";
|
|
3465
|
-
const ORIGIN_MARK = "bridge-from-figma";
|
|
3466
|
-
const hidden = (data) => `<!-- ${ORIGIN_MARK} ${JSON.stringify(data).replace(/-->/g, "--\\u003e")} -->`;
|
|
3467
|
-
function parseFigmaOrigin(body) {
|
|
3468
|
-
if (!body) return void 0;
|
|
3469
|
-
const m = new RegExp(`<!-- ${ORIGIN_MARK} (\\{[\\s\\S]*?\\}) -->`).exec(body);
|
|
3470
|
-
if (!m) return void 0;
|
|
3471
|
-
try {
|
|
3472
|
-
const o = JSON.parse(m[1]);
|
|
3473
|
-
return typeof o.component === "string" && (o.route === "token-change" || o.route === "claude-draft") ? o : void 0;
|
|
3474
|
-
} catch {
|
|
3475
|
-
return void 0;
|
|
3476
|
-
}
|
|
3477
|
-
}
|
|
3478
|
-
function figmaOriginSection(o) {
|
|
3479
|
-
const design = o.url ? `[${o.figmaName}](${o.url})` : `"${o.figmaName}"`;
|
|
3480
|
-
const who = o.requestedBy ? ` by @${o.requestedBy}` : "";
|
|
3481
|
-
const status = o.route === "token-change" ? "Exact token change, no AI: Bridge read the token swap from the Figma component." : o.status === "drafted" ? "**Drafted by Claude AI** from the Figma spec. A person reviews it before it ships." : `**Waiting for Claude AI:** this branch carries the spec only, as a request. Claude writes the code here (\`bridge_get_figma_change\`, or \`bridge figma show <number>\`), then it is labelled ${DRAFTED_BY_CLAUDE_LABEL}.`;
|
|
3482
|
-
return [`**From Figma:** ${design}${who}. Once it ships, a pull in the DS Bridge plugin rebuilds the component from this code.`, "", status, "", hidden(o)].join("\n");
|
|
3483
|
-
}
|
|
3484
|
-
function withFigmaOrigin(body, o) {
|
|
3485
|
-
let b = (body ?? "").trim();
|
|
3486
|
-
const at = b.indexOf("**From Figma:**");
|
|
3487
|
-
const mark = b.indexOf(`<!-- ${ORIGIN_MARK} `);
|
|
3488
|
-
if (at >= 0 && mark > at) {
|
|
3489
|
-
const end = b.indexOf("-->", mark);
|
|
3490
|
-
b = `${b.slice(0, at)}${b.slice(end + 3)}`.trim();
|
|
3491
|
-
}
|
|
3492
|
-
return `${figmaOriginSection(o)}${b ? `
|
|
3493
|
-
|
|
3494
|
-
${b}` : ""}`;
|
|
3495
|
-
}
|
|
3496
|
-
const figmaSpecSlug = (d) => slugify(`${d.component} ${d.title}`, 48);
|
|
3497
|
-
const figmaSpecPath = (d) => `${FIGMA_SPEC_DIR}/${figmaSpecSlug(d)}.json`;
|
|
3498
|
-
const kindLabel = (k) => REQUEST_KINDS.find((x) => x.value === k)?.label ?? "Change";
|
|
3499
|
-
function formatFigmaSpec(d, plan) {
|
|
3500
|
-
return `${JSON.stringify({ ...d, tokenEdits: plan.edits, reasons: plan.reasons, unknown: plan.issues }, null, 2)}
|
|
3501
|
-
`;
|
|
3502
|
-
}
|
|
3503
|
-
function parseFigmaSpec(text) {
|
|
3504
|
-
if (!text) return void 0;
|
|
3505
|
-
try {
|
|
3506
|
-
const s = JSON.parse(text);
|
|
3507
|
-
return s && s.v === 1 && typeof s.component === "string" && Array.isArray(s.changes) ? s : void 0;
|
|
3508
|
-
} catch {
|
|
3509
|
-
return void 0;
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
function figmaDraftBrief(d, ctx) {
|
|
3513
|
-
const dir = `${(ctx.componentsDir ?? "packages/react/src/components").replace(/\/+$/, "")}/${d.component}`;
|
|
3514
|
-
const suggested = "suggestion" in ctx;
|
|
3515
|
-
const variants = Object.entries(d.variantAxes).map(([axis, values2]) => `${axis}: ${values2.join(", ")}`);
|
|
3516
|
-
const props = d.properties.filter((p) => p.type !== "VARIANT").map((p) => `${p.name} (${p.type.toLowerCase().replace("_", " ")}, default ${p.default || "empty"})`);
|
|
3517
|
-
return [
|
|
3518
|
-
suggested ? "Draft the code for a component change made in Figma, sent as a Bridge suggestion (this issue)." : `Draft the code for a component change made in Figma, on the library branch ${ctx.branch} (draft pull request #${ctx.pr}).`,
|
|
3519
|
-
"",
|
|
3520
|
-
d.coded ? `Component: ${d.component} (${dir})` : `New component: ${d.component} (create ${dir})`,
|
|
3521
|
-
`Kind: ${kindLabel(d.kind)}`,
|
|
3522
|
-
`Change: ${d.title}`,
|
|
3523
|
-
...d.line?.trim() ? [`What changes: ${d.line.trim()}`] : [],
|
|
3524
|
-
"",
|
|
3525
|
-
d.summary.trim(),
|
|
3526
|
-
"",
|
|
3527
|
-
...d.changes.length ? ["Changes in Figma:", ...d.changes.map((c) => `- ${c.field}: ${c.before ?? "(none)"} -> ${c.after ?? "(removed)"}`), ""] : [],
|
|
3528
|
-
...variants.length ? ["Variant matrix in Figma:", ...variants.map((v) => `- ${v}`), ""] : [],
|
|
3529
|
-
...props.length ? ["Properties in Figma:", ...props.map((p) => `- ${p}`), ""] : [],
|
|
3530
|
-
...d.tokenEdits?.length ? ["Token swaps to make too (exact, with bridge_propose_token_change on this branch or by hand in the token files):", ...d.tokenEdits.map((e) => `- ${e.path} -> ${String(e.value)}`), ""] : [],
|
|
3531
|
-
...d.reasons?.length ? ["Why this needs code:", ...d.reasons.map((r) => `- ${r}`), ""] : [],
|
|
3532
|
-
`Spec: ${ctx.specPath} ${suggested ? "in the suggestion" : "on the branch"} has the bound variables of every variant (token paths), the layers of every variant and what code gave Figma before.`,
|
|
3533
|
-
...d.figma?.url ? [`Design: ${d.figma.url}`] : [],
|
|
3534
|
-
...suggested && d.figma?.fileKey && d.figma.nodeId ? [`Read the design straight from Figma: Figma MCP get_design_context { fileKey: "${d.figma.fileKey}", nodeId: "${d.figma.nodeId}" }${d.figma.before?.nodeId ? ` (before: node ${d.figma.before.nodeId})` : ""}, or the REST API GET https://api.figma.com/v1/files/${d.figma.fileKey}/nodes?ids=${encodeURIComponent(d.figma.nodeId)}.`] : [],
|
|
3535
|
-
...d.images?.length ? ["Screenshot:", ...d.images.map((u) => `- ${u}`)] : [],
|
|
3536
|
-
"",
|
|
3537
|
-
"Steps:",
|
|
3538
|
-
"suggestion" in ctx ? "1. Apply the suggestion on a new branch: bridge apply <this issue number> (it writes the spec file), or start a branch from the default branch." : `1. git fetch origin && git checkout ${ctx.branch}`,
|
|
3539
|
-
`2. Write the change in ${dir}: the TSX on Base UI, the CSS module with tokens only (the bound variables of the spec, as token paths), the stories (examples), docs.json and a changeset. Follow ${ctx.agentsFile ?? "AGENTS.md"}.`,
|
|
3540
|
-
"3. Run npm run specs so component-specs.json matches the code, then npm test.",
|
|
3541
|
-
"suggestion" in ctx ? '4. Commit, push and open a pull request that says "Closes #<this issue number>", so the suggestion closes when it ships. A person reviews it.' : `4. Commit and push to ${ctx.branch}, then call bridge_mark_figma_change_drafted { pr: ${ctx.pr} } (CLI: bridge figma drafted ${ctx.pr}). The branch is then labelled ${DRAFTED_BY_CLAUDE_LABEL} and a person reviews it.`,
|
|
3542
|
-
"",
|
|
3543
|
-
"Rules:",
|
|
3544
|
-
"- Never invent a token or a value: use the variables the spec lists. A value the library lacks is a token change, said in the pull request.",
|
|
3545
|
-
"- Keep every existing variant and prop unchanged unless the spec changes it.",
|
|
3546
|
-
`- If the spec is unclear, comment on the ${suggested ? "suggestion" : "pull request"} instead of guessing.`
|
|
3547
|
-
].join("\n");
|
|
3548
|
-
}
|
|
3549
|
-
function figmaDraftBody(d, plan, o) {
|
|
3550
|
-
const cell2 = (s) => s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
3551
|
-
const rows = d.changes.length ? ["| What | Code now | In Figma |", "| --- | --- | --- |", ...d.changes.map((c) => `| ${cell2(c.field)} | ${cell2(c.before ?? "")} | ${cell2(c.after ?? "")} |`), ""] : [];
|
|
3552
|
-
return withFigmaOrigin(
|
|
3553
|
-
[
|
|
3554
|
-
`## ${d.coded ? `${kindLabel(d.kind)} for ${d.component}` : `New component: ${d.component}`}`,
|
|
3555
|
-
"",
|
|
3556
|
-
d.summary.trim(),
|
|
3557
|
-
"",
|
|
3558
|
-
...d.line?.trim() ? [`**What changes:** ${d.line.trim()}`, ""] : [],
|
|
3559
|
-
...rows,
|
|
3560
|
-
...d.images?.length ? [...d.images.map((u, i) => ``), ""] : [],
|
|
3561
|
-
...plan.edits.length ? ["**Token swaps to make too:**", ...plan.edits.map((e) => `- \`${e.path}\` -> \`${String(e.value)}\``), ""] : [],
|
|
3562
|
-
...plan.reasons.length ? ["**Why this needs code:**", ...plan.reasons.map((r) => `- ${r}`), ""] : [],
|
|
3563
|
-
...plan.issues.length ? ["**Left in Figma (the designer fixes these first):**", ...plan.issues.map((i) => `- ${i.what} ${i.todo}`), ""] : [],
|
|
3564
|
-
`**Spec:** \`${o.spec ?? figmaSpecPath(d)}\` (variant matrix, properties, bound variables and layers of every variant).`
|
|
3565
|
-
].join("\n"),
|
|
3566
|
-
o
|
|
3567
|
-
);
|
|
3568
|
-
}
|
|
3569
|
-
const figmaBranchTitle = (d) => requestIssueTitle(d);
|
|
3570
|
-
function figmaPushStatus(x) {
|
|
3571
|
-
const labels = x.labels ?? [];
|
|
3572
|
-
if (x.state === "merged") return { key: "shipped", label: "Shipped", sentence: "It shipped. Pull to rebuild the component from the reviewed code.", rebuild: true };
|
|
3573
|
-
if (x.state !== "open") return { key: "closed", label: "Closed", sentence: "The library closed it without shipping. Pull to bring the component back to the code.", rebuild: true };
|
|
3574
|
-
if (labels.includes(WAITING_FOR_CLAUDE_LABEL)) return { key: "waiting-for-claude", label: "Waiting for Claude AI", sentence: "The spec is on the library branch. Claude AI writes the code there, then a person reviews it.", rebuild: false };
|
|
3575
|
-
if (labels.includes(DRAFTED_BY_CLAUDE_LABEL)) return { key: "drafted", label: "Drafted by Claude AI", sentence: "Claude AI drafted the code. A person reviews it before it ships.", rebuild: false };
|
|
3576
|
-
return { key: "in-review", label: "In review", sentence: x.route === "token-change" ? "Owners review the token change." : "Owners review it.", rebuild: false };
|
|
3577
|
-
}
|
|
3578
4145
|
const SUGGESTION_SCHEMA = "bridge.suggestion/1";
|
|
3579
4146
|
const SUGGESTION_LABEL = "bridge-suggestion";
|
|
3580
4147
|
const SUGGESTION_KINDS = ["token-change", "docs", "component-spec", "brand", "revert", "config"];
|
|
@@ -3836,6 +4403,11 @@ function suggestionKindFromLabels(labels) {
|
|
|
3836
4403
|
function suggestionStateFromLabels(labels) {
|
|
3837
4404
|
return Object.entries(SUGGESTION_STATE_LABELS).find(([, l]) => labels.includes(l))?.[0];
|
|
3838
4405
|
}
|
|
4406
|
+
function suggestionIsTokenValues(p) {
|
|
4407
|
+
if (p.kind !== "token-change" || !p.tokens?.edits.length || p.docs?.length || p.componentSpec) return false;
|
|
4408
|
+
if (!p.tokens.edits.every((e) => e.op === "set" || e.op === "add")) return false;
|
|
4409
|
+
return (p.files ?? []).every((f) => f.path.endsWith(".md"));
|
|
4410
|
+
}
|
|
3839
4411
|
function suggestionStatus(i) {
|
|
3840
4412
|
const prs = i.linkedPrs ?? [];
|
|
3841
4413
|
if (i.state === "closed") return prs.some((p) => p.merged) || i.stateReason === "completed" ? "applied" : "declined";
|
|
@@ -3906,7 +4478,7 @@ function checksSection(p, max) {
|
|
|
3906
4478
|
"",
|
|
3907
4479
|
`- Token validation: ${c.newErrors ? `**${c.newErrors} new error(s)**` : "no new errors"}`,
|
|
3908
4480
|
`- Contrast: ${c.contrast.length ? failing.length ? `**${failing.length} failing pair(s)**` : `${c.contrast.length} pair(s) checked, all pass` : "no pair affected"}`,
|
|
3909
|
-
`- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one approval" : "design and tech approval"}`,
|
|
4481
|
+
`- Risk tier: ${p.tier.tier} (\`${p.tier.bump}\`), ${p.tier.tier === 1 ? "one owner approval" : "design and tech approval"}`,
|
|
3910
4482
|
`- Impact: ${c.impact ? `${c.impact} token(s) resolve to a new value` : "no other token changes value"}`,
|
|
3911
4483
|
"",
|
|
3912
4484
|
...rows.length ? [...table(["Pair", "Mode", "Before", "After", "Min", "Result"], rows, max), ""] : []
|
|
@@ -4381,7 +4953,7 @@ async function planSuggestionApply(input) {
|
|
|
4381
4953
|
if (links.length) return out("refused", "Not applied: the suggestion writes through a symbolic link.", { problems: links });
|
|
4382
4954
|
}
|
|
4383
4955
|
const governance = input.governance ?? { owners: config.governance.owners, rights: config.governance.rights ?? null, soloOwnerCanApproveBoth: config.governance.soloOwnerCanApproveBoth };
|
|
4384
|
-
const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author });
|
|
4956
|
+
const approvals = summarizeSuggestionApprovals(input.comments, governance, { payloadSha: parsed.sha, author: input.author, valuesOnly: suggestionIsTokenValues(payload) });
|
|
4385
4957
|
const review = suggestionReviewState(payload.tier.tier, approvals);
|
|
4386
4958
|
const missing = pendingApprovals(payload.tier.tier, approvals);
|
|
4387
4959
|
const approved = review === "approved";
|
|
@@ -4458,16 +5030,16 @@ async function planSuggestionApply(input) {
|
|
|
4458
5030
|
const issueKey2 = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
4459
5031
|
const prevErrors = new Set(validateLayered(before).map(issueKey2));
|
|
4460
5032
|
const newErrors = tokenChanges.length ? validateLayered(after).filter((i) => i.level === "error" && !prevErrors.has(issueKey2(i))) : [];
|
|
4461
|
-
let
|
|
5033
|
+
let pairs2 = [];
|
|
4462
5034
|
if (config.contrast && tokenChanges.length) {
|
|
4463
5035
|
try {
|
|
4464
|
-
|
|
5036
|
+
pairs2 = parseContrastChecks((await ws.read(config.contrast.path))?.text);
|
|
4465
5037
|
} catch {
|
|
4466
5038
|
notes.push(`${config.contrast.path} does not parse: contrast was not checked.`);
|
|
4467
5039
|
}
|
|
4468
5040
|
}
|
|
4469
5041
|
const modes = config.contrast?.modes;
|
|
4470
|
-
const contrast =
|
|
5042
|
+
const contrast = pairs2.length ? contrastDelta(runLayeredContrast(before, pairs2, modes), runLayeredContrast(after, pairs2, modes)) : [];
|
|
4471
5043
|
const newFailures = contrast.filter((d) => d.passBefore && !d.passAfter);
|
|
4472
5044
|
const otherFiles = [...writes.keys()].filter((p) => !tokenFileOf(config, p));
|
|
4473
5045
|
const tier = classifyRisk({ tokenChanges: tokenChanges.map((c) => ({ kind: c.kind, path: c.path, ...c.kind === "changed" ? { descriptionOnly: c.fields.length > 0 && c.fields.every((x) => x === "description") } : {} })), files: otherFiles.map((path) => ({ path })), config });
|
|
@@ -4539,6 +5111,7 @@ function listItem(i, pulls) {
|
|
|
4539
5111
|
title: i.title,
|
|
4540
5112
|
url: i.url,
|
|
4541
5113
|
author: i.author,
|
|
5114
|
+
requester: requesterOf(i.body),
|
|
4542
5115
|
createdAt: i.createdAt,
|
|
4543
5116
|
state: i.state,
|
|
4544
5117
|
stateReason: i.stateReason,
|
|
@@ -4554,7 +5127,7 @@ async function getSuggestionOnGitHub(gh, n, gov, opts = {}) {
|
|
|
4554
5127
|
const parsed = parseSuggestionIssue(issue.body, { comments, author: issue.author || void 0 });
|
|
4555
5128
|
const linkedPrs = linkedPullsOf(n, pulls);
|
|
4556
5129
|
const applied = comments.flatMap((c) => parseSuggestionAppliedComment(c.body) ?? []);
|
|
4557
|
-
const approvals = parsed.ok ? summarizeSuggestionApprovals(comments, gov, { payloadSha: parsed.sha, author: issue.author }) : void 0;
|
|
5130
|
+
const approvals = parsed.ok ? summarizeSuggestionApprovals(comments, gov, { payloadSha: parsed.sha, author: issue.author, valuesOnly: suggestionIsTokenValues(parsed.payload) }) : void 0;
|
|
4558
5131
|
const status = suggestionStatus({
|
|
4559
5132
|
state: issue.state,
|
|
4560
5133
|
stateReason: issue.stateReason,
|
|
@@ -4565,6 +5138,19 @@ async function getSuggestionOnGitHub(gh, n, gov, opts = {}) {
|
|
|
4565
5138
|
});
|
|
4566
5139
|
return { issue, parsed, comments, approvals, linkedPrs, applied, status };
|
|
4567
5140
|
}
|
|
5141
|
+
async function approvedSuggestionOnGitHub(gh, body, gov) {
|
|
5142
|
+
for (const n of closingIssueNumbers(body).slice(0, 3)) {
|
|
5143
|
+
const found = await approvedSuggestionNumber(gh, n, gov).catch(() => null);
|
|
5144
|
+
if (found) return found;
|
|
5145
|
+
}
|
|
5146
|
+
return null;
|
|
5147
|
+
}
|
|
5148
|
+
async function approvedSuggestionNumber(gh, n, gov) {
|
|
5149
|
+
const d = await getSuggestionOnGitHub(gh, n, gov, { pulls: [] });
|
|
5150
|
+
if (!d.parsed.ok || !d.approvals) return null;
|
|
5151
|
+
const tier = d.parsed.payload.tier.tier;
|
|
5152
|
+
return suggestionReviewState(tier, d.approvals) === "approved" ? { number: n, approvals: d.approvals, tier } : null;
|
|
5153
|
+
}
|
|
4568
5154
|
async function syncSuggestionStateLabel(gh, n, labels, status) {
|
|
4569
5155
|
const { add, remove } = suggestionLabelChanges(labels, status);
|
|
4570
5156
|
let ok = true;
|
|
@@ -4689,6 +5275,11 @@ const DOCS_MCP_ORIGIN = {
|
|
|
4689
5275
|
footer: 'Proposed with the Bridge MCP server on behalf of a person. Docs Claude drafted say "draftedBy": "claude" until a reviewer accepts them.',
|
|
4690
5276
|
label: "source: mcp"
|
|
4691
5277
|
};
|
|
5278
|
+
const DOCS_CLI_ORIGIN = {
|
|
5279
|
+
where: "with the bridge CLI",
|
|
5280
|
+
footer: 'Proposed with the bridge CLI on behalf of a person. Docs Claude drafted say "draftedBy": "claude" until a reviewer accepts them.',
|
|
5281
|
+
label: "source: cli"
|
|
5282
|
+
};
|
|
4692
5283
|
const EM_DASH = /\u2014/;
|
|
4693
5284
|
function validateDocs(d) {
|
|
4694
5285
|
const out = [];
|
|
@@ -4821,7 +5412,7 @@ ${opts.notes.trim()}
|
|
|
4821
5412
|
].filter((x, i, a) => !(x === "" && a[i - 1] === "")).join("\n");
|
|
4822
5413
|
return { branch, title, body, files, risk, bump: "patch", labels: ["docs", origin.label, `tier-${risk.tier}`] };
|
|
4823
5414
|
}
|
|
4824
|
-
const docsSource = (origin) => origin.label === DOCS_MCP_ORIGIN.label ? "mcp" : "studio";
|
|
5415
|
+
const docsSource = (origin) => origin.label === DOCS_MCP_ORIGIN.label ? "mcp" : origin.label === DOCS_CLI_ORIGIN.label ? "cli" : "studio";
|
|
4825
5416
|
function docsSuggestionPayload(edits, opts) {
|
|
4826
5417
|
const origin = opts.origin ?? DOCS_EDITOR_ORIGIN;
|
|
4827
5418
|
const date = opts.date ?? /* @__PURE__ */ new Date();
|
|
@@ -4898,6 +5489,149 @@ async function commitFileOnGitHub(gh, input) {
|
|
|
4898
5489
|
if (content === cur.text) return "";
|
|
4899
5490
|
return gh.putFile({ path: input.path, content, branch: input.branch, message: input.message, sha: cur.sha });
|
|
4900
5491
|
}
|
|
5492
|
+
class StorybookIndexError extends Error {
|
|
5493
|
+
constructor(code, message, status) {
|
|
5494
|
+
super(message);
|
|
5495
|
+
this.code = code;
|
|
5496
|
+
this.status = status;
|
|
5497
|
+
this.name = "StorybookIndexError";
|
|
5498
|
+
}
|
|
5499
|
+
code;
|
|
5500
|
+
status;
|
|
5501
|
+
}
|
|
5502
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5503
|
+
const str = (v) => typeof v === "string" && v.trim() ? v : void 0;
|
|
5504
|
+
function entryFrom(key, raw, v3) {
|
|
5505
|
+
if (!isObj(raw)) return null;
|
|
5506
|
+
const id = str(raw.id) ?? key;
|
|
5507
|
+
const title = str(raw.title) ?? str(raw.kind);
|
|
5508
|
+
const name = str(raw.name) ?? str(raw.story);
|
|
5509
|
+
if (!id || !title || !name) return null;
|
|
5510
|
+
const params = isObj(raw.parameters) ? raw.parameters : {};
|
|
5511
|
+
const type = raw.type === "docs" || raw.type === "story" ? raw.type : v3 && params.docsOnly === true ? "docs" : "story";
|
|
5512
|
+
const e = {
|
|
5513
|
+
id,
|
|
5514
|
+
title,
|
|
5515
|
+
name,
|
|
5516
|
+
type,
|
|
5517
|
+
importPath: str(raw.importPath) ?? str(params.fileName) ?? "",
|
|
5518
|
+
tags: Array.isArray(raw.tags) ? raw.tags.filter((t) => typeof t === "string") : []
|
|
5519
|
+
};
|
|
5520
|
+
const subtype = str(raw.subtype);
|
|
5521
|
+
if (subtype) e.subtype = subtype;
|
|
5522
|
+
const exportName = str(raw.exportName);
|
|
5523
|
+
if (exportName) e.exportName = exportName;
|
|
5524
|
+
const componentPath = str(raw.componentPath);
|
|
5525
|
+
if (componentPath) e.componentPath = componentPath;
|
|
5526
|
+
return e;
|
|
5527
|
+
}
|
|
5528
|
+
function groupStorybookComponents(entries) {
|
|
5529
|
+
const byTitle = /* @__PURE__ */ new Map();
|
|
5530
|
+
for (const e of entries) {
|
|
5531
|
+
let c = byTitle.get(e.title);
|
|
5532
|
+
if (!c) {
|
|
5533
|
+
const parts = e.title.split("/").map((p) => p.trim()).filter(Boolean);
|
|
5534
|
+
c = { title: e.title, name: parts[parts.length - 1] ?? e.title, group: parts.slice(0, -1), stories: [], docs: [], tags: [] };
|
|
5535
|
+
byTitle.set(e.title, c);
|
|
5536
|
+
}
|
|
5537
|
+
if (e.type === "docs") c.docs.push(e);
|
|
5538
|
+
else if (e.subtype !== "test") c.stories.push(e);
|
|
5539
|
+
for (const t of e.tags) if (!c.tags.includes(t)) c.tags.push(t);
|
|
5540
|
+
}
|
|
5541
|
+
const out = [...byTitle.values()].filter((c) => c.stories.length || c.docs.length);
|
|
5542
|
+
for (const c of out) c.tags.sort();
|
|
5543
|
+
return out.sort((a, b) => a.title.localeCompare(b.title));
|
|
5544
|
+
}
|
|
5545
|
+
function parseStorybookIndex(json, source) {
|
|
5546
|
+
if (!isObj(json)) throw new StorybookIndexError("not-json", "The Storybook index is not a JSON object.");
|
|
5547
|
+
if (typeof json.loginUrl === "string" && !("entries" in json) && !("stories" in json)) {
|
|
5548
|
+
throw new StorybookIndexError("private", "This Storybook is private: it asks for a sign-in before showing its index.");
|
|
5549
|
+
}
|
|
5550
|
+
const map = isObj(json.entries) ? json.entries : isObj(json.stories) ? json.stories : null;
|
|
5551
|
+
if (!map) throw new StorybookIndexError("empty", "This file has no Storybook entries.");
|
|
5552
|
+
const v3 = !isObj(json.entries);
|
|
5553
|
+
const entries = Object.entries(map).map(([k, raw]) => entryFrom(k, raw, v3)).filter((e) => e !== null);
|
|
5554
|
+
if (!entries.length) throw new StorybookIndexError("empty", "This Storybook index lists no stories or docs.");
|
|
5555
|
+
const version = typeof json.v === "number" ? json.v : v3 ? 3 : 4;
|
|
5556
|
+
return { version, source: source ?? (v3 ? "stories.json" : "index.json"), entries, components: groupStorybookComponents(entries) };
|
|
5557
|
+
}
|
|
5558
|
+
function normalizeStorybookUrl(input) {
|
|
5559
|
+
let u;
|
|
5560
|
+
try {
|
|
5561
|
+
u = new URL(input.trim());
|
|
5562
|
+
} catch {
|
|
5563
|
+
throw new StorybookIndexError("invalid-url", "Enter the full Storybook address, for example https://storybook.example.com");
|
|
5564
|
+
}
|
|
5565
|
+
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
|
|
5566
|
+
if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
|
|
5567
|
+
throw new StorybookIndexError("invalid-url", "The Storybook address must start with https://");
|
|
5568
|
+
}
|
|
5569
|
+
if (u.username || u.password) throw new StorybookIndexError("invalid-url", "Remove the user name and password from the address.");
|
|
5570
|
+
const path = u.pathname.replace(/\/(index\.html|iframe\.html|index\.json|stories\.json)$/i, "/").replace(/\/+$/, "");
|
|
5571
|
+
return `${u.protocol}//${u.host}${path}`;
|
|
5572
|
+
}
|
|
5573
|
+
const storybookIndexUrls = (base) => {
|
|
5574
|
+
const b = base.replace(/\/+$/, "");
|
|
5575
|
+
return [
|
|
5576
|
+
{ url: `${b}/index.json`, source: "index.json" },
|
|
5577
|
+
{ url: `${b}/stories.json`, source: "stories.json" }
|
|
5578
|
+
];
|
|
5579
|
+
};
|
|
5580
|
+
const pairs = (o) => Object.entries(o).map(([k, v]) => `${encodeURIComponent(k)}:${encodeURIComponent(v)}`).join(";");
|
|
5581
|
+
function storybookIframeUrl(base, id, o = {}) {
|
|
5582
|
+
let url = `${base.replace(/\/+$/, "")}/iframe.html?id=${encodeURIComponent(id)}&viewMode=${o.viewMode ?? "story"}`;
|
|
5583
|
+
if (o.globals && Object.keys(o.globals).length) url += `&globals=${pairs(o.globals)}`;
|
|
5584
|
+
if (o.args && Object.keys(o.args).length) url += `&args=${pairs(o.args)}`;
|
|
5585
|
+
return url;
|
|
5586
|
+
}
|
|
5587
|
+
function storybookManagerUrl(base, id, viewMode = "story") {
|
|
5588
|
+
return `${base.replace(/\/+$/, "")}/?path=/${viewMode}/${encodeURIComponent(id)}`;
|
|
5589
|
+
}
|
|
5590
|
+
async function fetchStorybookIndex(base, o = {}) {
|
|
5591
|
+
const doFetch = o.fetch ?? ((url, init) => fetch(url, init));
|
|
5592
|
+
const rank = { private: 6, http: 5, network: 4, empty: 3, "not-json": 2, "not-found": 1, "invalid-url": 0 };
|
|
5593
|
+
let worst = null;
|
|
5594
|
+
const keep = (e) => {
|
|
5595
|
+
if (!worst || rank[e.code] > rank[worst.code]) worst = e;
|
|
5596
|
+
};
|
|
5597
|
+
for (const { url, source } of storybookIndexUrls(base)) {
|
|
5598
|
+
let res;
|
|
5599
|
+
try {
|
|
5600
|
+
res = await doFetch(url, { headers: { accept: "application/json" }, ...o.signal ? { signal: o.signal } : {} });
|
|
5601
|
+
} catch (err) {
|
|
5602
|
+
if (o.signal?.aborted) throw err;
|
|
5603
|
+
keep(new StorybookIndexError("network", `Could not reach ${url}. The host may be down or may not allow reading from another site.`));
|
|
5604
|
+
continue;
|
|
5605
|
+
}
|
|
5606
|
+
if (res.status === 404 || res.status === 410) {
|
|
5607
|
+
keep(new StorybookIndexError("not-found", `No Storybook index at ${base}: neither index.json nor stories.json was found.`, res.status));
|
|
5608
|
+
continue;
|
|
5609
|
+
}
|
|
5610
|
+
if (res.status === 401 || res.status === 403) {
|
|
5611
|
+
keep(new StorybookIndexError("private", "This Storybook is private: it asks for a sign-in before showing its index.", res.status));
|
|
5612
|
+
continue;
|
|
5613
|
+
}
|
|
5614
|
+
if (!res.ok) {
|
|
5615
|
+
keep(new StorybookIndexError("http", `The Storybook host answered ${res.status} for ${url}.`, res.status));
|
|
5616
|
+
continue;
|
|
5617
|
+
}
|
|
5618
|
+
const text = await res.text();
|
|
5619
|
+
let json;
|
|
5620
|
+
try {
|
|
5621
|
+
json = JSON.parse(text);
|
|
5622
|
+
} catch {
|
|
5623
|
+
keep(new StorybookIndexError("not-json", `${url} is not a Storybook index (the host answered with a web page).`));
|
|
5624
|
+
continue;
|
|
5625
|
+
}
|
|
5626
|
+
try {
|
|
5627
|
+
return parseStorybookIndex(json, source);
|
|
5628
|
+
} catch (err) {
|
|
5629
|
+
if (err instanceof StorybookIndexError) keep(err);
|
|
5630
|
+
else throw err;
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
throw worst ?? new StorybookIndexError("not-found", `No Storybook index at ${base}.`);
|
|
5634
|
+
}
|
|
4901
5635
|
function normalizeRepoPath(path) {
|
|
4902
5636
|
return path.replace(/\\/g, "/").split("/").filter((s) => s && s !== ".").join("/");
|
|
4903
5637
|
}
|
|
@@ -4944,274 +5678,317 @@ async function readRepoTexts(source, paths, concurrency = 4, onError = (_path, e
|
|
|
4944
5678
|
return out;
|
|
4945
5679
|
}
|
|
4946
5680
|
export {
|
|
4947
|
-
|
|
5681
|
+
composeProposalBody as $,
|
|
4948
5682
|
explorationPrTitle as A,
|
|
4949
5683
|
explorationPrBody as B,
|
|
4950
|
-
|
|
5684
|
+
compareSpecToMeasured as C,
|
|
4951
5685
|
DRAFT_LINE_MAX as D,
|
|
4952
5686
|
EXPLORATION_PREFIX as E,
|
|
4953
|
-
|
|
5687
|
+
variantKeyOf as F,
|
|
4954
5688
|
GitHubError as G,
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
5689
|
+
withoutAddedWrappers as H,
|
|
5690
|
+
pairParts as I,
|
|
5691
|
+
COLOR_FIELDS as J,
|
|
5692
|
+
hashSpec as K,
|
|
5693
|
+
overrideComment as L,
|
|
5694
|
+
overrideRefusedComment as M,
|
|
5695
|
+
suggestionStateFromLabels as N,
|
|
4962
5696
|
OVERRIDE_HEADING as O,
|
|
4963
5697
|
PROSE_KEYS as P,
|
|
4964
|
-
|
|
5698
|
+
suggestionStatus as Q,
|
|
4965
5699
|
REQUESTS_BRANCH as R,
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
5700
|
+
SPEC_FIELDS as S,
|
|
5701
|
+
composeRequestIssue as T,
|
|
5702
|
+
EXPLORATION_NOTES_DIR as U,
|
|
5703
|
+
isExplorationBranch as V,
|
|
5704
|
+
diffLayered as W,
|
|
5705
|
+
contrastDelta as X,
|
|
5706
|
+
runLayeredContrast as Y,
|
|
5707
|
+
layeredImpact as Z,
|
|
5708
|
+
classifyLayered as _,
|
|
4975
5709
|
parseChangelog as a,
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5710
|
+
REVIEW_STATUS_ORDER as a$,
|
|
5711
|
+
createSuggestionOnGitHub as a0,
|
|
5712
|
+
gitBlobSha as a1,
|
|
5713
|
+
SUGGESTION_SCHEMA as a2,
|
|
5714
|
+
branchWithJiraKey as a3,
|
|
5715
|
+
titleWithJiraKey as a4,
|
|
5716
|
+
figmaSpecPath as a5,
|
|
5717
|
+
formatFigmaSpec as a6,
|
|
5718
|
+
figmaDraftBrief as a7,
|
|
5719
|
+
parseFigmaOrigin as a8,
|
|
5720
|
+
figmaPushStatus as a9,
|
|
5721
|
+
requestIssueTitle as aA,
|
|
5722
|
+
pullState as aB,
|
|
5723
|
+
summarizeCi as aC,
|
|
5724
|
+
summarizeApprovals as aD,
|
|
5725
|
+
BULK_LIMIT as aE,
|
|
5726
|
+
CI_STATE_LABEL as aF,
|
|
5727
|
+
COMPONENT_SPECS_VERSION as aG,
|
|
5728
|
+
CONFLICT_REASON as aH,
|
|
5729
|
+
DEFAULT_COMPONENT_SPECS_FILE as aI,
|
|
5730
|
+
DEFAULT_PROTECTED_BRANCHES as aJ,
|
|
5731
|
+
DOCS_CHANGE_BUMP as aK,
|
|
5732
|
+
DOCS_CLI_ORIGIN as aL,
|
|
5733
|
+
DOCS_EDITOR_ORIGIN as aM,
|
|
5734
|
+
DOCS_KEYS as aN,
|
|
5735
|
+
DOCS_MCP_ORIGIN as aO,
|
|
5736
|
+
EXPLORATION_CHECK as aP,
|
|
5737
|
+
EXPLORATION_STATUS_LABEL as aQ,
|
|
5738
|
+
EXPLORATION_TIER_LABEL as aR,
|
|
5739
|
+
EXPLORATION_USE_CASES as aS,
|
|
5740
|
+
FORMER_MEMBER as aT,
|
|
5741
|
+
LOOK_AGAIN_STATUS_LABEL as aU,
|
|
5742
|
+
MeasuredSpecsError as aV,
|
|
5743
|
+
PULL_STATE_PILL as aW,
|
|
5744
|
+
ProtectedBranchError as aX,
|
|
5745
|
+
REDUCED_MOTION_NOTE as aY,
|
|
5746
|
+
REQUEST_STATUS_LABEL as aZ,
|
|
5747
|
+
REVIEW_STATUS_LABEL as a_,
|
|
5748
|
+
parseFigmaSpec as aa,
|
|
5749
|
+
FIGMA_SPEC_DIR as ab,
|
|
5750
|
+
FROM_FIGMA_LABEL as ac,
|
|
5751
|
+
withFigmaOrigin as ad,
|
|
5752
|
+
WAITING_FOR_CLAUDE_LABEL as ae,
|
|
5753
|
+
DRAFTED_BY_CLAUDE_LABEL as af,
|
|
5754
|
+
planComponentPush as ag,
|
|
5755
|
+
figmaOriginSection as ah,
|
|
5756
|
+
figmaBranchTitle as ai,
|
|
5757
|
+
figmaSpecSlug as aj,
|
|
5758
|
+
figmaDraftBody as ak,
|
|
5759
|
+
REQUEST_LABEL as al,
|
|
5760
|
+
figmaNodeUrl as am,
|
|
5761
|
+
parentDir as an,
|
|
5762
|
+
parseComponentSpecs as ao,
|
|
5763
|
+
readRepoTexts as ap,
|
|
5764
|
+
normalizeRepoPath as aq,
|
|
5765
|
+
isUnder as ar,
|
|
5766
|
+
componentMotion as as,
|
|
5767
|
+
GitHubClient as at,
|
|
5768
|
+
effectiveTier as au,
|
|
5769
|
+
parseRequestIssue as av,
|
|
5770
|
+
requestStatus as aw,
|
|
5771
|
+
listSuggestionsOnGitHub as ax,
|
|
5772
|
+
getSuggestionOnGitHub as ay,
|
|
5773
|
+
closingIssueNumbers as az,
|
|
5040
5774
|
buildReport as b,
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5775
|
+
docsText as b$,
|
|
5776
|
+
SUGGESTION_APPLIED_MARK as b0,
|
|
5777
|
+
SUGGESTION_APPLY_EXIT as b1,
|
|
5778
|
+
SUGGESTION_FINAL_LABELS as b2,
|
|
5779
|
+
SUGGESTION_KINDS as b3,
|
|
5780
|
+
SUGGESTION_LABEL as b4,
|
|
5781
|
+
SUGGESTION_LIMITS as b5,
|
|
5782
|
+
SUGGESTION_REVIEW_MARK as b6,
|
|
5783
|
+
SUGGESTION_STATE_LABELS as b7,
|
|
5784
|
+
SUGGESTION_STATUS_LABEL as b8,
|
|
5785
|
+
SUGGESTION_STATUS_LABELS as b9,
|
|
5786
|
+
cleanDocs as bA,
|
|
5787
|
+
clearSharedEtags as bB,
|
|
5788
|
+
commitFileOnGitHub as bC,
|
|
5789
|
+
componentContrastPairs as bD,
|
|
5790
|
+
componentsUsingMotionToken as bE,
|
|
5791
|
+
composeChangeset as bF,
|
|
5792
|
+
composeSuggestionAppliedComment as bG,
|
|
5793
|
+
composeSuggestionIssue as bH,
|
|
5794
|
+
composeSuggestionReviewComment as bI,
|
|
5795
|
+
continueDocsProposalOnGitHub as bJ,
|
|
5796
|
+
contrastDeltaTable as bK,
|
|
5797
|
+
countChanges as bL,
|
|
5798
|
+
coverageSummary as bM,
|
|
5799
|
+
cssColorToRgb as bN,
|
|
5800
|
+
cssColorsToRgb as bO,
|
|
5801
|
+
cssModuleClasses as bP,
|
|
5802
|
+
declineComment as bQ,
|
|
5803
|
+
declineReasonProblems as bR,
|
|
5804
|
+
declineRequestOnGitHub as bS,
|
|
5805
|
+
declineSuggestionOnGitHub as bT,
|
|
5806
|
+
defaultDocsFooter as bU,
|
|
5807
|
+
docsAuthorLine as bV,
|
|
5808
|
+
docsFileText as bW,
|
|
5809
|
+
docsProposal as bX,
|
|
5810
|
+
docsSectionShapes as bY,
|
|
5811
|
+
docsSlug as bZ,
|
|
5812
|
+
docsSuggestionPayload as b_,
|
|
5813
|
+
SUGGESTION_TOO_LARGE as ba,
|
|
5814
|
+
StorybookIndexError as bb,
|
|
5815
|
+
SuggestionError as bc,
|
|
5816
|
+
TOKEN_REJECTED_MESSAGE as bd,
|
|
5817
|
+
TOKEN_SETTINGS_URL as be,
|
|
5818
|
+
TRIGGER_LABELS as bf,
|
|
5819
|
+
UNRELEASED as bg,
|
|
5820
|
+
acceptDocsDraft as bh,
|
|
5821
|
+
acceptDocsText as bi,
|
|
5822
|
+
allowedMergeMethods as bj,
|
|
5823
|
+
anatomyLabel as bk,
|
|
5824
|
+
appliedStatus as bl,
|
|
5825
|
+
approvedSuggestionNumber as bm,
|
|
5826
|
+
approvedSuggestionOnGitHub as bn,
|
|
5827
|
+
assertSafeBranch as bo,
|
|
5828
|
+
assignRequestOnGitHub as bp,
|
|
5829
|
+
assignVersions as bq,
|
|
5830
|
+
base64ToUtf8 as br,
|
|
5831
|
+
branchVerdict as bs,
|
|
5832
|
+
bridgeMcpInstallCommand as bt,
|
|
5833
|
+
canOverride as bu,
|
|
5834
|
+
canonicalJson as bv,
|
|
5835
|
+
changeTable as bw,
|
|
5836
|
+
changesetBump as bx,
|
|
5837
|
+
classifyAuth as by,
|
|
5838
|
+
claudeStage as bz,
|
|
5105
5839
|
collectHistory as c,
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5840
|
+
parseOverrideComment as c$,
|
|
5841
|
+
draftChangeLine as c0,
|
|
5842
|
+
draftPushDescription as c1,
|
|
5843
|
+
draftRequestText as c2,
|
|
5844
|
+
eligibleReviewers as c3,
|
|
5845
|
+
evalCalc as c4,
|
|
5846
|
+
explorationBranch as c5,
|
|
5847
|
+
explorationRules as c6,
|
|
5848
|
+
explorationShareMessage as c7,
|
|
5849
|
+
explorationSlugFromBranch as c8,
|
|
5850
|
+
fetchStorybookIndex as c9,
|
|
5851
|
+
jiraKeyOf as cA,
|
|
5852
|
+
joinRepoPath as cB,
|
|
5853
|
+
linkRequestToExplorationOnGitHub as cC,
|
|
5854
|
+
linkSuggestionPrOnGitHub as cD,
|
|
5855
|
+
linkedPullsOf as cE,
|
|
5856
|
+
loadRepoFiles as cF,
|
|
5857
|
+
lookAgainReviewers as cG,
|
|
5858
|
+
lowerFirstWord as cH,
|
|
5859
|
+
markSuggestionsAppliedOnGitHub as cI,
|
|
5860
|
+
markUsedTokens as cJ,
|
|
5861
|
+
memoryEtagStore as cK,
|
|
5862
|
+
memorySource as cL,
|
|
5863
|
+
mergeGate as cM,
|
|
5864
|
+
missingPermissionMessage as cN,
|
|
5865
|
+
missingProse as cO,
|
|
5866
|
+
modeLabel as cP,
|
|
5867
|
+
needsDocsReview as cQ,
|
|
5868
|
+
normalizeStorybookUrl as cR,
|
|
5869
|
+
openPulls as cS,
|
|
5870
|
+
overrideHolders as cT,
|
|
5871
|
+
overrideReasonMin as cU,
|
|
5872
|
+
overrideReasonProblem as cV,
|
|
5873
|
+
ownerOverridePolicy as cW,
|
|
5874
|
+
parseBoxShadow as cX,
|
|
5875
|
+
parseChangesetText as cY,
|
|
5876
|
+
parseJiraKey as cZ,
|
|
5877
|
+
parseMeasuredSpecsFile as c_,
|
|
5878
|
+
figmaCell as ca,
|
|
5879
|
+
fileModes as cb,
|
|
5880
|
+
filterHistory as cc,
|
|
5881
|
+
findOpenDocsProposal as cd,
|
|
5882
|
+
findOpenDocsProposalOnGitHub as ce,
|
|
5883
|
+
firstParagraph as cf,
|
|
5884
|
+
formatDocsJson as cg,
|
|
5885
|
+
friendlyMessage as ch,
|
|
5886
|
+
githubSuggestionWorkspace as ci,
|
|
5887
|
+
groupStorybookComponents as cj,
|
|
5888
|
+
hasOwnerApproval as ck,
|
|
5889
|
+
hasSuggestionBlock as cl,
|
|
5890
|
+
hasWriteAccess as cm,
|
|
5891
|
+
iconComponentName as cn,
|
|
5892
|
+
installationFor as co,
|
|
5893
|
+
isActiveExploration as cp,
|
|
5894
|
+
isAutomationPull as cq,
|
|
5895
|
+
isDocsProposalBranch as cr,
|
|
5896
|
+
isDraft as cs,
|
|
5897
|
+
isGuardrailPath as ct,
|
|
5898
|
+
isJiraKey as cu,
|
|
5899
|
+
isMissingPermission as cv,
|
|
5900
|
+
isPlatformRefusal as cw,
|
|
5901
|
+
isReleasePull as cx,
|
|
5902
|
+
isTokenRejected as cy,
|
|
5903
|
+
jiraIssueFields as cz,
|
|
5170
5904
|
applyTokenEdits as d,
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5905
|
+
summarizeSuggestionApprovals as d$,
|
|
5906
|
+
parseRepo as d0,
|
|
5907
|
+
parseStorybookIndex as d1,
|
|
5908
|
+
parseStructureChange as d2,
|
|
5909
|
+
parseSuggestionAppliedComment as d3,
|
|
5910
|
+
parseSuggestionIssue as d4,
|
|
5911
|
+
parseSuggestionReviewComment as d5,
|
|
5912
|
+
parseTier as d6,
|
|
5913
|
+
patchDocsJson as d7,
|
|
5914
|
+
pendingApprovals as d8,
|
|
5915
|
+
permissionLabel as d9,
|
|
5916
|
+
sha256Hex as dA,
|
|
5917
|
+
sharedTokenPrefix as dB,
|
|
5918
|
+
shortVariant as dC,
|
|
5919
|
+
shortVariants as dD,
|
|
5920
|
+
specHash as dE,
|
|
5921
|
+
specsUsage as dF,
|
|
5922
|
+
stalenessStatus as dG,
|
|
5923
|
+
storybookIframeUrl as dH,
|
|
5924
|
+
storybookIndexUrls as dI,
|
|
5925
|
+
storybookManagerUrl as dJ,
|
|
5926
|
+
subjectTitle as dK,
|
|
5927
|
+
suggestionAllowedRoots as dL,
|
|
5928
|
+
suggestionAppliedComment as dM,
|
|
5929
|
+
suggestionApplyTable as dN,
|
|
5930
|
+
suggestionIsTokenValues as dO,
|
|
5931
|
+
suggestionIssueTitle as dP,
|
|
5932
|
+
suggestionKindFromLabels as dQ,
|
|
5933
|
+
suggestionLabelChanges as dR,
|
|
5934
|
+
suggestionLabels as dS,
|
|
5935
|
+
suggestionPathProblems as dT,
|
|
5936
|
+
suggestionPayloadSha as dU,
|
|
5937
|
+
suggestionReviewState as dV,
|
|
5938
|
+
suggestionShapeProblems as dW,
|
|
5939
|
+
suggestionStalenessOnGitHub as dX,
|
|
5940
|
+
suggestionStateLabel as dY,
|
|
5941
|
+
suggestionSubject as dZ,
|
|
5942
|
+
suggestionTouches as d_,
|
|
5943
|
+
planRestore as da,
|
|
5944
|
+
planRevert as db,
|
|
5945
|
+
planSuggestionApply as dc,
|
|
5946
|
+
planToJson as dd,
|
|
5947
|
+
prNumberFromSubject as de,
|
|
5948
|
+
prefillDocs as df,
|
|
5949
|
+
proposeDocsOnGitHub as dg,
|
|
5950
|
+
propsTable as dh,
|
|
5951
|
+
pushChangeRows as di,
|
|
5952
|
+
rateLimitMessage as dj,
|
|
5953
|
+
releasePull as dk,
|
|
5954
|
+
reopenEligibility as dl,
|
|
5955
|
+
requestBrief as dm,
|
|
5956
|
+
requestImagePath as dn,
|
|
5957
|
+
requesterFirstName as dp,
|
|
5958
|
+
requesterLine as dq,
|
|
5959
|
+
requesterOf as dr,
|
|
5960
|
+
requiredApprovals as ds,
|
|
5961
|
+
requiredPermission as dt,
|
|
5962
|
+
reviewQueue as du,
|
|
5963
|
+
reviewStatusKind as dv,
|
|
5964
|
+
reviewStatusLabel as dw,
|
|
5965
|
+
reviewSuggestionOnGitHub as dx,
|
|
5966
|
+
reviewersToAsk as dy,
|
|
5967
|
+
sectionsNeedingReview as dz,
|
|
5214
5968
|
buildHistoryEntry as e,
|
|
5969
|
+
syncSuggestionStateLabel as e0,
|
|
5970
|
+
titleOf as e1,
|
|
5971
|
+
toChangeSummary as e2,
|
|
5972
|
+
tokenCssVar as e3,
|
|
5973
|
+
tokenFingerprint as e4,
|
|
5974
|
+
tokenHistory as e5,
|
|
5975
|
+
tokenVariablePath as e6,
|
|
5976
|
+
unsafePathProblem as e7,
|
|
5977
|
+
utf8ToBase64 as e8,
|
|
5978
|
+
validateComponentSpec as e9,
|
|
5979
|
+
validateDocs as ea,
|
|
5980
|
+
validateIntegrations as eb,
|
|
5981
|
+
variantName as ec,
|
|
5982
|
+
versionTimeline as ed,
|
|
5983
|
+
webStorageEtagStore as ee,
|
|
5984
|
+
withAuthorLine as ef,
|
|
5985
|
+
withFigmaPushApprovals as eg,
|
|
5986
|
+
withOutdated as eh,
|
|
5987
|
+
withRequestExploration as ei,
|
|
5988
|
+
withSrgbColors as ej,
|
|
5989
|
+
withSuggestionApprovals as ek,
|
|
5990
|
+
withTokenValueApprovals as el,
|
|
5991
|
+
withoutJiraKey as em,
|
|
5215
5992
|
coerceValue as f,
|
|
5216
5993
|
checkLayeredPair as g,
|
|
5217
5994
|
formatChangeset as h,
|
|
@@ -5234,4 +6011,4 @@ export {
|
|
|
5234
6011
|
explorationNote as y,
|
|
5235
6012
|
explorationNotePath as z
|
|
5236
6013
|
};
|
|
5237
|
-
//# sourceMappingURL=repo-source-
|
|
6014
|
+
//# sourceMappingURL=repo-source-m0Tc603o.js.map
|