@bridge-ds/core 0.19.0 → 0.21.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/LICENSE +17 -0
- package/bridge.config.schema.json +5 -0
- package/dist/audit/index.js +275 -52
- package/dist/audit/index.js.map +1 -1
- package/dist/audit/missing-pieces.d.ts +4 -0
- package/dist/component-spec.d.ts +13 -0
- package/dist/config.d.ts +5 -0
- package/dist/exploration-done-github.d.ts +50 -0
- package/dist/exploration-resume.d.ts +136 -0
- package/dist/explorations-github.d.ts +75 -3
- package/dist/explorations.d.ts +137 -8
- package/dist/fidelity.d.ts +12 -1
- package/dist/figma-fix-loop.d.ts +1 -1
- package/dist/figma-fix-run.d.ts +103 -0
- package/dist/figma-fixes.d.ts +14 -2
- package/dist/gap-case.d.ts +1 -2
- package/dist/github-client.d.ts +17 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2139 -1290
- package/dist/index.js.map +1 -1
- package/dist/measure-location.d.ts +42 -0
- package/dist/measure-workflow.d.ts +48 -0
- package/dist/measured-only.d.ts +15 -1
- package/dist/node/index.js +15 -12
- package/dist/node/index.js.map +1 -1
- package/dist/previews-github.d.ts +13 -0
- package/dist/previews.d.ts +8 -1
- package/dist/{prod-branch-Bdx_2bRQ.js → prod-branch-C7tphFAA.js} +257 -192
- package/dist/prod-branch-C7tphFAA.js.map +1 -0
- package/dist/prod-branch-github.d.ts +54 -4
- package/dist/prod-branch.d.ts +42 -1
- package/dist/products.d.ts +17 -0
- package/dist/repo-model.d.ts +2 -0
- package/dist/{repo-source-m0Tc603o.js → repo-source-CEBBBtm1.js} +2183 -1937
- package/dist/repo-source-CEBBBtm1.js.map +1 -0
- package/dist/{report-DFPKygfk.js → report-B7wNAnSu.js} +38 -38
- package/dist/{report-DFPKygfk.js.map → report-B7wNAnSu.js.map} +1 -1
- package/dist/review-gate.d.ts +7 -1
- package/dist/suggestion-review.d.ts +43 -2
- package/dist/suggestions-github.d.ts +12 -1
- package/dist/tier1-publish.d.ts +48 -0
- package/dist/try-in-product.d.ts +4 -1
- package/package.json +1 -1
- package/dist/prod-branch-Bdx_2bRQ.js.map +0 -1
- package/dist/repo-source-m0Tc603o.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { m as modeDisplayName, b as modesOf$1, i as isMultiMode, T as TIER_NAMES, c as TIER_AXIS, d as collectRefs, f as findReferenceProblems, e as evaluatePairs, g as contrastScope, h as contextLabel, S as SINGLE_MODE, j as isAlias$1, k as aliasTarget, l as pathId, v as valuesEqual, n as flattenTokens, s as serializeTokenFile, r as removeToken, u as upsertToken, o as threeWayDiff, q as emptyTokenFile, w as parseTokenFile, E as EXT_ID, x as EXT_SCOPES, y as parseColor, z as rgbaToHex, A as formatValue, B as resolve, C as moveToken, D as effectiveScopes, F as sameScopes, G as defaultScopes, H as readTokenAt, I as modeFile, J as defaultFile, K as selectTokenFiles, L as LayeredTokenSet, M as resolveBranchPreviews, N as loadConfig, O as tokensPath, P as toConfigJson, Q as validateLayered, R as parseContrastChecks, U as classifyRisk, V as CONFIG_FILE, W as RISK_LABELS, X as previewLinks, Y as findPreviewComment, Z as previewsSettled, _ as
|
|
2
|
-
import {
|
|
3
|
-
import { G as GitHubError, c as collectHistory, p as packageDirCandidates, a as parseChangelog, r as releasesFromTags, m as mergeReleases, b as buildReport, d as applyTokenEdits, e as buildHistoryEntry, f as coerceValue, g as checkLayeredPair, h as formatChangeset, P as PROSE_KEYS, i as markClaudeDraft, j as cleanDraftText, k as docsPrompt, D as DRAFT_LINE_MAX, R as REQUESTS_BRANCH, l as REQUEST_KINDS, s as sanitizeDraftLine,
|
|
4
|
-
import {
|
|
1
|
+
import { m as modeDisplayName, b as modesOf$1, i as isMultiMode, T as TIER_NAMES, c as TIER_AXIS, d as collectRefs, f as findReferenceProblems, e as evaluatePairs, g as contrastScope, h as contextLabel, S as SINGLE_MODE, j as isAlias$1, k as aliasTarget, l as pathId, v as valuesEqual, n as flattenTokens, s as serializeTokenFile, r as removeToken, u as upsertToken, o as threeWayDiff, q as emptyTokenFile, w as parseTokenFile, E as EXT_ID, x as EXT_SCOPES, y as parseColor, z as rgbaToHex, A as formatValue, B as resolve, C as moveToken, D as effectiveScopes, F as sameScopes, G as defaultScopes, H as readTokenAt, I as modeFile, J as defaultFile, K as selectTokenFiles, L as LayeredTokenSet, M as resolveBranchPreviews, N as loadConfig, O as tokensPath, P as toConfigJson, Q as validateLayered, R as parseContrastChecks, U as classifyRisk, V as CONFIG_FILE, W as RISK_LABELS, X as previewLinks, Y as findPreviewComment, Z as previewsSettled, _ as previewUrl, $ as productRepos, a0 as linkStatus, a1 as libraryPreviewState, a2 as parsePiecesRecord, a3 as libraryBranchPlan, a4 as NEEDED_BY_LABEL, a5 as withNeededBy, a6 as withLink, a7 as withPiecesRecord, a8 as productBranchSentence, a9 as productOfBranch, aa as pieceRequest, ab as mergePiecesRecord, ac as defaultProduct, ad as productOfRepo, ae as baseBranchOf, af as DEFAULT_TOKEN_PATHS, ag as layoutOptions, ah as buildTokenSet, ai as readPreviewComment, aj as slugify, ak as defineConfig, al as globToRegExp, am as ConfigError, an as DEFAULT_CHANGES_MODE, ao as DEFAULT_OVERRIDE, ap as DEFAULT_PRODUCTS, aq as isGuardrail } from "./prod-branch-C7tphFAA.js";
|
|
2
|
+
import { ar, as, at, au, av, aw, ax, ay, az, aA, aB, aC, aD, aE, aF, aG, aH, aI, aJ, aK, aL, aM, aN, aO, aP, aQ, aR, aS, aT, aU, aV, aW, aX, aY, aZ, a_, a$, b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, ba, bb, bc, bd, be, bf, bg, bh, bi, bj, bk, bl, bm, bn, bo, bp, bq, br, bs, bt, bu, bv, bw, bx, by, bz, bA, bB, bC, bD, bE, bF, bG, bH, bI, bJ, bK, bL, bM, bN, bO, bP, bQ, bR, bS, bT, bU, bV, bW, p, bX, bY, bZ, b_, b$, c0, c1, c2, c3, c4, c5, c6, a, c7, c8, c9, ca, cb, cc, cd, ce, cf, cg, ch, ci, cj, ck, cl, cm, cn, co, cp, cq, cr, cs, ct, cu, cv, cw, t, cx, cy, cz, cA, cB, cC, cD, cE, cF } from "./prod-branch-C7tphFAA.js";
|
|
3
|
+
import { G as GitHubError, c as collectHistory, p as packageDirCandidates, a as parseChangelog, r as releasesFromTags, m as mergeReleases, b as buildReport, d as applyTokenEdits, e as buildHistoryEntry, f as coerceValue, g as checkLayeredPair, h as formatChangeset, P as PROSE_KEYS, i as markClaudeDraft, j as cleanDraftText, k as docsPrompt, D as DRAFT_LINE_MAX, R as REQUESTS_BRANCH, l as REQUEST_KINDS, s as sanitizeDraftLine, E as EXPLORATION_ARCHIVED_LABEL, n as parseExplorationBody, o as EXPLORATION_PREFIX, q as explorationSlug, t as isArchivedExploration, u as explorationStatus, v as explorationTryIt, w as isExplorationPull, x as promotionPlan, y as EXPLORATION_LABEL, z as explorationProblems, A as explorationNote, B as explorationNotePath, C as explorationPrTitle, F as explorationPrBody, H as suggestionStateFromLabels, I as suggestionStatus, J as composeRequestIssue, K as EXPLORATION_NOTES_DIR, L as isExplorationBranch, M as doneExploringPlan, N as withDoneExploring, O as explorationStartingPoint, Q as explorationRefLabel, S as compareSpecToMeasured, T as variantKeyOf, U as matchVariant, V as withoutAddedWrappers, W as pairParts, X as hashSpec, Y as SPEC_FIELDS, Z as COLOR_FIELDS, _ as DRAFTED_BY_CLAUDE_LABEL, $ as overrideComment, a0 as OVERRIDE_HEADING, a1 as overrideRefusedComment, a2 as diffLayered, a3 as contrastDelta, a4 as runLayeredContrast, a5 as layeredImpact, a6 as classifyLayered, a7 as composeProposalBody, a8 as createSuggestionOnGitHub, a9 as gitBlobSha, aa as SUGGESTION_SCHEMA, ab as branchWithJiraKey, ac as titleWithJiraKey, ad as figmaSpecPath, ae as formatFigmaSpec, af as figmaDraftBrief, ag as parseFigmaOrigin, ah as figmaPushStatus, ai as parseFigmaSpec, aj as FIGMA_SPEC_DIR, ak as FROM_FIGMA_LABEL, al as withFigmaOrigin, am as WAITING_FOR_CLAUDE_LABEL, an as planComponentPush, ao as figmaOriginSection, ap as figmaBranchTitle, aq as figmaSpecSlug, ar as figmaDraftBody, as as REQUEST_LABEL, at as figmaNodeUrl, au as parentDir, av as parseComponentSpecs, aw as readRepoTexts, ax as normalizeRepoPath, ay as isUnder, az as componentMotion, aA as GitHubClient, aB as effectiveTier, aC as parseRequestIssue, aD as requestStatus, aE as listSuggestionsOnGitHub, aF as getSuggestionOnGitHub, aG as closingIssueNumbers, aH as requestIssueTitle, aI as pullState, aJ as summarizeCi, aK as summarizeApprovals } from "./repo-source-CEBBBtm1.js";
|
|
4
|
+
import { aL as aL2, aM as aM2, aN as aN2, aO as aO2, aP as aP2, aQ as aQ2, aR as aR2, aS as aS2, aT as aT2, aU as aU2, aV as aV2, aW as aW2, aX as aX2, aY as aY2, aZ as aZ2, a_ as a_2, a$ as a$2, b0 as b02, b1 as b12, b2 as b22, b3 as b32, b4 as b42, b5 as b52, b6 as b62, b7 as b72, b8 as b82, b9 as b92, ba as ba2, bb as bb2, bc as bc2, bd as bd2, be as be2, bf as bf2, bg as bg2, bh as bh2, bi as bi2, bj as bj2, bk as bk2, bl as bl2, bm as bm2, bn as bn2, bo as bo2, bp as bp2, bq as bq2, br as br2, bs as bs2, bt as bt2, bu as bu2, bv as bv2, bw as bw2, bx as bx2, by as by2, bz as bz2, bA as bA2, bB as bB2, bC as bC2, bD as bD2, bE as bE2, bF as bF2, bG as bG2, bH as bH2, bI as bI2, bJ as bJ2, bK as bK2, bL as bL2, bM as bM2, bN as bN2, bO as bO2, bP as bP2, bQ as bQ2, bR as bR2, bS as bS2, bT as bT2, bU as bU2, bV as bV2, bW as bW2, bX as bX2, bY as bY2, bZ as bZ2, b_ as b_2, b$ as b$2, c0 as c02, c1 as c12, c2 as c22, c3 as c32, c4 as c42, c5 as c52, c6 as c62, c7 as c72, c8 as c82, c9 as c92, ca as ca2, cb as cb2, cc as cc2, cd as cd2, ce as ce2, cf as cf2, cg as cg2, ch as ch2, ci as ci2, cj as cj2, ck as ck2, cl as cl2, cm as cm2, cn as cn2, co as co2, cp as cp2, cq as cq2, cr as cr2, cs as cs2, ct as ct2, cu as cu2, cv as cv2, cw as cw2, cx as cx2, cy as cy2, cz as cz2, cA as cA2, cB as cB2, cC as cC2, cD as cD2, cE as cE2, cF as cF2, cG, cH, cI, cJ, cK, cL, cM, cN, cO, cP, cQ, cR, cS, cT, cU, cV, cW, cX, cY, cZ, c_, c$, d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, da, db, dc, dd, de, df, dg, dh, di, dj, dk, dl, dm, dn, dp, dq, dr, ds, dt, du, dv, dw, dx, dy, dz, dA, dB, dC, dD, dE, dF, dG, dH, dI, dJ, dK, dL, dM, dN, dO, dP, dQ, dR, dS, dT, dU, dV, dW, dX, dY, dZ, d_, d$, e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, ea, eb, ec, ed, ee, ef, eg, eh, ei, ej, ek, el, em, en, eo, ep, eq, er, es, et, eu, ev, ew, ex, ey, ez, eA, eB, eC, eD, eE, eF, eG } from "./repo-source-CEBBBtm1.js";
|
|
5
5
|
const issueKey$1 = (i) => `${i.code}|${i.path ?? ""}|${i.mode ?? ""}|${i.code === "file-problem" ? i.message : ""}`;
|
|
6
6
|
const modeName = (mode) => mode === SINGLE_MODE ? "all modes" : modeDisplayName(mode);
|
|
7
7
|
const isPalette = (t2) => t2.tier === "brand" && t2.path.startsWith("palette.");
|
|
@@ -162,7 +162,7 @@ function validateProposal(proposed, head, pairs, typeChanges = []) {
|
|
|
162
162
|
const warnings = [];
|
|
163
163
|
for (const i of after.issues) {
|
|
164
164
|
if (i.severity === "warning") warnings.push(i);
|
|
165
|
-
else if (existing.has(issueKey$1(i))) warnings.push({ ...i, severity: "warning", message: `Already
|
|
165
|
+
else if (existing.has(issueKey$1(i))) warnings.push({ ...i, severity: "warning", message: `Already in the design system: ${i.message}` });
|
|
166
166
|
else blocking.push(i);
|
|
167
167
|
}
|
|
168
168
|
for (const c of typeChanges) {
|
|
@@ -726,7 +726,7 @@ function importsIn(text) {
|
|
|
726
726
|
for (const m of text.matchAll(IMPORT_RE)) out.add(m[1] ?? m[2]);
|
|
727
727
|
return [...out];
|
|
728
728
|
}
|
|
729
|
-
const norm = (p2) => p2.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
729
|
+
const norm$1 = (p2) => p2.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
730
730
|
const isStory = (p2) => /\.stories\.[cm]?[jt]sx?$/.test(p2);
|
|
731
731
|
const isTest = (p2) => /\.(test|spec)\.[cm]?[jt]sx?$/.test(p2);
|
|
732
732
|
const isCss = (p2) => /\.(css|scss|less)$/.test(p2);
|
|
@@ -744,14 +744,14 @@ function componentOf(from, spec, componentsDir) {
|
|
|
744
744
|
return abs.slice(componentsDir.length + 1).split("/")[0] || null;
|
|
745
745
|
}
|
|
746
746
|
function componentForPath(path, componentsDir) {
|
|
747
|
-
const dir = norm(componentsDir);
|
|
748
|
-
const p2 = norm(path);
|
|
747
|
+
const dir = norm$1(componentsDir);
|
|
748
|
+
const p2 = norm$1(path);
|
|
749
749
|
if (!p2.startsWith(`${dir}/`)) return null;
|
|
750
750
|
const rest = p2.slice(dir.length + 1).split("/");
|
|
751
751
|
return rest.length >= 2 ? rest[0] : null;
|
|
752
752
|
}
|
|
753
753
|
function buildUsageIndex(files, componentsDir) {
|
|
754
|
-
const dir = norm(componentsDir);
|
|
754
|
+
const dir = norm$1(componentsDir);
|
|
755
755
|
const modules = {};
|
|
756
756
|
const vars = /* @__PURE__ */ new Map();
|
|
757
757
|
const imports = /* @__PURE__ */ new Map();
|
|
@@ -759,7 +759,7 @@ function buildUsageIndex(files, componentsDir) {
|
|
|
759
759
|
const globalVars = {};
|
|
760
760
|
const get = (m, k) => m.get(k) ?? m.set(k, /* @__PURE__ */ new Set()).get(k);
|
|
761
761
|
for (const f of files) {
|
|
762
|
-
const path = norm(f.path);
|
|
762
|
+
const path = norm$1(f.path);
|
|
763
763
|
const id = componentForPath(path, dir);
|
|
764
764
|
if (!id) {
|
|
765
765
|
if (isCss(path)) {
|
|
@@ -803,7 +803,7 @@ function impactedModules(index, input) {
|
|
|
803
803
|
for (const v of changedVars) for (const id of index.byVar[v] ?? []) add(id, `uses ${v}`, false);
|
|
804
804
|
for (const f of input.files ?? []) {
|
|
805
805
|
const id = componentForPath(f, index.componentsDir);
|
|
806
|
-
if (id && index.modules[id]) add(id, `file changed: ${norm(f)}`, false);
|
|
806
|
+
if (id && index.modules[id]) add(id, `file changed: ${norm$1(f)}`, false);
|
|
807
807
|
}
|
|
808
808
|
const renderedBy = /* @__PURE__ */ new Map();
|
|
809
809
|
for (const m of Object.values(index.modules)) for (const d of m.imports) (renderedBy.get(d) ?? renderedBy.set(d, []).get(d)).push(m.id);
|
|
@@ -1023,6 +1023,16 @@ async function branchPreviewUrls(gh, ref, config, opts = {}) {
|
|
|
1023
1023
|
]);
|
|
1024
1024
|
return resolveBranchPreviews(apps, { branch: branch ?? "", ...pr !== void 0 ? { pr } : {}, ...sha ? { sha } : {} }, { comments, statuses, deployments });
|
|
1025
1025
|
}
|
|
1026
|
+
async function productPreviewOnGitHub(gh, product, e) {
|
|
1027
|
+
if (!product.preview) return null;
|
|
1028
|
+
const [comments, statuses, deployments] = await Promise.all([
|
|
1029
|
+
gh.listIssueComments(e.number).catch(() => []),
|
|
1030
|
+
e.sha ? gh.listCommitStatuses(e.sha).catch(() => []) : Promise.resolve([]),
|
|
1031
|
+
e.sha ? gh.listDeployments(e.sha).catch(() => []) : Promise.resolve([])
|
|
1032
|
+
]);
|
|
1033
|
+
const p2 = resolveBranchPreviews({ admin: product.preview }, { branch: e.branch, pr: e.number, ...e.sha ? { sha: e.sha } : {} }, { comments, statuses, deployments })[0];
|
|
1034
|
+
return p2 ? { ...p2, app: product.key, label: product.name } : null;
|
|
1035
|
+
}
|
|
1026
1036
|
const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
|
1027
1037
|
const LIGHTNESS = { 50: 0.975, 100: 0.952, 200: 0.912, 300: 0.85, 400: 0.735, 500: 0.63, 600: 0.53, 700: 0.445, 800: 0.36, 900: 0.27, 950: 0.205 };
|
|
1028
1038
|
const BELL = { 50: 0.12, 100: 0.22, 200: 0.4, 300: 0.62, 400: 0.85, 500: 1, 600: 1, 700: 0.92, 800: 0.8, 900: 0.62, 950: 0.5 };
|
|
@@ -1130,7 +1140,7 @@ const BRAND_SLUG_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
|
1130
1140
|
const DEFAULT_PRIMARY_CHROMA = 0.14;
|
|
1131
1141
|
const isObj$4 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
1132
1142
|
const fileMap = (f) => f instanceof Map ? new Map(f) : new Map(Object.entries(f));
|
|
1133
|
-
const brandDisplayName = (
|
|
1143
|
+
const brandDisplayName = (slug2) => slug2.split("-").filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
1134
1144
|
function brandLayers(config) {
|
|
1135
1145
|
const out = {};
|
|
1136
1146
|
for (const l of config.tokens.layers) {
|
|
@@ -1200,7 +1210,7 @@ function aliasRamp(doc, role, ramp, description) {
|
|
|
1200
1210
|
}
|
|
1201
1211
|
group.$description = description;
|
|
1202
1212
|
}
|
|
1203
|
-
function addBrandToConfigText(text,
|
|
1213
|
+
function addBrandToConfigText(text, slug2) {
|
|
1204
1214
|
const canonical = /("brand"\s*:\s*\{\s*"values"\s*:\s*\[)([^\]]*)(\])/;
|
|
1205
1215
|
const flat = /("brands"\s*:\s*\[)([^\]]*)(\])/;
|
|
1206
1216
|
for (const re of [canonical, flat]) {
|
|
@@ -1208,7 +1218,7 @@ function addBrandToConfigText(text, slug) {
|
|
|
1208
1218
|
if (!m) continue;
|
|
1209
1219
|
const items = m[2].trim();
|
|
1210
1220
|
const sep = items.includes("\n") ? `,${/\n(\s*)"/.exec(m[2])?.[0]?.replace(/"$/, "") ?? " "}` : ", ";
|
|
1211
|
-
return text.slice(0, m.index) + m[1] + (items ? `${m[2].replace(/\s*$/, "")}${sep}"${
|
|
1221
|
+
return text.slice(0, m.index) + m[1] + (items ? `${m[2].replace(/\s*$/, "")}${sep}"${slug2}"` : `"${slug2}"`) + (m[2].match(/\s*$/)?.[0] ?? "") + m[3] + text.slice(m.index + m[0].length);
|
|
1212
1222
|
}
|
|
1213
1223
|
throw new Error("bridge.config.json has no modes.brand.values list to add the brand to.");
|
|
1214
1224
|
}
|
|
@@ -1219,21 +1229,21 @@ function listBrands(source) {
|
|
|
1219
1229
|
const layers = brandLayers(config);
|
|
1220
1230
|
const brandAxis = config.axes.brand;
|
|
1221
1231
|
if (!brandAxis || !layers.brand) return [];
|
|
1222
|
-
return brandAxis.values.map((
|
|
1223
|
-
const file = fill(layers.brand, { brand:
|
|
1232
|
+
return brandAxis.values.map((slug2) => {
|
|
1233
|
+
const file = fill(layers.brand, { brand: slug2 });
|
|
1224
1234
|
let doc = {};
|
|
1225
1235
|
try {
|
|
1226
1236
|
doc = JSON.parse(files.get(file) ?? "{}");
|
|
1227
1237
|
} catch {
|
|
1228
1238
|
}
|
|
1229
1239
|
const meta = isObj$4(doc.$extensions) && isObj$4(doc.$extensions["dev.bridge.brand"]) ? doc.$extensions["dev.bridge.brand"] : {};
|
|
1230
|
-
const combo = set.normalizeCombo({ brand:
|
|
1240
|
+
const combo = set.normalizeCombo({ brand: slug2 });
|
|
1231
1241
|
const primary = set.resolve("color.action.primary.bg", combo);
|
|
1232
|
-
const overrides = layers.override ? (config.axes[layers.overrideAxis]?.values ?? []).map((v) => fill(layers.override, { brand:
|
|
1242
|
+
const overrides = layers.override ? (config.axes[layers.overrideAxis]?.values ?? []).map((v) => fill(layers.override, { brand: slug2, [layers.overrideAxis]: v })).filter((f) => files.has(f)) : [];
|
|
1233
1243
|
return {
|
|
1234
|
-
slug,
|
|
1235
|
-
name: typeof meta.name === "string" ? meta.name : brandDisplayName(
|
|
1236
|
-
default:
|
|
1244
|
+
slug: slug2,
|
|
1245
|
+
name: typeof meta.name === "string" ? meta.name : brandDisplayName(slug2),
|
|
1246
|
+
default: slug2 === brandAxis.default,
|
|
1237
1247
|
file: tokensPath(config, file),
|
|
1238
1248
|
...typeof doc.$description === "string" ? { description: doc.$description } : {},
|
|
1239
1249
|
...primary.css && !primary.error ? { primary: primary.css } : {},
|
|
@@ -1241,9 +1251,9 @@ function listBrands(source) {
|
|
|
1241
1251
|
};
|
|
1242
1252
|
});
|
|
1243
1253
|
}
|
|
1244
|
-
function checkBrandSlug(config, files,
|
|
1254
|
+
function checkBrandSlug(config, files, slug2) {
|
|
1245
1255
|
const out = [];
|
|
1246
|
-
const s =
|
|
1256
|
+
const s = slug2.trim();
|
|
1247
1257
|
if (!s) return [{ level: "error", code: "slug-empty", message: "Give the brand a name." }];
|
|
1248
1258
|
if (s.length < 2 || s.length > 24 || !BRAND_SLUG_RE.test(s)) out.push({ level: "error", code: "slug-format", message: 'Use 2 to 24 lowercase letters, digits and single dashes, starting with a letter: "acme" or "north-star".' });
|
|
1249
1259
|
const taken = Object.values(config.axes).flatMap((a2) => a2.values);
|
|
@@ -1258,12 +1268,12 @@ function planNewBrand(source, input) {
|
|
|
1258
1268
|
const { config } = source;
|
|
1259
1269
|
const files = fileMap(source.files);
|
|
1260
1270
|
const issues = [];
|
|
1261
|
-
const
|
|
1262
|
-
const name = input.name?.trim() || brandDisplayName(
|
|
1271
|
+
const slug2 = input.slug.trim();
|
|
1272
|
+
const name = input.name?.trim() || brandDisplayName(slug2);
|
|
1263
1273
|
const layers = brandLayers(config);
|
|
1264
1274
|
const brandAxis = config.axes.brand;
|
|
1265
1275
|
if (!brandAxis || !layers.brand) throw new Error("This repo has no brand mode: bridge.config.json needs modes.brand and a tokens layer with {brand}.");
|
|
1266
|
-
issues.push(...checkBrandSlug(config, files,
|
|
1276
|
+
issues.push(...checkBrandSlug(config, files, slug2));
|
|
1267
1277
|
const from = input.from?.trim() || brandAxis.default;
|
|
1268
1278
|
const blank = from === "blank";
|
|
1269
1279
|
if (!blank && !brandAxis.values.includes(from)) throw new Error(`Unknown brand "${from}" to start from. Brands: ${brandAxis.values.join(", ")} (or "blank").`);
|
|
@@ -1290,8 +1300,8 @@ function planNewBrand(source, input) {
|
|
|
1290
1300
|
const prims = /* @__PURE__ */ new Map();
|
|
1291
1301
|
const addRamp = (role, ramp, description) => {
|
|
1292
1302
|
const steps = new Map(STEPS.map((st) => [st, dtcgColor(ramp[st])]));
|
|
1293
|
-
prims.set(`${
|
|
1294
|
-
return `${
|
|
1303
|
+
prims.set(`${slug2}-${role}`, { description, steps });
|
|
1304
|
+
return `${slug2}-${role}`;
|
|
1295
1305
|
};
|
|
1296
1306
|
if (input.primaryHue !== void 0 && input.primaryHue !== "") {
|
|
1297
1307
|
const ramp = safeRamp("primary", input.primaryHue, "color", actionStep);
|
|
@@ -1358,7 +1368,7 @@ function planNewBrand(source, input) {
|
|
|
1358
1368
|
if (role.startsWith("$") || !isObj$4(group)) continue;
|
|
1359
1369
|
for (const [step, node] of Object.entries(group)) {
|
|
1360
1370
|
if (step.startsWith("$") || !isObj$4(node) || !("$value" in node) || typeof node.$value === "string") continue;
|
|
1361
|
-
const ramp = `${
|
|
1371
|
+
const ramp = `${slug2}-${role}`;
|
|
1362
1372
|
const entry = prims.get(ramp) ?? { description: `${name} ${role}. Exact values set when the brand was created.`, steps: /* @__PURE__ */ new Map() };
|
|
1363
1373
|
entry.steps.set(Number(step), node.$value);
|
|
1364
1374
|
prims.set(ramp, entry);
|
|
@@ -1366,13 +1376,13 @@ function planNewBrand(source, input) {
|
|
|
1366
1376
|
}
|
|
1367
1377
|
}
|
|
1368
1378
|
const primDir = config.tokens.tiers.primitive?.[0]?.replace(/\/$/, "") ?? config.tokens.primitives;
|
|
1369
|
-
const primFile = `${primDir}/${
|
|
1379
|
+
const primFile = `${primDir}/${slug2}.tokens.json`;
|
|
1370
1380
|
if (prims.size && files.has(primFile)) issues.push({ level: "error", code: "slug-file-exists", message: `${tokensPath(config, primFile)} already exists. Pick another name.` });
|
|
1371
1381
|
for (const r of prims.keys()) if (before.allPaths().some((p2) => p2.startsWith(`color.${r}.`))) issues.push({ level: "error", code: "ramp-exists", message: `A primitive ramp color.${r} already exists. Pick another name.` });
|
|
1372
1382
|
doc.$description = `${name}: created with Bridge, started from ${blank ? "a blank brand" : brandDisplayName(from)}.`;
|
|
1373
1383
|
doc.$extensions = { ...isObj$4(doc.$extensions) ? doc.$extensions : {}, "dev.bridge.brand": { name, from, ...Object.keys(anchors).length ? { anchors } : {} } };
|
|
1374
1384
|
const { $description, $extensions, ...rest } = doc;
|
|
1375
|
-
const brandFile = fill(layers.brand, { brand:
|
|
1385
|
+
const brandFile = fill(layers.brand, { brand: slug2 });
|
|
1376
1386
|
const nextFiles = new Map(files);
|
|
1377
1387
|
const created = [];
|
|
1378
1388
|
if (prims.size) {
|
|
@@ -1382,7 +1392,7 @@ function planNewBrand(source, input) {
|
|
|
1382
1392
|
for (const st of [...steps.keys()].sort((a2, b) => a2 - b)) ramp[String(st)] = { $value: steps.get(st) };
|
|
1383
1393
|
color[r] = ramp;
|
|
1384
1394
|
}
|
|
1385
|
-
const prim = { $description: `${name} brand ramps, generated by Bridge (@bridge-ds/core ramp engine). Never used by components directly: brands/${
|
|
1395
|
+
const prim = { $description: `${name} brand ramps, generated by Bridge (@bridge-ds/core ramp engine). Never used by components directly: brands/${slug2}.tokens.json maps them to palette roles.`, color };
|
|
1386
1396
|
nextFiles.set(primFile, `${JSON.stringify(prim, null, 2)}
|
|
1387
1397
|
`);
|
|
1388
1398
|
created.push(primFile);
|
|
@@ -1397,7 +1407,7 @@ function planNewBrand(source, input) {
|
|
|
1397
1407
|
if (!text2) continue;
|
|
1398
1408
|
const o = JSON.parse(text2);
|
|
1399
1409
|
const out = { ...o, $description: `${name} overrides on top of semantic/${v}, copied from ${brandDisplayName(from)}.` };
|
|
1400
|
-
const dst = fill(layers.override, { brand:
|
|
1410
|
+
const dst = fill(layers.override, { brand: slug2, [layers.overrideAxis]: v });
|
|
1401
1411
|
nextFiles.set(dst, `${JSON.stringify(out, null, 2)}
|
|
1402
1412
|
`);
|
|
1403
1413
|
created.push(dst);
|
|
@@ -1408,7 +1418,7 @@ function planNewBrand(source, input) {
|
|
|
1408
1418
|
let configText = baseConfigText;
|
|
1409
1419
|
let nextConfig = config;
|
|
1410
1420
|
try {
|
|
1411
|
-
configText = addBrandToConfigText(baseConfigText,
|
|
1421
|
+
configText = addBrandToConfigText(baseConfigText, slug2);
|
|
1412
1422
|
nextConfig = issues.some((i) => i.code.startsWith("slug-")) ? config : loadConfig(configText);
|
|
1413
1423
|
} catch (e) {
|
|
1414
1424
|
issues.push({ level: "error", code: "config", message: e.message });
|
|
@@ -1416,21 +1426,21 @@ function planNewBrand(source, input) {
|
|
|
1416
1426
|
const after = new LayeredTokenSet(nextConfig, nextFiles);
|
|
1417
1427
|
const key2 = (i) => `${i.code}|${i.path ?? ""}|${i.message}`;
|
|
1418
1428
|
const known = new Set(validateLayered(before).map(key2));
|
|
1419
|
-
const brandKey = `brand=${
|
|
1429
|
+
const brandKey = `brand=${slug2}`;
|
|
1420
1430
|
for (const i of validateLayered(after)) {
|
|
1421
1431
|
if (known.has(key2(i))) continue;
|
|
1422
1432
|
if (i.modes && !i.modes.some((m) => m.split(",").includes(brandKey)) && !created.includes(i.file ?? "")) continue;
|
|
1423
1433
|
issues.push({ level: i.level, code: i.code, message: i.message, ...i.path ? { path: i.path } : {}, ...i.file ? { file: tokensPath(config, i.file) } : {} });
|
|
1424
1434
|
}
|
|
1425
1435
|
const pairs = parseContrastChecks(source.contrastText);
|
|
1426
|
-
const inGate = nextConfig.axes.brand?.values.includes(
|
|
1427
|
-
const combos = inGate ? after.combos(nextConfig.contrast?.modes).filter((c) => c.brand ===
|
|
1436
|
+
const inGate = nextConfig.axes.brand?.values.includes(slug2);
|
|
1437
|
+
const combos = inGate ? after.combos(nextConfig.contrast?.modes).filter((c) => c.brand === slug2) : [];
|
|
1428
1438
|
const results = combos.flatMap((c) => pairs.map((p2) => checkLayeredPair(after, p2, c)));
|
|
1429
1439
|
const failing = results.filter((r) => !r.pass);
|
|
1430
1440
|
const themes = [...new Set(combos.map((c) => c.theme).filter((t2) => !!t2))];
|
|
1431
1441
|
const ramps = {};
|
|
1432
1442
|
if (inGate) {
|
|
1433
|
-
const combo = after.normalizeCombo({ brand:
|
|
1443
|
+
const combo = after.normalizeCombo({ brand: slug2 });
|
|
1434
1444
|
const read = (role) => STEPS.map((s) => ({ step: s, hex: after.resolve(`palette.${role}.${s}`, combo).css })).filter((x) => /^#[0-9a-f]{6}$/i.test(x.hex));
|
|
1435
1445
|
ramps.primary = read("primary");
|
|
1436
1446
|
ramps.neutral = read("neutral");
|
|
@@ -1447,19 +1457,19 @@ function planNewBrand(source, input) {
|
|
|
1447
1457
|
];
|
|
1448
1458
|
const cs3 = config.changesets ?? (config.changeset ? { dir: ".changeset", package: config.changeset.package } : void 0);
|
|
1449
1459
|
const changeset = cs3 ? {
|
|
1450
|
-
path: `${cs3.dir}/bridge-brand-${
|
|
1451
|
-
content: formatChangeset(cs3.package, "minor", `Add the ${name} brand (\`${
|
|
1452
|
-
- \`${CONFIG_FILE}\`: modes.brand gains "${
|
|
1460
|
+
path: `${cs3.dir}/bridge-brand-${slug2}.md`,
|
|
1461
|
+
content: formatChangeset(cs3.package, "minor", `Add the ${name} brand (\`${slug2}\`), started from ${blank ? "a blank brand" : brandDisplayName(from)}.`, `- ${created.map((f) => `\`${tokensPath(config, f)}\``).join("\n- ")}
|
|
1462
|
+
- \`${CONFIG_FILE}\`: modes.brand gains "${slug2}", so the token build and the contrast gate cover it.`)
|
|
1453
1463
|
} : void 0;
|
|
1454
1464
|
if (changeset) planned.push({ path: changeset.path, action: "create", content: changeset.content });
|
|
1455
1465
|
const errors = issues.filter((i) => i.level === "error");
|
|
1456
1466
|
return {
|
|
1457
1467
|
ok: errors.length === 0 && failing.length === 0 && results.length > 0,
|
|
1458
|
-
slug,
|
|
1468
|
+
slug: slug2,
|
|
1459
1469
|
name,
|
|
1460
1470
|
from,
|
|
1461
1471
|
title: `Add the ${name} brand`,
|
|
1462
|
-
branch: `${config.governance.branchPrefix}brand-${
|
|
1472
|
+
branch: `${config.governance.branchPrefix}brand-${slug2 || "new"}`,
|
|
1463
1473
|
files: planned,
|
|
1464
1474
|
tokenFiles: Object.fromEntries(nextFiles),
|
|
1465
1475
|
configText,
|
|
@@ -2112,7 +2122,7 @@ class DraftPoller {
|
|
|
2112
2122
|
function productPreview(apps, ctx, sources) {
|
|
2113
2123
|
if (!apps.admin || ctx.fromFork) return null;
|
|
2114
2124
|
const admin = resolveBranchPreviews(apps, { branch: ctx.branch, pr: ctx.pr, sha: ctx.sha }, sources).find((p2) => p2.app === "admin");
|
|
2115
|
-
if (!admin?.url || admin.source === "template" || admin.state === "failed") return null;
|
|
2125
|
+
if (!admin?.url || admin.source === "template" || admin.state === "failed" || admin.skipped) return null;
|
|
2116
2126
|
return admin.url;
|
|
2117
2127
|
}
|
|
2118
2128
|
function tryInProductUrl(preview, pr, screen) {
|
|
@@ -2135,18 +2145,20 @@ function localTryCommand(branch) {
|
|
|
2135
2145
|
const name = /^[\w./-]+$/.test(branch) ? branch : `'${branch.replace(/'/g, `'\\''`)}'`;
|
|
2136
2146
|
return `git fetch origin && git switch ${name}`;
|
|
2137
2147
|
}
|
|
2148
|
+
const explorationStartMessage = (e) => [`chore(explore): start ${e.name.trim()}`, ...e.purpose?.trim() ? [e.purpose.trim()] : [], "Exploration: never merged as is. Promote it to review or close it."].join("\n\n");
|
|
2138
2149
|
function explorationSummary(p2, _owner, _repo, s) {
|
|
2139
2150
|
const e = parseExplorationBody(p2.body);
|
|
2140
|
-
const
|
|
2151
|
+
const slug2 = p2.headRef?.startsWith(EXPLORATION_PREFIX) ? p2.headRef.slice(EXPLORATION_PREFIX.length) : e?.slug ?? explorationSlug(p2.title);
|
|
2141
2152
|
return {
|
|
2142
2153
|
number: p2.number,
|
|
2143
2154
|
url: p2.url,
|
|
2144
2155
|
branch: p2.headRef,
|
|
2145
|
-
slug,
|
|
2156
|
+
slug: slug2,
|
|
2146
2157
|
name: e?.name ?? p2.title.replace(/^[A-Z][A-Z0-9_]*-\d+\s+/, "").replace(/^Exploration:\s*/i, ""),
|
|
2147
2158
|
purpose: e?.purpose ?? "",
|
|
2148
2159
|
...e?.useCase ? { useCase: e.useCase } : {},
|
|
2149
|
-
status: explorationStatus({ state: p2.state, labels: p2.labels, headRef: p2.headRef, body: p2.body }),
|
|
2160
|
+
status: explorationStatus({ state: p2.state, labels: p2.labels, headRef: p2.headRef, body: p2.body, headSha: p2.headSha, draft: p2.draft }),
|
|
2161
|
+
archived: isArchivedExploration(p2.labels),
|
|
2150
2162
|
draft: p2.draft,
|
|
2151
2163
|
author: e?.author ?? p2.author,
|
|
2152
2164
|
...e?.source ? { source: e.source } : {},
|
|
@@ -2158,35 +2170,52 @@ function explorationSummary(p2, _owner, _repo, s) {
|
|
|
2158
2170
|
}
|
|
2159
2171
|
async function listExplorationsOnGitHub(gh, s, opts = {}) {
|
|
2160
2172
|
const pulls = await gh.listPullRequests(opts.state ?? "open");
|
|
2161
|
-
|
|
2173
|
+
const archived = opts.archived ?? "exclude";
|
|
2174
|
+
return pulls.filter((p2) => isExplorationPull({ state: p2.state, labels: p2.labels, headRef: p2.headRef, body: p2.body })).filter((p2) => archived === "include" || archived === "only" === isArchivedExploration(p2.labels)).map((p2) => explorationSummary(p2, gh.owner, gh.name, s));
|
|
2175
|
+
}
|
|
2176
|
+
const isHiddenRepoError = (err) => err instanceof GitHubError && (err.status === 404 || err.status === 409 || err.status === 403 && err.auth !== "rejected" && !err.resetsAt);
|
|
2177
|
+
async function listProductExplorationsOnGitHub(clientFor, products, designSystemRepo, opts = {}) {
|
|
2178
|
+
const unreachable = [];
|
|
2179
|
+
const lists = await Promise.all(
|
|
2180
|
+
productRepos(products, designSystemRepo).map(async ({ product, repo }) => {
|
|
2181
|
+
try {
|
|
2182
|
+
const list = await listExplorationsOnGitHub(clientFor(repo), { packages: [], apps: {} }, opts);
|
|
2183
|
+
return list.map((x) => inProductRepo(x, product, repo));
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
unreachable.push({ product: product.key, name: product.name, repo, reason: isHiddenRepoError(err) ? "not-connected" : "unavailable" });
|
|
2186
|
+
return [];
|
|
2187
|
+
}
|
|
2188
|
+
})
|
|
2189
|
+
);
|
|
2190
|
+
return { explorations: lists.flat(), unreachable };
|
|
2191
|
+
}
|
|
2192
|
+
function inProductRepo(x, product, repo) {
|
|
2193
|
+
const url = previewUrl(product.preview, { branch: x.branch, pr: x.number, sha: x.headSha });
|
|
2194
|
+
return { ...x, repo, product: product.key, previews: url ? [{ app: product.key, label: product.name, url }] : [] };
|
|
2162
2195
|
}
|
|
2163
2196
|
async function startExplorationOnGitHub(gh, e, s) {
|
|
2164
2197
|
const problems = explorationProblems(e);
|
|
2165
2198
|
if (problems.length) throw new GitHubError(422, problems.join(" "));
|
|
2166
2199
|
if (!gh.canWrite) throw new GitHubError(401, "Starting an exploration needs a GitHub token (contents and pull requests, read and write, on this repository only).");
|
|
2167
|
-
const
|
|
2168
|
-
const branch = `${EXPLORATION_PREFIX}${
|
|
2200
|
+
const slug2 = e.slug || explorationSlug(e.name);
|
|
2201
|
+
const branch = `${EXPLORATION_PREFIX}${slug2}`;
|
|
2169
2202
|
const x = gh.withBranchPrefix(EXPLORATION_PREFIX);
|
|
2170
|
-
if (await x.getBranchSha(branch)) throw new GitHubError(422, `An exploration named "${
|
|
2171
|
-
const full = { ...e, slug, startedAt: e.startedAt ?? (/* @__PURE__ */ new Date()).toISOString() };
|
|
2203
|
+
if (await x.getBranchSha(branch)) throw new GitHubError(422, `An exploration named "${slug2}" already exists (${branch}). Pick another name, or continue that one.`);
|
|
2204
|
+
const full = { ...e, slug: slug2, startedAt: e.startedAt ?? (/* @__PURE__ */ new Date()).toISOString() };
|
|
2172
2205
|
const commit = await x.commitToNewBranch({
|
|
2173
2206
|
base: s.base,
|
|
2174
2207
|
branch,
|
|
2175
|
-
message:
|
|
2176
|
-
|
|
2177
|
-
${full.purpose.trim()}
|
|
2178
|
-
|
|
2179
|
-
Exploration: never merged as is. Promote it to review or close it.`,
|
|
2180
|
-
files: [{ path: explorationNotePath(slug), content: explorationNote(full) }]
|
|
2208
|
+
message: explorationStartMessage(full),
|
|
2209
|
+
files: [{ path: explorationNotePath(slug2), content: explorationNote(full) }]
|
|
2181
2210
|
});
|
|
2182
|
-
const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps, ...s.requests?.length ? { requests: s.requests } : {} };
|
|
2211
|
+
const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps, ...s.requests?.length ? { requests: s.requests } : {}, ...s.inProduct ? { inProduct: s.inProduct } : {} };
|
|
2183
2212
|
const title = full.jira ? `${full.jira.trim().toUpperCase()} ${explorationPrTitle(full)}` : explorationPrTitle(full);
|
|
2184
2213
|
const pr = await x.createPullRequest({ title, head: branch, base: s.base, body: explorationPrBody(full, ctx), draft: true });
|
|
2185
2214
|
await x.addLabels(pr.number, [EXPLORATION_LABEL, ...s.labels ?? []]);
|
|
2186
2215
|
await x.updatePullRequest(pr.number, { body: explorationPrBody(full, { ...ctx, pr: pr.number }) }).catch(() => void 0);
|
|
2187
2216
|
return {
|
|
2188
2217
|
branch,
|
|
2189
|
-
slug,
|
|
2218
|
+
slug: slug2,
|
|
2190
2219
|
pr: pr.number,
|
|
2191
2220
|
url: pr.url,
|
|
2192
2221
|
commit: commit.sha,
|
|
@@ -2194,6 +2223,16 @@ Exploration: never merged as is. Promote it to review or close it.`,
|
|
|
2194
2223
|
previews: previewLinks(s.apps, { branch, pr: pr.number, sha: commit.sha })
|
|
2195
2224
|
};
|
|
2196
2225
|
}
|
|
2226
|
+
async function productRepoAccess(gh) {
|
|
2227
|
+
try {
|
|
2228
|
+
const r = await gh.getRepo();
|
|
2229
|
+
if (r.permissions && r.permissions.push === false) return { ok: false, reason: "read-only" };
|
|
2230
|
+
return { ok: true, base: r.defaultBranch };
|
|
2231
|
+
} catch (err) {
|
|
2232
|
+
if (err instanceof GitHubError && (err.status === 404 || err.status === 403 && err.auth !== "rejected" && !err.resetsAt)) return { ok: false, reason: "not-connected" };
|
|
2233
|
+
throw err;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2197
2236
|
async function explorationTryItOnGitHub(gh, e) {
|
|
2198
2237
|
const [comments, head] = await Promise.all([gh.listIssueComments(e.number), gh.getBranchSha(e.branch).catch(() => void 0)]);
|
|
2199
2238
|
return explorationTryIt({ comment: findPreviewComment(comments)?.body, headSha: head });
|
|
@@ -2241,12 +2280,470 @@ async function promoteExplorationOnGitHub(gh, ref, opts = {}) {
|
|
|
2241
2280
|
});
|
|
2242
2281
|
return out;
|
|
2243
2282
|
}
|
|
2283
|
+
async function archiveExplorationOnGitHub(gh, ref, opts = {}) {
|
|
2284
|
+
if (!gh.canWrite) throw new GitHubError(401, "Archiving an exploration needs a GitHub token (pull requests or issues: write).");
|
|
2285
|
+
const p2 = await findExplorationOnGitHub(gh, ref);
|
|
2286
|
+
if (p2.state !== "open") throw new GitHubError(422, `#${p2.number} is ${p2.state === "merged" ? "shipped" : "closed"}: only an open exploration is archived. Its link already stays on GitHub.`);
|
|
2287
|
+
const archived = opts.archived ?? true;
|
|
2288
|
+
if (archived) await gh.addLabels(p2.number, [EXPLORATION_ARCHIVED_LABEL]);
|
|
2289
|
+
else
|
|
2290
|
+
await gh.removeLabel(p2.number, EXPLORATION_ARCHIVED_LABEL).catch((e) => {
|
|
2291
|
+
if (!(e instanceof GitHubError && e.status === 404)) throw e;
|
|
2292
|
+
});
|
|
2293
|
+
return { number: p2.number, url: p2.url, branch: p2.headRef, archived };
|
|
2294
|
+
}
|
|
2244
2295
|
async function closeExplorationOnGitHub(gh, ref) {
|
|
2245
2296
|
if (!gh.canWrite) throw new GitHubError(401, "Closing an exploration needs a GitHub token (pull requests: write).");
|
|
2246
2297
|
const p2 = await findExplorationOnGitHub(gh, ref);
|
|
2247
2298
|
await gh.updatePullRequest(p2.number, { state: "closed" });
|
|
2248
2299
|
return { number: p2.number, url: p2.url, branch: p2.headRef };
|
|
2249
2300
|
}
|
|
2301
|
+
const PRODUCT_FILE = /\.(css|scss|sass|less|tsx|jsx|ts|js|mjs|cjs|mts|cts|astro|vue|svelte)$/;
|
|
2302
|
+
const SKIP_FILE = /\.d\.[cm]?ts$|\.(test|spec|stories|story)\.[cm]?[jt]sx?$|\.min\.(css|js)$/;
|
|
2303
|
+
const PIECES_FILE_LIMIT = 40;
|
|
2304
|
+
const cleanPath = (p2) => p2.trim().replace(/^\.?\/+|\/+$/g, "");
|
|
2305
|
+
function ownProduct(products, repo) {
|
|
2306
|
+
const p2 = repo ? productOfRepo(products, repo) : void 0;
|
|
2307
|
+
return p2 && p2.key !== defaultProduct(products).key ? p2 : void 0;
|
|
2308
|
+
}
|
|
2309
|
+
const repoOfProductBranch = (gh, lib) => lib && lib.fullName.toLowerCase() !== gh.fullName.toLowerCase() ? gh.fullName : void 0;
|
|
2310
|
+
function productOf(products, pull, files, repo) {
|
|
2311
|
+
const own = ownProduct(products, repo);
|
|
2312
|
+
if (own) return own;
|
|
2313
|
+
const product = productOfBranch(products, { labels: pull.labels, files });
|
|
2314
|
+
if (product.key === defaultProduct(products).key) throw new GitHubError(422, `This branch is a library branch (${product.name}), not a product branch: missing pieces are checked on a branch started for a product.`);
|
|
2315
|
+
return product;
|
|
2316
|
+
}
|
|
2317
|
+
async function readProductBranchOnGitHub(gh, pr, opts) {
|
|
2318
|
+
const pull = await gh.getPullRequest(pr);
|
|
2319
|
+
const cmp = await gh.compareDetailed(pull.baseRef, pull.headSha);
|
|
2320
|
+
const product = productOf(opts.products, pull, cmp.files.map((f) => f.filename), gh.fullName);
|
|
2321
|
+
const dir = product.path && !ownProduct(opts.products, gh.fullName) ? `${cleanPath(product.path)}/` : "";
|
|
2322
|
+
const changed = cmp.files.filter((f) => f.status !== "removed" && (!dir || f.filename.startsWith(dir)) && PRODUCT_FILE.test(f.filename) && !SKIP_FILE.test(f.filename) && !f.filename.split("/").includes("node_modules"));
|
|
2323
|
+
const read = changed.slice(0, PIECES_FILE_LIMIT);
|
|
2324
|
+
const start = cmp.mergeBase ?? pull.baseRef;
|
|
2325
|
+
const files = await Promise.all(
|
|
2326
|
+
read.map(async (f) => {
|
|
2327
|
+
const [before, after] = await Promise.all([f.status === "added" ? Promise.resolve(void 0) : gh.getFileText(f.previous_filename ?? f.filename, start), gh.getFileText(f.filename, pull.headSha)]);
|
|
2328
|
+
return { path: f.filename, before: before ?? null, after: after ?? null };
|
|
2329
|
+
})
|
|
2330
|
+
);
|
|
2331
|
+
return { pull, product, files, skipped: changed.length - read.length, record: parsePiecesRecord(pull.body) };
|
|
2332
|
+
}
|
|
2333
|
+
async function recordPiecesOnGitHub(gh, pr, check) {
|
|
2334
|
+
const pull = await gh.getPullRequest(pr);
|
|
2335
|
+
const record = mergePiecesRecord(parsePiecesRecord(pull.body), { checkedAt: check.checkedAt ?? (/* @__PURE__ */ new Date()).toISOString(), headSha: check.headSha ?? pull.headSha, pieces: check.pieces, fixes: check.fixes });
|
|
2336
|
+
await gh.updatePullRequest(pr, { body: withPiecesRecord(pull.body, record) });
|
|
2337
|
+
return record;
|
|
2338
|
+
}
|
|
2339
|
+
const productWriter = (gh, branch) => isExplorationBranch(branch) ? gh.withBranchPrefix(EXPLORATION_PREFIX) : gh;
|
|
2340
|
+
async function useLibraryPreviewOnGitHub(gh, input) {
|
|
2341
|
+
const message = `chore(bridge): use the preview of ${input.libraryBranch} (#${input.libraryNumber})
|
|
2342
|
+
|
|
2343
|
+
The product branch takes the library branch before it ships. Switch to the published version once it does.`;
|
|
2344
|
+
try {
|
|
2345
|
+
const r = await productWriter(gh, input.productBranch).mergeIntoBranch(input.productBranch, input.libraryBranch, message);
|
|
2346
|
+
return { ...r, said: r.status === "merged" ? "Your branch now uses the preview of the library branch." : "Your branch already has the latest preview." };
|
|
2347
|
+
} catch (err) {
|
|
2348
|
+
if (err instanceof GitHubError && err.status === 409) return { status: "conflict", sha: null, said: `Your branch and the library branch change the same lines. A developer merges ${input.libraryBranch} into ${input.productBranch} by hand.` };
|
|
2349
|
+
throw err;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
function productBranchRef(pull, product, repo) {
|
|
2353
|
+
const name = /<!-- bridge-exploration (\{[\s\S]*?\}) -->/.exec(pull.body ?? "");
|
|
2354
|
+
let title = pull.title.replace(/^[A-Z][A-Z0-9_]*-\d+\s+/, "").replace(/^Exploration:\s*/i, "");
|
|
2355
|
+
let author = pull.author;
|
|
2356
|
+
if (name) {
|
|
2357
|
+
try {
|
|
2358
|
+
const e = JSON.parse(name[1]);
|
|
2359
|
+
if (e.name) title = e.name;
|
|
2360
|
+
if (e.author) author = e.author;
|
|
2361
|
+
} catch {
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
return { pr: pull.number, title: title.charAt(0).toUpperCase() + title.slice(1), branch: pull.headRef, url: pull.url, productKey: product.key, productName: product.name, ...author ? { author } : {}, ...repo ? { repo } : {} };
|
|
2365
|
+
}
|
|
2366
|
+
async function openLibraryBranchOnGitHub(gh, input) {
|
|
2367
|
+
const pull = await gh.getPullRequest(input.pr);
|
|
2368
|
+
const record = parsePiecesRecord(pull.body);
|
|
2369
|
+
const piece = record?.pieces.find((p2) => p2.id === input.piece);
|
|
2370
|
+
if (!record || !piece) throw new GitHubError(404, record ? `No piece "${input.piece}" on #${input.pr}. Pieces: ${record.pieces.map((p2) => p2.id).join(", ") || "none"}.` : `#${input.pr} was not checked yet: find its missing pieces first (bridge_missing_pieces with record, or \`bridge pieces ${input.pr} --record\`).`);
|
|
2371
|
+
const product = productOf(input.products, pull, await gh.listPullRequestFiles(input.pr).then((f) => f.map((x) => x.filename), () => []), gh.fullName);
|
|
2372
|
+
const mode = input.mode ?? "direct";
|
|
2373
|
+
const lib = input.library ?? gh;
|
|
2374
|
+
const repo = repoOfProductBranch(gh, input.library);
|
|
2375
|
+
const plan = libraryBranchPlan(piece, productBranchRef(pull, product, repo), { source: input.source, requestedBy: input.openedBy, mode });
|
|
2376
|
+
const existing = record.links.find((l) => l.piece === piece.id);
|
|
2377
|
+
if (existing) return { status: "already-linked", plan, link: existing, preview: null, warnings: [] };
|
|
2378
|
+
if (input.dryRun) return { status: "dry-run", plan, link: null, preview: null, warnings: [] };
|
|
2379
|
+
if (!gh.canWrite || !lib.canWrite) throw new GitHubError(401, mode === "suggest" ? "Suggesting a library change needs a GitHub token (issues: write)." : "Opening a library branch needs a GitHub token (contents, pull requests and issues: write).");
|
|
2380
|
+
const warnings = [];
|
|
2381
|
+
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2382
|
+
const who = { ...input.openedBy ? { openedBy: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {}, ...repo ? { repo: lib.fullName } : {} };
|
|
2383
|
+
let link;
|
|
2384
|
+
if (plan.kind === "token-change") {
|
|
2385
|
+
if (!input.writeTokens) throw new GitHubError(422, "This piece is a token: open it where the token files are read (Bridge Studio, bridge_open_library_branch or `bridge pieces open`).");
|
|
2386
|
+
const r = await input.writeTokens(plan, { mode });
|
|
2387
|
+
link = r.status === "suggested" ? { piece: piece.id, kind: "suggestion", number: r.number, url: r.url, title: plan.title, at: at2, ...who } : { piece: piece.id, kind: "token-change", number: r.number, url: r.url, title: plan.title, branch: r.branch, at: at2, ...who };
|
|
2388
|
+
} else if (plan.kind === "exploration") {
|
|
2389
|
+
const r = await startExplorationOnGitHub(lib, { ...plan.exploration, ...input.openedBy ? { author: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {} }, { base: input.base, ...input.exploration, labels: [NEEDED_BY_LABEL] });
|
|
2390
|
+
link = { piece: piece.id, kind: "exploration", number: r.pr, url: r.url, title: plan.title, branch: r.branch, at: at2, ...who };
|
|
2391
|
+
} else {
|
|
2392
|
+
const issue = composeRequestIssue(plan.request, { labels: [NEEDED_BY_LABEL] });
|
|
2393
|
+
const r = await lib.createIssue(issue);
|
|
2394
|
+
link = { piece: piece.id, kind: "request", number: r.number, url: r.url, title: plan.title, at: at2, ...who };
|
|
2395
|
+
}
|
|
2396
|
+
try {
|
|
2397
|
+
if (link.kind === "request" || link.kind === "suggestion") {
|
|
2398
|
+
const issue = await lib.getIssue(link.number);
|
|
2399
|
+
await lib.updateIssue(link.number, { body: withNeededBy(issue.body, plan.neededBy) });
|
|
2400
|
+
if (link.kind === "suggestion" && !issue.labels.includes(NEEDED_BY_LABEL)) await lib.addLabels(link.number, [NEEDED_BY_LABEL]);
|
|
2401
|
+
} else {
|
|
2402
|
+
const libPull = await lib.getPullRequest(link.number);
|
|
2403
|
+
await lib.updatePullRequest(link.number, { body: withNeededBy(libPull.body, plan.neededBy) });
|
|
2404
|
+
await lib.addLabels(link.number, [NEEDED_BY_LABEL]);
|
|
2405
|
+
}
|
|
2406
|
+
} catch (err) {
|
|
2407
|
+
if (err instanceof GitHubError && err.auth === "rejected") throw err;
|
|
2408
|
+
warnings.push(`The library side does not say who needs it yet (${err.message}). Add "Needed by ${repo ?? ""}#${input.pr}" to #${link.number} on GitHub.`);
|
|
2409
|
+
}
|
|
2410
|
+
try {
|
|
2411
|
+
const fresh = await gh.getPullRequest(input.pr);
|
|
2412
|
+
const next = withLink(parsePiecesRecord(fresh.body) ?? record, link);
|
|
2413
|
+
await gh.updatePullRequest(input.pr, { body: withPiecesRecord(fresh.body, next) });
|
|
2414
|
+
} catch (err) {
|
|
2415
|
+
if (mode !== "suggest") throw err;
|
|
2416
|
+
warnings.push(`#${input.pr} does not list #${link.number} yet (${err.message}). Add "Library: ${repo ? lib.fullName : ""}#${link.number}" to #${input.pr} on GitHub, so this piece is not sent twice.`);
|
|
2417
|
+
}
|
|
2418
|
+
let preview = null;
|
|
2419
|
+
if (!repo && (input.usePreview ?? true) && link.kind === "token-change" && link.branch) {
|
|
2420
|
+
preview = await useLibraryPreviewOnGitHub(gh, { productBranch: pull.headRef, libraryBranch: link.branch, libraryNumber: link.number }).catch((err) => {
|
|
2421
|
+
warnings.push(`Your branch does not use the preview yet: ${err.message}`);
|
|
2422
|
+
return null;
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
return { status: link.kind === "suggestion" ? "suggested" : "opened", plan, link, preview, warnings };
|
|
2426
|
+
}
|
|
2427
|
+
function acrossRepos(p2, lifecycle, across) {
|
|
2428
|
+
if (!across || !p2.action) return p2;
|
|
2429
|
+
if (lifecycle === "merged") return { ...p2, sentence: "The library published it. Update @bridge-ds/react in your product to use it.", action: null };
|
|
2430
|
+
return { ...p2, action: null };
|
|
2431
|
+
}
|
|
2432
|
+
const isNote = (path) => path.startsWith(`${EXPLORATION_NOTES_DIR}/`);
|
|
2433
|
+
async function linkedLibraryBranchOnGitHub(gh, link, product, piece, opts = {}) {
|
|
2434
|
+
const nearest = piece?.nearest?.label;
|
|
2435
|
+
const behind = () => opts.across ? Promise.resolve(null) : gh.compare(product.baseRef, product.headSha).then((c) => c.behindBy, () => null);
|
|
2436
|
+
if (link.kind === "suggestion") {
|
|
2437
|
+
const issue = await gh.getIssue(link.number);
|
|
2438
|
+
const cached = suggestionStateFromLabels(issue.labels);
|
|
2439
|
+
const s = suggestionStatus({ state: issue.state, stateReason: issue.stateReason, applied: cached === "applying", review: cached === "applying" ? void 0 : cached });
|
|
2440
|
+
const status2 = linkStatus({ kind: "suggestion", status: s });
|
|
2441
|
+
const lifecycle2 = s === "applied" ? "merged" : issue.state === "closed" ? "closed" : "open";
|
|
2442
|
+
const behindMain2 = lifecycle2 === "merged" ? await behind() : null;
|
|
2443
|
+
const preview2 = libraryPreviewState({ kind: "suggestion", lifecycle: lifecycle2, hasChanges: false, missing: null, total: null, behindMain: behindMain2, nearest });
|
|
2444
|
+
return { link, piece, status: status2, preview: acrossRepos(preview2, lifecycle2, opts.across), people: status2.people };
|
|
2445
|
+
}
|
|
2446
|
+
if (link.kind === "request") {
|
|
2447
|
+
const issue = await gh.getIssue(link.number);
|
|
2448
|
+
const status2 = linkStatus({ kind: "request", state: issue.state, stateReason: issue.stateReason });
|
|
2449
|
+
const lifecycle2 = issue.state === "closed" ? issue.stateReason === "not_planned" ? "closed" : "merged" : "open";
|
|
2450
|
+
const behindMain2 = lifecycle2 === "merged" ? await behind() : null;
|
|
2451
|
+
const preview2 = libraryPreviewState({ kind: "request", lifecycle: lifecycle2, hasChanges: false, missing: null, total: null, behindMain: behindMain2, nearest });
|
|
2452
|
+
return { link, piece, status: status2, preview: acrossRepos(preview2, lifecycle2, opts.across), people: status2.people };
|
|
2453
|
+
}
|
|
2454
|
+
const lib = await gh.getPullRequest(link.number);
|
|
2455
|
+
const reviews = await gh.listReviews(link.number).catch(() => []);
|
|
2456
|
+
const status = linkStatus({ kind: link.kind, state: lib.state, draft: lib.draft, labels: lib.labels, requestedReviewers: lib.requestedReviewers, reviews: reviews.map((r) => ({ user: r.user, state: r.state })) });
|
|
2457
|
+
const lifecycle = lib.state === "merged" ? "merged" : lib.state === "open" ? "open" : "closed";
|
|
2458
|
+
let missing = null;
|
|
2459
|
+
let total = null;
|
|
2460
|
+
let behindMain = null;
|
|
2461
|
+
let hasChanges = true;
|
|
2462
|
+
if (lifecycle === "open") {
|
|
2463
|
+
const [files, own, inProduct] = await Promise.all([
|
|
2464
|
+
gh.listPullRequestFiles(link.number).catch(() => null),
|
|
2465
|
+
gh.compare(lib.baseRef, lib.headSha).catch(() => null),
|
|
2466
|
+
opts.across ? Promise.resolve(null) : gh.compare(product.headSha, lib.headSha).catch(() => null)
|
|
2467
|
+
]);
|
|
2468
|
+
hasChanges = files ? files.some((f) => !isNote(f.filename)) : true;
|
|
2469
|
+
total = own?.aheadBy ?? null;
|
|
2470
|
+
missing = inProduct?.aheadBy ?? null;
|
|
2471
|
+
} else if (lifecycle === "merged") {
|
|
2472
|
+
behindMain = await behind();
|
|
2473
|
+
}
|
|
2474
|
+
const preview = libraryPreviewState({ kind: link.kind, lifecycle, hasChanges, missing, total, behindMain, nearest });
|
|
2475
|
+
return { link, piece, status, preview: acrossRepos(preview, lifecycle, opts.across), people: status.people };
|
|
2476
|
+
}
|
|
2477
|
+
async function productBranchViewOnGitHub(gh, pr, opts) {
|
|
2478
|
+
const pull = await gh.getPullRequest(pr);
|
|
2479
|
+
const files = await gh.listPullRequestFiles(pr).then((f) => f.map((x) => x.filename), () => []);
|
|
2480
|
+
const product = productOf(opts.products, pull, files, gh.fullName);
|
|
2481
|
+
const record = parsePiecesRecord(pull.body);
|
|
2482
|
+
const across = Boolean(repoOfProductBranch(gh, opts.library));
|
|
2483
|
+
const lib = opts.library ?? gh;
|
|
2484
|
+
const linked = await Promise.all(
|
|
2485
|
+
(record?.links ?? []).map((l) => linkedLibraryBranchOnGitHub(lib, l, { headSha: pull.headSha, baseRef: pull.baseRef }, record?.pieces.find((p2) => p2.id === l.piece), { across }))
|
|
2486
|
+
);
|
|
2487
|
+
const open = (record?.pieces ?? []).filter((p2) => !record.links.some((l) => l.piece === p2.id));
|
|
2488
|
+
return { pull, product, record, sentence: productBranchSentence(record, { viewer: opts.viewer, agentName: opts.agentName }), linked, open };
|
|
2489
|
+
}
|
|
2490
|
+
async function switchToPublishedOnGitHub(gh, pr) {
|
|
2491
|
+
if (!gh.canWrite) throw new GitHubError(401, "Switching needs a GitHub token (contents and pull requests: write).");
|
|
2492
|
+
const pull = await gh.getPullRequest(pr);
|
|
2493
|
+
if (pull.state !== "open") throw new GitHubError(422, `#${pr} is ${pull.state}: only an open branch switches.`);
|
|
2494
|
+
const cmp = await gh.compare(pull.baseRef, pull.headSha);
|
|
2495
|
+
if (cmp.behindBy === 0) return { status: "up-to-date", said: "Your branch already uses the published version." };
|
|
2496
|
+
try {
|
|
2497
|
+
await gh.updatePullRequestBranch(pr, pull.headSha);
|
|
2498
|
+
} catch (err) {
|
|
2499
|
+
if (err instanceof GitHubError && err.status === 422) throw new GitHubError(422, `Your branch and ${pull.baseRef} change the same lines, so Bridge cannot switch it alone. Use "Update from main" to pick the token values, or ask a developer.`, err.detail);
|
|
2500
|
+
throw err;
|
|
2501
|
+
}
|
|
2502
|
+
return { status: "switched", said: `Your branch now uses the published version from ${pull.baseRef}.` };
|
|
2503
|
+
}
|
|
2504
|
+
async function proposePiecesOnGitHub(gh, pr, input) {
|
|
2505
|
+
const out = { proposed: [], already: [], unknown: [], warnings: [] };
|
|
2506
|
+
const ids = [...new Set(input.pieces.map((p2) => p2.trim()).filter(Boolean))];
|
|
2507
|
+
if (!ids.length) return out;
|
|
2508
|
+
const pull = await gh.getPullRequest(pr);
|
|
2509
|
+
const record = parsePiecesRecord(pull.body);
|
|
2510
|
+
if (!record) throw new GitHubError(404, `#${pr} was not checked against the library yet: find its missing pieces first (bridge_missing_pieces with record, or \`bridge pieces ${pr} --record\`).`);
|
|
2511
|
+
const files = await gh.listPullRequestFiles(pr).then((f) => f.map((x) => x.filename), () => []);
|
|
2512
|
+
const lib = input.library ?? gh;
|
|
2513
|
+
const repo = repoOfProductBranch(gh, input.library);
|
|
2514
|
+
const ref = productBranchRef(pull, ownProduct(input.products, repo) ?? productOfBranch(input.products, { labels: pull.labels, files }), repo);
|
|
2515
|
+
const todo = [];
|
|
2516
|
+
for (const id of ids) {
|
|
2517
|
+
const piece = record.pieces.find((p2) => p2.id === id);
|
|
2518
|
+
const link = record.links.find((l) => l.piece === id);
|
|
2519
|
+
if (!piece) out.unknown.push(id);
|
|
2520
|
+
else if (link) out.already.push({ piece: id, number: link.number });
|
|
2521
|
+
else todo.push({ piece, ask: pieceRequest(piece, ref, { previewUrl: input.previewUrl, source: input.source, requestedBy: input.openedBy }) });
|
|
2522
|
+
}
|
|
2523
|
+
if (input.dryRun) return { ...out, proposed: todo.map((t2) => ({ piece: t2.piece.id, title: t2.ask.request.title, number: 0, url: "" })) };
|
|
2524
|
+
if (!todo.length) return out;
|
|
2525
|
+
if (!lib.canWrite) throw new GitHubError(401, "Sending pieces to the library needs a GitHub token (issues: write).");
|
|
2526
|
+
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2527
|
+
const who = { ...input.openedBy ? { openedBy: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {}, ...repo ? { repo: lib.fullName } : {} };
|
|
2528
|
+
const links = [];
|
|
2529
|
+
for (const t2 of todo) {
|
|
2530
|
+
const issue = composeRequestIssue(t2.ask.request, { labels: [NEEDED_BY_LABEL] });
|
|
2531
|
+
const r = await lib.createIssue({ ...issue, body: withNeededBy(issue.body, t2.ask.neededBy) });
|
|
2532
|
+
links.push({ piece: t2.piece.id, kind: "request", number: r.number, url: r.url, title: t2.ask.request.title, at: at2, ...who });
|
|
2533
|
+
out.proposed.push({ piece: t2.piece.id, title: t2.ask.request.title, number: r.number, url: r.url });
|
|
2534
|
+
}
|
|
2535
|
+
try {
|
|
2536
|
+
const fresh = await gh.getPullRequest(pr);
|
|
2537
|
+
const next = links.reduce((rec, l) => withLink(rec, l), parsePiecesRecord(fresh.body) ?? record);
|
|
2538
|
+
await gh.updatePullRequest(pr, { body: withPiecesRecord(fresh.body, next) });
|
|
2539
|
+
} catch (err) {
|
|
2540
|
+
if (err instanceof GitHubError && err.auth === "rejected") throw err;
|
|
2541
|
+
out.warnings.push(`#${pr} does not list the requests yet (${err.message}). Add "Library: ${links.map((l) => `${repo ? lib.fullName : ""}#${l.number}`).join(", ")}" to #${pr} on GitHub, so they are not sent twice.`);
|
|
2542
|
+
}
|
|
2543
|
+
return out;
|
|
2544
|
+
}
|
|
2545
|
+
async function doneExploringOnGitHub(gh, ref, input = {}) {
|
|
2546
|
+
const p2 = await findExplorationOnGitHub(gh, ref);
|
|
2547
|
+
const status = explorationStatus({ state: p2.state, labels: p2.labels, headRef: p2.headRef, body: p2.body, headSha: p2.headSha, draft: p2.draft });
|
|
2548
|
+
if (status === "closed") throw new GitHubError(422, `Exploration #${p2.number} is closed. Reopen it on GitHub first: its preview comes back with it.`);
|
|
2549
|
+
if (status === "shipped") throw new GitHubError(422, `#${p2.number} is already shipped.`);
|
|
2550
|
+
if (status === "promoted") throw new GitHubError(422, `#${p2.number} was already sent to review.`);
|
|
2551
|
+
const fresh = status === "active";
|
|
2552
|
+
const files = fresh ? await gh.listPullRequestFiles(p2.number) : [];
|
|
2553
|
+
const plan = fresh ? doneExploringPlan({ body: p2.body, branch: p2.headRef, files: files.map((f) => ({ path: f.filename, status: f.status })), date: input.date }) : null;
|
|
2554
|
+
const take = plan?.take ?? ["git fetch origin", `git checkout ${p2.headRef}`];
|
|
2555
|
+
const checks = p2.headSha ? await Promise.resolve(p2.headSha).then((sha) => gh.getChecks(sha)).then((c) => c.state, () => null) : null;
|
|
2556
|
+
const out = { number: p2.number, url: p2.url, branch: p2.headRef, status: fresh ? "ready" : status, handedOff: fresh, plan, pieces: { proposed: [], already: [], unknown: [], warnings: [] }, take, checks, note: explorationStartingPoint(checks), warnings: [] };
|
|
2557
|
+
const products = input.products ?? [];
|
|
2558
|
+
const ask = { pieces: input.pieces ?? [], products, previewUrl: input.previewUrl, openedBy: input.openedBy, agent: input.agent, source: input.source, library: input.library };
|
|
2559
|
+
if (input.dryRun) {
|
|
2560
|
+
out.pieces = await proposePiecesOnGitHub(gh, p2.number, { ...ask, dryRun: true }).catch((e) => ({ proposed: [], already: [], unknown: [], warnings: [e.message] }));
|
|
2561
|
+
return out;
|
|
2562
|
+
}
|
|
2563
|
+
if (!gh.canWrite) throw new GitHubError(401, 'Saying "Done exploring" needs a GitHub token (contents, pull requests and issues: write).');
|
|
2564
|
+
out.pieces = await proposePiecesOnGitHub(gh, p2.number, ask);
|
|
2565
|
+
out.warnings.push(...out.pieces.warnings);
|
|
2566
|
+
if (!plan) return out;
|
|
2567
|
+
const x = gh.withBranchPrefix(EXPLORATION_PREFIX);
|
|
2568
|
+
let doneSha = p2.headSha;
|
|
2569
|
+
if (plan.removeFiles.length) {
|
|
2570
|
+
const c = await x.commitToBranch({ branch: p2.headRef, message: "chore(explore): done exploring, remove the exploration note", files: plan.removeFiles.map((path) => ({ path, content: null })) }).catch((e) => {
|
|
2571
|
+
if (e instanceof GitHubError && e.auth === "rejected") throw e;
|
|
2572
|
+
out.warnings.push(`The exploration note stays on the branch (${e.message}). Delete ${plan.removeFiles.join(", ")} before merging.`);
|
|
2573
|
+
return null;
|
|
2574
|
+
});
|
|
2575
|
+
if (c) doneSha = c.sha;
|
|
2576
|
+
}
|
|
2577
|
+
await x.addLabels(p2.number, plan.addLabels);
|
|
2578
|
+
await x.removeLabel(p2.number, plan.removeLabels[0]).catch((e) => {
|
|
2579
|
+
if (!(e instanceof GitHubError && e.status === 404)) out.warnings.push(`The exploration label is still on (${e.message}). Remove it on GitHub so developers can ship it later.`);
|
|
2580
|
+
});
|
|
2581
|
+
const latest = await gh.getPullRequest(p2.number).catch(() => p2);
|
|
2582
|
+
const doneAt = parseExplorationBody(plan.body)?.doneAt ?? (input.date ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2583
|
+
await x.updatePullRequest(p2.number, { body: withDoneExploring(latest.body, { branch: p2.headRef, doneAt, doneSha, checks }) });
|
|
2584
|
+
return out;
|
|
2585
|
+
}
|
|
2586
|
+
const CONTINUE_PHRASE = "Continue Bridge exploration";
|
|
2587
|
+
const onlyNotes = (files) => Boolean(files?.length) && files.every((f) => f.startsWith(".bridge/"));
|
|
2588
|
+
function explorationStage(x) {
|
|
2589
|
+
if (x.status !== "active" || x.archived) return "handed";
|
|
2590
|
+
if (x.files && (x.files.length === 0 || onlyNotes(x.files))) return "not-started";
|
|
2591
|
+
return "exploring";
|
|
2592
|
+
}
|
|
2593
|
+
const continueRef = (x) => explorationRefLabel(x);
|
|
2594
|
+
function continueLines(x) {
|
|
2595
|
+
const ref = continueRef(x);
|
|
2596
|
+
const name = x.name.replace(/"/g, "'").trim();
|
|
2597
|
+
return {
|
|
2598
|
+
app: `${CONTINUE_PHRASE} ${ref} "${name}" in ${x.productName} and use the Bridge library.`,
|
|
2599
|
+
terminal: `claude "${CONTINUE_PHRASE} ${ref}"`
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
function parseContinueLine(text) {
|
|
2603
|
+
const m = /continue\s+bridge\s+exploration\s+((?:[a-z0-9][\w.-]*)?#\d+)/i.exec(text);
|
|
2604
|
+
return m ? m[1].toLowerCase() : null;
|
|
2605
|
+
}
|
|
2606
|
+
function wordsOf(name) {
|
|
2607
|
+
const spaced = name.replace(/[-_]+/g, " ").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
|
|
2608
|
+
const words = spaced.split(/\s+/).filter(Boolean);
|
|
2609
|
+
return words.map((w, i) => i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : /^[A-Z]{2,}$/.test(w) ? w : w.toLowerCase()).join(" ");
|
|
2610
|
+
}
|
|
2611
|
+
const SKIP$1 = [/^\.bridge\//, /^\.changeset\//, /^\.github\//, /\.(test|spec|stories)\.[jt]sx?$/, /\.d\.ts$/, /(^|\/)package(-lock)?\.json$/, /\.md$/, /(^|\/)__snapshots__\//, /\.docs\.json$/];
|
|
2612
|
+
const CODE = /\.(tsx|jsx|ts|js|vue|svelte|astro|css|scss)$/;
|
|
2613
|
+
const SCREEN_DIRS = /(^|\/)(pages|routes|screens|views|app)\//;
|
|
2614
|
+
function thingOf(path) {
|
|
2615
|
+
if (SKIP$1.some((r) => r.test(path))) return null;
|
|
2616
|
+
if (/(^|\/)tokens\/src\//.test(path) || /\.tokens\.json$/.test(path)) return "Tokens";
|
|
2617
|
+
if (!CODE.test(path)) return null;
|
|
2618
|
+
const parts = path.split("/");
|
|
2619
|
+
const file = parts[parts.length - 1];
|
|
2620
|
+
const base = file.replace(/\.module\.(css|scss)$/, "").replace(/\.[^.]+$/, "");
|
|
2621
|
+
const ci3 = parts.lastIndexOf("components");
|
|
2622
|
+
if (ci3 >= 0 && ci3 < parts.length - 2 && /^[A-Z]/.test(parts[ci3 + 1])) return wordsOf(parts[ci3 + 1]);
|
|
2623
|
+
if (/^[A-Z][A-Za-z0-9]+$/.test(base)) return wordsOf(base);
|
|
2624
|
+
if (SCREEN_DIRS.test(path)) {
|
|
2625
|
+
const name = base === "index" || base === "page" ? parts[parts.length - 2] : base;
|
|
2626
|
+
if (name && !/^\[/.test(name) && !["pages", "routes", "screens", "views", "app", "src"].includes(name)) return `${wordsOf(name)} screen`;
|
|
2627
|
+
}
|
|
2628
|
+
return null;
|
|
2629
|
+
}
|
|
2630
|
+
function changedThings(files) {
|
|
2631
|
+
const out = [];
|
|
2632
|
+
for (const f of files) {
|
|
2633
|
+
const t2 = thingOf(f);
|
|
2634
|
+
if (t2 && !out.includes(t2)) out.push(t2);
|
|
2635
|
+
}
|
|
2636
|
+
return out;
|
|
2637
|
+
}
|
|
2638
|
+
function listThings(things, max = 2) {
|
|
2639
|
+
if (things.length <= max) return things.length === 2 ? `${things[0]} and ${things[1]}` : things[0] ?? "";
|
|
2640
|
+
return `${things.slice(0, max).join(", ")} and ${things.length - max} more`;
|
|
2641
|
+
}
|
|
2642
|
+
const PAST = {
|
|
2643
|
+
add: "added",
|
|
2644
|
+
build: "built",
|
|
2645
|
+
change: "changed",
|
|
2646
|
+
drop: "dropped",
|
|
2647
|
+
fix: "fixed",
|
|
2648
|
+
make: "made",
|
|
2649
|
+
move: "moved",
|
|
2650
|
+
polish: "polished",
|
|
2651
|
+
remove: "removed",
|
|
2652
|
+
rename: "renamed",
|
|
2653
|
+
replace: "replaced",
|
|
2654
|
+
rework: "reworked",
|
|
2655
|
+
set: "set",
|
|
2656
|
+
show: "showed",
|
|
2657
|
+
split: "split",
|
|
2658
|
+
swap: "swapped",
|
|
2659
|
+
try: "tried",
|
|
2660
|
+
tweak: "tweaked",
|
|
2661
|
+
update: "updated",
|
|
2662
|
+
use: "used",
|
|
2663
|
+
wire: "wired"
|
|
2664
|
+
};
|
|
2665
|
+
function isClaudeAssisted(c) {
|
|
2666
|
+
const m = c.commit.message;
|
|
2667
|
+
return /co-authored-by:\s*claude/i.test(m) || /generated with \[?claude/i.test(m);
|
|
2668
|
+
}
|
|
2669
|
+
const isClaudeAccount = (c) => /claude/i.test(c.author?.login ?? "") || /^claude\b/i.test(c.commit.author?.name ?? "");
|
|
2670
|
+
const isBot = (name) => /\[bot\]$|^github-actions\b|-bot$/i.test(name);
|
|
2671
|
+
function humanAuthorOf(c) {
|
|
2672
|
+
if (isClaudeAccount(c)) return null;
|
|
2673
|
+
const login = c.author?.login?.trim();
|
|
2674
|
+
if (login) return isBot(login) ? null : login;
|
|
2675
|
+
const name = c.commit.author?.name?.trim();
|
|
2676
|
+
return name && !isBot(name) ? name : null;
|
|
2677
|
+
}
|
|
2678
|
+
function isClaudeCommit(c) {
|
|
2679
|
+
return isClaudeAccount(c) || isClaudeAssisted(c) && !humanAuthorOf(c);
|
|
2680
|
+
}
|
|
2681
|
+
const isStartCommit = (c) => /^chore\(explore\): start /.test(c.commit.message);
|
|
2682
|
+
function subjectWords(subject) {
|
|
2683
|
+
const s = subject.replace(/^[a-z]+(\([^)]*\))?!?:\s*/i, "").replace(/\s*—\s*/g, ", ").replace(/\s*\(#\d+\)\s*$/, "").trim();
|
|
2684
|
+
if (!s) return "made a change";
|
|
2685
|
+
const [first2, ...rest] = s.split(/\s+/);
|
|
2686
|
+
const past = PAST[first2.toLowerCase()];
|
|
2687
|
+
if (past) return [past, ...rest].join(" ");
|
|
2688
|
+
if (/^(added|built|changed|fixed|made|moved|removed|renamed|replaced|updated|used|tried|swapped|tweaked)$/i.test(first2)) return [first2.toLowerCase(), ...rest].join(" ");
|
|
2689
|
+
return `changed ${/^[A-Z][a-z]/.test(s) ? s.charAt(0).toLowerCase() + s.slice(1) : s}`;
|
|
2690
|
+
}
|
|
2691
|
+
const dateOf = (c) => c.commit.author?.date ?? c.commit.committer?.date ?? "";
|
|
2692
|
+
function explorationActivity(input) {
|
|
2693
|
+
const commits = input.commits.filter((c) => !isStartCommit(c));
|
|
2694
|
+
const events = commits.map((c) => {
|
|
2695
|
+
const claude = isClaudeCommit(c);
|
|
2696
|
+
const withClaude = !claude && isClaudeAssisted(c);
|
|
2697
|
+
const who = claude ? "claude" : humanAuthorOf(c) ?? input.author;
|
|
2698
|
+
return { who, claude, ...withClaude ? { withClaude } : {}, text: subjectWords(c.commit.message.split("\n")[0]), at: dateOf(c), sha: c.sha };
|
|
2699
|
+
}).sort((a2, b) => b.at.localeCompare(a2.at));
|
|
2700
|
+
const start = input.commits.find(isStartCommit);
|
|
2701
|
+
const startedAt = input.startedAt ?? (start ? dateOf(start) : null);
|
|
2702
|
+
if (startedAt) events.push({ who: input.author, claude: false, text: "started the exploration", at: startedAt });
|
|
2703
|
+
return events.slice(0, input.limit ?? events.length);
|
|
2704
|
+
}
|
|
2705
|
+
function lastActivity(input) {
|
|
2706
|
+
const latest = explorationActivity({ commits: input.commits, author: input.author }).find((e) => e.text !== "started the exploration");
|
|
2707
|
+
if (!latest) return null;
|
|
2708
|
+
const things = changedThings(input.files ?? []);
|
|
2709
|
+
return { who: latest.who, claude: latest.claude, ...latest.withClaude ? { withClaude: true } : {}, text: things.length ? `changed ${listThings(things)}` : latest.text, at: latest.at };
|
|
2710
|
+
}
|
|
2711
|
+
function shortAgo(iso, now = /* @__PURE__ */ new Date()) {
|
|
2712
|
+
const t2 = new Date(iso).getTime();
|
|
2713
|
+
if (!Number.isFinite(t2)) return "";
|
|
2714
|
+
const s = Math.max(0, (now.getTime() - t2) / 1e3);
|
|
2715
|
+
if (s < 60) return "just now";
|
|
2716
|
+
if (s < 3600) return `${Math.round(s / 60)} min ago`;
|
|
2717
|
+
if (s < 86400) return `${Math.round(s / 3600)} h ago`;
|
|
2718
|
+
const days = Math.floor(s / 86400);
|
|
2719
|
+
if (days === 1) return "Yesterday";
|
|
2720
|
+
if (days < 7) return `${days} days ago`;
|
|
2721
|
+
return new Date(t2).toLocaleDateString("en", { month: "short", day: "numeric", ...new Date(t2).getFullYear() !== now.getFullYear() ? { year: "numeric" } : {} });
|
|
2722
|
+
}
|
|
2723
|
+
function startedWhen(iso, now = /* @__PURE__ */ new Date()) {
|
|
2724
|
+
const t2 = new Date(iso);
|
|
2725
|
+
if (!Number.isFinite(t2.getTime())) return "";
|
|
2726
|
+
const day = (d) => Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / 864e5);
|
|
2727
|
+
const diff = day(now) - day(t2);
|
|
2728
|
+
if (diff <= 0) return "today";
|
|
2729
|
+
if (diff === 1) return "yesterday";
|
|
2730
|
+
if (diff < 7) return `${diff} days ago`;
|
|
2731
|
+
return t2.toLocaleDateString("en", { month: "short", day: "numeric", ...t2.getFullYear() !== now.getFullYear() ? { year: "numeric" } : {} });
|
|
2732
|
+
}
|
|
2733
|
+
function actorName(e, names2) {
|
|
2734
|
+
if (e.claude) return "Claude";
|
|
2735
|
+
const n = names2?.[e.who] ?? e.who;
|
|
2736
|
+
return e.withClaude ? `${n} with Claude` : n;
|
|
2737
|
+
}
|
|
2738
|
+
function whereItStands(input) {
|
|
2739
|
+
if (input.stage === "not-started") return `"${input.name}" is started but nothing is built yet.`;
|
|
2740
|
+
if (input.stage === "handed") return `"${input.name}" is ${input.statusLabel ? input.statusLabel.toLowerCase() : "with the developers"}.`;
|
|
2741
|
+
const things = changedThings(input.files ?? []);
|
|
2742
|
+
const latest = input.events.find((e) => e.text !== "started the exploration");
|
|
2743
|
+
const who = (e) => actorName(e, input.names);
|
|
2744
|
+
const so = things.length ? `So far it changes ${listThings(things, 3)}.` : "Something changed already.";
|
|
2745
|
+
return latest ? `${so} Last: ${who(latest)} ${latest.text}, ${shortAgo(latest.at, input.now).toLowerCase()}.` : so;
|
|
2746
|
+
}
|
|
2250
2747
|
const hex2 = (x) => Math.round(Math.max(0, Math.min(255, x))).toString(16).padStart(2, "0");
|
|
2251
2748
|
function rgbOf(v) {
|
|
2252
2749
|
const m = /^rgba?\(([^)]*)\)$/i.exec(v.trim());
|
|
@@ -2664,8 +3161,8 @@ function colorDelta(a2, b) {
|
|
|
2664
3161
|
const db2 = a2[2] - b[2];
|
|
2665
3162
|
const y = dr2 * 0.29889531 + dg2 * 0.58662247 + db2 * 0.11448223;
|
|
2666
3163
|
const i = dr2 * 0.59597799 - dg2 * 0.2741761 - db2 * 0.32180189;
|
|
2667
|
-
const
|
|
2668
|
-
return 0.5053 * y * y + 0.299 * i * i + 0.1957 *
|
|
3164
|
+
const q2 = dr2 * 0.21147017 - dg2 * 0.52261711 + db2 * 0.31114694;
|
|
3165
|
+
return 0.5053 * y * y + 0.299 * i * i + 0.1957 * q2 * q2;
|
|
2669
3166
|
}
|
|
2670
3167
|
function foundNear(img, x, y, c, radius, max) {
|
|
2671
3168
|
for (let dy2 = -radius; dy2 <= radius; dy2++) for (let dx2 = -radius; dx2 <= radius; dx2++) if (colorDelta(c, rgbAt(img, x + dx2, y + dy2)) <= max) return true;
|
|
@@ -2764,10 +3261,20 @@ function buildFidelityReport(input) {
|
|
|
2764
3261
|
}
|
|
2765
3262
|
const fmtPercent = (p2) => p2 >= 10 ? `${Math.round(p2)}%` : p2 >= 1 ? `${Math.round(p2 * 10) / 10}%` : p2 > 0 ? "under 1%" : "0%";
|
|
2766
3263
|
const fmtSize = (s) => `${s.width}x${s.height}`;
|
|
3264
|
+
function sizeGap(s, tolerance = FIDELITY_DEFAULTS.sizeTolerance) {
|
|
3265
|
+
const r = (n) => Math.round(n);
|
|
3266
|
+
const parts = [];
|
|
3267
|
+
if (Math.abs(s.figma.width - s.code.width) > tolerance) parts.push(`width ${r(s.figma.width)} in Figma vs ${r(s.code.width)} in code`);
|
|
3268
|
+
if (Math.abs(s.figma.height - s.code.height) > tolerance) parts.push(`height ${r(s.figma.height)} in Figma vs ${r(s.code.height)} in code`);
|
|
3269
|
+
return parts.length ? parts.join(", ") : void 0;
|
|
3270
|
+
}
|
|
2767
3271
|
function fidelityVerdict(v) {
|
|
2768
3272
|
if (v.status === "skipped") return `Not compared: ${v.reason ?? "no image of the code"}`;
|
|
2769
3273
|
if (v.status === "matches") return "Matches the code";
|
|
2770
|
-
const
|
|
3274
|
+
const pixels = `${fmtPercent(v.percent ?? 0)} of pixels`;
|
|
3275
|
+
const gap = v.size ? sizeGap(v.size) : void 0;
|
|
3276
|
+
if (gap) return `Differs from the code: ${gap}, ${pixels}`;
|
|
3277
|
+
const parts = [pixels];
|
|
2771
3278
|
if (v.size && !sameSize(v.size)) parts.push(`size ${fmtSize(v.size.figma)} vs ${fmtSize(v.size.code)}`);
|
|
2772
3279
|
return `Differs from the code: ${parts.join(", ")}`;
|
|
2773
3280
|
}
|
|
@@ -2920,9 +3427,9 @@ function planPopup(spec, variant) {
|
|
|
2920
3427
|
};
|
|
2921
3428
|
}
|
|
2922
3429
|
function referenceImagePath(component, key2) {
|
|
2923
|
-
const
|
|
2924
|
-
const file = key2.split(",").map((kv) =>
|
|
2925
|
-
return `${
|
|
3430
|
+
const slug2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
3431
|
+
const file = key2.split(",").map((kv) => slug2(kv)).filter(Boolean).join("__");
|
|
3432
|
+
return `${slug2(component)}/${file || "default"}.png`;
|
|
2926
3433
|
}
|
|
2927
3434
|
function planReferences(file) {
|
|
2928
3435
|
return file.components.flatMap(
|
|
@@ -2961,8 +3468,10 @@ function storyForVariant(spec, props, stories, shot) {
|
|
|
2961
3468
|
}
|
|
2962
3469
|
return order(ok).find(fits);
|
|
2963
3470
|
}
|
|
3471
|
+
const ICON_LIBRARY = /^(lucide|heroicons?|phosphor|tabler|feather|material-?icons?|fa|fontawesome|svg)(?:$|[-_])/i;
|
|
2964
3472
|
function partWords(part, id = part?.id ?? "") {
|
|
2965
3473
|
if (id === "root") return "component";
|
|
3474
|
+
if (ICON_LIBRARY.test(part?.id ?? id)) return "icon";
|
|
2966
3475
|
const base = (part?.id ?? id).replace(/^[a-z]{1,6}--/i, "").replace(/[-_]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim().toLowerCase();
|
|
2967
3476
|
return base || (part?.name ?? id).toLowerCase();
|
|
2968
3477
|
}
|
|
@@ -3031,7 +3540,7 @@ function locate(parentBuilt, parentMeasured, axis, built, measured, builtParts,
|
|
|
3031
3540
|
if (!stacked) {
|
|
3032
3541
|
const tallest = [...kids].sort((a2, b) => b.m[len] - a2.m[len])[0];
|
|
3033
3542
|
const change = tallest.b[len] - tallest.m[len];
|
|
3034
|
-
if (Math.abs(change) >= 1) out.push(...
|
|
3543
|
+
if (Math.abs(change) >= 1) out.push(...ownSize2(tallest, change));
|
|
3035
3544
|
return out;
|
|
3036
3545
|
}
|
|
3037
3546
|
let prevB = pb[axis];
|
|
@@ -3040,13 +3549,13 @@ function locate(parentBuilt, parentMeasured, axis, built, measured, builtParts,
|
|
|
3040
3549
|
const gap = k.b[axis] - prevB - (k.m[axis] - prevM);
|
|
3041
3550
|
say(gap, i === 0 ? `${before} the ${partWords(k.p)}` : `${after} the ${partWords(kids[i - 1].p)}`);
|
|
3042
3551
|
const change = k.b[len] - k.m[len];
|
|
3043
|
-
if (Math.abs(change) >= 1) out.push(...
|
|
3552
|
+
if (Math.abs(change) >= 1) out.push(...ownSize2(k, change));
|
|
3044
3553
|
prevB = k.b[axis] + k.b[len];
|
|
3045
3554
|
prevM = k.m[axis] + k.m[len];
|
|
3046
3555
|
});
|
|
3047
3556
|
say(pb[axis] + pb[len] - prevB - (pm[axis] + pm[len] - prevM), `${after} the ${partWords(kids.at(-1).p)}`);
|
|
3048
3557
|
return out;
|
|
3049
|
-
function
|
|
3558
|
+
function ownSize2(k, change) {
|
|
3050
3559
|
const inner = depth < 3 ? locate(k.p.id, pairs.get(k.p.id), axis, built, measured, builtParts, pairs, depth + 1) : [];
|
|
3051
3560
|
if (inner.length) return inner;
|
|
3052
3561
|
return [{ amount: change, text: `the ${partWords(k.p)} is ${round$1(Math.abs(change))} px ${change < 0 ? axis === "y" ? "shorter" : "narrower" : axis === "y" ? "taller" : "wider"} in Figma` }];
|
|
@@ -3151,12 +3660,16 @@ function buildGapCase(input) {
|
|
|
3151
3660
|
const checkedVariants = [];
|
|
3152
3661
|
const specVariant = new Map((spec?.variants ?? []).map((v) => [variantKeyOf(v.props), v]));
|
|
3153
3662
|
if (input.fidelity) {
|
|
3154
|
-
for (const r of input.fidelity.variants)
|
|
3663
|
+
for (const r of input.fidelity.variants) {
|
|
3664
|
+
if (r.status === "skipped") continue;
|
|
3665
|
+
const variant = matchVariant(spec, propsOfKey(r.key));
|
|
3666
|
+
checkedVariants.push({ key: r.key, props: variant?.props ?? propsOfKey(r.key), result: r, variant });
|
|
3667
|
+
}
|
|
3155
3668
|
} else for (const [key2, v] of specVariant) checkedVariants.push({ key: key2, props: v.props, variant: v });
|
|
3156
3669
|
const variants = [];
|
|
3157
3670
|
for (const cv3 of checkedVariants) {
|
|
3158
3671
|
if (cv3.result && cv3.result.status !== "differs") continue;
|
|
3159
|
-
const shot = input.shots?.[cv3.key];
|
|
3672
|
+
const shot = input.shots?.[cv3.key] ?? (cv3.variant ? input.shots?.[variantKeyOf(cv3.variant.props)] : void 0);
|
|
3160
3673
|
const story = spec ? storyForVariant(spec, cv3.props, stories, shot) : shot ? stories.find((s) => s.shot?.file === shot.file) : void 0;
|
|
3161
3674
|
const gv = {
|
|
3162
3675
|
key: cv3.key,
|
|
@@ -3226,6 +3739,8 @@ function gapSummary(component, variants, checked, fidelity) {
|
|
|
3226
3739
|
if (!variants.length) return checked ? `${component} matches the code on ${checked} variant${checked === 1 ? "" : "s"}.` : `${component} has no variant to compare.`;
|
|
3227
3740
|
const first2 = variants.flatMap((v) => v.differences)[0];
|
|
3228
3741
|
const head = `${component} differs from the code on ${variants.length} of ${checked} variant${checked === 1 ? "" : "s"}`;
|
|
3742
|
+
const sized = variants.map((v) => v.size ? sizeGap(v.size) : void 0).find(Boolean);
|
|
3743
|
+
if (!first2 && sized) return `${head}: ${sized}.`;
|
|
3229
3744
|
if (!first2) return variants.some((v) => (v.percent ?? 0) > UNMEASURED_PERCENT) ? `${head}: Bridge doesn't measure what differs here yet (the measured values agree, the images do not).` : `${head}: the measure agrees, compare the images.`;
|
|
3230
3745
|
const more = variants.reduce((n, v) => n + v.differences.length, 0) - 1;
|
|
3231
3746
|
return `${head}. ${first2.text}${more > 0 ? `, and ${more} more` : ""}.`;
|
|
@@ -3243,622 +3758,255 @@ function gapDetail(component, variants, checked, referee) {
|
|
|
3243
3758
|
}
|
|
3244
3759
|
return lines.join("\n");
|
|
3245
3760
|
}
|
|
3246
|
-
const
|
|
3247
|
-
const
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
}
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
const
|
|
3256
|
-
const
|
|
3257
|
-
const
|
|
3258
|
-
|
|
3259
|
-
const
|
|
3260
|
-
|
|
3261
|
-
axis: ["action", "arg", "property", "values"],
|
|
3262
|
-
story: ["use"],
|
|
3263
|
-
status: ["status"]
|
|
3264
|
-
};
|
|
3265
|
-
const PRIMARY = ["MIN", "CENTER", "MAX", "SPACE_BETWEEN"];
|
|
3266
|
-
const COUNTER = ["MIN", "CENTER", "MAX", "STRETCH"];
|
|
3267
|
-
const plain = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
3268
|
-
function whereProblems(w, at2) {
|
|
3269
|
-
if (!isObj$3(w)) return at2("where must be an object with args and/or state");
|
|
3270
|
-
for (const k of Object.keys(w)) if (k !== "args" && k !== "state") at2(`where.${k} is not known (args, state)`);
|
|
3271
|
-
if (w.args !== void 0 && (!isObj$3(w.args) || !Object.values(w.args).every(plain))) at2("where.args maps arg names to a text, number or true/false");
|
|
3272
|
-
if (w.state !== void 0 && !STATES$1.includes(w.state)) at2(`where.state is one of ${STATES$1.join(", ")}`);
|
|
3761
|
+
const NOT_AN_AXIS = /^(as|asChild|render|component|className|class|style|id|key|ref|role|type|name|form|dir|lang|tabIndex|slot|children|htmlFor|value|defaultValue|placeholder|href|target|rel|src|alt)$/i;
|
|
3762
|
+
const STATE_PROPS = /* @__PURE__ */ new Set(["disabled", "checked", "indeterminate"]);
|
|
3763
|
+
const STATES$1 = [
|
|
3764
|
+
{ value: "Default", of: (s) => !s.state && s.props?.disabled !== true },
|
|
3765
|
+
{ value: "Hover", of: (s) => s.state === "hover" && s.props?.disabled !== true },
|
|
3766
|
+
{ value: "Pressed", of: (s) => s.state === "active" && s.props?.disabled !== true },
|
|
3767
|
+
{ value: "Focus", of: (s) => s.state === "focus-visible" && s.props?.disabled !== true },
|
|
3768
|
+
{ value: "Disabled", of: (s) => s.props?.disabled === true }
|
|
3769
|
+
];
|
|
3770
|
+
const setNameOf$2 = (title) => title.split("/").at(-1).trim();
|
|
3771
|
+
const axisName = (prop) => prop.charAt(0).toUpperCase() + prop.slice(1).replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
3772
|
+
const propsOf$1 = (s) => ({ ...s.props ?? {}, ...s.overrides ?? {} });
|
|
3773
|
+
function checkedOf(s) {
|
|
3774
|
+
const p2 = propsOf$1(s);
|
|
3775
|
+
return p2.indeterminate === true ? "mixed" : String(p2.checked === true);
|
|
3273
3776
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
const
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
if (typeof
|
|
3289
|
-
if (typeof
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
if (f.status === "matches") at2('status cannot be "matches": only the shot comparison says a component matches');
|
|
3310
|
-
else if (f.status !== void 0 && f.status !== "differs" && f.status !== "cant-check") at2("status is differs or cant-check (absent: a note)");
|
|
3311
|
-
}
|
|
3312
|
-
});
|
|
3777
|
+
const LITERAL_FIELDS = ["fill", "textFill", "stroke", "fontFamily", "shadow"];
|
|
3778
|
+
function valuesOf(spec, partId, hasChildren, known, fills = false, own = { width: true, height: true }) {
|
|
3779
|
+
const sized = { ...hasChildren ? boxSized(spec, partId, own) : {}, ...spec.sized?.[partId] };
|
|
3780
|
+
const v = { ...spec.values[partId] ?? {} };
|
|
3781
|
+
const raw = spec.raw[partId] ?? {};
|
|
3782
|
+
for (const field of Object.keys(v)) {
|
|
3783
|
+
const value = v[field];
|
|
3784
|
+
if (typeof value !== "string" || value.startsWith("calc(") || known(value)) continue;
|
|
3785
|
+
const literal = raw[field];
|
|
3786
|
+
if (literal === void 0) delete v[field];
|
|
3787
|
+
else v[field] = literal;
|
|
3788
|
+
}
|
|
3789
|
+
for (const field of LITERAL_FIELDS) if (v[field] === void 0 && typeof raw[field] === "string") v[field] = raw[field];
|
|
3790
|
+
if (hasChildren) {
|
|
3791
|
+
if (typeof v.width === "number" && !fills) delete v.width;
|
|
3792
|
+
if (typeof v.height === "number") delete v.height;
|
|
3793
|
+
}
|
|
3794
|
+
if (sized?.width !== void 0) v.width = sized.width;
|
|
3795
|
+
if (sized?.height !== void 0) v.height = sized.height;
|
|
3796
|
+
return v;
|
|
3797
|
+
}
|
|
3798
|
+
function boxSized(spec, partId, own) {
|
|
3799
|
+
const box = spec.boxes?.[partId];
|
|
3800
|
+
const kids = spec.parts.filter((c) => c.parent === partId && !c.pin && !c.cover && !c.overlay).map((c) => spec.boxes?.[c.id]).filter((b) => !!b);
|
|
3801
|
+
if (!box || !kids.length) return {};
|
|
3802
|
+
const raw = spec.raw[partId] ?? {};
|
|
3803
|
+
const num = (f) => typeof raw[f] === "number" ? raw[f] : 0;
|
|
3804
|
+
const left = Math.min(...kids.map((k) => k.x));
|
|
3805
|
+
const top = Math.min(...kids.map((k) => k.y));
|
|
3806
|
+
const side = (f) => typeof raw[f] === "number" ? raw[f] : num("strokeWeight");
|
|
3807
|
+
const contentW = Math.max(...kids.map((k) => k.x + k.width)) - left + num("paddingLeft") + num("paddingRight") + side("strokeLeftWeight") + side("strokeRightWeight");
|
|
3808
|
+
const contentH = Math.max(...kids.map((k) => k.y + k.height)) - top + num("paddingTop") + num("paddingBottom") + side("strokeTopWeight") + side("strokeBottomWeight");
|
|
3809
|
+
const out = {};
|
|
3810
|
+
if (own.width && box.width - contentW > 1) out.width = box.width;
|
|
3811
|
+
if (own.height && box.height - contentH > 1) out.height = box.height;
|
|
3313
3812
|
return out;
|
|
3314
3813
|
}
|
|
3315
|
-
function
|
|
3316
|
-
|
|
3317
|
-
const
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
else if (!Number.isFinite(value)) at2(`set.${field} must be a finite number`);
|
|
3337
|
-
} else if (typeof value !== "string" || !value.trim()) at2(`set.${field} must be a token path, a number or null`);
|
|
3338
|
-
else if (field === "textDecoration" && !/^(UNDERLINE|STRIKETHROUGH|NONE)$/.test(value)) at2(`set.textDecoration is UNDERLINE, STRIKETHROUGH or NONE`);
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
for (const side of ["width", "height"]) {
|
|
3342
|
-
const v = f[side];
|
|
3343
|
-
if (v !== void 0 && !["hug", "fill", "fixed"].includes(v) && !(typeof v === "number" && v > 0)) at2(`${side} is hug, fill, fixed or a size in px`);
|
|
3814
|
+
function setOf(name, stories, argTypes, source, known, hints = {}) {
|
|
3815
|
+
const propName = (prop) => hints.properties?.[prop] ?? axisName(prop);
|
|
3816
|
+
const figmaValue = (prop, value) => hints.values?.[prop]?.[value] ?? value;
|
|
3817
|
+
const lead = stories.find((s) => s.name === "Default" && !s.state) ?? stories[0];
|
|
3818
|
+
const baseline = (k, a2) => (lead ? propsOf$1(lead)[k] : void 0) ?? a2.default ?? false;
|
|
3819
|
+
const shown = stories.filter((s) => !/skeleton/i.test(s.name));
|
|
3820
|
+
const ordered = [...shown.length ? shown : stories].sort((a2, b) => Number(!!a2.gallery) - Number(!!b.gallery));
|
|
3821
|
+
const valueOf = (s, k, a2) => (propsOf$1(s)[k] ?? a2.default ?? false) === true;
|
|
3822
|
+
const flags = Object.entries(argTypes).filter(
|
|
3823
|
+
([k, a2]) => a2.kind === "boolean" && !STATE_PROPS.has(k) && !NOT_AN_AXIS.test(k) && ordered.some((s) => valueOf(s, k, a2)) && ordered.some((s) => !valueOf(s, k, a2))
|
|
3824
|
+
);
|
|
3825
|
+
const plain2 = ordered.filter(
|
|
3826
|
+
(s) => Object.entries(argTypes).every(
|
|
3827
|
+
([k, a2]) => a2.kind !== "boolean" || STATE_PROPS.has(k) || NOT_AN_AXIS.test(k) || flags.some(([f]) => f === k) || (propsOf$1(s)[k] ?? a2.default ?? false) === baseline(k, a2)
|
|
3828
|
+
)
|
|
3829
|
+
);
|
|
3830
|
+
if (!plain2.length) return void 0;
|
|
3831
|
+
const axes = [];
|
|
3832
|
+
for (const [prop, a2] of flags) {
|
|
3833
|
+
const def = String(baseline(prop, a2) === true);
|
|
3834
|
+
axes.push({ name: propName(prop), prop, values: [figmaValue(prop, def), figmaValue(prop, def === "true" ? "false" : "true")], default: figmaValue(prop, def) });
|
|
3344
3835
|
}
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
}
|
|
3836
|
+
for (const [prop, a2] of Object.entries(argTypes)) {
|
|
3837
|
+
if (a2.kind !== "enum" || NOT_AN_AXIS.test(prop) || !a2.options || a2.options.length < 2 || a2.options.length > 12) continue;
|
|
3838
|
+
const seen = new Set(plain2.map((s) => String(propsOf$1(s)[prop] ?? a2.default ?? a2.options[0])));
|
|
3839
|
+
const values = a2.options.filter((o) => seen.has(o));
|
|
3840
|
+
if (values.length < 2) continue;
|
|
3841
|
+
const def = a2.default !== void 0 && values.includes(String(a2.default)) ? String(a2.default) : values[0];
|
|
3842
|
+
axes.push({ name: propName(prop), prop, values: [...new Set(values.map((v) => figmaValue(prop, v)))], default: figmaValue(prop, def) });
|
|
3353
3843
|
}
|
|
3354
|
-
if (
|
|
3355
|
-
const
|
|
3356
|
-
if (
|
|
3844
|
+
if ("checked" in argTypes || plain2.some((s) => "checked" in propsOf$1(s))) {
|
|
3845
|
+
const values = ["false", "true", "mixed"].filter((v) => plain2.some((s) => checkedOf(s) === v));
|
|
3846
|
+
if (values.length > 1) axes.push({ name: "Checked", values, default: values[0] });
|
|
3357
3847
|
}
|
|
3358
|
-
|
|
3359
|
-
}
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
if (f.values !== void 0 && (!isObj$3(f.values) || !Object.values(f.values).every((v) => typeof v === "string" && v.trim()))) at2("values maps code values to Figma values (texts)");
|
|
3368
|
-
else if (isObj$3(f.values)) {
|
|
3369
|
-
const figma = Object.values(f.values);
|
|
3370
|
-
if (new Set(figma).size !== figma.length) at2("values gives two code values the same Figma value: use prune for the one to drop");
|
|
3848
|
+
const states = STATES$1.filter((st) => plain2.some(st.of)).map((st) => st.value);
|
|
3849
|
+
if (states.length > 1) axes.push({ name: "State", values: states, default: states[0] });
|
|
3850
|
+
const keyOf = (s) => {
|
|
3851
|
+
const props = {};
|
|
3852
|
+
for (const axis of axes) {
|
|
3853
|
+
const flag = flags.find(([k]) => k === axis.prop);
|
|
3854
|
+
const value = axis.name === "State" ? STATES$1.find((st) => st.of(s))?.value : axis.name === "Checked" ? checkedOf(s) : flag ? figmaValue(flag[0], String(valueOf(s, flag[0], flag[1]))) : propsOf$1(s)[axis.prop] === void 0 ? axis.default : figmaValue(axis.prop, String(propsOf$1(s)[axis.prop]));
|
|
3855
|
+
if (value === void 0 || !axis.values.includes(value)) return void 0;
|
|
3856
|
+
props[axis.name] = value;
|
|
3371
3857
|
}
|
|
3858
|
+
return props;
|
|
3859
|
+
};
|
|
3860
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
3861
|
+
for (const s of plain2) {
|
|
3862
|
+
const props = keyOf(s);
|
|
3863
|
+
if (!props) continue;
|
|
3864
|
+
const k = JSON.stringify(props);
|
|
3865
|
+
if (!candidates.has(k)) candidates.set(k, { props, stories: [] });
|
|
3866
|
+
candidates.get(k).stories.push(s);
|
|
3372
3867
|
}
|
|
3373
|
-
|
|
3374
|
-
if (
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3868
|
+
const storyFor = /* @__PURE__ */ new Map();
|
|
3869
|
+
if (axes.some((a2) => a2.name === "State")) {
|
|
3870
|
+
const rest = (p2) => JSON.stringify(Object.entries(p2).filter(([k]) => k !== "State"));
|
|
3871
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3872
|
+
for (const c of candidates.values()) groups.set(rest(c.props), [...groups.get(rest(c.props)) ?? [], c]);
|
|
3873
|
+
for (const [key2, group] of groups) {
|
|
3874
|
+
const ids = [...new Set(group.flatMap((c) => c.stories.map((s) => s.id)))];
|
|
3875
|
+
const score = (id) => group.filter((c) => c.stories.some((s) => s.id === id)).length;
|
|
3876
|
+
storyFor.set(key2, ids.reduce((best, id) => score(id) > score(best) ? id : best, ids[0]));
|
|
3877
|
+
}
|
|
3878
|
+
for (const c of candidates.values()) {
|
|
3879
|
+
const same = c.stories.filter((s) => s.id === storyFor.get(rest(c.props)));
|
|
3880
|
+
if (same.length) c.stories = same;
|
|
3386
3881
|
}
|
|
3387
3882
|
}
|
|
3388
|
-
const
|
|
3389
|
-
if (
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
const
|
|
3393
|
-
const
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
if (where.args && !Object.entries(where.args).every(([k, v]) => sameArg(p2[k], v))) return false;
|
|
3400
|
-
if (where.state && storyState(s) !== where.state) return false;
|
|
3401
|
-
return true;
|
|
3402
|
-
}
|
|
3403
|
-
function partsNamed(spec, sel) {
|
|
3404
|
-
if (sel.startsWith(".")) {
|
|
3405
|
-
const cls = sel.slice(1);
|
|
3406
|
-
return spec.parts.filter((p2) => (p2.source ?? "").split(/[\s,]+/).map((x) => x.replace(/^\./, "")).includes(cls));
|
|
3883
|
+
const byVariant = new Map([...candidates].map(([k, c]) => [k, { props: c.props, story: c.stories[0] }]));
|
|
3884
|
+
if (!byVariant.size) return void 0;
|
|
3885
|
+
const isDefault = (p2) => axes.every((a2) => p2[a2.name] === a2.default);
|
|
3886
|
+
const base = [...byVariant.values()].find((v) => isDefault(v.props)) ?? [...byVariant.values()][0];
|
|
3887
|
+
const baseSpec = base.story.spec;
|
|
3888
|
+
const parts = baseSpec.parts.map((p2) => baseSpec.vectors?.[p2.id] ? { ...p2, kind: "vector" } : { ...p2 });
|
|
3889
|
+
for (const p2 of parts) {
|
|
3890
|
+
if (!p2.overlay || p2.pin || p2.cover) continue;
|
|
3891
|
+
const pin = pinFromBoxes(baseSpec, p2);
|
|
3892
|
+
if (pin) p2.pin = pin;
|
|
3893
|
+
else p2.cover = true;
|
|
3407
3894
|
}
|
|
3408
|
-
|
|
3409
|
-
}
|
|
3410
|
-
|
|
3411
|
-
function patchSpec(spec, fix) {
|
|
3412
|
-
const targets = partsNamed(spec, fix.part);
|
|
3413
|
-
if (!targets.length) return false;
|
|
3414
|
-
for (const part of targets) {
|
|
3415
|
-
const id = part.id;
|
|
3416
|
-
const box = spec.boxes[id];
|
|
3417
|
-
const parent = part.parent ? spec.parts.find((p2) => p2.id === part.parent) : void 0;
|
|
3418
|
-
if (fix.hide) {
|
|
3419
|
-
const gone = /* @__PURE__ */ new Set([id]);
|
|
3420
|
-
for (const p2 of spec.parts) if (p2.parent && gone.has(p2.parent)) gone.add(p2.id);
|
|
3421
|
-
spec.parts = spec.parts.filter((p2) => !gone.has(p2.id));
|
|
3422
|
-
for (const map of [spec.values, spec.raw, spec.tokens, spec.boxes, spec.sized ?? {}, spec.spacers ?? {}, spec.vectors ?? {}]) for (const g of gone) delete map[g];
|
|
3423
|
-
continue;
|
|
3424
|
-
}
|
|
3425
|
-
const values = spec.values[id] ??= {};
|
|
3426
|
-
for (const [field, value] of Object.entries(fix.set ?? {})) {
|
|
3427
|
-
values[field] = value === "full" ? box ? Math.min(box.width, box.height) / 2 : 9999 : value;
|
|
3428
|
-
}
|
|
3429
|
-
for (const side of ["width", "height"]) {
|
|
3430
|
-
const how = fix[side];
|
|
3431
|
-
if (how === void 0) continue;
|
|
3432
|
-
const flowsAlong = parent?.layout?.direction === (side === "width" ? "HORIZONTAL" : "VERTICAL");
|
|
3433
|
-
if (how === "hug") {
|
|
3434
|
-
delete values[side];
|
|
3435
|
-
if (spec.sized?.[id]) delete spec.sized[id][side];
|
|
3436
|
-
if (part.stretch) delete part.stretch[side];
|
|
3437
|
-
if (flowsAlong) delete part.grow;
|
|
3438
|
-
if (!part.parent && side === "width") delete part.fill;
|
|
3439
|
-
} else if (how === "fill") {
|
|
3440
|
-
if (spec.sized?.[id]) delete spec.sized[id][side];
|
|
3441
|
-
if (!part.parent) {
|
|
3442
|
-
if (side === "width") part.fill = true;
|
|
3443
|
-
} else if (flowsAlong) part.grow = true;
|
|
3444
|
-
else part.stretch = { ...part.stretch ?? {}, [side]: true };
|
|
3445
|
-
} else {
|
|
3446
|
-
const px = how === "fixed" ? typeof values[side] === "number" ? values[side] : box?.[side] : how;
|
|
3447
|
-
if (px === void 0) continue;
|
|
3448
|
-
values[side] = px;
|
|
3449
|
-
spec.sized = { ...spec.sized ?? {}, [id]: { ...spec.sized?.[id] ?? {}, [side]: px } };
|
|
3450
|
-
if (part.stretch) delete part.stretch[side];
|
|
3451
|
-
if (flowsAlong) delete part.grow;
|
|
3452
|
-
if (!part.parent && side === "width") delete part.fill;
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
if (fix.direction || fix.align) {
|
|
3456
|
-
part.layout = { direction: "HORIZONTAL", primary: "MIN", counter: "MIN", ...part.layout ?? {} };
|
|
3457
|
-
if (fix.direction) part.layout.direction = fix.direction;
|
|
3458
|
-
if (fix.align?.primary) part.layout.primary = fix.align.primary;
|
|
3459
|
-
if (fix.align?.counter) part.layout.counter = fix.align.counter;
|
|
3460
|
-
}
|
|
3461
|
-
if (fix.overlay !== void 0) {
|
|
3462
|
-
delete part.cover;
|
|
3463
|
-
delete part.pin;
|
|
3464
|
-
if (fix.overlay === true) part.cover = true;
|
|
3465
|
-
else if (fix.overlay !== false) part.pin = { ...fix.overlay };
|
|
3466
|
-
}
|
|
3467
|
-
}
|
|
3468
|
-
return true;
|
|
3469
|
-
}
|
|
3470
|
-
function applyFixes(measured, fixes) {
|
|
3471
|
-
const list = Array.isArray(fixes) ? fixes : fixes.fixes;
|
|
3472
|
-
const file = clone(measured);
|
|
3473
|
-
const before = new Set((file.figmaFixes?.applied ?? []).filter((a2) => a2.result === "applied").map((a2) => a2.id));
|
|
3474
|
-
const applied = [];
|
|
3475
|
-
const entryOf = (component) => (file.components ?? []).find((c) => lc$1(setNameOf$2(c.title)) === lc$1(component));
|
|
3476
|
-
const hints = (component) => {
|
|
3477
|
-
const c = entryOf(component);
|
|
3478
|
-
return c ? c.figma ??= {} : void 0;
|
|
3479
|
-
};
|
|
3480
|
-
const mark = (s, id) => {
|
|
3481
|
-
s.fixes = [.../* @__PURE__ */ new Set([...s.fixes ?? [], id])];
|
|
3895
|
+
const paints = (id) => {
|
|
3896
|
+
const v = baseSpec.values[id] ?? {};
|
|
3897
|
+
return [v.fill, v.stroke].some((x) => x !== void 0 && x !== null);
|
|
3482
3898
|
};
|
|
3483
|
-
for (const
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
file.stories = file.stories.filter((s) => !drop.has(s));
|
|
3516
|
-
if (h2) h2.pruned = [...h2.pruned ?? [], fix.where];
|
|
3517
|
-
record("applied", drop.size, `dropped ${drop.size} variant stor${drop.size === 1 ? "y" : "ies"}`);
|
|
3518
|
-
} else {
|
|
3519
|
-
const arg = fix.arg;
|
|
3520
|
-
const entry = entryOf(fix.component);
|
|
3521
|
-
const known = !!entry?.argTypes[arg] || mine().some((s) => arg in propsOf$1(s));
|
|
3522
|
-
if (!known) {
|
|
3523
|
-
record("not-found", 0, `${fix.component} has no arg ${arg}`);
|
|
3524
|
-
continue;
|
|
3525
|
-
}
|
|
3526
|
-
if (fix.action === "map") {
|
|
3527
|
-
if (h2) {
|
|
3528
|
-
if (fix.property) h2.properties = { ...h2.properties ?? {}, [arg]: fix.property };
|
|
3529
|
-
if (fix.values) h2.values = { ...h2.values ?? {}, [arg]: { ...h2.values?.[arg] ?? {}, ...fix.values } };
|
|
3530
|
-
}
|
|
3531
|
-
record("applied", 0, `${arg} drawn as ${fix.property ?? arg}${fix.values ? ` (${Object.entries(fix.values).map(([c, f]) => `${c} as ${f}`).join(", ")})` : ""}`);
|
|
3532
|
-
} else {
|
|
3533
|
-
const at2 = entry?.argTypes[arg];
|
|
3534
|
-
const lead = mine().find((s) => !s.overrides || !(arg in s.overrides));
|
|
3535
|
-
const baseline = at2?.default ?? (lead ? propsOf$1(lead)[arg] : void 0) ?? (at2?.kind === "boolean" ? false : void 0);
|
|
3536
|
-
const drop = new Set(scoped().filter((s) => baseline !== void 0 && propsOf$1(s)[arg] !== void 0 && !sameArg(propsOf$1(s)[arg], baseline)));
|
|
3537
|
-
file.stories = file.stories.filter((s) => !drop.has(s));
|
|
3538
|
-
if (entry) {
|
|
3539
|
-
delete entry.argTypes[arg];
|
|
3540
|
-
entry.figma ??= {};
|
|
3541
|
-
entry.figma.ignored = [.../* @__PURE__ */ new Set([...entry.figma.ignored ?? [], arg])];
|
|
3542
|
-
}
|
|
3543
|
-
record("applied", drop.size, `${arg} is not a Figma property${drop.size ? `: ${drop.size} stor${drop.size === 1 ? "y" : "ies"} at another value left out` : ""}`);
|
|
3544
|
-
}
|
|
3545
|
-
}
|
|
3546
|
-
} else if (fix.kind === "story") {
|
|
3547
|
-
const src = file.stories.find(
|
|
3548
|
-
(s) => s.id === fix.use.story && s.spec && !s.error && (!fix.use.args || Object.entries(fix.use.args).every(([k, v]) => sameArg(propsOf$1(s)[k], v))) && (!fix.use.state || storyState(s) === fix.use.state)
|
|
3549
|
-
);
|
|
3550
|
-
if (!src) {
|
|
3551
|
-
record("not-found", 0, `no measured render of story ${fix.use.story}${fix.use.args ? ` with ${JSON.stringify(fix.use.args)}` : ""}`);
|
|
3899
|
+
for (const p2 of [...parts].reverse()) {
|
|
3900
|
+
if (!p2.parent || p2.kind !== "frame" || p2.pin || p2.cover || p2.overlay || paints(p2.id)) continue;
|
|
3901
|
+
const kids = parts.filter((c) => c.parent === p2.id);
|
|
3902
|
+
if (kids.length && kids.every((c) => c.overlay || c.pin || c.cover)) p2.cover = true;
|
|
3903
|
+
}
|
|
3904
|
+
const aligners = new Set(
|
|
3905
|
+
parts.filter((p2) => {
|
|
3906
|
+
const flow = parts.filter((c) => c.parent === p2.id && !c.pin && !c.cover && !c.overlay);
|
|
3907
|
+
return flow.length === 1 && !!p2.layout?.align?.[flow[0].id];
|
|
3908
|
+
}).map((p2) => p2.id)
|
|
3909
|
+
);
|
|
3910
|
+
for (const p2 of parts) if (p2.layout) p2.layout = alignedLayout(p2, parts);
|
|
3911
|
+
const parents = new Set(parts.filter((p2) => !p2.pin && !p2.cover).map((p2) => p2.parent).filter(Boolean));
|
|
3912
|
+
const variants = [];
|
|
3913
|
+
const axesMemo = /* @__PURE__ */ new Map();
|
|
3914
|
+
const shotOf = /* @__PURE__ */ new Map();
|
|
3915
|
+
const spacersOf = /* @__PURE__ */ new Map();
|
|
3916
|
+
const wrapsOf = /* @__PURE__ */ new Map();
|
|
3917
|
+
for (const { props, story: story2 } of byVariant.values()) {
|
|
3918
|
+
const m = story2.spec;
|
|
3919
|
+
const pairs = story2 === base.story ? new Map(parts.map((p2) => [p2.id, p2.id])) : pairParts(baseSpec.parts, m);
|
|
3920
|
+
const values = {};
|
|
3921
|
+
const vectors = {};
|
|
3922
|
+
const pins = {};
|
|
3923
|
+
const hidden = [];
|
|
3924
|
+
const spacers = {};
|
|
3925
|
+
const wraps = {};
|
|
3926
|
+
const layouts = {};
|
|
3927
|
+
for (const p2 of parts) {
|
|
3928
|
+
const mid = pairs.get(p2.id);
|
|
3929
|
+
if (mid === void 0) {
|
|
3930
|
+
hidden.push(p2.id);
|
|
3552
3931
|
continue;
|
|
3553
3932
|
}
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
const
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
if (
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
mark(copy, fix.id);
|
|
3564
|
-
let placed = false;
|
|
3565
|
-
file.stories = file.stories.flatMap((s) => others.has(s) ? placed ? [] : (placed = true, [copy]) : [s]);
|
|
3566
|
-
if (!placed) file.stories.push(copy);
|
|
3567
|
-
record("applied", others.size + 1, `variant ${whereWords(target)} drawn from ${src.id}${others.size ? `, in place of ${others.size} other stor${others.size === 1 ? "y" : "ies"}` : ""}`);
|
|
3568
|
-
} else {
|
|
3569
|
-
const h2 = hints(fix.component);
|
|
3570
|
-
if (!h2) {
|
|
3571
|
-
record("not-found", 0, `no component entry for ${fix.component} in the measure`);
|
|
3572
|
-
continue;
|
|
3933
|
+
values[p2.id] = valuesOf(m, mid, parents.has(p2.id) && !aligners.has(p2.id), known, !p2.parent && !!p2.fill, ownAxes(p2, parts, baseSpec, axesMemo));
|
|
3934
|
+
if (m.spacers?.[mid]) spacers[p2.id] = m.spacers[mid];
|
|
3935
|
+
if (p2.kind === "icon" || p2.kind === "vector") delete values[p2.id].fill;
|
|
3936
|
+
const mp = m.parts.find((x) => x.id === mid);
|
|
3937
|
+
const pin = mp?.pin ?? (mp?.overlay ? pinFromBoxes(m, mp) : void 0);
|
|
3938
|
+
if (p2.pin && pin && JSON.stringify(pin) !== JSON.stringify(p2.pin)) pins[p2.id] = pin;
|
|
3939
|
+
if (mp?.layout && p2.layout) {
|
|
3940
|
+
const layout = alignedLayout({ ...mp, id: p2.id }, m.parts.map((x) => ({ ...x, parent: x.parent === mid ? p2.id : x.parent })));
|
|
3941
|
+
if (layout && (layout.primary !== p2.layout.primary || layout.counter !== p2.layout.counter || layout.direction !== p2.layout.direction)) layouts[p2.id] = layout;
|
|
3573
3942
|
}
|
|
3574
|
-
if (
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
const st = (file.statuses ?? []).find((x) => lc$1(x.name) === lc$1(fix.component));
|
|
3578
|
-
if (st && st.status !== fix.status) {
|
|
3579
|
-
st.status = fix.status;
|
|
3580
|
-
delete st.reason;
|
|
3581
|
-
st.summary = fix.reason;
|
|
3582
|
-
}
|
|
3583
|
-
}
|
|
3584
|
-
} else h2.notes = [...h2.notes ?? [], { text: fix.reason, fix: fix.id, ...fix.where ? { where: fix.where } : {} }];
|
|
3585
|
-
record("applied", 0, fix.status ? `${fix.status === "differs" ? "Differs" : "Can't check"}: ${fix.reason}` : `note: ${fix.reason}`);
|
|
3943
|
+
if (mp?.wrap) wraps[p2.id] = mp.wrap.padding;
|
|
3944
|
+
const vec = p2.kind === "vector" ? m.vectors?.[mid] : void 0;
|
|
3945
|
+
if (vec) vectors[p2.id] = { svg: vec.svg, strokes: vec.strokes.every((v) => typeof v !== "string" || known(v)) ? [...vec.strokes] : [] };
|
|
3586
3946
|
}
|
|
3587
|
-
const
|
|
3588
|
-
|
|
3947
|
+
const variant = { props, values, ...hidden.length ? { hidden } : {}, ...Object.keys(vectors).length ? { vectors } : {}, ...Object.keys(pins).length ? { pins } : {}, ...Object.keys(layouts).length ? { layout: layouts } : {} };
|
|
3948
|
+
variants.push(variant);
|
|
3949
|
+
spacersOf.set(variant, spacers);
|
|
3950
|
+
wrapsOf.set(variant, wraps);
|
|
3951
|
+
if (story2.shot) shotOf.set(variant, story2.shot);
|
|
3589
3952
|
}
|
|
3590
|
-
|
|
3591
|
-
|
|
3953
|
+
textRootInFrame(parts, variants);
|
|
3954
|
+
spacerWrappers(parts, variants, spacersOf, wrapsOf);
|
|
3955
|
+
const properties = textProperties(parts, base.story, argTypes, new Set(axes.map((a2) => a2.name)));
|
|
3956
|
+
const kept = drawnAxes(axes, variants);
|
|
3957
|
+
const story = `${source.replace(/\/+$/, "")}/?path=/story/${base.story.id}`;
|
|
3958
|
+
const spec = { name, hash: "", source: [story], axes: kept.axes, properties, parts, variants: withoutIdleStates(kept.variants), unmapped: [] };
|
|
3959
|
+
const shots = {};
|
|
3960
|
+
kept.variants.forEach((v) => {
|
|
3961
|
+
const from = variants.find((x) => JSON.stringify(x.values) === JSON.stringify(v.values) && Object.entries(v.props).every(([k, val]) => x.props[k] === val));
|
|
3962
|
+
const shot = from && shotOf.get(from);
|
|
3963
|
+
if (shot) shots[variantKeyOf(v.props)] = shot;
|
|
3964
|
+
});
|
|
3965
|
+
return { spec: { ...spec, hash: hashSpec(spec) }, shots };
|
|
3592
3966
|
}
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3967
|
+
const TEXT_FIELDS = /* @__PURE__ */ new Set(["textFill", "fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "textDecoration"]);
|
|
3968
|
+
function textRootInFrame(parts, variants) {
|
|
3969
|
+
const root = parts.find((p2) => !p2.parent);
|
|
3970
|
+
if (!root || root.kind !== "text") return;
|
|
3971
|
+
const id = `${root.id}-text`;
|
|
3972
|
+
const text = { ...root, id, name: `${root.name} text`, parent: root.id };
|
|
3973
|
+
parts.splice(parts.indexOf(root), 1, { id: root.id, name: root.name, kind: "frame", ...root.source ? { source: root.source } : {}, layout: { direction: "HORIZONTAL", primary: "MIN", counter: "MIN" } }, text);
|
|
3974
|
+
for (const v of variants) {
|
|
3975
|
+
const all = v.values[root.id] ?? {};
|
|
3976
|
+
v.values[root.id] = Object.fromEntries(Object.entries(all).filter(([f, value]) => !TEXT_FIELDS.has(f) && !((f === "width" || f === "height") && typeof value === "number")));
|
|
3977
|
+
v.values[id] = Object.fromEntries(Object.entries(all).filter(([f]) => TEXT_FIELDS.has(f)));
|
|
3978
|
+
if (v.hidden?.includes(root.id)) v.hidden.push(id);
|
|
3979
|
+
}
|
|
3597
3980
|
}
|
|
3598
|
-
function
|
|
3599
|
-
|
|
3600
|
-
const
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3981
|
+
function ownAxes(part, parts, spec, memo = /* @__PURE__ */ new Map()) {
|
|
3982
|
+
const parent = parts.find((x) => x.id === part.parent);
|
|
3983
|
+
const dir = parent?.layout?.direction;
|
|
3984
|
+
const stretch = parent?.layout?.counter === "STRETCH";
|
|
3985
|
+
const sizedBy = (axis) => !parent || !spec || definiteAxis(parent, axis, parts, spec, memo);
|
|
3986
|
+
const acrossW = dir === "VERTICAL" && stretch && sizedBy("width");
|
|
3987
|
+
const alongW = dir === "HORIZONTAL" && !!part.grow && sizedBy("width");
|
|
3988
|
+
const acrossH = dir === "HORIZONTAL" && stretch && sizedBy("height");
|
|
3989
|
+
const alongH = dir === "VERTICAL" && !!part.grow && sizedBy("height");
|
|
3990
|
+
return {
|
|
3991
|
+
width: !(!part.parent && part.fill) && !acrossW && !alongW && !part.stretch?.width,
|
|
3992
|
+
height: !acrossH && !alongH && !part.stretch?.height
|
|
3993
|
+
};
|
|
3609
3994
|
}
|
|
3610
|
-
function
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
{
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
];
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
function checkedOf(s) {
|
|
3626
|
-
const p2 = propsOf(s);
|
|
3627
|
-
return p2.indeterminate === true ? "mixed" : String(p2.checked === true);
|
|
3628
|
-
}
|
|
3629
|
-
const LITERAL_FIELDS = ["fill", "textFill", "stroke", "fontFamily", "shadow"];
|
|
3630
|
-
function valuesOf(spec, partId, hasChildren, known, fills = false, own = { width: true, height: true }) {
|
|
3631
|
-
const sized = { ...hasChildren ? boxSized(spec, partId, own) : {}, ...spec.sized?.[partId] };
|
|
3632
|
-
const v = { ...spec.values[partId] ?? {} };
|
|
3633
|
-
const raw = spec.raw[partId] ?? {};
|
|
3634
|
-
for (const field of Object.keys(v)) {
|
|
3635
|
-
const value = v[field];
|
|
3636
|
-
if (typeof value !== "string" || value.startsWith("calc(") || known(value)) continue;
|
|
3637
|
-
const literal = raw[field];
|
|
3638
|
-
if (literal === void 0) delete v[field];
|
|
3639
|
-
else v[field] = literal;
|
|
3640
|
-
}
|
|
3641
|
-
for (const field of LITERAL_FIELDS) if (v[field] === void 0 && typeof raw[field] === "string") v[field] = raw[field];
|
|
3642
|
-
if (hasChildren) {
|
|
3643
|
-
if (typeof v.width === "number" && !fills) delete v.width;
|
|
3644
|
-
if (typeof v.height === "number") delete v.height;
|
|
3645
|
-
}
|
|
3646
|
-
if (sized?.width !== void 0) v.width = sized.width;
|
|
3647
|
-
if (sized?.height !== void 0) v.height = sized.height;
|
|
3648
|
-
return v;
|
|
3649
|
-
}
|
|
3650
|
-
function boxSized(spec, partId, own) {
|
|
3651
|
-
const box = spec.boxes?.[partId];
|
|
3652
|
-
const kids = spec.parts.filter((c) => c.parent === partId && !c.pin && !c.cover && !c.overlay).map((c) => spec.boxes?.[c.id]).filter((b) => !!b);
|
|
3653
|
-
if (!box || !kids.length) return {};
|
|
3654
|
-
const raw = spec.raw[partId] ?? {};
|
|
3655
|
-
const num = (f) => typeof raw[f] === "number" ? raw[f] : 0;
|
|
3656
|
-
const left = Math.min(...kids.map((k) => k.x));
|
|
3657
|
-
const top = Math.min(...kids.map((k) => k.y));
|
|
3658
|
-
const side = (f) => typeof raw[f] === "number" ? raw[f] : num("strokeWeight");
|
|
3659
|
-
const contentW = Math.max(...kids.map((k) => k.x + k.width)) - left + num("paddingLeft") + num("paddingRight") + side("strokeLeftWeight") + side("strokeRightWeight");
|
|
3660
|
-
const contentH = Math.max(...kids.map((k) => k.y + k.height)) - top + num("paddingTop") + num("paddingBottom") + side("strokeTopWeight") + side("strokeBottomWeight");
|
|
3661
|
-
const out = {};
|
|
3662
|
-
if (own.width && box.width - contentW > 1) out.width = box.width;
|
|
3663
|
-
if (own.height && box.height - contentH > 1) out.height = box.height;
|
|
3664
|
-
return out;
|
|
3665
|
-
}
|
|
3666
|
-
function setOf(name, stories, argTypes, source, known, hints = {}) {
|
|
3667
|
-
const propName = (prop) => hints.properties?.[prop] ?? axisName(prop);
|
|
3668
|
-
const figmaValue = (prop, value) => hints.values?.[prop]?.[value] ?? value;
|
|
3669
|
-
const lead = stories.find((s) => s.name === "Default" && !s.state) ?? stories[0];
|
|
3670
|
-
const baseline = (k, a2) => (lead ? propsOf(lead)[k] : void 0) ?? a2.default ?? false;
|
|
3671
|
-
const shown = stories.filter((s) => !/skeleton/i.test(s.name));
|
|
3672
|
-
const ordered = [...shown.length ? shown : stories].sort((a2, b) => Number(!!a2.gallery) - Number(!!b.gallery));
|
|
3673
|
-
const valueOf = (s, k, a2) => (propsOf(s)[k] ?? a2.default ?? false) === true;
|
|
3674
|
-
const flags = Object.entries(argTypes).filter(
|
|
3675
|
-
([k, a2]) => a2.kind === "boolean" && !STATE_PROPS.has(k) && !NOT_AN_AXIS.test(k) && ordered.some((s) => valueOf(s, k, a2)) && ordered.some((s) => !valueOf(s, k, a2))
|
|
3676
|
-
);
|
|
3677
|
-
const plain2 = ordered.filter(
|
|
3678
|
-
(s) => Object.entries(argTypes).every(
|
|
3679
|
-
([k, a2]) => a2.kind !== "boolean" || STATE_PROPS.has(k) || NOT_AN_AXIS.test(k) || flags.some(([f]) => f === k) || (propsOf(s)[k] ?? a2.default ?? false) === baseline(k, a2)
|
|
3680
|
-
)
|
|
3681
|
-
);
|
|
3682
|
-
if (!plain2.length) return void 0;
|
|
3683
|
-
const axes = [];
|
|
3684
|
-
for (const [prop, a2] of flags) {
|
|
3685
|
-
const def = String(baseline(prop, a2) === true);
|
|
3686
|
-
axes.push({ name: propName(prop), prop, values: [figmaValue(prop, def), figmaValue(prop, def === "true" ? "false" : "true")], default: figmaValue(prop, def) });
|
|
3687
|
-
}
|
|
3688
|
-
for (const [prop, a2] of Object.entries(argTypes)) {
|
|
3689
|
-
if (a2.kind !== "enum" || NOT_AN_AXIS.test(prop) || !a2.options || a2.options.length < 2 || a2.options.length > 12) continue;
|
|
3690
|
-
const seen = new Set(plain2.map((s) => String(propsOf(s)[prop] ?? a2.default ?? a2.options[0])));
|
|
3691
|
-
const values = a2.options.filter((o) => seen.has(o));
|
|
3692
|
-
if (values.length < 2) continue;
|
|
3693
|
-
const def = a2.default !== void 0 && values.includes(String(a2.default)) ? String(a2.default) : values[0];
|
|
3694
|
-
axes.push({ name: propName(prop), prop, values: [...new Set(values.map((v) => figmaValue(prop, v)))], default: figmaValue(prop, def) });
|
|
3695
|
-
}
|
|
3696
|
-
if ("checked" in argTypes || plain2.some((s) => "checked" in propsOf(s))) {
|
|
3697
|
-
const values = ["false", "true", "mixed"].filter((v) => plain2.some((s) => checkedOf(s) === v));
|
|
3698
|
-
if (values.length > 1) axes.push({ name: "Checked", values, default: values[0] });
|
|
3699
|
-
}
|
|
3700
|
-
const states = STATES.filter((st) => plain2.some(st.of)).map((st) => st.value);
|
|
3701
|
-
if (states.length > 1) axes.push({ name: "State", values: states, default: states[0] });
|
|
3702
|
-
const keyOf = (s) => {
|
|
3703
|
-
const props = {};
|
|
3704
|
-
for (const axis of axes) {
|
|
3705
|
-
const flag = flags.find(([k]) => k === axis.prop);
|
|
3706
|
-
const value = axis.name === "State" ? STATES.find((st) => st.of(s))?.value : axis.name === "Checked" ? checkedOf(s) : flag ? figmaValue(flag[0], String(valueOf(s, flag[0], flag[1]))) : propsOf(s)[axis.prop] === void 0 ? axis.default : figmaValue(axis.prop, String(propsOf(s)[axis.prop]));
|
|
3707
|
-
if (value === void 0 || !axis.values.includes(value)) return void 0;
|
|
3708
|
-
props[axis.name] = value;
|
|
3709
|
-
}
|
|
3710
|
-
return props;
|
|
3711
|
-
};
|
|
3712
|
-
const candidates = /* @__PURE__ */ new Map();
|
|
3713
|
-
for (const s of plain2) {
|
|
3714
|
-
const props = keyOf(s);
|
|
3715
|
-
if (!props) continue;
|
|
3716
|
-
const k = JSON.stringify(props);
|
|
3717
|
-
if (!candidates.has(k)) candidates.set(k, { props, stories: [] });
|
|
3718
|
-
candidates.get(k).stories.push(s);
|
|
3719
|
-
}
|
|
3720
|
-
const storyFor = /* @__PURE__ */ new Map();
|
|
3721
|
-
if (axes.some((a2) => a2.name === "State")) {
|
|
3722
|
-
const rest = (p2) => JSON.stringify(Object.entries(p2).filter(([k]) => k !== "State"));
|
|
3723
|
-
const groups = /* @__PURE__ */ new Map();
|
|
3724
|
-
for (const c of candidates.values()) groups.set(rest(c.props), [...groups.get(rest(c.props)) ?? [], c]);
|
|
3725
|
-
for (const [key2, group] of groups) {
|
|
3726
|
-
const ids = [...new Set(group.flatMap((c) => c.stories.map((s) => s.id)))];
|
|
3727
|
-
const score = (id) => group.filter((c) => c.stories.some((s) => s.id === id)).length;
|
|
3728
|
-
storyFor.set(key2, ids.reduce((best, id) => score(id) > score(best) ? id : best, ids[0]));
|
|
3729
|
-
}
|
|
3730
|
-
for (const c of candidates.values()) {
|
|
3731
|
-
const same = c.stories.filter((s) => s.id === storyFor.get(rest(c.props)));
|
|
3732
|
-
if (same.length) c.stories = same;
|
|
3733
|
-
}
|
|
3734
|
-
}
|
|
3735
|
-
const byVariant = new Map([...candidates].map(([k, c]) => [k, { props: c.props, story: c.stories[0] }]));
|
|
3736
|
-
if (!byVariant.size) return void 0;
|
|
3737
|
-
const isDefault = (p2) => axes.every((a2) => p2[a2.name] === a2.default);
|
|
3738
|
-
const base = [...byVariant.values()].find((v) => isDefault(v.props)) ?? [...byVariant.values()][0];
|
|
3739
|
-
const baseSpec = base.story.spec;
|
|
3740
|
-
const parts = baseSpec.parts.map((p2) => baseSpec.vectors?.[p2.id] ? { ...p2, kind: "vector" } : { ...p2 });
|
|
3741
|
-
for (const p2 of parts) {
|
|
3742
|
-
if (!p2.overlay || p2.pin || p2.cover) continue;
|
|
3743
|
-
const pin = pinFromBoxes(baseSpec, p2);
|
|
3744
|
-
if (pin) p2.pin = pin;
|
|
3745
|
-
else p2.cover = true;
|
|
3746
|
-
}
|
|
3747
|
-
const paints = (id) => {
|
|
3748
|
-
const v = baseSpec.values[id] ?? {};
|
|
3749
|
-
return [v.fill, v.stroke].some((x) => x !== void 0 && x !== null);
|
|
3750
|
-
};
|
|
3751
|
-
for (const p2 of [...parts].reverse()) {
|
|
3752
|
-
if (!p2.parent || p2.kind !== "frame" || p2.pin || p2.cover || p2.overlay || paints(p2.id)) continue;
|
|
3753
|
-
const kids = parts.filter((c) => c.parent === p2.id);
|
|
3754
|
-
if (kids.length && kids.every((c) => c.overlay || c.pin || c.cover)) p2.cover = true;
|
|
3755
|
-
}
|
|
3756
|
-
const aligners = new Set(
|
|
3757
|
-
parts.filter((p2) => {
|
|
3758
|
-
const flow = parts.filter((c) => c.parent === p2.id && !c.pin && !c.cover && !c.overlay);
|
|
3759
|
-
return flow.length === 1 && !!p2.layout?.align?.[flow[0].id];
|
|
3760
|
-
}).map((p2) => p2.id)
|
|
3761
|
-
);
|
|
3762
|
-
for (const p2 of parts) if (p2.layout) p2.layout = alignedLayout(p2, parts);
|
|
3763
|
-
const parents = new Set(parts.filter((p2) => !p2.pin && !p2.cover).map((p2) => p2.parent).filter(Boolean));
|
|
3764
|
-
const variants = [];
|
|
3765
|
-
const axesMemo = /* @__PURE__ */ new Map();
|
|
3766
|
-
const shotOf = /* @__PURE__ */ new Map();
|
|
3767
|
-
const spacersOf = /* @__PURE__ */ new Map();
|
|
3768
|
-
const wrapsOf = /* @__PURE__ */ new Map();
|
|
3769
|
-
for (const { props, story: story2 } of byVariant.values()) {
|
|
3770
|
-
const m = story2.spec;
|
|
3771
|
-
const pairs = story2 === base.story ? new Map(parts.map((p2) => [p2.id, p2.id])) : pairParts(baseSpec.parts, m);
|
|
3772
|
-
const values = {};
|
|
3773
|
-
const vectors = {};
|
|
3774
|
-
const pins = {};
|
|
3775
|
-
const hidden = [];
|
|
3776
|
-
const spacers = {};
|
|
3777
|
-
const wraps = {};
|
|
3778
|
-
const layouts = {};
|
|
3779
|
-
for (const p2 of parts) {
|
|
3780
|
-
const mid = pairs.get(p2.id);
|
|
3781
|
-
if (mid === void 0) {
|
|
3782
|
-
hidden.push(p2.id);
|
|
3783
|
-
continue;
|
|
3784
|
-
}
|
|
3785
|
-
values[p2.id] = valuesOf(m, mid, parents.has(p2.id) && !aligners.has(p2.id), known, !p2.parent && !!p2.fill, ownAxes(p2, parts, baseSpec, axesMemo));
|
|
3786
|
-
if (m.spacers?.[mid]) spacers[p2.id] = m.spacers[mid];
|
|
3787
|
-
if (p2.kind === "icon" || p2.kind === "vector") delete values[p2.id].fill;
|
|
3788
|
-
const mp = m.parts.find((x) => x.id === mid);
|
|
3789
|
-
const pin = mp?.pin ?? (mp?.overlay ? pinFromBoxes(m, mp) : void 0);
|
|
3790
|
-
if (p2.pin && pin && JSON.stringify(pin) !== JSON.stringify(p2.pin)) pins[p2.id] = pin;
|
|
3791
|
-
if (mp?.layout && p2.layout) {
|
|
3792
|
-
const layout = alignedLayout({ ...mp, id: p2.id }, m.parts.map((x) => ({ ...x, parent: x.parent === mid ? p2.id : x.parent })));
|
|
3793
|
-
if (layout && (layout.primary !== p2.layout.primary || layout.counter !== p2.layout.counter || layout.direction !== p2.layout.direction)) layouts[p2.id] = layout;
|
|
3794
|
-
}
|
|
3795
|
-
if (mp?.wrap) wraps[p2.id] = mp.wrap.padding;
|
|
3796
|
-
const vec = p2.kind === "vector" ? m.vectors?.[mid] : void 0;
|
|
3797
|
-
if (vec) vectors[p2.id] = { svg: vec.svg, strokes: vec.strokes.every((v) => typeof v !== "string" || known(v)) ? [...vec.strokes] : [] };
|
|
3798
|
-
}
|
|
3799
|
-
const variant = { props, values, ...hidden.length ? { hidden } : {}, ...Object.keys(vectors).length ? { vectors } : {}, ...Object.keys(pins).length ? { pins } : {}, ...Object.keys(layouts).length ? { layout: layouts } : {} };
|
|
3800
|
-
variants.push(variant);
|
|
3801
|
-
spacersOf.set(variant, spacers);
|
|
3802
|
-
wrapsOf.set(variant, wraps);
|
|
3803
|
-
if (story2.shot) shotOf.set(variant, story2.shot);
|
|
3804
|
-
}
|
|
3805
|
-
textRootInFrame(parts, variants);
|
|
3806
|
-
spacerWrappers(parts, variants, spacersOf, wrapsOf);
|
|
3807
|
-
const properties = textProperties(parts, base.story, argTypes, new Set(axes.map((a2) => a2.name)));
|
|
3808
|
-
const kept = drawnAxes(axes, variants);
|
|
3809
|
-
const story = `${source.replace(/\/+$/, "")}/?path=/story/${base.story.id}`;
|
|
3810
|
-
const spec = { name, hash: "", source: [story], axes: kept.axes, properties, parts, variants: withoutIdleStates(kept.variants), unmapped: [] };
|
|
3811
|
-
const shots = {};
|
|
3812
|
-
kept.variants.forEach((v) => {
|
|
3813
|
-
const from = variants.find((x) => JSON.stringify(x.values) === JSON.stringify(v.values) && Object.entries(v.props).every(([k, val]) => x.props[k] === val));
|
|
3814
|
-
const shot = from && shotOf.get(from);
|
|
3815
|
-
if (shot) shots[variantKeyOf(v.props)] = shot;
|
|
3816
|
-
});
|
|
3817
|
-
return { spec: { ...spec, hash: hashSpec(spec) }, shots };
|
|
3818
|
-
}
|
|
3819
|
-
const TEXT_FIELDS = /* @__PURE__ */ new Set(["textFill", "fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "textDecoration"]);
|
|
3820
|
-
function textRootInFrame(parts, variants) {
|
|
3821
|
-
const root = parts.find((p2) => !p2.parent);
|
|
3822
|
-
if (!root || root.kind !== "text") return;
|
|
3823
|
-
const id = `${root.id}-text`;
|
|
3824
|
-
const text = { ...root, id, name: `${root.name} text`, parent: root.id };
|
|
3825
|
-
parts.splice(parts.indexOf(root), 1, { id: root.id, name: root.name, kind: "frame", ...root.source ? { source: root.source } : {}, layout: { direction: "HORIZONTAL", primary: "MIN", counter: "MIN" } }, text);
|
|
3826
|
-
for (const v of variants) {
|
|
3827
|
-
const all = v.values[root.id] ?? {};
|
|
3828
|
-
v.values[root.id] = Object.fromEntries(Object.entries(all).filter(([f, value]) => !TEXT_FIELDS.has(f) && !((f === "width" || f === "height") && typeof value === "number")));
|
|
3829
|
-
v.values[id] = Object.fromEntries(Object.entries(all).filter(([f]) => TEXT_FIELDS.has(f)));
|
|
3830
|
-
if (v.hidden?.includes(root.id)) v.hidden.push(id);
|
|
3831
|
-
}
|
|
3832
|
-
}
|
|
3833
|
-
function ownAxes(part, parts, spec, memo = /* @__PURE__ */ new Map()) {
|
|
3834
|
-
const parent = parts.find((x) => x.id === part.parent);
|
|
3835
|
-
const dir = parent?.layout?.direction;
|
|
3836
|
-
const stretch = parent?.layout?.counter === "STRETCH";
|
|
3837
|
-
const sizedBy = (axis) => !parent || !spec || definiteAxis(parent, axis, parts, spec, memo);
|
|
3838
|
-
const acrossW = dir === "VERTICAL" && stretch && sizedBy("width");
|
|
3839
|
-
const alongW = dir === "HORIZONTAL" && !!part.grow && sizedBy("width");
|
|
3840
|
-
const acrossH = dir === "HORIZONTAL" && stretch && sizedBy("height");
|
|
3841
|
-
const alongH = dir === "VERTICAL" && !!part.grow && sizedBy("height");
|
|
3842
|
-
return {
|
|
3843
|
-
width: !(!part.parent && part.fill) && !acrossW && !alongW && !part.stretch?.width,
|
|
3844
|
-
height: !acrossH && !alongH && !part.stretch?.height
|
|
3845
|
-
};
|
|
3846
|
-
}
|
|
3847
|
-
function definiteAxis(part, axis, parts, spec, memo) {
|
|
3848
|
-
const key2 = `${part.id}:${axis}`;
|
|
3849
|
-
const known = memo.get(key2);
|
|
3850
|
-
if (known !== void 0) return known;
|
|
3851
|
-
memo.set(key2, false);
|
|
3852
|
-
let result;
|
|
3853
|
-
if (!part.parent) result = axis === "width" && !!part.fill;
|
|
3854
|
-
else if (spec.sized?.[part.id]?.[axis] !== void 0) result = true;
|
|
3855
|
-
else {
|
|
3856
|
-
const own = ownAxes(part, parts, spec, memo);
|
|
3857
|
-
const hasKids = parts.some((c) => c.parent === part.id && !c.pin && !c.cover && !c.overlay);
|
|
3858
|
-
result = !own[axis] || (hasKids ? boxSized(spec, part.id, own)[axis] !== void 0 : typeof spec.values[part.id]?.[axis] === "number");
|
|
3859
|
-
}
|
|
3860
|
-
memo.set(key2, result);
|
|
3861
|
-
return result;
|
|
3995
|
+
function definiteAxis(part, axis, parts, spec, memo) {
|
|
3996
|
+
const key2 = `${part.id}:${axis}`;
|
|
3997
|
+
const known = memo.get(key2);
|
|
3998
|
+
if (known !== void 0) return known;
|
|
3999
|
+
memo.set(key2, false);
|
|
4000
|
+
let result;
|
|
4001
|
+
if (!part.parent) result = axis === "width" && !!part.fill;
|
|
4002
|
+
else if (spec.sized?.[part.id]?.[axis] !== void 0) result = true;
|
|
4003
|
+
else {
|
|
4004
|
+
const own = ownAxes(part, parts, spec, memo);
|
|
4005
|
+
const hasKids = parts.some((c) => c.parent === part.id && !c.pin && !c.cover && !c.overlay);
|
|
4006
|
+
result = !own[axis] || (hasKids ? boxSized(spec, part.id, own)[axis] !== void 0 : typeof spec.values[part.id]?.[axis] === "number");
|
|
4007
|
+
}
|
|
4008
|
+
memo.set(key2, result);
|
|
4009
|
+
return result;
|
|
3862
4010
|
}
|
|
3863
4011
|
function pinFromBoxes(spec, part) {
|
|
3864
4012
|
const box = spec.boxes?.[part.id];
|
|
@@ -3899,7 +4047,7 @@ function spacerWrappers(parts, variants, spacersOf, wrapsOf = /* @__PURE__ */ ne
|
|
|
3899
4047
|
}
|
|
3900
4048
|
function textProperties(parts, story, argTypes, axes) {
|
|
3901
4049
|
const properties = [];
|
|
3902
|
-
const props = propsOf(story);
|
|
4050
|
+
const props = propsOf$1(story);
|
|
3903
4051
|
for (const [k, a2] of Object.entries(argTypes)) {
|
|
3904
4052
|
const value = props[k];
|
|
3905
4053
|
if (a2.kind !== "text" || NOT_AN_AXIS.test(k) || typeof value !== "string" || !value.trim()) continue;
|
|
@@ -3948,36 +4096,465 @@ function drawnAxes(axes, variants) {
|
|
|
3948
4096
|
variants: variants2.map((v) => ({ ...v, props: Object.fromEntries(Object.entries(v.props).filter(([k]) => k !== axis.name)) }))
|
|
3949
4097
|
};
|
|
3950
4098
|
}
|
|
3951
|
-
return current;
|
|
4099
|
+
return current;
|
|
4100
|
+
}
|
|
4101
|
+
function specsFromMeasure(file, opts = {}) {
|
|
4102
|
+
const known = opts.known ?? (() => true);
|
|
4103
|
+
const key2 = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
4104
|
+
const only = opts.only === void 0 ? void 0 : key2(opts.only);
|
|
4105
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4106
|
+
for (const s of file.stories) {
|
|
4107
|
+
if (s.error || !s.spec) continue;
|
|
4108
|
+
const name = setNameOf$2(s.title);
|
|
4109
|
+
if (only !== void 0 && key2(name) !== only) continue;
|
|
4110
|
+
groups.set(name, [...groups.get(name) ?? [], s]);
|
|
4111
|
+
}
|
|
4112
|
+
const argTypesOf = (name) => Object.assign({}, ...(file.components ?? []).filter((c) => setNameOf$2(c.title) === name).map((c) => c.argTypes));
|
|
4113
|
+
const hints = {};
|
|
4114
|
+
for (const c of file.components ?? []) if (c.figma) hints[setNameOf$2(c.title)] = { ...hints[setNameOf$2(c.title)], ...c.figma };
|
|
4115
|
+
const components = [];
|
|
4116
|
+
const shots = {};
|
|
4117
|
+
const report = { sets: 0, variants: 0, stories: 0, skipped: [] };
|
|
4118
|
+
for (const [name, stories] of [...groups].sort(([a2], [b]) => a2.localeCompare(b))) {
|
|
4119
|
+
const built = setOf(name, stories, argTypesOf(name), file.storybook, known, hints[name]);
|
|
4120
|
+
if (!built) {
|
|
4121
|
+
report.skipped.push(name);
|
|
4122
|
+
continue;
|
|
4123
|
+
}
|
|
4124
|
+
const spec = built.spec;
|
|
4125
|
+
components.push(spec);
|
|
4126
|
+
if (Object.keys(built.shots).length) shots[name] = built.shots;
|
|
4127
|
+
report.sets++;
|
|
4128
|
+
report.variants += spec.variants.length;
|
|
4129
|
+
report.stories += stories.length;
|
|
4130
|
+
}
|
|
4131
|
+
return { specs: { version: 1, generated: `Measured from ${file.storybook} on ${file.measuredAt} (bridge specs --measure).`, components }, report, shots, hints };
|
|
4132
|
+
}
|
|
4133
|
+
const FIGMA_FIXES_VERSION = 1;
|
|
4134
|
+
const DEFAULT_FIGMA_FIXES_FILE = "figma-fixes.json";
|
|
4135
|
+
class FigmaFixesError extends Error {
|
|
4136
|
+
constructor(problems) {
|
|
4137
|
+
super(`figma-fixes.json: ${problems.join("; ")}`);
|
|
4138
|
+
this.problems = problems;
|
|
4139
|
+
}
|
|
4140
|
+
problems;
|
|
4141
|
+
}
|
|
4142
|
+
const isObj$3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4143
|
+
const STATES = ["rest", "hover", "active", "focus-visible", "disabled"];
|
|
4144
|
+
const KINDS = ["part", "axis", "story", "status"];
|
|
4145
|
+
const BASE_KEYS = ["id", "component", "reason", "by", "where", "kind"];
|
|
4146
|
+
const KEYS = {
|
|
4147
|
+
part: ["part", "set", "width", "height", "direction", "align", "overlay", "hide"],
|
|
4148
|
+
axis: ["action", "arg", "property", "values"],
|
|
4149
|
+
story: ["use"],
|
|
4150
|
+
status: ["status"]
|
|
4151
|
+
};
|
|
4152
|
+
const PRIMARY = ["MIN", "CENTER", "MAX", "SPACE_BETWEEN"];
|
|
4153
|
+
const COUNTER = ["MIN", "CENTER", "MAX", "STRETCH"];
|
|
4154
|
+
const plain = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
4155
|
+
function whereProblems(w, at2) {
|
|
4156
|
+
if (!isObj$3(w)) return at2("where must be an object with args and/or state");
|
|
4157
|
+
for (const k of Object.keys(w)) if (k !== "args" && k !== "state") at2(`where.${k} is not known (args, state)`);
|
|
4158
|
+
if (w.args !== void 0 && (!isObj$3(w.args) || !Object.values(w.args).every(plain))) at2("where.args maps arg names to a text, number or true/false");
|
|
4159
|
+
if (w.state !== void 0 && !STATES.includes(w.state)) at2(`where.state is one of ${STATES.join(", ")}`);
|
|
4160
|
+
}
|
|
4161
|
+
function validateFigmaFixes(data) {
|
|
4162
|
+
const out = [];
|
|
4163
|
+
if (!isObj$3(data)) return ["expected an object with version and fixes"];
|
|
4164
|
+
if (data.version !== FIGMA_FIXES_VERSION) out.push(`version ${String(data.version)} is not supported (expected ${FIGMA_FIXES_VERSION})`);
|
|
4165
|
+
for (const k of Object.keys(data)) if (!["$schema", "version", "fixes"].includes(k)) out.push(`${k} is not known (version, fixes)`);
|
|
4166
|
+
if (!Array.isArray(data.fixes)) return [...out, "fixes must be a list"];
|
|
4167
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4168
|
+
data.fixes.forEach((f, i) => {
|
|
4169
|
+
const label = isObj$3(f) && typeof f.id === "string" ? `fix ${f.id}` : `fix ${i + 1}`;
|
|
4170
|
+
const at2 = (why) => out.push(`${label}: ${why}`);
|
|
4171
|
+
if (!isObj$3(f)) return at2("must be an object");
|
|
4172
|
+
if (typeof f.id !== "string" || !/^[a-z0-9][a-z0-9-]{0,79}$/.test(f.id)) at2("id must be lower case letters, digits and dashes");
|
|
4173
|
+
else if (ids.has(f.id)) at2("id is repeated");
|
|
4174
|
+
else ids.add(f.id);
|
|
4175
|
+
if (typeof f.component !== "string" || !f.component.trim()) at2('component is missing (the Figma set name, like "Toggle")');
|
|
4176
|
+
if (typeof f.reason !== "string" || f.reason.trim().length < 8) at2("reason is missing: say why, in a sentence");
|
|
4177
|
+
else if (/\u2014/.test(f.reason)) at2("reason uses an em dash: use a colon or a comma");
|
|
4178
|
+
if (f.by !== void 0 && typeof f.by !== "string") at2("by must be a text");
|
|
4179
|
+
if (f.where !== void 0) whereProblems(f.where, at2);
|
|
4180
|
+
if (!KINDS.includes(f.kind)) return at2(`kind must be one of ${KINDS.join(", ")}`);
|
|
4181
|
+
const kind = f.kind;
|
|
4182
|
+
for (const k of Object.keys(f)) if (!BASE_KEYS.includes(k) && !KEYS[kind].includes(k)) at2(`${k} is not a key of a ${kind} fix`);
|
|
4183
|
+
if (kind === "part") partProblems(f, at2);
|
|
4184
|
+
if (kind === "axis") axisProblems(f, at2);
|
|
4185
|
+
if (kind === "story") {
|
|
4186
|
+
const u = f.use;
|
|
4187
|
+
if (!isObj$3(u) || typeof u.story !== "string" || !u.story) at2("use.story is the story id to draw the variant from");
|
|
4188
|
+
else {
|
|
4189
|
+
for (const k of Object.keys(u)) if (!["story", "args", "state"].includes(k)) at2(`use.${k} is not known (story, args, state)`);
|
|
4190
|
+
if (u.args !== void 0 && (!isObj$3(u.args) || !Object.values(u.args).every(plain))) at2("use.args maps arg names to a text, number or true/false");
|
|
4191
|
+
if (u.state !== void 0 && !STATES.includes(u.state)) at2(`use.state is one of ${STATES.join(", ")}`);
|
|
4192
|
+
}
|
|
4193
|
+
if (!isObj$3(f.where) || !f.where.args && !f.where.state) at2("where names the variant (args and/or state) the story is for");
|
|
4194
|
+
}
|
|
4195
|
+
if (kind === "status") {
|
|
4196
|
+
if (f.status === "matches") at2('status cannot be "matches": only the shot comparison says a component matches');
|
|
4197
|
+
else if (f.status !== void 0 && f.status !== "differs" && f.status !== "cant-check") at2("status is differs or cant-check (absent: a note)");
|
|
4198
|
+
}
|
|
4199
|
+
});
|
|
4200
|
+
return out;
|
|
4201
|
+
}
|
|
4202
|
+
function partProblems(f, at2) {
|
|
4203
|
+
if (typeof f.part !== "string" || !f.part.trim()) at2("part is a part id or a .class");
|
|
4204
|
+
const changes = ["set", "width", "height", "direction", "align", "overlay", "hide"].filter((k) => f[k] !== void 0);
|
|
4205
|
+
if (!changes.length) at2("changes nothing: give set, width, height, direction, align, overlay or hide");
|
|
4206
|
+
if (f.hide !== void 0 && typeof f.hide !== "boolean") at2("hide is true or false");
|
|
4207
|
+
if (f.hide === true && changes.length > 1) at2("hide goes alone: a hidden part has nothing else to change");
|
|
4208
|
+
if (f.set !== void 0) {
|
|
4209
|
+
if (!isObj$3(f.set) || !Object.keys(f.set).length) at2("set maps fields to values");
|
|
4210
|
+
else
|
|
4211
|
+
for (const [field, value] of Object.entries(f.set)) {
|
|
4212
|
+
if (!SPEC_FIELDS.includes(field)) {
|
|
4213
|
+
at2(`set.${field} is not a field (${SPEC_FIELDS.join(", ")})`);
|
|
4214
|
+
continue;
|
|
4215
|
+
}
|
|
4216
|
+
const color = COLOR_FIELDS.has(field);
|
|
4217
|
+
if (value === "full") {
|
|
4218
|
+
if (field !== "cornerRadius") at2(`set.${field}: "full" is for cornerRadius only`);
|
|
4219
|
+
} else if (value === null) {
|
|
4220
|
+
if (!color) at2(`set.${field} cannot be none (only colors can)`);
|
|
4221
|
+
} else if (typeof value === "number") {
|
|
4222
|
+
if (color) at2(`set.${field} is a color: a token path or null, not a number`);
|
|
4223
|
+
else if (!Number.isFinite(value)) at2(`set.${field} must be a finite number`);
|
|
4224
|
+
} else if (typeof value !== "string" || !value.trim()) at2(`set.${field} must be a token path, a number or null`);
|
|
4225
|
+
else if (field === "textDecoration" && !/^(UNDERLINE|STRIKETHROUGH|NONE)$/.test(value)) at2(`set.textDecoration is UNDERLINE, STRIKETHROUGH or NONE`);
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
for (const side of ["width", "height"]) {
|
|
4229
|
+
const v = f[side];
|
|
4230
|
+
if (v !== void 0 && !["hug", "fill", "fixed"].includes(v) && !(typeof v === "number" && v > 0)) at2(`${side} is hug, fill, fixed or a size in px`);
|
|
4231
|
+
}
|
|
4232
|
+
if (f.direction !== void 0 && f.direction !== "HORIZONTAL" && f.direction !== "VERTICAL") at2("direction is HORIZONTAL or VERTICAL");
|
|
4233
|
+
if (f.align !== void 0) {
|
|
4234
|
+
const a2 = f.align;
|
|
4235
|
+
if (!isObj$3(a2) || a2.primary === void 0 && a2.counter === void 0) at2("align gives primary and/or counter");
|
|
4236
|
+
else {
|
|
4237
|
+
if (a2.primary !== void 0 && !PRIMARY.includes(a2.primary)) at2(`align.primary is ${PRIMARY.join(", ")}`);
|
|
4238
|
+
if (a2.counter !== void 0 && !COUNTER.includes(a2.counter)) at2(`align.counter is ${COUNTER.join(", ")}`);
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
if (f.overlay !== void 0 && typeof f.overlay !== "boolean") {
|
|
4242
|
+
const o = f.overlay;
|
|
4243
|
+
if (!isObj$3(o) || !Object.keys(o).length || !Object.entries(o).every(([k, v]) => ["top", "right", "bottom", "left"].includes(k) && typeof v === "number")) at2('overlay is true, false, or offsets in px ({ "top": 3, "left": 3 })');
|
|
4244
|
+
}
|
|
4245
|
+
if (f.part === "root" && (f.hide === true || f.overlay !== void 0)) at2("the root cannot be hidden or overlaid");
|
|
4246
|
+
}
|
|
4247
|
+
function axisProblems(f, at2) {
|
|
4248
|
+
const action = f.action;
|
|
4249
|
+
if (action !== "map" && action !== "ignore" && action !== "prune") return at2("action is map, ignore or prune");
|
|
4250
|
+
if (action !== "prune" && (typeof f.arg !== "string" || !f.arg)) at2(`${action} needs arg (the story arg, like "kind")`);
|
|
4251
|
+
if (action === "map") {
|
|
4252
|
+
if (f.property === void 0 && f.values === void 0) at2("map gives property (the Figma name) and/or values");
|
|
4253
|
+
if (f.property !== void 0 && (typeof f.property !== "string" || !f.property.trim())) at2("property is the Figma property name");
|
|
4254
|
+
if (f.values !== void 0 && (!isObj$3(f.values) || !Object.values(f.values).every((v) => typeof v === "string" && v.trim()))) at2("values maps code values to Figma values (texts)");
|
|
4255
|
+
else if (isObj$3(f.values)) {
|
|
4256
|
+
const figma = Object.values(f.values);
|
|
4257
|
+
if (new Set(figma).size !== figma.length) at2("values gives two code values the same Figma value: use prune for the one to drop");
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
if (action === "ignore" && (f.property !== void 0 || f.values !== void 0)) at2("ignore takes no property or values");
|
|
4261
|
+
if (action === "prune") {
|
|
4262
|
+
if (!isObj$3(f.where) || !f.where.args && !f.where.state) at2("prune needs where (the args and/or state of the variants to drop)");
|
|
4263
|
+
if (f.property !== void 0 || f.values !== void 0) at2("prune takes no property or values");
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
function parseFigmaFixes(input) {
|
|
4267
|
+
let data = input;
|
|
4268
|
+
if (typeof input === "string") {
|
|
4269
|
+
try {
|
|
4270
|
+
data = JSON.parse(input);
|
|
4271
|
+
} catch (e) {
|
|
4272
|
+
throw new FigmaFixesError([`not JSON: ${e.message}`]);
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
const problems = validateFigmaFixes(data);
|
|
4276
|
+
if (problems.length) throw new FigmaFixesError(problems);
|
|
4277
|
+
return data;
|
|
4278
|
+
}
|
|
4279
|
+
const lc$1 = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
4280
|
+
const setNameOf$1 = (title) => title.split("/").at(-1).trim();
|
|
4281
|
+
const propsOf = (s) => ({ ...s.props ?? {}, ...s.overrides ?? {} });
|
|
4282
|
+
const sameArg = (a2, b) => a2 === b || a2 !== void 0 && b !== void 0 && String(a2).toLowerCase() === String(b).toLowerCase();
|
|
4283
|
+
function storyMatchesWhere(s, where) {
|
|
4284
|
+
if (!where) return true;
|
|
4285
|
+
const p2 = propsOf(s);
|
|
4286
|
+
if (where.args && !Object.entries(where.args).every(([k, v]) => sameArg(p2[k], v))) return false;
|
|
4287
|
+
if (where.state && storyState(s) !== where.state) return false;
|
|
4288
|
+
return true;
|
|
4289
|
+
}
|
|
4290
|
+
function partsNamed(spec, sel) {
|
|
4291
|
+
if (sel.startsWith(".")) {
|
|
4292
|
+
const cls = sel.slice(1);
|
|
4293
|
+
return spec.parts.filter((p2) => (p2.source ?? "").split(/[\s,]+/).map((x) => x.replace(/^\./, "")).includes(cls));
|
|
4294
|
+
}
|
|
4295
|
+
return spec.parts.filter((p2) => p2.id === sel);
|
|
4296
|
+
}
|
|
4297
|
+
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
4298
|
+
function wordsIn(spec, id) {
|
|
4299
|
+
const kids = spec.parts.filter((p2) => p2.parent === id);
|
|
4300
|
+
return kids.some((k) => k.kind === "text" && !!k.characters?.trim() || wordsIn(spec, k.id));
|
|
4301
|
+
}
|
|
4302
|
+
function ownSize(spec, part, side) {
|
|
4303
|
+
const id = part.id;
|
|
4304
|
+
const box = spec.boxes[id];
|
|
4305
|
+
const value = spec.values[id]?.[side];
|
|
4306
|
+
const measured = typeof value === "number" ? value : box?.[side];
|
|
4307
|
+
const skip = (why) => ({ part: id, side, why, ...measured !== void 0 && why !== "unmeasured" ? { measured } : {} });
|
|
4308
|
+
if (!part.parent && side === "width" && part.fill) return skip("fills");
|
|
4309
|
+
const css = spec.sized?.[id]?.[side];
|
|
4310
|
+
if (typeof css === "number") return css;
|
|
4311
|
+
if (side === "width" && box && wordsIn(spec, id) && boxSized(spec, id, { width: true, height: true }).width === void 0) return skip("words");
|
|
4312
|
+
return measured ?? skip("unmeasured");
|
|
4313
|
+
}
|
|
4314
|
+
function patchSpec(spec, fix) {
|
|
4315
|
+
const skipped = [];
|
|
4316
|
+
const targets = partsNamed(spec, fix.part);
|
|
4317
|
+
if (!targets.length) return { hit: false, skipped };
|
|
4318
|
+
let laid = false;
|
|
4319
|
+
for (const part of targets) {
|
|
4320
|
+
const id = part.id;
|
|
4321
|
+
const box = spec.boxes[id];
|
|
4322
|
+
const parent = part.parent ? spec.parts.find((p2) => p2.id === part.parent) : void 0;
|
|
4323
|
+
if (fix.hide) {
|
|
4324
|
+
const gone = /* @__PURE__ */ new Set([id]);
|
|
4325
|
+
for (const p2 of spec.parts) if (p2.parent && gone.has(p2.parent)) gone.add(p2.id);
|
|
4326
|
+
spec.parts = spec.parts.filter((p2) => !gone.has(p2.id));
|
|
4327
|
+
for (const map of [spec.values, spec.raw, spec.tokens, spec.boxes, spec.sized ?? {}, spec.spacers ?? {}, spec.vectors ?? {}]) for (const g of gone) delete map[g];
|
|
4328
|
+
laid = true;
|
|
4329
|
+
continue;
|
|
4330
|
+
}
|
|
4331
|
+
const values = spec.values[id] ??= {};
|
|
4332
|
+
for (const [field, value] of Object.entries(fix.set ?? {})) {
|
|
4333
|
+
values[field] = value === "full" ? box ? Math.min(box.width, box.height) / 2 : 9999 : value;
|
|
4334
|
+
laid = true;
|
|
4335
|
+
}
|
|
4336
|
+
for (const side of ["width", "height"]) {
|
|
4337
|
+
const how = fix[side];
|
|
4338
|
+
if (how === void 0) continue;
|
|
4339
|
+
const flowsAlong = parent?.layout?.direction === (side === "width" ? "HORIZONTAL" : "VERTICAL");
|
|
4340
|
+
const px = how === "fixed" ? ownSize(spec, part, side) : how;
|
|
4341
|
+
if (typeof px === "object") {
|
|
4342
|
+
skipped.push(px);
|
|
4343
|
+
continue;
|
|
4344
|
+
}
|
|
4345
|
+
laid = true;
|
|
4346
|
+
if (px === "hug") {
|
|
4347
|
+
delete values[side];
|
|
4348
|
+
if (spec.sized?.[id]) delete spec.sized[id][side];
|
|
4349
|
+
if (part.stretch) delete part.stretch[side];
|
|
4350
|
+
if (flowsAlong) delete part.grow;
|
|
4351
|
+
if (!part.parent && side === "width") delete part.fill;
|
|
4352
|
+
} else if (px === "fill") {
|
|
4353
|
+
if (spec.sized?.[id]) delete spec.sized[id][side];
|
|
4354
|
+
if (!part.parent) {
|
|
4355
|
+
if (side === "width") part.fill = true;
|
|
4356
|
+
} else if (flowsAlong) part.grow = true;
|
|
4357
|
+
else part.stretch = { ...part.stretch ?? {}, [side]: true };
|
|
4358
|
+
} else {
|
|
4359
|
+
values[side] = px;
|
|
4360
|
+
spec.sized = { ...spec.sized ?? {}, [id]: { ...spec.sized?.[id] ?? {}, [side]: px } };
|
|
4361
|
+
if (part.stretch) delete part.stretch[side];
|
|
4362
|
+
if (flowsAlong) delete part.grow;
|
|
4363
|
+
if (!part.parent && side === "width") delete part.fill;
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
if (fix.direction || fix.align) {
|
|
4367
|
+
laid = true;
|
|
4368
|
+
part.layout = { direction: "HORIZONTAL", primary: "MIN", counter: "MIN", ...part.layout ?? {} };
|
|
4369
|
+
if (fix.direction) part.layout.direction = fix.direction;
|
|
4370
|
+
if (fix.align?.primary) part.layout.primary = fix.align.primary;
|
|
4371
|
+
if (fix.align?.counter) part.layout.counter = fix.align.counter;
|
|
4372
|
+
}
|
|
4373
|
+
if (fix.overlay !== void 0) {
|
|
4374
|
+
laid = true;
|
|
4375
|
+
delete part.cover;
|
|
4376
|
+
delete part.pin;
|
|
4377
|
+
if (fix.overlay === true) part.cover = true;
|
|
4378
|
+
else if (fix.overlay !== false) part.pin = { ...fix.overlay };
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
return { hit: laid, skipped };
|
|
4382
|
+
}
|
|
4383
|
+
function applyFixes(measured, fixes) {
|
|
4384
|
+
const list = Array.isArray(fixes) ? fixes : fixes.fixes;
|
|
4385
|
+
const file = clone(measured);
|
|
4386
|
+
const before = new Set((file.figmaFixes?.applied ?? []).filter((a2) => a2.result === "applied").map((a2) => a2.id));
|
|
4387
|
+
const applied = [];
|
|
4388
|
+
const entryOf = (component) => (file.components ?? []).find((c) => lc$1(setNameOf$1(c.title)) === lc$1(component));
|
|
4389
|
+
const hints = (component) => {
|
|
4390
|
+
const c = entryOf(component);
|
|
4391
|
+
return c ? c.figma ??= {} : void 0;
|
|
4392
|
+
};
|
|
4393
|
+
const mark = (s, id) => {
|
|
4394
|
+
s.fixes = [.../* @__PURE__ */ new Set([...s.fixes ?? [], id])];
|
|
4395
|
+
};
|
|
4396
|
+
for (const fix of list) {
|
|
4397
|
+
const record = (result, stories, detail) => applied.push({ id: fix.id, kind: fix.kind, component: fix.component, result, stories, detail });
|
|
4398
|
+
if (before.has(fix.id)) {
|
|
4399
|
+
record("already", 0, "already applied to this file");
|
|
4400
|
+
continue;
|
|
4401
|
+
}
|
|
4402
|
+
const mine = () => file.stories.filter((s) => lc$1(setNameOf$1(s.title)) === lc$1(fix.component));
|
|
4403
|
+
if (!mine().length) {
|
|
4404
|
+
record("not-found", 0, `no measured story of ${fix.component}`);
|
|
4405
|
+
continue;
|
|
4406
|
+
}
|
|
4407
|
+
const scoped = () => mine().filter((s) => storyMatchesWhere(s, fix.where));
|
|
4408
|
+
if (fix.kind === "part") {
|
|
4409
|
+
let n = 0;
|
|
4410
|
+
let matched = false;
|
|
4411
|
+
const unpinned = [];
|
|
4412
|
+
for (const s of scoped()) {
|
|
4413
|
+
let hit = false;
|
|
4414
|
+
for (const spec of [s.spec, ...s.instances ?? []]) {
|
|
4415
|
+
if (!spec) continue;
|
|
4416
|
+
const r = patchSpec(spec, fix);
|
|
4417
|
+
if (r.hit) hit = true;
|
|
4418
|
+
if (r.hit || r.skipped.length) matched = true;
|
|
4419
|
+
for (const skip of r.skipped) unpinned.push({ story: s, skip });
|
|
4420
|
+
}
|
|
4421
|
+
if (hit) {
|
|
4422
|
+
n++;
|
|
4423
|
+
mark(s, fix.id);
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
const skippedWords = unpinned.length ? skippedSizeWords(unpinned) : "";
|
|
4427
|
+
if (n) record("applied", n, `${describePartFix(fix)} on ${n} stor${n === 1 ? "y" : "ies"}${skippedWords ? `; ${skippedWords}` : ""}`);
|
|
4428
|
+
else if (matched) record("skipped", 0, `nothing laid on: ${skippedWords}`);
|
|
4429
|
+
else record("not-found", 0, `no part ${fix.part} in the ${scoped().length ? "stories it names" : "variants where names"}`);
|
|
4430
|
+
} else if (fix.kind === "axis") {
|
|
4431
|
+
const h2 = hints(fix.component);
|
|
4432
|
+
if (fix.action === "prune") {
|
|
4433
|
+
const drop = new Set(scoped());
|
|
4434
|
+
if (!drop.size) {
|
|
4435
|
+
record("not-found", 0, "no measured variant matches where");
|
|
4436
|
+
continue;
|
|
4437
|
+
}
|
|
4438
|
+
file.stories = file.stories.filter((s) => !drop.has(s));
|
|
4439
|
+
if (h2) h2.pruned = [...h2.pruned ?? [], fix.where];
|
|
4440
|
+
record("applied", drop.size, `dropped ${drop.size} variant stor${drop.size === 1 ? "y" : "ies"}`);
|
|
4441
|
+
} else {
|
|
4442
|
+
const arg = fix.arg;
|
|
4443
|
+
const entry = entryOf(fix.component);
|
|
4444
|
+
const known = !!entry?.argTypes[arg] || mine().some((s) => arg in propsOf(s));
|
|
4445
|
+
if (!known) {
|
|
4446
|
+
record("not-found", 0, `${fix.component} has no arg ${arg}`);
|
|
4447
|
+
continue;
|
|
4448
|
+
}
|
|
4449
|
+
if (fix.action === "map") {
|
|
4450
|
+
if (h2) {
|
|
4451
|
+
if (fix.property) h2.properties = { ...h2.properties ?? {}, [arg]: fix.property };
|
|
4452
|
+
if (fix.values) h2.values = { ...h2.values ?? {}, [arg]: { ...h2.values?.[arg] ?? {}, ...fix.values } };
|
|
4453
|
+
}
|
|
4454
|
+
record("applied", 0, `${arg} drawn as ${fix.property ?? arg}${fix.values ? ` (${Object.entries(fix.values).map(([c, f]) => `${c} as ${f}`).join(", ")})` : ""}`);
|
|
4455
|
+
} else {
|
|
4456
|
+
const at2 = entry?.argTypes[arg];
|
|
4457
|
+
const lead = mine().find((s) => !s.overrides || !(arg in s.overrides));
|
|
4458
|
+
const baseline = at2?.default ?? (lead ? propsOf(lead)[arg] : void 0) ?? (at2?.kind === "boolean" ? false : void 0);
|
|
4459
|
+
const drop = new Set(scoped().filter((s) => baseline !== void 0 && propsOf(s)[arg] !== void 0 && !sameArg(propsOf(s)[arg], baseline)));
|
|
4460
|
+
file.stories = file.stories.filter((s) => !drop.has(s));
|
|
4461
|
+
if (entry) {
|
|
4462
|
+
delete entry.argTypes[arg];
|
|
4463
|
+
entry.figma ??= {};
|
|
4464
|
+
entry.figma.ignored = [.../* @__PURE__ */ new Set([...entry.figma.ignored ?? [], arg])];
|
|
4465
|
+
}
|
|
4466
|
+
record("applied", drop.size, `${arg} is not a Figma property${drop.size ? `: ${drop.size} stor${drop.size === 1 ? "y" : "ies"} at another value left out` : ""}`);
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
} else if (fix.kind === "story") {
|
|
4470
|
+
const src = file.stories.find(
|
|
4471
|
+
(s) => s.id === fix.use.story && s.spec && !s.error && (!fix.use.args || Object.entries(fix.use.args).every(([k, v]) => sameArg(propsOf(s)[k], v))) && (!fix.use.state || storyState(s) === fix.use.state)
|
|
4472
|
+
);
|
|
4473
|
+
if (!src) {
|
|
4474
|
+
record("not-found", 0, `no measured render of story ${fix.use.story}${fix.use.args ? ` with ${JSON.stringify(fix.use.args)}` : ""}`);
|
|
4475
|
+
continue;
|
|
4476
|
+
}
|
|
4477
|
+
const target = fix.where ?? {};
|
|
4478
|
+
const others = new Set(scoped().filter((s) => s !== src));
|
|
4479
|
+
const copy = clone(src);
|
|
4480
|
+
const overrides = { ...copy.overrides ?? {}, ...target.args ?? {} };
|
|
4481
|
+
if (target.state === "disabled") overrides.disabled = true;
|
|
4482
|
+
copy.overrides = overrides;
|
|
4483
|
+
if (target.state && target.state !== "rest" && target.state !== "disabled") copy.state = target.state;
|
|
4484
|
+
else if (target.state) delete copy.state;
|
|
4485
|
+
if (lc$1(setNameOf$1(copy.title)) !== lc$1(fix.component)) copy.title = copy.title.split("/").slice(0, -1).concat(fix.component).join("/");
|
|
4486
|
+
mark(copy, fix.id);
|
|
4487
|
+
let placed = false;
|
|
4488
|
+
file.stories = file.stories.flatMap((s) => others.has(s) ? placed ? [] : (placed = true, [copy]) : [s]);
|
|
4489
|
+
if (!placed) file.stories.push(copy);
|
|
4490
|
+
record("applied", others.size + 1, `variant ${whereWords(target)} drawn from ${src.id}${others.size ? `, in place of ${others.size} other stor${others.size === 1 ? "y" : "ies"}` : ""}`);
|
|
4491
|
+
} else {
|
|
4492
|
+
const h2 = hints(fix.component);
|
|
4493
|
+
if (!h2) {
|
|
4494
|
+
record("not-found", 0, `no component entry for ${fix.component} in the measure`);
|
|
4495
|
+
continue;
|
|
4496
|
+
}
|
|
4497
|
+
if (fix.status) {
|
|
4498
|
+
h2.status = [...h2.status ?? [], { status: fix.status, reason: fix.reason, fix: fix.id, ...fix.where ? { where: fix.where } : {} }];
|
|
4499
|
+
if (!fix.where) {
|
|
4500
|
+
const st = (file.statuses ?? []).find((x) => lc$1(x.name) === lc$1(fix.component));
|
|
4501
|
+
if (st && st.status !== fix.status) {
|
|
4502
|
+
st.status = fix.status;
|
|
4503
|
+
delete st.reason;
|
|
4504
|
+
st.summary = fix.reason;
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
} else h2.notes = [...h2.notes ?? [], { text: fix.reason, fix: fix.id, ...fix.where ? { where: fix.where } : {} }];
|
|
4508
|
+
record("applied", 0, fix.status ? `${fix.status === "differs" ? "Differs" : "Can't check"}: ${fix.reason}` : `note: ${fix.reason}`);
|
|
4509
|
+
}
|
|
4510
|
+
const h = hints(fix.component);
|
|
4511
|
+
if (h && applied.at(-1).result === "applied") h.fixes = [.../* @__PURE__ */ new Set([...h.fixes ?? [], fix.id])];
|
|
4512
|
+
}
|
|
4513
|
+
file.figmaFixes = { applied: [...(file.figmaFixes?.applied ?? []).filter((a2) => a2.result === "applied"), ...applied.filter((a2) => a2.result !== "already")] };
|
|
4514
|
+
return { file, applied };
|
|
3952
4515
|
}
|
|
3953
|
-
|
|
3954
|
-
const
|
|
4516
|
+
const variantWords = (s) => {
|
|
4517
|
+
const p2 = propsOf(s);
|
|
4518
|
+
const args = Object.keys(s.overrides ?? {}).filter((k) => k !== "disabled").map((k) => `${k}=${String(p2[k])}`);
|
|
4519
|
+
const state = storyState(s);
|
|
4520
|
+
return `${s.id}${args.length || state !== "rest" ? ` (${[...args, ...state !== "rest" ? [state] : []].join(", ")})` : ""}`;
|
|
4521
|
+
};
|
|
4522
|
+
function skippedSizeWords(list) {
|
|
4523
|
+
const why = {
|
|
4524
|
+
words: "it hugs its text in code, so the measured px is that story's words, not a width of its own",
|
|
4525
|
+
fills: "it fills its container in code, so the measured px is the container's",
|
|
4526
|
+
unmeasured: "the measure has no size for it in that story"
|
|
4527
|
+
};
|
|
3955
4528
|
const groups = /* @__PURE__ */ new Map();
|
|
3956
|
-
for (const
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
groups.set(name, [...groups.get(name) ?? [], s]);
|
|
3960
|
-
}
|
|
3961
|
-
const argTypesOf = (name) => Object.assign({}, ...(file.components ?? []).filter((c) => setNameOf$1(c.title) === name).map((c) => c.argTypes));
|
|
3962
|
-
const hints = {};
|
|
3963
|
-
for (const c of file.components ?? []) if (c.figma) hints[setNameOf$1(c.title)] = { ...hints[setNameOf$1(c.title)], ...c.figma };
|
|
3964
|
-
const components = [];
|
|
3965
|
-
const shots = {};
|
|
3966
|
-
const report = { sets: 0, variants: 0, stories: 0, skipped: [] };
|
|
3967
|
-
for (const [name, stories] of [...groups].sort(([a2], [b]) => a2.localeCompare(b))) {
|
|
3968
|
-
const built = setOf(name, stories, argTypesOf(name), file.storybook, known, hints[name]);
|
|
3969
|
-
if (!built) {
|
|
3970
|
-
report.skipped.push(name);
|
|
3971
|
-
continue;
|
|
3972
|
-
}
|
|
3973
|
-
const spec = built.spec;
|
|
3974
|
-
components.push(spec);
|
|
3975
|
-
if (Object.keys(built.shots).length) shots[name] = built.shots;
|
|
3976
|
-
report.sets++;
|
|
3977
|
-
report.variants += spec.variants.length;
|
|
3978
|
-
report.stories += stories.length;
|
|
4529
|
+
for (const x of list) {
|
|
4530
|
+
const k = `${x.skip.part}|${x.skip.side}|${x.skip.why}`;
|
|
4531
|
+
groups.set(k, [...groups.get(k) ?? [], x]);
|
|
3979
4532
|
}
|
|
3980
|
-
return
|
|
4533
|
+
return [...groups.values()].map((g) => {
|
|
4534
|
+
const { part, side, why: w } = g[0].skip;
|
|
4535
|
+
const stories = [...new Set(g.map((x) => `${variantWords(x.story)}${x.skip.measured !== void 0 ? ` ${Math.round(x.skip.measured * 100) / 100} px` : ""}`))];
|
|
4536
|
+
return `${part} ${side} not fixed on ${stories.length} stor${stories.length === 1 ? "y" : "ies"} (${why[w]}):${stories.join(", ")}`;
|
|
4537
|
+
}).join("; ");
|
|
4538
|
+
}
|
|
4539
|
+
function whereWords(w) {
|
|
4540
|
+
const args = Object.entries(w.args ?? {}).map(([k, v]) => `${k}=${String(v)}`);
|
|
4541
|
+
if (w.state) args.push(`state=${w.state}`);
|
|
4542
|
+
return args.join(", ") || "every variant";
|
|
4543
|
+
}
|
|
4544
|
+
function describePartFix(f) {
|
|
4545
|
+
if (f.hide) return `${f.part} hidden`;
|
|
4546
|
+
const bits = [
|
|
4547
|
+
...Object.entries(f.set ?? {}).map(([k, v]) => `${k} ${v === null ? "none" : String(v)}`),
|
|
4548
|
+
...f.width !== void 0 ? [`width ${String(f.width)}`] : [],
|
|
4549
|
+
...f.height !== void 0 ? [`height ${String(f.height)}`] : [],
|
|
4550
|
+
...f.direction ? [`direction ${f.direction}`] : [],
|
|
4551
|
+
...f.align ? [`align ${[f.align.primary, f.align.counter].filter(Boolean).join(" ")}`] : [],
|
|
4552
|
+
...f.overlay !== void 0 ? [`overlay ${typeof f.overlay === "boolean" ? String(f.overlay) : JSON.stringify(f.overlay)}`] : []
|
|
4553
|
+
];
|
|
4554
|
+
return `${f.part}: ${bits.join(", ")}`;
|
|
4555
|
+
}
|
|
4556
|
+
function appliedFixLines(applied) {
|
|
4557
|
+
return applied.map((a2) => `${a2.result === "applied" ? "applied" : a2.result === "already" ? "already applied" : a2.result === "skipped" ? "skipped" : "not found"} ${a2.id} (${a2.component}, ${a2.kind}): ${a2.detail}`);
|
|
3981
4558
|
}
|
|
3982
4559
|
const DEFAULT_FIDELITY_RUN_FILE = "figma-fidelity.json";
|
|
3983
4560
|
const FIX_LOOP_MAX_ROUNDS = 3;
|
|
@@ -4048,7 +4625,7 @@ function figmaGaps(measured, run) {
|
|
|
4048
4625
|
}
|
|
4049
4626
|
function gapCaseFor(input) {
|
|
4050
4627
|
const fixed = input.fixes ? applyFixes(input.measured, input.fixes).file : input.measured;
|
|
4051
|
-
const { specs, shots } = specsFromMeasure(fixed);
|
|
4628
|
+
const { specs, shots } = specsFromMeasure(fixed, { only: input.component });
|
|
4052
4629
|
const spec = specs.components.find((c) => lc(c.name) === lc(input.component));
|
|
4053
4630
|
const fidelity = input.run?.report.components.find((c) => lc(c.component) === lc(input.component));
|
|
4054
4631
|
const setShots = byName(shots, input.component) ?? {};
|
|
@@ -4065,7 +4642,7 @@ function gapCaseFor(input) {
|
|
|
4065
4642
|
}
|
|
4066
4643
|
const FIX_SCHEMA_SUMMARY = `figma-fixes.json fixes (version ${FIGMA_FIXES_VERSION}). Each fix is one JSON object:
|
|
4067
4644
|
- Every fix: "id" (lower case, digits, dashes, unique), "kind", "component" (the Figma set, e.g. "TextInput"), "reason" (one plain sentence, no em dash), optional "by": "claude", optional "where": { "args": { "<arg>": value }, "state": "rest" | "hover" | "active" | "focus-visible" | "disabled" } (absent: every variant).
|
|
4068
|
-
- kind "part": "part" is a part id from the gap case ("root", "cds--text-input-label-wrapper") or ".class". At least one of: "set" { field: token path | px number | null (colors only) | "full" (cornerRadius only) }, "width" / "height": "fixed" (
|
|
4645
|
+
- kind "part": "part" is a part id from the gap case ("root", "cds--text-input-label-wrapper") or ".class". At least one of: "set" { field: token path | px number | null (colors only) | "full" (cornerRadius only) }, "width" / "height": "fixed" (each story's own measured size; a width that only hugs text in code is skipped, since it is the story's words) | px number | "hug" | "fill", "direction": "HORIZONTAL" | "VERTICAL", "align": { "primary": MIN|CENTER|MAX|SPACE_BETWEEN, "counter": MIN|CENTER|MAX|STRETCH }, "overlay": true | false | { "top": 3, "left": 3 }, "hide": true (alone; never the root). Fields: fill, stroke, strokeWeight, strokeTopWeight, strokeRightWeight, strokeBottomWeight, strokeLeftWeight, cornerRadius, paddingTop, paddingRight, paddingBottom, paddingLeft, itemSpacing, counterAxisSpacing, width, height, minWidth, minHeight, opacity, textFill, fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, textDecoration.
|
|
4069
4646
|
- kind "axis": "action" "map" ("arg", "property" and/or "values" { code value: Figma value }), "ignore" ("arg"), or "prune" ("where" names the variants to drop).
|
|
4070
4647
|
- kind "story": "where" names the variant, "use": { "story": "<story id>", "args"?, "state"? } is the measured render to draw it from.
|
|
4071
4648
|
- kind "status": "status": "differs" | "cant-check" with the reason; never "matches" (only the shots say a component matches). Without "status": a note.
|
|
@@ -4110,25 +4687,25 @@ function tryFigmaFixes(input) {
|
|
|
4110
4687
|
const before = gapCaseFor({ component, measured: input.measured, fixes: input.fixes, run: input.run });
|
|
4111
4688
|
const afterRun = applyFixes(input.measured, file);
|
|
4112
4689
|
const applied = afterRun.applied.filter((a2) => proposedIds.has(a2.id));
|
|
4113
|
-
const afterBuilt = specsFromMeasure(afterRun.file);
|
|
4690
|
+
const afterBuilt = specsFromMeasure(afterRun.file, { only: component });
|
|
4114
4691
|
const afterSpec = afterBuilt.specs.components.find((c) => lc(c.name) === lc(component));
|
|
4115
4692
|
const hintsA = afterSpec ? afterBuilt.hints[afterSpec.name] : void 0;
|
|
4116
|
-
const hintsB = before.spec ? specsFromMeasure(before.measured).hints[before.spec.name] : void 0;
|
|
4693
|
+
const hintsB = before.spec ? specsFromMeasure(before.measured, { only: before.spec.name }).hints[before.spec.name] : void 0;
|
|
4117
4694
|
const resolve2 = measuredResolver(before.measured);
|
|
4118
4695
|
const built = byName(input.run?.built, component);
|
|
4119
4696
|
const stories = storiesOfComponent(before.measured, component);
|
|
4120
4697
|
const variants = [];
|
|
4121
4698
|
for (const gv of before.gap.variants) {
|
|
4122
|
-
const vb = before.spec
|
|
4123
|
-
const want = before.spec ? renamedProps(gv.props, before.spec, afterSpec, hintsB, hintsA) : gv.props;
|
|
4124
|
-
const va = afterSpec
|
|
4699
|
+
const vb = matchVariant(before.spec, gv.props);
|
|
4700
|
+
const want = before.spec ? renamedProps(vb?.props ?? gv.props, before.spec, afterSpec, hintsB, hintsA) : gv.props;
|
|
4701
|
+
const va = matchVariant(afterSpec, gv.props) ?? matchVariant(afterSpec, want);
|
|
4125
4702
|
if (!va || !afterSpec) {
|
|
4126
4703
|
variants.push({ key: gv.key, ...gv.story ? { story: gv.story.id } : {}, verdict: "gone", differences: [], regressions: [] });
|
|
4127
4704
|
continue;
|
|
4128
4705
|
}
|
|
4129
4706
|
const differences = gv.differences.map((d) => tryDifference(d, before.spec, vb, afterSpec, va, built?.[gv.key], resolve2));
|
|
4130
4707
|
let regressions2 = [];
|
|
4131
|
-
const story = before.spec ? storyForVariant(before.spec, gv.props, stories, before.shots[gv.key]) : void 0;
|
|
4708
|
+
const story = before.spec ? storyForVariant(before.spec, gv.props, stories, before.shots[gv.key] ?? (vb ? before.shots[variantKeyOf(vb.props)] : void 0)) : void 0;
|
|
4132
4709
|
const m = story ? story.instances?.[0] ?? story.spec : void 0;
|
|
4133
4710
|
if (m && before.spec && vb) {
|
|
4134
4711
|
const key2 = (d) => `${d.part}|${d.property}`;
|
|
@@ -4141,7 +4718,7 @@ function tryFigmaFixes(input) {
|
|
|
4141
4718
|
const all = variants.flatMap((v) => v.differences);
|
|
4142
4719
|
const regressions = variants.reduce((n, v) => n + v.regressions.length, 0);
|
|
4143
4720
|
const verdict = verdictOf(all, variants.flatMap((v) => v.regressions));
|
|
4144
|
-
const notFound = applied.filter((a2) => a2.result === "not-found");
|
|
4721
|
+
const notFound = applied.filter((a2) => a2.result === "not-found" || a2.result === "skipped");
|
|
4145
4722
|
return {
|
|
4146
4723
|
status: "checked",
|
|
4147
4724
|
component,
|
|
@@ -4150,12 +4727,11 @@ function tryFigmaFixes(input) {
|
|
|
4150
4727
|
verdict,
|
|
4151
4728
|
referee: "values",
|
|
4152
4729
|
variants,
|
|
4153
|
-
summary: trySummary(component, before.gap, all, regressions, verdict, notFound, proposed),
|
|
4730
|
+
summary: trySummary(component, before.gap, all, regressions, verdict, notFound, proposed, variants.filter((v) => v.verdict === "gone").length),
|
|
4154
4731
|
detail: tryDetail(variants, applied),
|
|
4155
4732
|
file
|
|
4156
4733
|
};
|
|
4157
4734
|
}
|
|
4158
|
-
const sameProps = (a2, b) => Object.keys(a2).length === Object.keys(b).length && Object.entries(a2).every(([k, v]) => b[k] === v);
|
|
4159
4735
|
function renamedProps(props, specB, specA, hintsB, hintsA) {
|
|
4160
4736
|
const out = {};
|
|
4161
4737
|
for (const [name, value] of Object.entries(props)) {
|
|
@@ -4284,17 +4860,24 @@ function sizeShift(field, specB, vb, specA, va, built, resolve2) {
|
|
|
4284
4860
|
walk(root);
|
|
4285
4861
|
return unknown ? void 0 : round(shift);
|
|
4286
4862
|
}
|
|
4287
|
-
function trySummary(component, gap, diffs, regressions, verdict, notFound, proposed) {
|
|
4863
|
+
function trySummary(component, gap, diffs, regressions, verdict, notFound, proposed, gone) {
|
|
4288
4864
|
if (!gap.variants.length) return `${component} has no differing variant in the gap case: nothing for these fixes to move (the values referee needs a Check fidelity report or a difference in the measure).`;
|
|
4865
|
+
const missing = notFound.filter((a2) => a2.result === "not-found");
|
|
4866
|
+
const skipped = notFound.filter((a2) => a2.result === "skipped");
|
|
4867
|
+
const absent = (missing.length ? ` ${missing.map((a2) => a2.id).join(", ")} found nothing in the measure.` : "") + (skipped.length ? ` ${skipped.map((a2) => a2.id).join(", ")} laid nothing on: the measure has no size of the part's own to fix it at (see the detail).` : "");
|
|
4868
|
+
if (!diffs.length && gone) {
|
|
4869
|
+
const n = gap.variants.length;
|
|
4870
|
+
const which = gone < n ? `${gone} of the ${n} differing variants are not` : n === 1 ? "the differing variant is not" : `none of the ${n} differing variants is`;
|
|
4871
|
+
return `The values referee compared nothing: ${which} in the rebuilt set.${absent} Pull this branch in the DS Bridge Figma plugin and run Check fidelity to see what the fixes do.`;
|
|
4872
|
+
}
|
|
4289
4873
|
const resolved = diffs.filter((d) => d.result === "resolved").length;
|
|
4290
4874
|
const closer = diffs.filter((d) => d.result === "closer").length;
|
|
4291
4875
|
const worse = diffs.filter((d) => d.result === "worse").length;
|
|
4292
|
-
const bits = [`${resolved} of ${diffs.length} difference${diffs.length === 1 ? "" : "s"} resolved`, ...closer ? [`${closer} closer`] : [], ...worse ? [`${worse} worse`] : [], ...regressions ? [`${regressions} new`] : []];
|
|
4876
|
+
const bits = [`${resolved} of ${diffs.length} difference${diffs.length === 1 ? "" : "s"} resolved`, ...closer ? [`${closer} closer`] : [], ...worse ? [`${worse} worse`] : [], ...regressions ? [`${regressions} new`] : [], ...gone ? [`${gone} not in the rebuilt set`] : []];
|
|
4293
4877
|
const word = verdict === "closer" ? "Closer to the code" : verdict === "worse" ? "Further from the code" : "No closer to the code";
|
|
4294
4878
|
const statusOnly = proposed.length > 0 && proposed.every((f) => f.kind === "status" || f.kind === "axis" && f.action === "map");
|
|
4295
|
-
const missing = notFound.length ? ` ${notFound.map((a2) => a2.id).join(", ")} found nothing in the measure.` : "";
|
|
4296
4879
|
const note = statusOnly && verdict === "same" ? " These fixes change how the set is named or labelled, not what it draws." : "";
|
|
4297
|
-
return `${word} on the measured values: ${bits.join(", ")}.${
|
|
4880
|
+
return `${word} on the measured values: ${bits.join(", ")}.${absent}${note} The pixel check runs in the plugin after the next pull.`;
|
|
4298
4881
|
}
|
|
4299
4882
|
function tryDetail(variants, applied) {
|
|
4300
4883
|
const fmt = (v) => v === null ? "none" : v === void 0 ? "unknown" : typeof v === "string" ? v : JSON.stringify(v);
|
|
@@ -4319,6 +4902,32 @@ function fixWords(f) {
|
|
|
4319
4902
|
function touchesLayout(f) {
|
|
4320
4903
|
return f.kind === "part" && (f.width !== void 0 || f.height !== void 0 || f.direction !== void 0 || f.align !== void 0 || f.overlay !== void 0);
|
|
4321
4904
|
}
|
|
4905
|
+
const VARIANTS_INLINE = 8;
|
|
4906
|
+
const valueWords = (v) => v === null ? "none" : v === void 0 ? "unknown" : typeof v === "string" ? v : JSON.stringify(v);
|
|
4907
|
+
function triedHighlights(variants) {
|
|
4908
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4909
|
+
for (const v of variants) {
|
|
4910
|
+
for (const d of v.differences) {
|
|
4911
|
+
const key2 = [d.part, d.property, valueWords(d.before), valueWords(d.expected), valueWords(d.after), d.result].join("|");
|
|
4912
|
+
const seen = groups.get(key2);
|
|
4913
|
+
if (seen) seen.n += 1;
|
|
4914
|
+
else groups.set(key2, { n: 1, d });
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
return [...groups.values()].map(
|
|
4918
|
+
({ n, d }) => `- ${d.part} ${d.property}, ${n === 1 ? "1 variant" : `${n} variants`}: ${valueWords(d.before)} in Figma against ${valueWords(d.expected)} in code, now ${valueWords(d.after)}: ${d.result}`
|
|
4919
|
+
);
|
|
4920
|
+
}
|
|
4921
|
+
function triedEvidence(variants) {
|
|
4922
|
+
const lines = variants.flatMap((v) => [
|
|
4923
|
+
`- ${v.key}: ${v.verdict === "gone" ? "not in the rebuilt set" : v.verdict}`,
|
|
4924
|
+
...v.differences.map((d) => ` - ${d.text}: ${d.result}${d.deltaBefore !== void 0 ? ` (${d.deltaBefore} px, now ${d.deltaAfter ?? "?"} px${d.estimated ? ", estimated" : ""})` : ""}`),
|
|
4925
|
+
...v.regressions.map((r) => ` - new difference: ${r.text}`)
|
|
4926
|
+
]);
|
|
4927
|
+
if (variants.length <= VARIANTS_INLINE) return [...lines, ""];
|
|
4928
|
+
const highlights = triedHighlights(variants);
|
|
4929
|
+
return [...highlights.length ? [...highlights, ""] : [], "<details>", `<summary>Variant by variant (${variants.length})</summary>`, "", ...lines, "", "</details>", ""];
|
|
4930
|
+
}
|
|
4322
4931
|
function figmaFixesPrBody(input) {
|
|
4323
4932
|
const { tried } = input;
|
|
4324
4933
|
const where = (f) => {
|
|
@@ -4345,13 +4954,9 @@ function figmaFixesPrBody(input) {
|
|
|
4345
4954
|
"",
|
|
4346
4955
|
tried.summary,
|
|
4347
4956
|
"",
|
|
4348
|
-
|
|
4349
|
-
`- ${v.key}: ${v.verdict}`,
|
|
4350
|
-
...v.differences.map((d) => ` - ${d.text}: ${d.result}${d.deltaBefore !== void 0 ? ` (${d.deltaBefore} px, now ${d.deltaAfter ?? "?"} px${d.estimated ? ", estimated" : ""})` : ""}`),
|
|
4351
|
-
...v.regressions.map((r) => ` - new difference: ${r.text}`)
|
|
4352
|
-
]),
|
|
4353
|
-
"",
|
|
4957
|
+
// The caveat is the reader's key to the section, so it comes before the evidence, not after it.
|
|
4354
4958
|
...tried.verdict === "same" && input.fixes.some(touchesLayout) ? ["These fixes size, place or lay out parts. The values referee can't judge layout, so it says same: Check fidelity decides.", ""] : [],
|
|
4959
|
+
...triedEvidence(tried.variants),
|
|
4355
4960
|
"The values referee compares the rebuilt set with what the code renders. The story shot is the final referee: pull this branch in the DS Bridge Figma plugin and run Check fidelity before approving. A fix stays only when the variant moved closer to its shot.",
|
|
4356
4961
|
"",
|
|
4357
4962
|
`Drafted by Claude through ${input.tool}. figma-fixes.json only: no component, token or plugin change.`
|
|
@@ -4402,6 +5007,345 @@ function statusFixFor(gap, existingIds = []) {
|
|
|
4402
5007
|
const reason = `${gap.summary.replace(/\u2014/g, ",").replace(/\.$/, "")}. Claude found no fix that brings it closer in ${FIX_LOOP_MAX_ROUNDS} rounds.`;
|
|
4403
5008
|
return { id, kind: "status", component: gap.component, status: "differs", reason, by: "claude" };
|
|
4404
5009
|
}
|
|
5010
|
+
const DEFAULT_MEASURED_SPECS_FILE = "measured-specs.json";
|
|
5011
|
+
const FIGMA_FIXES_LABEL = "figma-fixes";
|
|
5012
|
+
const startFixLoop = (component) => ({ component, round: 0, attempts: [], done: false });
|
|
5013
|
+
async function fixLoopRound(input, state, ask, gap = gapCaseFor(input).gap) {
|
|
5014
|
+
if (state.done || state.round >= FIX_LOOP_MAX_ROUNDS) return { state: { ...state, done: true }, round: { round: state.round, fixes: [], summary: "No round left." } };
|
|
5015
|
+
const round2 = state.round + 1;
|
|
5016
|
+
const reply = await ask(fixPrompt(gap, state.attempts));
|
|
5017
|
+
const next = (attempt2, r2, extra = {}) => {
|
|
5018
|
+
const s = { ...state, ...extra, round: round2, attempts: [...state.attempts, attempt2] };
|
|
5019
|
+
if (s.round >= FIX_LOOP_MAX_ROUNDS) s.done = true;
|
|
5020
|
+
return { state: s, round: r2 };
|
|
5021
|
+
};
|
|
5022
|
+
let fixes;
|
|
5023
|
+
try {
|
|
5024
|
+
fixes = parseFixReply(reply);
|
|
5025
|
+
} catch (e) {
|
|
5026
|
+
const msg = e.message;
|
|
5027
|
+
return next({ fixes: [], feedback: `${msg} Answer with a JSON array of fix objects only.` }, { round: round2, fixes: [], summary: msg });
|
|
5028
|
+
}
|
|
5029
|
+
const tried = tryFigmaFixes({ ...input, proposed: fixes });
|
|
5030
|
+
if (tried.status === "invalid") return next({ fixes, feedback: `Invalid, fix these problems:
|
|
5031
|
+
${tried.problems.join("\n")}` }, { round: round2, fixes, verdict: "invalid", summary: tried.summary });
|
|
5032
|
+
const r = { round: round2, fixes, verdict: tried.verdict, summary: tried.summary };
|
|
5033
|
+
const attempt = { fixes, feedback: `${tried.summary}
|
|
5034
|
+
${tried.detail}`, verdict: tried.verdict };
|
|
5035
|
+
if (tried.verdict !== "closer") return next(attempt, r);
|
|
5036
|
+
const all = tried.variants.flatMap((v) => v.differences);
|
|
5037
|
+
const score = all.filter((d) => d.result === "resolved").length * 2 + all.filter((d) => d.result === "closer").length - tried.variants.reduce((n, v) => n + v.regressions.length, 0);
|
|
5038
|
+
const best = !state.best || score > state.best.score ? { fixes, score, summary: tried.summary } : state.best;
|
|
5039
|
+
const resolved = all.every((d) => d.result === "resolved");
|
|
5040
|
+
return next(attempt, r, { best, ...resolved ? { done: true } : {} });
|
|
5041
|
+
}
|
|
5042
|
+
function fixLoopOutcome(gap, state, existingIds = []) {
|
|
5043
|
+
if (state.best) return { kind: "fixes", fixes: state.best.fixes, summary: `Claude's fixes bring ${gap.component} closer to its story. ${state.best.summary}` };
|
|
5044
|
+
return {
|
|
5045
|
+
kind: "status",
|
|
5046
|
+
fixes: [statusFixFor(gap, existingIds)],
|
|
5047
|
+
summary: `No fix brought ${gap.component} closer to its story in ${FIX_LOOP_MAX_ROUNDS} rounds: it keeps an honest Differs status with the reason.`
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
const slug = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "component";
|
|
5051
|
+
const figmaFixesBranchStem = (config, component) => `${config.governance.branchPrefix}figma-fixes-${slug(component)}`;
|
|
5052
|
+
function figmaFixesProposal(input) {
|
|
5053
|
+
const { config } = input;
|
|
5054
|
+
const tried = tryFigmaFixes(input);
|
|
5055
|
+
if (tried.status === "invalid") throw new Error(`Not proposed: ${tried.problems.join("; ")}.`);
|
|
5056
|
+
const summary = (input.summary ?? "").trim();
|
|
5057
|
+
if (summary.length < 8) throw new Error("Not proposed: summary is one or two sentences a reviewer reads first (what the fixes do for the designer).");
|
|
5058
|
+
if (/—/.test(summary)) throw new Error("Not proposed: the summary uses an em dash: use a colon or a comma.");
|
|
5059
|
+
const ids = new Set(input.proposed.map((f) => f.id));
|
|
5060
|
+
const proposed = tried.file.fixes.filter((f) => ids.has(f.id));
|
|
5061
|
+
const { gap } = gapCaseFor(input);
|
|
5062
|
+
const drawing = proposed.some((f) => f.kind === "part" || f.kind === "story" || f.kind === "axis" && f.action !== "map");
|
|
5063
|
+
const measurable = gap.variants.some((v) => v.differences.length);
|
|
5064
|
+
if (tried.verdict === "worse" || drawing && measurable && tried.verdict === "same" && !proposed.some(touchesLayout)) {
|
|
5065
|
+
return {
|
|
5066
|
+
status: "blocked",
|
|
5067
|
+
verdict: tried.verdict,
|
|
5068
|
+
why: `${tried.summary} Bridge does not open fixes that move ${gap.component} ${tried.verdict === "worse" ? "further from" : "no closer to"} the code.`,
|
|
5069
|
+
detail: tried.detail,
|
|
5070
|
+
component: gap.component
|
|
5071
|
+
};
|
|
5072
|
+
}
|
|
5073
|
+
const content = `${JSON.stringify(tried.file, null, 2)}
|
|
5074
|
+
`;
|
|
5075
|
+
const fileContents = [{ path: input.fixesPath, content }];
|
|
5076
|
+
const risk = classifyRisk({ files: [{ path: input.fixesPath, status: input.fixes ? "modified" : "added" }], config });
|
|
5077
|
+
const prBody = `${figmaFixesPrBody({ component: gap.component, summary, gap, tried, fixes: proposed, tool: input.tool })}
|
|
5078
|
+
|
|
5079
|
+
Risk: ${risk.label}.`;
|
|
5080
|
+
const title = `fix(figma): ${gap.component} drawn closer to its story`;
|
|
5081
|
+
const message = `${title}
|
|
5082
|
+
|
|
5083
|
+
${summary}
|
|
5084
|
+
|
|
5085
|
+
Fixes: ${proposed.map((f) => f.id).join(", ")}. Drafted by Claude through ${input.tool}.`;
|
|
5086
|
+
const labels = [.../* @__PURE__ */ new Set([...config.governance.labels, `tier: ${risk.tier}`, `source: ${input.source}`, FIGMA_FIXES_LABEL, DRAFTED_BY_CLAUDE_LABEL])];
|
|
5087
|
+
const report = { component: gap.component, verdict: tried.verdict, summary: tried.summary, variants: tried.variants, fixes: proposed.map((f) => f.id), tier: risk.tier, prTitle: title };
|
|
5088
|
+
return { status: "ready", report, fixes: proposed, labels, prBody, fileContents, title, message, draft: risk.tier >= 2, base: baseBranchOf(config), branchStem: figmaFixesBranchStem(config, gap.component) };
|
|
5089
|
+
}
|
|
5090
|
+
const MEASURE_BRANCH = REQUESTS_BRANCH;
|
|
5091
|
+
const MEASURED_SHOTS_DIR = "measured-shots";
|
|
5092
|
+
const MEASURE_WORKFLOW_FILE = ".github/workflows/bridge-measure.yml";
|
|
5093
|
+
const MEASURE_SETUP_ACTION = "Set up automatic measure";
|
|
5094
|
+
const MEASURE_MISSING_TEXT = `Bridge hasn't measured your components yet. A developer turns it on once: ${MEASURE_SETUP_ACTION}.`;
|
|
5095
|
+
function measureSetupSteps(repo) {
|
|
5096
|
+
return [
|
|
5097
|
+
`${MEASURE_SETUP_ACTION}${repo ? ` for ${repo}` : ""}, once:`,
|
|
5098
|
+
`- In Bridge Studio: Settings, For developers, ${MEASURE_SETUP_ACTION}. It opens a pull request that adds ${MEASURE_WORKFLOW_FILE}.`,
|
|
5099
|
+
`- Or in the repository: npx @bridge-ds/cli measure setup, then commit ${MEASURE_WORKFLOW_FILE}.`,
|
|
5100
|
+
`Once it is merged, the workflow measures the Storybook each time it changes and publishes ${DEFAULT_MEASURED_SPECS_FILE} to the ${MEASURE_BRANCH} branch.`
|
|
5101
|
+
];
|
|
5102
|
+
}
|
|
5103
|
+
async function findMeasure(files, path = DEFAULT_MEASURED_SPECS_FILE) {
|
|
5104
|
+
const local = await files.repo(path);
|
|
5105
|
+
if (local) return { text: local, path };
|
|
5106
|
+
if (!files.branch) return void 0;
|
|
5107
|
+
const remote = await files.branch(path, MEASURE_BRANCH).catch(() => void 0);
|
|
5108
|
+
return remote ? { text: remote, path, branch: MEASURE_BRANCH } : void 0;
|
|
5109
|
+
}
|
|
5110
|
+
const measureFromWords = (m) => m.branch ? `${m.path} on ${m.branch}` : m.path;
|
|
5111
|
+
const measureDir = (path) => path.includes("/") ? path.replace(/\/[^/]*$/, "") : "";
|
|
5112
|
+
class MeasureMissingError extends Error {
|
|
5113
|
+
code = "measure-missing";
|
|
5114
|
+
steps;
|
|
5115
|
+
constructor(repo, extra = []) {
|
|
5116
|
+
const steps = [...measureSetupSteps(repo), ...extra];
|
|
5117
|
+
super([MEASURE_MISSING_TEXT, "", ...steps].join("\n"));
|
|
5118
|
+
this.name = "MeasureMissingError";
|
|
5119
|
+
this.steps = steps;
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
const MEASURE_CLI_PACKAGE = "@bridge-ds/cli@^0.13.0";
|
|
5123
|
+
const MEASURE_FLAGS = "--variants --states rest,hover,active,disabled --tokens --shots --check";
|
|
5124
|
+
const MEASURE_DEFAULT_PATHS = ["src/**", "packages/**", ".storybook/**", "package.json", "package-lock.json"];
|
|
5125
|
+
function measureWorkflowOptionsFromConfig(config) {
|
|
5126
|
+
const c = config && typeof config === "object" ? config : {};
|
|
5127
|
+
const str2 = (v) => typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
5128
|
+
const storybook = str2(c.previews?.production?.storybook);
|
|
5129
|
+
const dir = str2(c.components?.dir)?.replace(/^\.?\/+|\/+$/g, "");
|
|
5130
|
+
const tokens = str2(c.tokens?.root)?.replace(/^\.?\/+|\/+$/g, "");
|
|
5131
|
+
const source = dir ? dir.includes("/") ? dir.replace(/\/[^/]*$/, "") : dir : void 0;
|
|
5132
|
+
const paths = [.../* @__PURE__ */ new Set([...source ? [`${source}/**`] : [], ...tokens ? [`${tokens}/**`] : [], ".storybook/**", "package.json", "package-lock.json"])];
|
|
5133
|
+
return {
|
|
5134
|
+
...str2(c.repo?.defaultBranch) ? { defaultBranch: str2(c.repo?.defaultBranch) } : {},
|
|
5135
|
+
...storybook && /^https:\/\//.test(storybook) ? { storybookUrl: storybook } : {},
|
|
5136
|
+
...source || tokens ? { paths } : {}
|
|
5137
|
+
};
|
|
5138
|
+
}
|
|
5139
|
+
const q = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
5140
|
+
const MEASURE_WORKFLOW_HEADER = `# Bridge measure: keeps the measure of your components fresh, so Claude can fix what differs in Figma.
|
|
5141
|
+
# Copy it to ${MEASURE_WORKFLOW_FILE} in your design system repository, or let Bridge add it: Studio, Settings,
|
|
5142
|
+
# For developers, Set up automatic measure (a pull request), or \`npx @bridge-ds/cli measure setup\`.
|
|
5143
|
+
#
|
|
5144
|
+
# It measures what your Storybook really draws (\`bridge specs --measure\`, headless Chrome): every variant at rest, on
|
|
5145
|
+
# hover, pressed and disabled, the CSS variables behind each value, and a screenshot of each. It publishes
|
|
5146
|
+
# ${DEFAULT_MEASURED_SPECS_FILE} and ${MEASURED_SHOTS_DIR}/ to the \`${MEASURE_BRANCH}\` branch, next to figma-fidelity.json (the Figma plugin's
|
|
5147
|
+
# Compare with code). The Claude fix tools, the Figma plugin ("Ask Claude to fix it") and Studio read it there.
|
|
5148
|
+
# It commits only when the measure changed.
|
|
5149
|
+
#
|
|
5150
|
+
# When: after each deploy of your Storybook (deployed Storybook), or on each push to the default branch that changes it
|
|
5151
|
+
# (built here), every Monday as a fallback, and by hand (Actions, Bridge measure, Run workflow).
|
|
5152
|
+
#
|
|
5153
|
+
# Token: only the GITHUB_TOKEN of this run, with \`contents: write\` to push the \`${MEASURE_BRANCH}\` branch. Your
|
|
5154
|
+
# default branch is never written. No secret to add.
|
|
5155
|
+
# A Storybook behind a login: set it up without an address (\`npx @bridge-ds/cli measure setup --build\`), the run then
|
|
5156
|
+
# builds Storybook itself. If installing needs a private registry, add an NPM_TOKEN repository secret (optional); the
|
|
5157
|
+
# install step passes it as NODE_AUTH_TOKEN.
|
|
5158
|
+
# Docs: https://bridge.floriangouloubi.com/docs/automatic-measure/`;
|
|
5159
|
+
function measureWorkflowYaml(o = {}) {
|
|
5160
|
+
const branch = o.defaultBranch?.trim() || "main";
|
|
5161
|
+
const url = o.storybookUrl?.trim().replace(/\/+$/, "") ?? "";
|
|
5162
|
+
const deployed = Boolean(url);
|
|
5163
|
+
const deployName = (o.deployName?.trim() || "storybook").toLowerCase();
|
|
5164
|
+
const paths = o.paths?.length ? o.paths : MEASURE_DEFAULT_PATHS;
|
|
5165
|
+
const install = o.install?.trim() || "npm ci";
|
|
5166
|
+
const build = o.buildCommand?.trim() || "npm run build-storybook";
|
|
5167
|
+
const dir = o.storybookDir?.trim().replace(/\/+$/, "") || "storybook-static";
|
|
5168
|
+
const cli = o.cli?.trim() || `npx --yes ${MEASURE_CLI_PACKAGE}`;
|
|
5169
|
+
const node = o.node?.trim() || "20";
|
|
5170
|
+
const nodeLine = node.startsWith("file:") ? `node-version-file: ${node.slice(5)}` : `node-version: ${q(node)}`;
|
|
5171
|
+
const pathList = paths.map((p2) => ` - ${q(p2)}`).join("\n");
|
|
5172
|
+
const deployIf = `(github.event_name == 'status' && github.event.state == 'success' && contains(github.event.branches.*.name, github.event.repository.default_branch) && contains(github.event.context, ${q(deployName)})) || (github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' && contains(github.event.deployment.environment, ${q(deployName)}) && !contains(github.event.deployment.environment, 'preview'))`;
|
|
5173
|
+
const triggers = deployed ? `on:
|
|
5174
|
+
# After each successful deploy of the Storybook from the default branch (Vercel and Netlify report it as a commit
|
|
5175
|
+
# status, others as a GitHub deployment).
|
|
5176
|
+
status:
|
|
5177
|
+
deployment_status:
|
|
5178
|
+
schedule:
|
|
5179
|
+
- cron: '17 5 * * 1' # Mondays, a fallback
|
|
5180
|
+
workflow_dispatch:` : `on:
|
|
5181
|
+
push:
|
|
5182
|
+
branches: [${branch}]
|
|
5183
|
+
# What changes the Storybook: a push touching these measures again.
|
|
5184
|
+
paths:
|
|
5185
|
+
${pathList}
|
|
5186
|
+
schedule:
|
|
5187
|
+
- cron: '17 5 * * 1' # Mondays, a fallback
|
|
5188
|
+
workflow_dispatch:`;
|
|
5189
|
+
const jobIf = deployed ? ` # A manual run from another branch would measure another branch. A deploy: the Storybook's, from the default branch.
|
|
5190
|
+
if: >-
|
|
5191
|
+
(github.event_name == 'schedule')
|
|
5192
|
+
|| (github.event_name == 'workflow_dispatch' && github.ref_name == github.event.repository.default_branch)
|
|
5193
|
+
|| ${deployIf}` : ` # A manual run from another branch would measure another branch.
|
|
5194
|
+
if: github.event_name != 'workflow_dispatch' || github.ref_name == github.event.repository.default_branch`;
|
|
5195
|
+
const changedGuard = deployed ? `
|
|
5196
|
+
# A deploy runs on every change to the default branch: measure only when this one can change the Storybook.
|
|
5197
|
+
- name: Did the Storybook change?
|
|
5198
|
+
id: changed
|
|
5199
|
+
run: |
|
|
5200
|
+
if [ "$GITHUB_EVENT_NAME" = "status" ] || [ "$GITHUB_EVENT_NAME" = "deployment_status" ]; then
|
|
5201
|
+
if git rev-parse -q --verify HEAD^ > /dev/null && git diff --quiet HEAD^ HEAD -- ${paths.map(q).join(" ")}; then
|
|
5202
|
+
echo "Nothing that changes the Storybook in this deploy: the measure stays as it is."
|
|
5203
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
5204
|
+
fi
|
|
5205
|
+
fi
|
|
5206
|
+
` : "";
|
|
5207
|
+
const whenStep = (s) => deployed ? s.replace(/^( {6}- name: .*\n)/, `$1 if: steps.changed.outputs.skip != 'true'
|
|
5208
|
+
`) : s;
|
|
5209
|
+
const storybookSteps = deployed ? "" : whenStep(` - name: Build Storybook
|
|
5210
|
+
run: ${build}
|
|
5211
|
+
|
|
5212
|
+
- name: Serve the build
|
|
5213
|
+
run: |
|
|
5214
|
+
python3 -m http.server 6006 --bind 127.0.0.1 --directory ${q(dir)} > /dev/null 2>&1 &
|
|
5215
|
+
for i in $(seq 1 30); do curl -sf http://127.0.0.1:6006/index.json > /dev/null && exit 0; sleep 1; done
|
|
5216
|
+
echo "Storybook did not answer on http://127.0.0.1:6006 (is ${dir} the build folder?)"; exit 1
|
|
5217
|
+
|
|
5218
|
+
`);
|
|
5219
|
+
const storybook = deployed ? url : "http://127.0.0.1:6006";
|
|
5220
|
+
const steps = ` - uses: actions/checkout@v4
|
|
5221
|
+
with:
|
|
5222
|
+
# A deploy status names its commit; other events run on the default branch head.
|
|
5223
|
+
ref: \${{ github.event.sha || github.sha }}
|
|
5224
|
+
fetch-depth: 2
|
|
5225
|
+
${changedGuard}
|
|
5226
|
+
${whenStep(` - name: Set up Node
|
|
5227
|
+
uses: actions/setup-node@v4
|
|
5228
|
+
with:
|
|
5229
|
+
${nodeLine}
|
|
5230
|
+
cache: npm
|
|
5231
|
+
`)}
|
|
5232
|
+
${whenStep(` - name: Install
|
|
5233
|
+
env:
|
|
5234
|
+
NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}
|
|
5235
|
+
run: ${install}
|
|
5236
|
+
`)}${o.prepare ? `
|
|
5237
|
+
${whenStep(` - name: Prepare
|
|
5238
|
+
run: ${o.prepare}
|
|
5239
|
+
`)}` : ""}
|
|
5240
|
+
${storybookSteps}${whenStep(` - name: Measure the Storybook
|
|
5241
|
+
env:
|
|
5242
|
+
# ubuntu-latest ships Google Chrome; the measure drives it headless, no browser download.
|
|
5243
|
+
BRIDGE_CHROME_CHANNEL: chrome
|
|
5244
|
+
run: |
|
|
5245
|
+
set -o pipefail
|
|
5246
|
+
${cli} specs --measure --storybook "$STORYBOOK_URL" ${MEASURE_FLAGS} --out "$RUNNER_TEMP/bridge-measure/${DEFAULT_MEASURED_SPECS_FILE}" | tail -n 40 | tee -a "$GITHUB_STEP_SUMMARY"
|
|
5247
|
+
`)}
|
|
5248
|
+
${whenStep(` - name: Publish to the ${MEASURE_BRANCH} branch, if the measure changed
|
|
5249
|
+
env:
|
|
5250
|
+
OUT: \${{ runner.temp }}/bridge-measure
|
|
5251
|
+
run: |
|
|
5252
|
+
set -euo pipefail
|
|
5253
|
+
git config --global user.name "github-actions[bot]"
|
|
5254
|
+
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
5255
|
+
dir="$RUNNER_TEMP/measure-branch"
|
|
5256
|
+
if git ls-remote --exit-code --heads origin "$MEASURE_BRANCH" > /dev/null; then
|
|
5257
|
+
git fetch -q --depth 1 origin "$MEASURE_BRANCH"
|
|
5258
|
+
git worktree add -q --detach "$dir" FETCH_HEAD
|
|
5259
|
+
else
|
|
5260
|
+
# Like the Figma plugin: the requests branch starts from the default branch.
|
|
5261
|
+
git worktree add -q --detach "$dir" HEAD
|
|
5262
|
+
fi
|
|
5263
|
+
# The same measure at another time is not a change: compare without the timestamps.
|
|
5264
|
+
same() { node -e 'const f=require("fs");const r=(p)=>{try{const j=JSON.parse(f.readFileSync(p,"utf8"));delete j.measuredAt;delete j.createdAt;return JSON.stringify(j)}catch{return null}};const a=r(process.argv[1]);process.exit(a!==null&&a===r(process.argv[2])?0:1)' "$1" "$2"; }
|
|
5265
|
+
if same "$dir/${DEFAULT_MEASURED_SPECS_FILE}" "$OUT/${DEFAULT_MEASURED_SPECS_FILE}" && { [ ! -f "$OUT/${MEASURED_SHOTS_DIR}/manifest.json" ] || same "$dir/${MEASURED_SHOTS_DIR}/manifest.json" "$OUT/${MEASURED_SHOTS_DIR}/manifest.json"; }; then
|
|
5266
|
+
echo "The measure did not change: nothing to publish." | tee -a "$GITHUB_STEP_SUMMARY"
|
|
5267
|
+
exit 0
|
|
5268
|
+
fi
|
|
5269
|
+
rm -rf "$dir/${DEFAULT_MEASURED_SPECS_FILE}" "$dir/${MEASURED_SHOTS_DIR}"
|
|
5270
|
+
cp "$OUT/${DEFAULT_MEASURED_SPECS_FILE}" "$dir/"
|
|
5271
|
+
if [ -d "$OUT/${MEASURED_SHOTS_DIR}" ]; then cp -R "$OUT/${MEASURED_SHOTS_DIR}" "$dir/"; fi
|
|
5272
|
+
cd "$dir"
|
|
5273
|
+
git add -A
|
|
5274
|
+
git commit -qm "bridge measure: \${GITHUB_SHA:0:7}"
|
|
5275
|
+
# The Figma plugin writes to this branch too: on a race, rebase and retry.
|
|
5276
|
+
for i in 1 2 3 4; do
|
|
5277
|
+
if git push -q origin "HEAD:refs/heads/$MEASURE_BRANCH"; then echo "Published to $MEASURE_BRANCH." | tee -a "$GITHUB_STEP_SUMMARY"; exit 0; fi
|
|
5278
|
+
sleep $((i * 3))
|
|
5279
|
+
git fetch -q --depth 20 origin "$MEASURE_BRANCH" && git rebase -q FETCH_HEAD
|
|
5280
|
+
done
|
|
5281
|
+
exit 1
|
|
5282
|
+
`)}`;
|
|
5283
|
+
return `${o.header ?? MEASURE_WORKFLOW_HEADER}
|
|
5284
|
+
|
|
5285
|
+
name: Bridge measure
|
|
5286
|
+
|
|
5287
|
+
${triggers}
|
|
5288
|
+
|
|
5289
|
+
concurrency:
|
|
5290
|
+
# One measure at a time; a newer one replaces a waiting one.
|
|
5291
|
+
group: bridge-measure
|
|
5292
|
+
cancel-in-progress: false
|
|
5293
|
+
|
|
5294
|
+
permissions:
|
|
5295
|
+
contents: write # push to the ${MEASURE_BRANCH} branch only
|
|
5296
|
+
|
|
5297
|
+
env:
|
|
5298
|
+
MEASURE_BRANCH: ${MEASURE_BRANCH}
|
|
5299
|
+
# ${deployed ? "The deployed Storybook the measure reads." : "Storybook as this run builds and serves it (below)."}
|
|
5300
|
+
STORYBOOK_URL: ${q(storybook)}
|
|
5301
|
+
|
|
5302
|
+
jobs:
|
|
5303
|
+
measure:
|
|
5304
|
+
name: Measure the Storybook
|
|
5305
|
+
${jobIf}
|
|
5306
|
+
runs-on: ubuntu-latest
|
|
5307
|
+
timeout-minutes: 120
|
|
5308
|
+
steps:
|
|
5309
|
+
${steps.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "")}
|
|
5310
|
+
`;
|
|
5311
|
+
}
|
|
5312
|
+
const leadsPublishTier1On = (gov) => gov?.leadsPublishTier1 !== false;
|
|
5313
|
+
const CODE_TIER1 = [/\.stories\.[cm]?[jt]sx?$/, /\.test\.[cm]?[jt]sx?$/];
|
|
5314
|
+
function tier1Domain(paths) {
|
|
5315
|
+
return paths.some((p2) => CODE_TIER1.some((r) => r.test(p2))) ? "tech" : "design";
|
|
5316
|
+
}
|
|
5317
|
+
const norm = (x) => x.trim().replace(/^@/, "").toLowerCase();
|
|
5318
|
+
function canPublishTier1(x) {
|
|
5319
|
+
if (!x.enabled || x.tier !== 1 || !x.viewer) return false;
|
|
5320
|
+
if (x.admin) return true;
|
|
5321
|
+
const list = x.domain === "design" ? x.approvers.design : x.approvers.tech;
|
|
5322
|
+
return list.some((l) => !l.includes("/") && norm(l) === norm(x.viewer));
|
|
5323
|
+
}
|
|
5324
|
+
const PUBLISH_LABEL = "Publish";
|
|
5325
|
+
const TIER1_PUBLISH_HEADING = "**Bridge: published without review (Tier 1)**";
|
|
5326
|
+
const publishedWithoutReview = (name) => `Published by ${name.trim() || "a lead"} without review (Tier 1).`;
|
|
5327
|
+
function tier1PublishReviewBody(name, login) {
|
|
5328
|
+
return `${TIER1_PUBLISH_HEADING}
|
|
5329
|
+
|
|
5330
|
+
${publishedWithoutReview(name)} @${norm(login)} leads this part of the design system and the team lets leads publish Tier 1 changes (governance.leadsPublishTier1). It merges once the automatic checks pass; a failing check sends it to review instead.`;
|
|
5331
|
+
}
|
|
5332
|
+
function tier1PublishedBy(body) {
|
|
5333
|
+
if (!body || !body.trimStart().startsWith(TIER1_PUBLISH_HEADING)) return null;
|
|
5334
|
+
const m = /Published by (.+?) without review \(Tier 1\)\./.exec(body);
|
|
5335
|
+
return m ? m[1] : null;
|
|
5336
|
+
}
|
|
5337
|
+
function tier1PublishOf(reviews, headSha) {
|
|
5338
|
+
for (const r of [...reviews ?? []].reverse()) {
|
|
5339
|
+
if (r.state !== "APPROVED") continue;
|
|
5340
|
+
if (headSha && r.commit_id && r.commit_id !== headSha) continue;
|
|
5341
|
+
const by3 = tier1PublishedBy(r.body);
|
|
5342
|
+
if (by3) return by3;
|
|
5343
|
+
}
|
|
5344
|
+
return null;
|
|
5345
|
+
}
|
|
5346
|
+
function tier1PublishSentence(enabled) {
|
|
5347
|
+
return enabled ? "Tier 1 (docs, guidance, token descriptions, story and test fixes): team admins and the reviewers of that part publish it without review; Bridge merges it once the checks pass." : "Tier 1 needs one owner approval: leads publishing small changes without review is off.";
|
|
5348
|
+
}
|
|
4405
5349
|
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
4406
5350
|
const names$1 = (files) => files.flatMap((f) => f.previous_filename ? [f.filename, f.previous_filename] : [f.filename]);
|
|
4407
5351
|
function overlap(changeFiles, libraryFiles, isTokenFile) {
|
|
@@ -4737,192 +5681,6 @@ async function readCombineBranch(gh, pull, opts) {
|
|
|
4737
5681
|
);
|
|
4738
5682
|
return { number: pull.number, title: pull.title, tokens: true, edits: changeEdits(source) };
|
|
4739
5683
|
}
|
|
4740
|
-
const PRODUCT_FILE = /\.(css|scss|sass|less|tsx|jsx|ts|js|mjs|cjs|mts|cts|astro|vue|svelte)$/;
|
|
4741
|
-
const SKIP_FILE = /\.d\.[cm]?ts$|\.(test|spec|stories|story)\.[cm]?[jt]sx?$|\.min\.(css|js)$/;
|
|
4742
|
-
const PIECES_FILE_LIMIT = 40;
|
|
4743
|
-
const cleanPath = (p2) => p2.trim().replace(/^\.?\/+|\/+$/g, "");
|
|
4744
|
-
function productOf(products, pull, files) {
|
|
4745
|
-
const product = productOfBranch(products, { labels: pull.labels, files });
|
|
4746
|
-
if (product.key === defaultProduct(products).key) throw new GitHubError(422, `This branch is a library branch (${product.name}), not a product branch: missing pieces are checked on a branch started for a product.`);
|
|
4747
|
-
return product;
|
|
4748
|
-
}
|
|
4749
|
-
async function readProductBranchOnGitHub(gh, pr, opts) {
|
|
4750
|
-
const pull = await gh.getPullRequest(pr);
|
|
4751
|
-
const cmp = await gh.compareDetailed(pull.baseRef, pull.headSha);
|
|
4752
|
-
const product = productOf(opts.products, pull, cmp.files.map((f) => f.filename));
|
|
4753
|
-
const dir = product.path ? `${cleanPath(product.path)}/` : "";
|
|
4754
|
-
const changed = cmp.files.filter((f) => f.status !== "removed" && (!dir || f.filename.startsWith(dir)) && PRODUCT_FILE.test(f.filename) && !SKIP_FILE.test(f.filename) && !f.filename.split("/").includes("node_modules"));
|
|
4755
|
-
const read = changed.slice(0, PIECES_FILE_LIMIT);
|
|
4756
|
-
const start = cmp.mergeBase ?? pull.baseRef;
|
|
4757
|
-
const files = await Promise.all(
|
|
4758
|
-
read.map(async (f) => {
|
|
4759
|
-
const [before, after] = await Promise.all([f.status === "added" ? Promise.resolve(void 0) : gh.getFileText(f.previous_filename ?? f.filename, start), gh.getFileText(f.filename, pull.headSha)]);
|
|
4760
|
-
return { path: f.filename, before: before ?? null, after: after ?? null };
|
|
4761
|
-
})
|
|
4762
|
-
);
|
|
4763
|
-
return { pull, product, files, skipped: changed.length - read.length, record: parsePiecesRecord(pull.body) };
|
|
4764
|
-
}
|
|
4765
|
-
async function recordPiecesOnGitHub(gh, pr, check) {
|
|
4766
|
-
const pull = await gh.getPullRequest(pr);
|
|
4767
|
-
const record = mergePiecesRecord(parsePiecesRecord(pull.body), { checkedAt: check.checkedAt ?? (/* @__PURE__ */ new Date()).toISOString(), headSha: check.headSha ?? pull.headSha, pieces: check.pieces, fixes: check.fixes });
|
|
4768
|
-
await gh.updatePullRequest(pr, { body: withPiecesRecord(pull.body, record) });
|
|
4769
|
-
return record;
|
|
4770
|
-
}
|
|
4771
|
-
const productWriter = (gh, branch) => isExplorationBranch(branch) ? gh.withBranchPrefix(EXPLORATION_PREFIX) : gh;
|
|
4772
|
-
async function useLibraryPreviewOnGitHub(gh, input) {
|
|
4773
|
-
const message = `chore(bridge): use the preview of ${input.libraryBranch} (#${input.libraryNumber})
|
|
4774
|
-
|
|
4775
|
-
The product branch takes the library branch before it ships. Switch to the published version once it does.`;
|
|
4776
|
-
try {
|
|
4777
|
-
const r = await productWriter(gh, input.productBranch).mergeIntoBranch(input.productBranch, input.libraryBranch, message);
|
|
4778
|
-
return { ...r, said: r.status === "merged" ? "Your branch now uses the preview of the library branch." : "Your branch already has the latest preview." };
|
|
4779
|
-
} catch (err) {
|
|
4780
|
-
if (err instanceof GitHubError && err.status === 409) return { status: "conflict", sha: null, said: `Your branch and the library branch change the same lines. A developer merges ${input.libraryBranch} into ${input.productBranch} by hand.` };
|
|
4781
|
-
throw err;
|
|
4782
|
-
}
|
|
4783
|
-
}
|
|
4784
|
-
function productBranchRef(pull, product) {
|
|
4785
|
-
const name = /<!-- bridge-exploration (\{[\s\S]*?\}) -->/.exec(pull.body ?? "");
|
|
4786
|
-
let title = pull.title.replace(/^[A-Z][A-Z0-9_]*-\d+\s+/, "").replace(/^Exploration:\s*/i, "");
|
|
4787
|
-
let author = pull.author;
|
|
4788
|
-
if (name) {
|
|
4789
|
-
try {
|
|
4790
|
-
const e = JSON.parse(name[1]);
|
|
4791
|
-
if (e.name) title = e.name;
|
|
4792
|
-
if (e.author) author = e.author;
|
|
4793
|
-
} catch {
|
|
4794
|
-
}
|
|
4795
|
-
}
|
|
4796
|
-
return { pr: pull.number, title: title.charAt(0).toUpperCase() + title.slice(1), branch: pull.headRef, url: pull.url, productKey: product.key, productName: product.name, ...author ? { author } : {} };
|
|
4797
|
-
}
|
|
4798
|
-
async function openLibraryBranchOnGitHub(gh, input) {
|
|
4799
|
-
const pull = await gh.getPullRequest(input.pr);
|
|
4800
|
-
const record = parsePiecesRecord(pull.body);
|
|
4801
|
-
const piece = record?.pieces.find((p2) => p2.id === input.piece);
|
|
4802
|
-
if (!record || !piece) throw new GitHubError(404, record ? `No piece "${input.piece}" on #${input.pr}. Pieces: ${record.pieces.map((p2) => p2.id).join(", ") || "none"}.` : `#${input.pr} was not checked yet: find its missing pieces first (bridge_missing_pieces with record, or \`bridge pieces ${input.pr} --record\`).`);
|
|
4803
|
-
const product = productOf(input.products, pull, await gh.listPullRequestFiles(input.pr).then((f) => f.map((x) => x.filename), () => []));
|
|
4804
|
-
const mode = input.mode ?? "direct";
|
|
4805
|
-
const plan = libraryBranchPlan(piece, productBranchRef(pull, product), { source: input.source, requestedBy: input.openedBy, mode });
|
|
4806
|
-
const existing = record.links.find((l) => l.piece === piece.id);
|
|
4807
|
-
if (existing) return { status: "already-linked", plan, link: existing, preview: null, warnings: [] };
|
|
4808
|
-
if (input.dryRun) return { status: "dry-run", plan, link: null, preview: null, warnings: [] };
|
|
4809
|
-
if (!gh.canWrite) throw new GitHubError(401, mode === "suggest" ? "Suggesting a library change needs a GitHub token (issues: write)." : "Opening a library branch needs a GitHub token (contents, pull requests and issues: write).");
|
|
4810
|
-
const warnings = [];
|
|
4811
|
-
const at2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
4812
|
-
const who = { ...input.openedBy ? { openedBy: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {} };
|
|
4813
|
-
let link;
|
|
4814
|
-
if (plan.kind === "token-change") {
|
|
4815
|
-
if (!input.writeTokens) throw new GitHubError(422, "This piece is a token: open it where the token files are read (Bridge Studio, bridge_open_library_branch or `bridge pieces open`).");
|
|
4816
|
-
const r = await input.writeTokens(plan, { mode });
|
|
4817
|
-
link = r.status === "suggested" ? { piece: piece.id, kind: "suggestion", number: r.number, url: r.url, title: plan.title, at: at2, ...who } : { piece: piece.id, kind: "token-change", number: r.number, url: r.url, title: plan.title, branch: r.branch, at: at2, ...who };
|
|
4818
|
-
} else if (plan.kind === "exploration") {
|
|
4819
|
-
const r = await startExplorationOnGitHub(gh, { ...plan.exploration, ...input.openedBy ? { author: input.openedBy } : {}, ...input.agent ? { agent: input.agent } : {} }, { base: input.base, ...input.exploration, labels: [NEEDED_BY_LABEL] });
|
|
4820
|
-
link = { piece: piece.id, kind: "exploration", number: r.pr, url: r.url, title: plan.title, branch: r.branch, at: at2, ...who };
|
|
4821
|
-
} else {
|
|
4822
|
-
const issue = composeRequestIssue(plan.request, { labels: [NEEDED_BY_LABEL] });
|
|
4823
|
-
const r = await gh.createIssue(issue);
|
|
4824
|
-
link = { piece: piece.id, kind: "request", number: r.number, url: r.url, title: plan.title, at: at2, ...who };
|
|
4825
|
-
}
|
|
4826
|
-
try {
|
|
4827
|
-
if (link.kind === "request" || link.kind === "suggestion") {
|
|
4828
|
-
const issue = await gh.getIssue(link.number);
|
|
4829
|
-
await gh.updateIssue(link.number, { body: withNeededBy(issue.body, plan.neededBy) });
|
|
4830
|
-
if (link.kind === "suggestion" && !issue.labels.includes(NEEDED_BY_LABEL)) await gh.addLabels(link.number, [NEEDED_BY_LABEL]);
|
|
4831
|
-
} else {
|
|
4832
|
-
const lib = await gh.getPullRequest(link.number);
|
|
4833
|
-
await gh.updatePullRequest(link.number, { body: withNeededBy(lib.body, plan.neededBy) });
|
|
4834
|
-
await gh.addLabels(link.number, [NEEDED_BY_LABEL]);
|
|
4835
|
-
}
|
|
4836
|
-
} catch (err) {
|
|
4837
|
-
if (err instanceof GitHubError && err.auth === "rejected") throw err;
|
|
4838
|
-
warnings.push(`The library side does not say who needs it yet (${err.message}). Add "Needed by #${input.pr}" to #${link.number} on GitHub.`);
|
|
4839
|
-
}
|
|
4840
|
-
try {
|
|
4841
|
-
const fresh = await gh.getPullRequest(input.pr);
|
|
4842
|
-
const next = withLink(parsePiecesRecord(fresh.body) ?? record, link);
|
|
4843
|
-
await gh.updatePullRequest(input.pr, { body: withPiecesRecord(fresh.body, next) });
|
|
4844
|
-
} catch (err) {
|
|
4845
|
-
if (mode !== "suggest") throw err;
|
|
4846
|
-
warnings.push(`#${input.pr} does not list #${link.number} yet (${err.message}). Add "Library: #${link.number}" to #${input.pr} on GitHub, so this piece is not sent twice.`);
|
|
4847
|
-
}
|
|
4848
|
-
let preview = null;
|
|
4849
|
-
if ((input.usePreview ?? true) && link.kind === "token-change" && link.branch) {
|
|
4850
|
-
preview = await useLibraryPreviewOnGitHub(gh, { productBranch: pull.headRef, libraryBranch: link.branch, libraryNumber: link.number }).catch((err) => {
|
|
4851
|
-
warnings.push(`Your branch does not use the preview yet: ${err.message}`);
|
|
4852
|
-
return null;
|
|
4853
|
-
});
|
|
4854
|
-
}
|
|
4855
|
-
return { status: link.kind === "suggestion" ? "suggested" : "opened", plan, link, preview, warnings };
|
|
4856
|
-
}
|
|
4857
|
-
const isNote = (path) => path.startsWith(`${EXPLORATION_NOTES_DIR}/`);
|
|
4858
|
-
async function linkedLibraryBranchOnGitHub(gh, link, product, piece) {
|
|
4859
|
-
const nearest = piece?.nearest?.label;
|
|
4860
|
-
if (link.kind === "suggestion") {
|
|
4861
|
-
const issue = await gh.getIssue(link.number);
|
|
4862
|
-
const cached = suggestionStateFromLabels(issue.labels);
|
|
4863
|
-
const s = suggestionStatus({ state: issue.state, stateReason: issue.stateReason, applied: cached === "applying", review: cached === "applying" ? void 0 : cached });
|
|
4864
|
-
const status2 = linkStatus({ kind: "suggestion", status: s });
|
|
4865
|
-
const lifecycle2 = s === "applied" ? "merged" : issue.state === "closed" ? "closed" : "open";
|
|
4866
|
-
const behindMain2 = lifecycle2 === "merged" ? await gh.compare(product.baseRef, product.headSha).then((c) => c.behindBy, () => null) : null;
|
|
4867
|
-
const preview2 = libraryPreviewState({ kind: "suggestion", lifecycle: lifecycle2, hasChanges: false, missing: null, total: null, behindMain: behindMain2, nearest });
|
|
4868
|
-
return { link, piece, status: status2, preview: preview2, people: status2.people };
|
|
4869
|
-
}
|
|
4870
|
-
if (link.kind === "request") {
|
|
4871
|
-
const issue = await gh.getIssue(link.number);
|
|
4872
|
-
const status2 = linkStatus({ kind: "request", state: issue.state, stateReason: issue.stateReason });
|
|
4873
|
-
const lifecycle2 = issue.state === "closed" ? issue.stateReason === "not_planned" ? "closed" : "merged" : "open";
|
|
4874
|
-
const behindMain2 = lifecycle2 === "merged" ? await gh.compare(product.baseRef, product.headSha).then((c) => c.behindBy, () => null) : null;
|
|
4875
|
-
const preview2 = libraryPreviewState({ kind: "request", lifecycle: lifecycle2, hasChanges: false, missing: null, total: null, behindMain: behindMain2, nearest });
|
|
4876
|
-
return { link, piece, status: status2, preview: preview2, people: status2.people };
|
|
4877
|
-
}
|
|
4878
|
-
const lib = await gh.getPullRequest(link.number);
|
|
4879
|
-
const reviews = await gh.listReviews(link.number).catch(() => []);
|
|
4880
|
-
const status = linkStatus({ kind: link.kind, state: lib.state, draft: lib.draft, labels: lib.labels, requestedReviewers: lib.requestedReviewers, reviews: reviews.map((r) => ({ user: r.user, state: r.state })) });
|
|
4881
|
-
const lifecycle = lib.state === "merged" ? "merged" : lib.state === "open" ? "open" : "closed";
|
|
4882
|
-
let missing = null;
|
|
4883
|
-
let total = null;
|
|
4884
|
-
let behindMain = null;
|
|
4885
|
-
let hasChanges = true;
|
|
4886
|
-
if (lifecycle === "open") {
|
|
4887
|
-
const [files, own, inProduct] = await Promise.all([
|
|
4888
|
-
gh.listPullRequestFiles(link.number).catch(() => null),
|
|
4889
|
-
gh.compare(lib.baseRef, lib.headSha).catch(() => null),
|
|
4890
|
-
gh.compare(product.headSha, lib.headSha).catch(() => null)
|
|
4891
|
-
]);
|
|
4892
|
-
hasChanges = files ? files.some((f) => !isNote(f.filename)) : true;
|
|
4893
|
-
total = own?.aheadBy ?? null;
|
|
4894
|
-
missing = inProduct?.aheadBy ?? null;
|
|
4895
|
-
} else if (lifecycle === "merged") {
|
|
4896
|
-
behindMain = await gh.compare(product.baseRef, product.headSha).then((c) => c.behindBy, () => null);
|
|
4897
|
-
}
|
|
4898
|
-
const preview = libraryPreviewState({ kind: link.kind, lifecycle, hasChanges, missing, total, behindMain, nearest });
|
|
4899
|
-
return { link, piece, status, preview, people: status.people };
|
|
4900
|
-
}
|
|
4901
|
-
async function productBranchViewOnGitHub(gh, pr, opts) {
|
|
4902
|
-
const pull = await gh.getPullRequest(pr);
|
|
4903
|
-
const files = await gh.listPullRequestFiles(pr).then((f) => f.map((x) => x.filename), () => []);
|
|
4904
|
-
const product = productOf(opts.products, pull, files);
|
|
4905
|
-
const record = parsePiecesRecord(pull.body);
|
|
4906
|
-
const linked = await Promise.all(
|
|
4907
|
-
(record?.links ?? []).map((l) => linkedLibraryBranchOnGitHub(gh, l, { headSha: pull.headSha, baseRef: pull.baseRef }, record?.pieces.find((p2) => p2.id === l.piece)))
|
|
4908
|
-
);
|
|
4909
|
-
const open = (record?.pieces ?? []).filter((p2) => !record.links.some((l) => l.piece === p2.id));
|
|
4910
|
-
return { pull, product, record, sentence: productBranchSentence(record, { viewer: opts.viewer, agentName: opts.agentName }), linked, open };
|
|
4911
|
-
}
|
|
4912
|
-
async function switchToPublishedOnGitHub(gh, pr) {
|
|
4913
|
-
if (!gh.canWrite) throw new GitHubError(401, "Switching needs a GitHub token (contents and pull requests: write).");
|
|
4914
|
-
const pull = await gh.getPullRequest(pr);
|
|
4915
|
-
if (pull.state !== "open") throw new GitHubError(422, `#${pr} is ${pull.state}: only an open branch switches.`);
|
|
4916
|
-
const cmp = await gh.compare(pull.baseRef, pull.headSha);
|
|
4917
|
-
if (cmp.behindBy === 0) return { status: "up-to-date", said: "Your branch already uses the published version." };
|
|
4918
|
-
try {
|
|
4919
|
-
await gh.updatePullRequestBranch(pr, pull.headSha);
|
|
4920
|
-
} catch (err) {
|
|
4921
|
-
if (err instanceof GitHubError && err.status === 422) throw new GitHubError(422, `Your branch and ${pull.baseRef} change the same lines, so Bridge cannot switch it alone. Use "Update from main" to pick the token values, or ask a developer.`, err.detail);
|
|
4922
|
-
throw err;
|
|
4923
|
-
}
|
|
4924
|
-
return { status: "switched", said: `Your branch now uses the published version from ${pull.baseRef}.` };
|
|
4925
|
-
}
|
|
4926
5684
|
const issueKey = (i) => `${i.code}|${i.path ?? ""}|${i.file ?? ""}|${i.message}`;
|
|
4927
5685
|
function planTokenProposal(input) {
|
|
4928
5686
|
const { before, config } = input;
|
|
@@ -4939,15 +5697,15 @@ function planTokenProposal(input) {
|
|
|
4939
5697
|
const tier = classifyLayered(changes, [], config);
|
|
4940
5698
|
const summary = input.summary?.trim() || input.title;
|
|
4941
5699
|
const body = [composeProposalBody({ summary, changes, contrast, impact, tier, validationErrors: newErrors.length, tool: input.tool ?? "Bridge MCP", ...input.fromFigma ? { fromFigma: true } : {} }), ...input.bodyExtra ?? []].filter(Boolean).join("\n\n");
|
|
4942
|
-
const
|
|
4943
|
-
const changesetPath = `${config.changesets?.dir ?? ".changeset"}/bridge-${
|
|
5700
|
+
const slug2 = slugify(input.title, 48);
|
|
5701
|
+
const changesetPath = `${config.changesets?.dir ?? ".changeset"}/bridge-${slug2}.md`;
|
|
4944
5702
|
const touched = r.touched.map((f) => after.repoPath(f));
|
|
4945
5703
|
const files = [
|
|
4946
5704
|
...r.touched.map((f) => ({ path: after.repoPath(f), content: after.files.get(f) })),
|
|
4947
5705
|
...config.changesets ? [{ path: changesetPath, content: formatChangeset(config.changesets.package, tier.bump, summary) }] : []
|
|
4948
5706
|
];
|
|
4949
5707
|
const blocked = newErrors.length || newFailures.length ? [newErrors.length ? `${newErrors.length} new validation error(s)` : "", newFailures.length ? `${newFailures.length} contrast pair(s) would fail` : ""].filter(Boolean).join(" and ") : null;
|
|
4950
|
-
return { title: input.title, summary, edits: input.edits, before, after, changes, tier, contrast, newFailures, newErrors, impact, notes: r.notes, touched, files, body, slug, blocked };
|
|
5708
|
+
return { title: input.title, summary, edits: input.edits, before, after, changes, tier, contrast, newFailures, newErrors, impact, notes: r.notes, touched, files, body, slug: slug2, blocked };
|
|
4951
5709
|
}
|
|
4952
5710
|
async function writeTokenProposalOnGitHub(gh, plan, opts) {
|
|
4953
5711
|
if (plan.blocked) throw new Error(`Not opened: ${plan.blocked}. Bridge never opens a branch that breaks the gate.`);
|
|
@@ -5656,6 +6414,7 @@ function siteRepoFromConfig(config, raw, fallback = REPO_MODEL_DEFAULTS.repo) {
|
|
|
5656
6414
|
override: g.override ? { ...g.override } : { ...DEFAULT_OVERRIDE },
|
|
5657
6415
|
rights: g.rights ?? null,
|
|
5658
6416
|
soloOwnerCanApproveBoth: g.soloOwnerCanApproveBoth === true,
|
|
6417
|
+
leadsPublishTier1: g.leadsPublishTier1 !== false,
|
|
5659
6418
|
visualImpact: { mode: config.visualImpact?.mode === "on-demand" ? "on-demand" : "auto" },
|
|
5660
6419
|
tokensRoot: config.tokens.root,
|
|
5661
6420
|
integrations: config.integrations ?? {},
|
|
@@ -6123,14 +6882,14 @@ const semverDesc = (a2, b) => {
|
|
|
6123
6882
|
for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pb[i] - pa[i];
|
|
6124
6883
|
return 0;
|
|
6125
6884
|
};
|
|
6126
|
-
function groupReleases(sections,
|
|
6885
|
+
function groupReleases(sections, dateOf2, libraryPackage = "@bridge-ds/react") {
|
|
6127
6886
|
const all = sections;
|
|
6128
6887
|
const parent = all.map((_, i) => i);
|
|
6129
6888
|
const find = (i) => parent[i] === i ? i : parent[i] = find(parent[i]);
|
|
6130
6889
|
const union = (a2, b) => {
|
|
6131
6890
|
parent[find(a2)] = find(b);
|
|
6132
6891
|
};
|
|
6133
|
-
const dates = all.map((s) =>
|
|
6892
|
+
const dates = all.map((s) => dateOf2(s.pkg, s.version));
|
|
6134
6893
|
const byKey = /* @__PURE__ */ new Map();
|
|
6135
6894
|
all.forEach((s, i) => {
|
|
6136
6895
|
const keys = [...dates[i] ? [`d:${dates[i]}`] : [], ...s.notes.map((n) => `n:${n.id}`)];
|
|
@@ -6563,8 +7322,8 @@ function activityKey(url) {
|
|
|
6563
7322
|
parts[2] = parts[2].toLowerCase();
|
|
6564
7323
|
parts[3] = parts[3].toLowerCase();
|
|
6565
7324
|
} else if (parts[1] === "users" && parts.length > 2) parts[2] = parts[2].toLowerCase();
|
|
6566
|
-
const
|
|
6567
|
-
const search =
|
|
7325
|
+
const q2 = [...u.searchParams].sort(([a2, av2], [b, bv3]) => a2 === b ? av2 < bv3 ? -1 : 1 : a2 < b ? -1 : 1);
|
|
7326
|
+
const search = q2.length ? `?${new URLSearchParams(q2).toString()}` : "";
|
|
6568
7327
|
return parts.join("/") + search;
|
|
6569
7328
|
}
|
|
6570
7329
|
const KEPT_URLS = /* @__PURE__ */ new Set(["html_url", "avatar_url", "target_url", "details_url", "environment_url", "raw_url"]);
|
|
@@ -7386,8 +8145,8 @@ async function libraryArrivalOnGitHub(gh, n) {
|
|
|
7386
8145
|
return libraryArrival({ state: pr.state, mergeable: pr.mergeable, ci: ci3, changesRequested: approvals.changesRequested.length });
|
|
7387
8146
|
}
|
|
7388
8147
|
export {
|
|
7389
|
-
|
|
7390
|
-
|
|
8148
|
+
ar as AAA_MIN,
|
|
8149
|
+
as as AA_MIN,
|
|
7391
8150
|
ACTIVITY_FILE,
|
|
7392
8151
|
ACTIVITY_LIMITS,
|
|
7393
8152
|
ACTIVITY_MAX_AGE_MS,
|
|
@@ -7407,39 +8166,41 @@ export {
|
|
|
7407
8166
|
BUILD_STALE_DAYS,
|
|
7408
8167
|
BUILD_STATUS_LABEL,
|
|
7409
8168
|
BUILD_STEPS,
|
|
7410
|
-
|
|
8169
|
+
aL2 as BULK_LIMIT,
|
|
7411
8170
|
CANT_CHECK_COPY,
|
|
7412
8171
|
CHANGELOG_DETAIL_MAX,
|
|
7413
|
-
|
|
7414
|
-
|
|
8172
|
+
at as CHANGES_MODES,
|
|
8173
|
+
aM2 as CI_STATE_LABEL,
|
|
7415
8174
|
COLOR_FIELDS,
|
|
7416
8175
|
COMBINE_COMPARE_LIMIT,
|
|
7417
8176
|
COMPARE_FILE_LIMIT,
|
|
7418
|
-
|
|
8177
|
+
aN2 as COMPONENT_SPECS_VERSION,
|
|
7419
8178
|
CONFIG_FILE,
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
8179
|
+
aO2 as CONFLICT_REASON,
|
|
8180
|
+
CONTINUE_PHRASE,
|
|
8181
|
+
au as CONTRAST_CELL_MAX,
|
|
8182
|
+
av as CONTRAST_CHECKS_FILE,
|
|
7423
8183
|
CONTRAST_THRESHOLDS,
|
|
7424
8184
|
ConfigError,
|
|
7425
8185
|
DEFAULT_CHANGES_MODE,
|
|
7426
|
-
|
|
8186
|
+
aP2 as DEFAULT_COMPONENT_SPECS_FILE,
|
|
7427
8187
|
DEFAULT_FIDELITY_RUN_FILE,
|
|
7428
8188
|
DEFAULT_FIGMA_FIXES_FILE,
|
|
7429
|
-
|
|
8189
|
+
aw as DEFAULT_GOVERNANCE,
|
|
8190
|
+
DEFAULT_MEASURED_SPECS_FILE,
|
|
7430
8191
|
DEFAULT_OVERRIDE,
|
|
7431
8192
|
DEFAULT_PRODUCTS,
|
|
7432
|
-
|
|
7433
|
-
|
|
8193
|
+
aQ2 as DEFAULT_PROTECTED_BRANCHES,
|
|
8194
|
+
ax as DEFAULT_RIGHTS,
|
|
7434
8195
|
DEFAULT_TOKEN_PATHS,
|
|
7435
|
-
|
|
8196
|
+
ay as DEFAULT_VISUAL_IMPACT,
|
|
7436
8197
|
DEMO_RELEASES,
|
|
7437
|
-
|
|
7438
|
-
|
|
8198
|
+
aR2 as DOCS_CHANGE_BUMP,
|
|
8199
|
+
aS2 as DOCS_CLI_ORIGIN,
|
|
7439
8200
|
DOCS_DRAFT_MODEL,
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
8201
|
+
aT2 as DOCS_EDITOR_ORIGIN,
|
|
8202
|
+
aU2 as DOCS_KEYS,
|
|
8203
|
+
aV2 as DOCS_MCP_ORIGIN,
|
|
7443
8204
|
DOCS_SELECTION_MODES,
|
|
7444
8205
|
DRAFTED_BY_CLAUDE_LABEL,
|
|
7445
8206
|
DRAFTS_DIR,
|
|
@@ -7452,19 +8213,24 @@ export {
|
|
|
7452
8213
|
DRAFT_SUMMARY_SENTENCES,
|
|
7453
8214
|
DRAFT_TITLE_MAX,
|
|
7454
8215
|
DRAFT_VOICE_RULES,
|
|
7455
|
-
|
|
8216
|
+
az as DTCG_TYPES,
|
|
7456
8217
|
DraftPoller,
|
|
7457
|
-
|
|
8218
|
+
EXPLORATION_ARCHIVED_LABEL,
|
|
8219
|
+
aW2 as EXPLORATION_CHECK,
|
|
7458
8220
|
EXPLORATION_LABEL,
|
|
7459
8221
|
EXPLORATION_NOTES_DIR,
|
|
7460
8222
|
EXPLORATION_PREFIX,
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
8223
|
+
aX2 as EXPLORATION_READY_LABEL,
|
|
8224
|
+
aY2 as EXPLORATION_SLUG_MAX,
|
|
8225
|
+
aZ2 as EXPLORATION_STARTING_POINT,
|
|
8226
|
+
a_2 as EXPLORATION_STATUS_LABEL,
|
|
8227
|
+
a$2 as EXPLORATION_TIER_LABEL,
|
|
8228
|
+
b02 as EXPLORATION_USE_CASES,
|
|
7464
8229
|
EXT_ID,
|
|
7465
8230
|
EXT_SCOPES,
|
|
7466
8231
|
FIDELITY_DEFAULTS,
|
|
7467
8232
|
FIGMA_FILL_WIDTH_TOKEN,
|
|
8233
|
+
FIGMA_FIXES_LABEL,
|
|
7468
8234
|
FIGMA_FIXES_VERSION,
|
|
7469
8235
|
FIGMA_REFERENCES_BRANCH,
|
|
7470
8236
|
FIGMA_REFERENCES_DIR,
|
|
@@ -7475,53 +8241,64 @@ export {
|
|
|
7475
8241
|
FIX_LOOP_MAX_ROUNDS,
|
|
7476
8242
|
FIX_LOOP_MODEL,
|
|
7477
8243
|
FIX_SCHEMA_SUMMARY,
|
|
7478
|
-
|
|
8244
|
+
b12 as FORMER_MEMBER,
|
|
7479
8245
|
FROM_FIGMA_LABEL,
|
|
7480
8246
|
FigmaFixesError,
|
|
7481
8247
|
GAP_CASE_VERSION,
|
|
7482
8248
|
GitHubClient,
|
|
7483
8249
|
GitHubError,
|
|
7484
8250
|
HISTORY_MAX_CHANGES,
|
|
7485
|
-
|
|
8251
|
+
aA as JsonParseError,
|
|
7486
8252
|
LIGHTNESS,
|
|
7487
|
-
|
|
8253
|
+
aB as LINK_STATUS_TONE,
|
|
7488
8254
|
LLMS_MAX_TOKENS,
|
|
7489
|
-
|
|
8255
|
+
b22 as LOOK_AGAIN_STATUS_LABEL,
|
|
7490
8256
|
LayeredTokenSet,
|
|
7491
8257
|
MAX_TOGETHER,
|
|
8258
|
+
MEASURED_SHOTS_DIR,
|
|
8259
|
+
MEASURE_BRANCH,
|
|
8260
|
+
MEASURE_CLI_PACKAGE,
|
|
8261
|
+
MEASURE_DEFAULT_PATHS,
|
|
8262
|
+
MEASURE_FLAGS,
|
|
8263
|
+
MEASURE_MISSING_TEXT,
|
|
8264
|
+
MEASURE_SETUP_ACTION,
|
|
8265
|
+
MEASURE_WORKFLOW_FILE,
|
|
8266
|
+
MEASURE_WORKFLOW_HEADER,
|
|
7492
8267
|
MIN_TOGETHER,
|
|
7493
|
-
|
|
8268
|
+
MeasureMissingError,
|
|
8269
|
+
b32 as MeasuredSpecsError,
|
|
7494
8270
|
NEEDED_BY_LABEL,
|
|
7495
8271
|
NEUTRAL_ANCHORS,
|
|
7496
8272
|
OVERRIDE_HEADING,
|
|
7497
|
-
|
|
8273
|
+
aC as OWNER_GROUPS,
|
|
7498
8274
|
OverrideMergeError,
|
|
7499
8275
|
PIECES_FILE_LIMIT,
|
|
7500
8276
|
PIN_TOLERANCE,
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
8277
|
+
aD as PREVIEW_APP_LABELS,
|
|
8278
|
+
aE as PREVIEW_COMMENT_MARKER,
|
|
8279
|
+
aF as PREVIEW_PLACEHOLDERS,
|
|
8280
|
+
aG as PRIMITIVE_MODE,
|
|
8281
|
+
aH as PRODUCT_LABEL_PREFIX,
|
|
7506
8282
|
PROSE_KEYS,
|
|
7507
8283
|
PROTECTION_PARTS,
|
|
7508
8284
|
PROTECTION_PART_PHRASE,
|
|
8285
|
+
PUBLISH_LABEL,
|
|
7509
8286
|
PUBLISH_ROUTE_COPY,
|
|
7510
|
-
|
|
7511
|
-
|
|
8287
|
+
b42 as PULL_STATE_PILL,
|
|
8288
|
+
b52 as ProtectedBranchError,
|
|
7512
8289
|
RADIUS_PRESETS,
|
|
7513
|
-
|
|
8290
|
+
b62 as REDUCED_MOTION_NOTE,
|
|
7514
8291
|
REPO_MODEL_DEFAULTS,
|
|
7515
8292
|
REQUESTS_BRANCH,
|
|
7516
8293
|
REQUEST_KINDS,
|
|
7517
8294
|
REQUEST_LABEL,
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
8295
|
+
b72 as REQUEST_STATUS_LABEL,
|
|
8296
|
+
b82 as REVIEW_STATUS_LABEL,
|
|
8297
|
+
b92 as REVIEW_STATUS_ORDER,
|
|
8298
|
+
aI as RIGHTS,
|
|
8299
|
+
aJ as RIGHT_LABEL,
|
|
7523
8300
|
RISK_LABELS,
|
|
7524
|
-
|
|
8301
|
+
aK as RISK_TIER,
|
|
7525
8302
|
SHARE_EXPLAIN,
|
|
7526
8303
|
SIMILARITY_THRESHOLD,
|
|
7527
8304
|
SINGLE_MODE,
|
|
@@ -7536,36 +8313,38 @@ export {
|
|
|
7536
8313
|
SOURCE_FIGMA_LABEL,
|
|
7537
8314
|
SPEC_FIELDS,
|
|
7538
8315
|
STEPS,
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
8316
|
+
ba2 as SUGGESTION_APPLIED_MARK,
|
|
8317
|
+
bb2 as SUGGESTION_APPLY_EXIT,
|
|
8318
|
+
bc2 as SUGGESTION_FINAL_LABELS,
|
|
8319
|
+
bd2 as SUGGESTION_KINDS,
|
|
8320
|
+
be2 as SUGGESTION_LABEL,
|
|
8321
|
+
bf2 as SUGGESTION_LIMITS,
|
|
8322
|
+
bg2 as SUGGESTION_OVERRIDE_MARK,
|
|
8323
|
+
bh2 as SUGGESTION_REVIEW_MARK,
|
|
7546
8324
|
SUGGESTION_SCHEMA,
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
8325
|
+
bi2 as SUGGESTION_STATE_LABELS,
|
|
8326
|
+
bj2 as SUGGESTION_STATUS_LABEL,
|
|
8327
|
+
bk2 as SUGGESTION_STATUS_LABELS,
|
|
8328
|
+
bl2 as SUGGESTION_TOO_LARGE,
|
|
7551
8329
|
SnapshotError,
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
8330
|
+
bm2 as StorybookIndexError,
|
|
8331
|
+
bn2 as SuggestionError,
|
|
8332
|
+
aL as TEAM_NAME_MAX,
|
|
8333
|
+
TIER1_PUBLISH_HEADING,
|
|
7555
8334
|
TIER_AXIS,
|
|
7556
8335
|
TIER_NAMES,
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
8336
|
+
aM as TIER_ORDER,
|
|
8337
|
+
bo2 as TOKEN_REJECTED_MESSAGE,
|
|
8338
|
+
bp2 as TOKEN_SETTINGS_URL,
|
|
8339
|
+
bq2 as TRIGGER_LABELS,
|
|
8340
|
+
aN as TeamDirectory,
|
|
8341
|
+
br2 as UNRELEASED,
|
|
8342
|
+
aO as VISUAL_CHECKS_DOCS,
|
|
8343
|
+
aP as VISUAL_CHECK_LABEL,
|
|
8344
|
+
aQ as VISUAL_WORKFLOW_FILES,
|
|
7566
8345
|
WAITING_FOR_CLAUDE_LABEL,
|
|
7567
|
-
|
|
7568
|
-
|
|
8346
|
+
bs2 as acceptDocsDraft,
|
|
8347
|
+
bt2 as acceptDocsText,
|
|
7569
8348
|
accessAtLeast,
|
|
7570
8349
|
accessState,
|
|
7571
8350
|
activityAge,
|
|
@@ -7576,47 +8355,50 @@ export {
|
|
|
7576
8355
|
activityRawUrl,
|
|
7577
8356
|
activityShaper,
|
|
7578
8357
|
activityTreeFilter,
|
|
8358
|
+
actorName,
|
|
7579
8359
|
addBrandToConfigText,
|
|
7580
8360
|
addedComment,
|
|
7581
|
-
|
|
7582
|
-
|
|
8361
|
+
aR as affectedContrast,
|
|
8362
|
+
aS as aliasOf,
|
|
7583
8363
|
aliasTarget,
|
|
7584
|
-
|
|
7585
|
-
|
|
8364
|
+
bu2 as allowedMergeMethods,
|
|
8365
|
+
bv2 as anatomyLabel,
|
|
7586
8366
|
anchoredRamp,
|
|
7587
8367
|
anthropicReplyText,
|
|
7588
8368
|
anthropicRequest,
|
|
7589
|
-
|
|
7590
|
-
|
|
8369
|
+
aT as appForProject,
|
|
8370
|
+
aU as appUrl,
|
|
7591
8371
|
appliedFixLines,
|
|
7592
|
-
|
|
8372
|
+
bw2 as appliedStatus,
|
|
7593
8373
|
applyConfirmedRenames,
|
|
7594
8374
|
applyDocsSections,
|
|
7595
8375
|
applyFixes,
|
|
7596
8376
|
applyTokenEdits,
|
|
7597
8377
|
applyTokenMerge,
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
8378
|
+
bx2 as approvedSuggestionNumber,
|
|
8379
|
+
by2 as approvedSuggestionOnGitHub,
|
|
8380
|
+
aV as approverEntries,
|
|
8381
|
+
aW as approverLists,
|
|
8382
|
+
archiveExplorationOnGitHub,
|
|
7602
8383
|
askOthersToReviewOnGitHub,
|
|
7603
8384
|
assembleBuildRequests,
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
8385
|
+
bz2 as assertSafeBranch,
|
|
8386
|
+
bA2 as assignRequestOnGitHub,
|
|
8387
|
+
bB2 as assignVersions,
|
|
7607
8388
|
auditId,
|
|
7608
|
-
|
|
7609
|
-
|
|
8389
|
+
bC2 as base64ToUtf8,
|
|
8390
|
+
baseBranchOf,
|
|
7610
8391
|
bellChroma,
|
|
7611
8392
|
boundedCombos,
|
|
7612
|
-
|
|
8393
|
+
boxSized,
|
|
8394
|
+
aX as branchName,
|
|
7613
8395
|
branchPreviewUrls,
|
|
7614
|
-
|
|
7615
|
-
|
|
8396
|
+
aY as branchSlug,
|
|
8397
|
+
bD2 as branchVerdict,
|
|
7616
8398
|
branchWithJiraKey,
|
|
7617
8399
|
brandDisplayName,
|
|
7618
8400
|
brandPreviewVariables,
|
|
7619
|
-
|
|
8401
|
+
bE2 as bridgeMcpInstallCommand,
|
|
7620
8402
|
buildFidelityReport,
|
|
7621
8403
|
buildGapCase,
|
|
7622
8404
|
buildHistoryEntry,
|
|
@@ -7631,30 +8413,32 @@ export {
|
|
|
7631
8413
|
buildTokenSet,
|
|
7632
8414
|
buildUsageIndex,
|
|
7633
8415
|
canAddTo,
|
|
7634
|
-
|
|
8416
|
+
bF2 as canOverride,
|
|
8417
|
+
canPublishTier1,
|
|
7635
8418
|
canShare,
|
|
7636
|
-
|
|
7637
|
-
|
|
8419
|
+
aZ as canShip,
|
|
8420
|
+
bG2 as canonicalJson,
|
|
7638
8421
|
capHistory,
|
|
7639
8422
|
changeEdits,
|
|
7640
|
-
|
|
8423
|
+
a_ as changeKind,
|
|
7641
8424
|
changeList,
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
8425
|
+
bH2 as changeTable,
|
|
8426
|
+
a$ as changedModes,
|
|
8427
|
+
changedThings,
|
|
8428
|
+
b0 as changesMode,
|
|
8429
|
+
bI2 as changesetBump,
|
|
7646
8430
|
checkBrandSlug,
|
|
7647
8431
|
checkLayeredPair,
|
|
7648
8432
|
choiceFor,
|
|
7649
|
-
|
|
8433
|
+
bJ2 as classifyAuth,
|
|
7650
8434
|
classifyLayered,
|
|
7651
8435
|
classifyRisk,
|
|
7652
|
-
|
|
7653
|
-
|
|
8436
|
+
bK2 as claudeStage,
|
|
8437
|
+
bL2 as cleanDocs,
|
|
7654
8438
|
cleanDocsText,
|
|
7655
8439
|
cleanDraftText,
|
|
7656
|
-
|
|
7657
|
-
|
|
8440
|
+
b1 as clearRawDeep,
|
|
8441
|
+
bM2 as clearSharedEtags,
|
|
7658
8442
|
closeExplorationOnGitHub,
|
|
7659
8443
|
closeOnGitHub,
|
|
7660
8444
|
closingIssueNumbers,
|
|
@@ -7664,445 +8448,494 @@ export {
|
|
|
7664
8448
|
collectRefs,
|
|
7665
8449
|
collectReleases,
|
|
7666
8450
|
colorDelta,
|
|
7667
|
-
|
|
7668
|
-
|
|
8451
|
+
b2 as colorToCss,
|
|
8452
|
+
b3 as colorsEqual,
|
|
7669
8453
|
combineChanges,
|
|
7670
8454
|
combineConflicts,
|
|
7671
|
-
|
|
8455
|
+
b4 as comboKey,
|
|
7672
8456
|
commentOnGitHub,
|
|
7673
|
-
|
|
8457
|
+
bN2 as commitFileOnGitHub,
|
|
7674
8458
|
commitLibraryResolution,
|
|
7675
|
-
|
|
7676
|
-
|
|
8459
|
+
b5 as commitMessage,
|
|
8460
|
+
b6 as compactJson,
|
|
7677
8461
|
compareImages,
|
|
7678
8462
|
compareSpecToMeasured,
|
|
7679
|
-
|
|
8463
|
+
bO2 as componentContrastPairs,
|
|
7680
8464
|
componentForPath,
|
|
7681
8465
|
componentMotion,
|
|
7682
8466
|
componentSlug,
|
|
7683
8467
|
componentSpecSuggestionPayload,
|
|
7684
8468
|
componentStatus,
|
|
7685
|
-
|
|
8469
|
+
bP2 as componentsUsingMotionToken,
|
|
7686
8470
|
composeBuildRequestIssue,
|
|
7687
|
-
|
|
8471
|
+
bQ2 as composeChangeset,
|
|
7688
8472
|
composeFiles,
|
|
7689
8473
|
composeNewBrandBody,
|
|
7690
8474
|
composePendingDrafts,
|
|
7691
|
-
|
|
8475
|
+
b7 as composePrBody,
|
|
7692
8476
|
composeProposalBody,
|
|
7693
8477
|
composeRequestIssue,
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
8478
|
+
bR2 as composeSuggestionAppliedComment,
|
|
8479
|
+
bS2 as composeSuggestionIssue,
|
|
8480
|
+
bT2 as composeSuggestionOverrideComment,
|
|
8481
|
+
bU2 as composeSuggestionReviewComment,
|
|
8482
|
+
b8 as composite,
|
|
7698
8483
|
conflictRoute,
|
|
7699
|
-
|
|
8484
|
+
b9 as contextFor,
|
|
7700
8485
|
contextLabel,
|
|
7701
|
-
|
|
8486
|
+
bV2 as continueDocsProposalOnGitHub,
|
|
8487
|
+
continueLines,
|
|
8488
|
+
continueRef,
|
|
7702
8489
|
contrastDelta,
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
8490
|
+
bW2 as contrastDeltaTable,
|
|
8491
|
+
ba as contrastLevel,
|
|
8492
|
+
bb as contrastPair,
|
|
8493
|
+
bc as contrastRatio,
|
|
7707
8494
|
contrastScope,
|
|
7708
|
-
|
|
7709
|
-
|
|
8495
|
+
bX2 as countChanges,
|
|
8496
|
+
bY2 as coverageSummary,
|
|
7710
8497
|
createBuildRequestOnGitHub,
|
|
7711
8498
|
createDocsDraftSession,
|
|
7712
8499
|
createPendingDrafts,
|
|
7713
8500
|
createSuggestionOnGitHub,
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
8501
|
+
bZ2 as cssColorToRgb,
|
|
8502
|
+
b_2 as cssColorsToRgb,
|
|
8503
|
+
b$2 as cssModuleClasses,
|
|
7717
8504
|
cssOf,
|
|
7718
8505
|
cssVarOf,
|
|
7719
8506
|
cssVarsIn,
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
8507
|
+
c02 as declineComment,
|
|
8508
|
+
c12 as declineReasonProblems,
|
|
8509
|
+
c22 as declineRequestOnGitHub,
|
|
8510
|
+
c32 as declineSuggestionOnGitHub,
|
|
7724
8511
|
decodePicks,
|
|
7725
8512
|
defaultChoiceFor,
|
|
7726
|
-
|
|
7727
|
-
|
|
8513
|
+
bd as defaultContext,
|
|
8514
|
+
c42 as defaultDocsFooter,
|
|
7728
8515
|
defaultFile,
|
|
7729
8516
|
defaultModelConfig,
|
|
7730
8517
|
defaultProduct,
|
|
7731
8518
|
defaultScopes,
|
|
7732
|
-
|
|
8519
|
+
be as defaultTitle,
|
|
7733
8520
|
defineConfig,
|
|
7734
8521
|
deletePendingDrafts,
|
|
7735
8522
|
demoHistory,
|
|
7736
8523
|
describeDiff,
|
|
7737
8524
|
describePartFix,
|
|
7738
|
-
|
|
8525
|
+
bf as detectLayout,
|
|
7739
8526
|
diffLayered,
|
|
7740
8527
|
dismissStaleFrom,
|
|
7741
|
-
|
|
7742
|
-
|
|
8528
|
+
c52 as docsAuthorLine,
|
|
8529
|
+
c62 as docsFileText,
|
|
7743
8530
|
docsPrompt,
|
|
7744
|
-
|
|
7745
|
-
|
|
8531
|
+
c72 as docsProposal,
|
|
8532
|
+
c82 as docsSectionShapes,
|
|
7746
8533
|
docsSectionsAnswer,
|
|
7747
8534
|
docsSectionsResult,
|
|
7748
8535
|
docsSelectionPrompt,
|
|
7749
|
-
|
|
7750
|
-
|
|
8536
|
+
c92 as docsSlug,
|
|
8537
|
+
ca2 as docsSuggestionPayload,
|
|
7751
8538
|
docsTaskKeys,
|
|
7752
8539
|
docsTaskPrompt,
|
|
7753
|
-
|
|
7754
|
-
|
|
8540
|
+
cb2 as docsText,
|
|
8541
|
+
doneExploringOnGitHub,
|
|
8542
|
+
doneExploringPlan,
|
|
8543
|
+
cc2 as draftChangeLine,
|
|
7755
8544
|
draftImagePath,
|
|
7756
8545
|
draftItemBrief,
|
|
7757
|
-
|
|
7758
|
-
|
|
8546
|
+
cd2 as draftPushDescription,
|
|
8547
|
+
ce2 as draftRequestText,
|
|
7759
8548
|
draftSessionPath,
|
|
7760
8549
|
dtcgColor,
|
|
7761
8550
|
durationValue,
|
|
7762
8551
|
easingValue,
|
|
7763
8552
|
effectiveScopes,
|
|
7764
8553
|
effectiveTier,
|
|
7765
|
-
|
|
8554
|
+
cf2 as eligibleReviewers,
|
|
7766
8555
|
emptyTokenFile,
|
|
7767
8556
|
encodePicks,
|
|
7768
8557
|
ensureRequestsBranch,
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
8558
|
+
bg as entriesFor,
|
|
8559
|
+
bh as entryLabel,
|
|
8560
|
+
cg2 as evalCalc,
|
|
7772
8561
|
evaluateContrastPairs,
|
|
7773
8562
|
evaluatePairs,
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
8563
|
+
bi as expandEntries,
|
|
8564
|
+
bj as expandOwners,
|
|
8565
|
+
explorationActivity,
|
|
8566
|
+
ch2 as explorationBranch,
|
|
7777
8567
|
explorationNote,
|
|
7778
8568
|
explorationNotePath,
|
|
7779
8569
|
explorationPrBody,
|
|
7780
8570
|
explorationPrTitle,
|
|
7781
8571
|
explorationProblems,
|
|
7782
|
-
|
|
7783
|
-
|
|
8572
|
+
explorationRefLabel,
|
|
8573
|
+
ci2 as explorationRules,
|
|
8574
|
+
cj2 as explorationShareMessage,
|
|
7784
8575
|
explorationSlug,
|
|
7785
|
-
|
|
8576
|
+
ck2 as explorationSlugFromBranch,
|
|
8577
|
+
explorationStage,
|
|
8578
|
+
explorationStartMessage,
|
|
8579
|
+
explorationStartingPoint,
|
|
7786
8580
|
explorationStatus,
|
|
7787
8581
|
explorationSummary,
|
|
8582
|
+
cl2 as explorationTakeCommands,
|
|
7788
8583
|
explorationTestBuildOnGitHub,
|
|
7789
8584
|
explorationTryIt,
|
|
7790
8585
|
explorationTryItOnGitHub,
|
|
7791
|
-
|
|
8586
|
+
cm2 as fetchStorybookIndex,
|
|
7792
8587
|
fidelityComponentLine,
|
|
7793
8588
|
fidelityVerdict,
|
|
7794
|
-
|
|
8589
|
+
bk as fieldChanges,
|
|
7795
8590
|
figmaAheadLine,
|
|
7796
8591
|
figmaBranchTitle,
|
|
7797
|
-
|
|
8592
|
+
cn2 as figmaCell,
|
|
7798
8593
|
figmaChangeOnGitHub,
|
|
7799
8594
|
figmaDraftBody,
|
|
7800
8595
|
figmaDraftBrief,
|
|
8596
|
+
figmaFixesBranchStem,
|
|
7801
8597
|
figmaFixesPrBody,
|
|
8598
|
+
figmaFixesProposal,
|
|
7802
8599
|
figmaGaps,
|
|
7803
8600
|
figmaNodeUrl,
|
|
7804
8601
|
figmaNodes,
|
|
7805
8602
|
figmaOriginSection,
|
|
7806
|
-
|
|
8603
|
+
bl as figmaPushNextLine,
|
|
7807
8604
|
figmaPushOf,
|
|
7808
8605
|
figmaPushScope,
|
|
7809
8606
|
figmaPushStatus,
|
|
7810
8607
|
figmaPushStatusesOnGitHub,
|
|
7811
|
-
|
|
8608
|
+
bm as figmaPushTierLabel,
|
|
7812
8609
|
figmaShipReviewBody,
|
|
7813
8610
|
figmaSpecPath,
|
|
7814
8611
|
figmaSpecSlug,
|
|
7815
|
-
|
|
7816
|
-
|
|
8612
|
+
co2 as fileModes,
|
|
8613
|
+
cp2 as filterHistory,
|
|
7817
8614
|
findExplorationOnGitHub,
|
|
7818
|
-
|
|
7819
|
-
|
|
8615
|
+
findMeasure,
|
|
8616
|
+
cq2 as findOpenDocsProposal,
|
|
8617
|
+
cr2 as findOpenDocsProposalOnGitHub,
|
|
7820
8618
|
findPreviewComment,
|
|
7821
|
-
|
|
8619
|
+
bn as findProduct,
|
|
7822
8620
|
findReferenceProblems,
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
8621
|
+
bo as findToken,
|
|
8622
|
+
bp as findVercelComment,
|
|
8623
|
+
cs2 as firstParagraph,
|
|
7826
8624
|
fitActivity,
|
|
8625
|
+
fixLoopOutcome,
|
|
8626
|
+
fixLoopRound,
|
|
7827
8627
|
fixPrompt,
|
|
7828
8628
|
fixWords,
|
|
7829
|
-
|
|
8629
|
+
bq as flattenLayerFile,
|
|
7830
8630
|
flattenTokenFile,
|
|
7831
8631
|
flattenTokens,
|
|
7832
8632
|
formatChangeset,
|
|
7833
|
-
|
|
8633
|
+
ct2 as formatDocsJson,
|
|
7834
8634
|
formatFigmaSpec,
|
|
7835
|
-
|
|
7836
|
-
|
|
8635
|
+
br as formatNumber,
|
|
8636
|
+
bs as formatOwnerEntry,
|
|
7837
8637
|
formatValue,
|
|
7838
8638
|
freshness,
|
|
7839
|
-
|
|
8639
|
+
cu2 as friendlyMessage,
|
|
7840
8640
|
fromHex,
|
|
7841
|
-
|
|
8641
|
+
bt as fromPlain,
|
|
7842
8642
|
gapCaseFor,
|
|
7843
8643
|
generateRamp,
|
|
7844
|
-
|
|
7845
|
-
|
|
8644
|
+
bu as getChild,
|
|
8645
|
+
bv as getEntry,
|
|
7846
8646
|
getSuggestionOnGitHub,
|
|
7847
8647
|
gitBlobSha,
|
|
7848
8648
|
gitHubHistoryBackend,
|
|
7849
8649
|
gitHubSource,
|
|
7850
|
-
|
|
8650
|
+
cv2 as githubSuggestionWorkspace,
|
|
7851
8651
|
globBase,
|
|
7852
8652
|
globToRegExp,
|
|
7853
8653
|
groupReleases,
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
8654
|
+
cw2 as groupStorybookComponents,
|
|
8655
|
+
bw as groupsPhrase,
|
|
8656
|
+
cx2 as hasOwnerApproval,
|
|
8657
|
+
cy2 as hasSuggestionBlock,
|
|
8658
|
+
bx as hasVisualWorkflow,
|
|
8659
|
+
cz2 as hasWriteAccess,
|
|
7860
8660
|
hashSpec,
|
|
7861
8661
|
hexContrastRatio,
|
|
7862
8662
|
hexLuminance,
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
8663
|
+
by as hexToRgba,
|
|
8664
|
+
bz as holdersOf,
|
|
8665
|
+
humanAuthorOf,
|
|
8666
|
+
cA2 as iconComponentName,
|
|
7866
8667
|
idExtensionFor,
|
|
7867
8668
|
impactedModules,
|
|
7868
8669
|
importsIn,
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
8670
|
+
inProductRepo,
|
|
8671
|
+
bA as indexByPath,
|
|
8672
|
+
bB as inheritedTypeAt,
|
|
8673
|
+
bC as installCommand,
|
|
8674
|
+
bD as installCommandFromPreviewComment,
|
|
8675
|
+
cB2 as installationFor,
|
|
8676
|
+
cC2 as isActiveExploration,
|
|
7875
8677
|
isAlias$1 as isAlias,
|
|
7876
|
-
|
|
8678
|
+
isArchivedExploration,
|
|
8679
|
+
cD2 as isAutomationPull,
|
|
7877
8680
|
isBuildRequestIssue,
|
|
7878
|
-
|
|
8681
|
+
isClaudeAssisted,
|
|
8682
|
+
isClaudeCommit,
|
|
8683
|
+
cE2 as isDocsProposalBranch,
|
|
7879
8684
|
isDocsSelectionMode,
|
|
7880
|
-
|
|
8685
|
+
cF2 as isDraft,
|
|
7881
8686
|
isDraftSessionId,
|
|
7882
8687
|
isExplorationBranch,
|
|
7883
8688
|
isExplorationPull,
|
|
7884
8689
|
isFigmaPush,
|
|
7885
8690
|
isGuardrail,
|
|
7886
|
-
|
|
8691
|
+
cG as isGuardrailPath,
|
|
8692
|
+
cH as isHandedOff,
|
|
8693
|
+
cI as isHeldExploration,
|
|
7887
8694
|
isHex,
|
|
7888
|
-
|
|
7889
|
-
|
|
8695
|
+
isHiddenRepoError,
|
|
8696
|
+
cJ as isJiraKey,
|
|
8697
|
+
cK as isMissingPermission,
|
|
7890
8698
|
isMultiMode,
|
|
7891
8699
|
isPlanRefusal,
|
|
7892
|
-
|
|
8700
|
+
cL as isPlatformRefusal,
|
|
7893
8701
|
isProseKey,
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
8702
|
+
cM as isReleasePull,
|
|
8703
|
+
isStartCommit,
|
|
8704
|
+
bE as isTeamEntry,
|
|
8705
|
+
bF as isTokenFile,
|
|
7897
8706
|
isTokenPath,
|
|
7898
|
-
|
|
8707
|
+
cN as isTokenRejected,
|
|
7899
8708
|
isTokenValueChange,
|
|
7900
8709
|
isUnder,
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
8710
|
+
bG as isVercelComment,
|
|
8711
|
+
cO as jiraIssueFields,
|
|
8712
|
+
cP as jiraKeyOf,
|
|
7904
8713
|
jiraKeyOfLabels,
|
|
7905
|
-
|
|
7906
|
-
|
|
8714
|
+
cQ as joinRepoPath,
|
|
8715
|
+
bH as kindForMin,
|
|
8716
|
+
lastActivity,
|
|
7907
8717
|
layeredImpact,
|
|
7908
|
-
|
|
8718
|
+
bI as layeredParityIssues,
|
|
7909
8719
|
layoutOptions,
|
|
8720
|
+
leadsPublishTier1On,
|
|
7910
8721
|
levenshtein,
|
|
7911
8722
|
libraryArrival,
|
|
7912
8723
|
libraryArrivalOnGitHub,
|
|
7913
8724
|
libraryBranchPlan,
|
|
7914
|
-
|
|
8725
|
+
bJ as libraryIsPublic,
|
|
7915
8726
|
libraryPreviewState,
|
|
7916
|
-
|
|
8727
|
+
cR as linkRequestToExplorationOnGitHub,
|
|
7917
8728
|
linkStatus,
|
|
7918
|
-
|
|
8729
|
+
cS as linkSuggestionPrOnGitHub,
|
|
7919
8730
|
linkedIssueNumbers,
|
|
7920
8731
|
linkedLibraryBranchOnGitHub,
|
|
7921
|
-
|
|
8732
|
+
cT as linkedPullsOf,
|
|
7922
8733
|
listBrands,
|
|
7923
8734
|
listBuildRequestsOnGitHub,
|
|
7924
8735
|
listExplorationsOnGitHub,
|
|
7925
8736
|
listFigmaChangesOnGitHub,
|
|
7926
8737
|
listPendingDrafts,
|
|
8738
|
+
listProductExplorationsOnGitHub,
|
|
7927
8739
|
listStylesheets,
|
|
7928
8740
|
listSuggestionsOnGitHub,
|
|
8741
|
+
listThings,
|
|
7929
8742
|
loadConfig,
|
|
7930
8743
|
loadGitHubHistory,
|
|
7931
8744
|
loadProtection,
|
|
7932
|
-
|
|
8745
|
+
cU as loadRepoFiles,
|
|
7933
8746
|
loadRepoModel,
|
|
7934
8747
|
loadRequiredChecks,
|
|
7935
8748
|
localTryCommand,
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
8749
|
+
cV as lookAgainReviewers,
|
|
8750
|
+
cW as lowerFirstWord,
|
|
8751
|
+
bK as luminance,
|
|
7939
8752
|
mailtoHref,
|
|
7940
8753
|
mapPropsToFigma,
|
|
7941
8754
|
markClaudeDraft,
|
|
7942
8755
|
markFigmaChangeDraftedOnGitHub,
|
|
7943
|
-
|
|
7944
|
-
|
|
8756
|
+
cX as markSuggestionsAppliedOnGitHub,
|
|
8757
|
+
cY as markUsedTokens,
|
|
8758
|
+
matchVariant,
|
|
7945
8759
|
matchesBuildFilter,
|
|
7946
8760
|
mayBeTokenValueChange,
|
|
8761
|
+
measureDir,
|
|
8762
|
+
measureFromWords,
|
|
8763
|
+
measureSetupSteps,
|
|
8764
|
+
measureWorkflowOptionsFromConfig,
|
|
8765
|
+
measureWorkflowYaml,
|
|
7947
8766
|
measuredResolver,
|
|
7948
|
-
|
|
7949
|
-
|
|
8767
|
+
cZ as memoryEtagStore,
|
|
8768
|
+
c_ as memorySource,
|
|
7950
8769
|
mergeDocsDraft,
|
|
7951
8770
|
mergeDraftResults,
|
|
7952
8771
|
mergeFigmaFixes,
|
|
7953
|
-
|
|
8772
|
+
c$ as mergeGate,
|
|
7954
8773
|
mergeOnGitHub,
|
|
7955
8774
|
mergePiecesRecord,
|
|
7956
8775
|
mergeReleases,
|
|
7957
8776
|
missingChoices,
|
|
7958
|
-
|
|
7959
|
-
|
|
8777
|
+
d0 as missingPermissionMessage,
|
|
8778
|
+
d1 as missingProse,
|
|
7960
8779
|
modeDisplayName,
|
|
7961
8780
|
modeFile,
|
|
7962
|
-
|
|
8781
|
+
d2 as modeLabel,
|
|
7963
8782
|
modesOf$1 as modesOf,
|
|
7964
8783
|
motionDocs,
|
|
7965
8784
|
motionMoments,
|
|
7966
8785
|
moveToken,
|
|
7967
|
-
|
|
8786
|
+
bL as nearestQuestion,
|
|
7968
8787
|
nearestStep,
|
|
7969
|
-
|
|
7970
|
-
|
|
8788
|
+
bM as neededBySection,
|
|
8789
|
+
d3 as needsDocsReview,
|
|
7971
8790
|
neutralChroma,
|
|
7972
8791
|
neutralOffset,
|
|
7973
8792
|
newBrandSection,
|
|
7974
8793
|
newDraftSessionId,
|
|
7975
8794
|
noOnlinePreview,
|
|
7976
|
-
|
|
8795
|
+
bN as nodesEqual,
|
|
7977
8796
|
normalizeRepoPath,
|
|
7978
|
-
|
|
7979
|
-
|
|
8797
|
+
bO as normalizeRights,
|
|
8798
|
+
d4 as normalizeStorybookUrl,
|
|
7980
8799
|
noteSentences,
|
|
7981
8800
|
oklch,
|
|
7982
8801
|
oklchToLinear,
|
|
8802
|
+
onlyNotes,
|
|
7983
8803
|
onlyTokens,
|
|
7984
8804
|
openBuildRequests,
|
|
7985
8805
|
openComponentPushOnGitHub,
|
|
7986
8806
|
openLibraryBranchOnGitHub,
|
|
7987
|
-
|
|
8807
|
+
d5 as openPulls,
|
|
7988
8808
|
overlap,
|
|
7989
8809
|
overrideComment,
|
|
7990
|
-
|
|
7991
|
-
|
|
8810
|
+
d6 as overrideHolders,
|
|
8811
|
+
bP as overrideKey,
|
|
7992
8812
|
overrideMergeOnGitHub,
|
|
7993
|
-
|
|
7994
|
-
|
|
8813
|
+
d7 as overrideReasonMin,
|
|
8814
|
+
d8 as overrideReasonProblem,
|
|
7995
8815
|
overrideRefusedComment,
|
|
7996
|
-
|
|
7997
|
-
|
|
8816
|
+
d9 as overrideSuggestionOnGitHub,
|
|
8817
|
+
bQ as ownerEntryProblems,
|
|
8818
|
+
da as ownerOverridePolicy,
|
|
7998
8819
|
packageDir,
|
|
7999
8820
|
packageDirCandidates,
|
|
8000
|
-
|
|
8821
|
+
bR as pairKey,
|
|
8001
8822
|
pairParts,
|
|
8002
8823
|
parentDir,
|
|
8003
8824
|
parseActivity,
|
|
8004
|
-
|
|
8825
|
+
db as parseBoxShadow,
|
|
8005
8826
|
parseBuildRequestIssue,
|
|
8006
8827
|
parseBuildSpec,
|
|
8007
8828
|
parseChangelog,
|
|
8008
8829
|
parseChangelogSections,
|
|
8009
8830
|
parseChangeset,
|
|
8010
|
-
|
|
8831
|
+
dc as parseChangesetText,
|
|
8011
8832
|
parseColor,
|
|
8012
8833
|
parseComponentCss,
|
|
8013
8834
|
parseComponentFamilies,
|
|
8014
8835
|
parseComponentSpecs,
|
|
8015
8836
|
parseComponentStories,
|
|
8016
8837
|
parseComponentTsx,
|
|
8838
|
+
parseContinueLine,
|
|
8017
8839
|
parseContrastChecks,
|
|
8018
8840
|
parseContrastSpecs,
|
|
8019
|
-
|
|
8841
|
+
bS as parseDimension,
|
|
8020
8842
|
parseDocsReply,
|
|
8021
8843
|
parseDocsTaskAnswer,
|
|
8022
8844
|
parseExplorationBody,
|
|
8845
|
+
dd as parseExplorationRef,
|
|
8023
8846
|
parseFidelityRun,
|
|
8024
8847
|
parseFigmaFixes,
|
|
8025
8848
|
parseFigmaOrigin,
|
|
8026
8849
|
parseFigmaSpec,
|
|
8027
8850
|
parseFixReply,
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8851
|
+
de as parseJiraKey,
|
|
8852
|
+
bT as parseJson,
|
|
8853
|
+
df as parseMeasuredSpecsFile,
|
|
8031
8854
|
parseModelConfig,
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8855
|
+
bU as parseNeededBy,
|
|
8856
|
+
dg as parseOverrideComment,
|
|
8857
|
+
bV as parseOwnerEntry,
|
|
8035
8858
|
parsePendingDrafts,
|
|
8036
8859
|
parsePiecesRecord,
|
|
8037
|
-
|
|
8860
|
+
dh as parseRepo,
|
|
8038
8861
|
parseRequestIssue,
|
|
8039
8862
|
parseSnapshot,
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8863
|
+
di as parseStorybookIndex,
|
|
8864
|
+
dj as parseStructureChange,
|
|
8865
|
+
dk as parseSuggestionAppliedComment,
|
|
8866
|
+
dl as parseSuggestionIssue,
|
|
8867
|
+
dm as parseSuggestionOverrideComment,
|
|
8868
|
+
dn as parseSuggestionReviewComment,
|
|
8869
|
+
dp as parseTier,
|
|
8046
8870
|
parseTokenFile,
|
|
8047
|
-
|
|
8871
|
+
bW as parseVercelComment,
|
|
8048
8872
|
partLabel,
|
|
8049
8873
|
partWords,
|
|
8050
|
-
|
|
8874
|
+
dq as patchDocsJson,
|
|
8051
8875
|
pathId,
|
|
8052
8876
|
pathSimilarity,
|
|
8053
|
-
|
|
8054
|
-
|
|
8877
|
+
dr as pendingApprovals,
|
|
8878
|
+
ds as permissionLabel,
|
|
8055
8879
|
pickedOption,
|
|
8056
8880
|
p as pieceId,
|
|
8057
|
-
|
|
8058
|
-
|
|
8881
|
+
pieceRequest,
|
|
8882
|
+
bX as pieceValueField,
|
|
8883
|
+
bY as piecesSection,
|
|
8884
|
+
bZ as plainEqual,
|
|
8059
8885
|
planComponentPush,
|
|
8060
8886
|
planNewBrand,
|
|
8061
8887
|
planReference,
|
|
8062
8888
|
planReferences,
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8889
|
+
dt as planRestore,
|
|
8890
|
+
du as planRevert,
|
|
8891
|
+
dv as planSuggestionApply,
|
|
8892
|
+
dw as planToJson,
|
|
8067
8893
|
planTokenMerge,
|
|
8068
8894
|
planTokenProposal,
|
|
8069
8895
|
planVariantMeasures,
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8896
|
+
dx as prNumberFromSubject,
|
|
8897
|
+
dy as prefillDocs,
|
|
8898
|
+
b_ as previewComment,
|
|
8073
8899
|
previewLinks,
|
|
8074
|
-
|
|
8900
|
+
b$ as previewLinksOf,
|
|
8075
8901
|
previewOverrides,
|
|
8076
|
-
|
|
8902
|
+
previewUrl,
|
|
8077
8903
|
previewsSettled,
|
|
8078
8904
|
productBranchRef,
|
|
8079
8905
|
productBranchSentence,
|
|
8080
8906
|
productBranchViewOnGitHub,
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8907
|
+
c0 as productForPlace,
|
|
8908
|
+
c1 as productLabel,
|
|
8909
|
+
c2 as productMarks,
|
|
8084
8910
|
productOfBranch,
|
|
8911
|
+
productOfRepo,
|
|
8085
8912
|
productPreview,
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8913
|
+
productPreviewOnGitHub,
|
|
8914
|
+
c3 as productProblems,
|
|
8915
|
+
productRepoAccess,
|
|
8916
|
+
c4 as productRepoOf,
|
|
8917
|
+
productRepos,
|
|
8918
|
+
c5 as productsOf,
|
|
8919
|
+
c6 as projectOfLabel,
|
|
8089
8920
|
promoteExplorationOnGitHub,
|
|
8090
8921
|
promotionPlan,
|
|
8091
8922
|
propertyWords,
|
|
8092
|
-
|
|
8923
|
+
dz as proposeDocsOnGitHub,
|
|
8924
|
+
proposePiecesOnGitHub,
|
|
8093
8925
|
a as proposedTokenPath,
|
|
8094
|
-
|
|
8926
|
+
dA as propsTable,
|
|
8095
8927
|
protectLibrary,
|
|
8096
8928
|
protectRefusal,
|
|
8097
|
-
|
|
8929
|
+
c7 as protectedBranchesOf,
|
|
8098
8930
|
protectionRuleset,
|
|
8099
8931
|
protectionRulesetName,
|
|
8100
8932
|
protectionSentence,
|
|
8101
8933
|
protectionState,
|
|
8102
8934
|
publishSummary,
|
|
8935
|
+
publishedWithoutReview,
|
|
8103
8936
|
pullState,
|
|
8104
|
-
|
|
8105
|
-
|
|
8937
|
+
dB as pushChangeRows,
|
|
8938
|
+
dC as rateLimitMessage,
|
|
8106
8939
|
readCombineBranch,
|
|
8107
8940
|
readLibraryConflicts,
|
|
8108
8941
|
readPendingDrafts,
|
|
@@ -8118,24 +8951,24 @@ export {
|
|
|
8118
8951
|
referenceImagePath,
|
|
8119
8952
|
releaseAnnouncement,
|
|
8120
8953
|
releasePackagesLabel,
|
|
8121
|
-
|
|
8954
|
+
dD as releasePull,
|
|
8122
8955
|
releasesFromTags,
|
|
8123
8956
|
remoteRenames,
|
|
8124
8957
|
removeToken,
|
|
8125
8958
|
removedSide,
|
|
8126
|
-
|
|
8127
|
-
|
|
8959
|
+
c8 as renderedColor,
|
|
8960
|
+
dE as reopenEligibility,
|
|
8128
8961
|
reopenOnGitHub,
|
|
8129
|
-
|
|
8130
|
-
|
|
8962
|
+
dF as requestBrief,
|
|
8963
|
+
dG as requestImagePath,
|
|
8131
8964
|
requestIssueTitle,
|
|
8132
8965
|
requestStatus,
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8966
|
+
dH as requesterFirstName,
|
|
8967
|
+
dI as requesterLine,
|
|
8968
|
+
dJ as requesterOf,
|
|
8969
|
+
dK as requiredApprovals,
|
|
8137
8970
|
requiredCheckNames,
|
|
8138
|
-
|
|
8971
|
+
dL as requiredPermission,
|
|
8139
8972
|
resolutionMessage,
|
|
8140
8973
|
resolve,
|
|
8141
8974
|
resolveBranchPreviews,
|
|
@@ -8143,21 +8976,21 @@ export {
|
|
|
8143
8976
|
resolveRepoAccess,
|
|
8144
8977
|
resolvedTokenFiles,
|
|
8145
8978
|
reviewOnGitHub,
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8979
|
+
dM as reviewQueue,
|
|
8980
|
+
dN as reviewStatusKind,
|
|
8981
|
+
dO as reviewStatusLabel,
|
|
8982
|
+
dP as reviewSuggestionOnGitHub,
|
|
8983
|
+
dQ as reviewersToAsk,
|
|
8984
|
+
c9 as rgbaToDtcg,
|
|
8152
8985
|
rgbaToHex,
|
|
8153
|
-
|
|
8986
|
+
ca as rightsProblems,
|
|
8154
8987
|
round3,
|
|
8155
8988
|
rulesSettingsUrl,
|
|
8156
8989
|
rulesetTargetsDefault,
|
|
8157
8990
|
runLayeredContrast,
|
|
8158
|
-
|
|
8991
|
+
cb as sameProducts,
|
|
8159
8992
|
sameScopes,
|
|
8160
|
-
|
|
8993
|
+
cc as sameToken,
|
|
8161
8994
|
sanitizeDocsReplacement,
|
|
8162
8995
|
sanitizeDocsResult,
|
|
8163
8996
|
sanitizeDocsSections,
|
|
@@ -8166,24 +8999,26 @@ export {
|
|
|
8166
8999
|
sanitizeDraftTitle,
|
|
8167
9000
|
sanitizeResultLines,
|
|
8168
9001
|
scanComponents,
|
|
8169
|
-
|
|
9002
|
+
dR as sectionsNeedingReview,
|
|
8170
9003
|
selectTokenFiles,
|
|
8171
|
-
|
|
9004
|
+
cd as serializeJson,
|
|
8172
9005
|
serializePendingDrafts,
|
|
8173
9006
|
serializeTokenFile,
|
|
8174
|
-
|
|
9007
|
+
dS as sha256Hex,
|
|
8175
9008
|
shadowedEdits,
|
|
8176
9009
|
shareDescription,
|
|
8177
9010
|
shareMessage,
|
|
8178
9011
|
shareState,
|
|
8179
|
-
|
|
8180
|
-
|
|
9012
|
+
dT as sharedTokenPrefix,
|
|
9013
|
+
ce as shipPolicy,
|
|
8181
9014
|
shipsByItself,
|
|
9015
|
+
shortAgo,
|
|
8182
9016
|
shortPackage,
|
|
8183
|
-
|
|
8184
|
-
|
|
9017
|
+
dU as shortVariant,
|
|
9018
|
+
dV as shortVariants,
|
|
8185
9019
|
showShare,
|
|
8186
9020
|
siteRepoFromConfig,
|
|
9021
|
+
sizeGap,
|
|
8187
9022
|
slugify,
|
|
8188
9023
|
snapshotAudit,
|
|
8189
9024
|
snapshotIsFresh,
|
|
@@ -8191,84 +9026,95 @@ export {
|
|
|
8191
9026
|
snapshotMotion,
|
|
8192
9027
|
snapshotSource,
|
|
8193
9028
|
snapshotSummary,
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
9029
|
+
cf as snapshotUrl,
|
|
9030
|
+
cg as soloApprover,
|
|
9031
|
+
ch as soloOwnerOf,
|
|
9032
|
+
ci as soloOwnerSentence,
|
|
8198
9033
|
sortChangelogEntries,
|
|
8199
|
-
|
|
9034
|
+
dW as specHash,
|
|
8200
9035
|
specsFromMeasure,
|
|
8201
|
-
|
|
9036
|
+
dX as specsUsage,
|
|
8202
9037
|
splitNote,
|
|
8203
9038
|
stableCommand,
|
|
8204
|
-
|
|
9039
|
+
dY as stalenessStatus,
|
|
8205
9040
|
startExplorationOnGitHub,
|
|
9041
|
+
startFixLoop,
|
|
9042
|
+
startedWhen,
|
|
8206
9043
|
stateSentence,
|
|
8207
9044
|
statusFixFor,
|
|
8208
9045
|
storiesOfComponent,
|
|
8209
9046
|
storyForVariant,
|
|
8210
9047
|
storyMatchesWhere,
|
|
8211
9048
|
storyState,
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
9049
|
+
dZ as storybookIframeUrl,
|
|
9050
|
+
d_ as storybookIndexUrls,
|
|
9051
|
+
d$ as storybookManagerUrl,
|
|
9052
|
+
cj as storybookUrl,
|
|
8216
9053
|
styleGlobs,
|
|
8217
|
-
|
|
8218
|
-
|
|
9054
|
+
e0 as subjectTitle,
|
|
9055
|
+
subjectWords,
|
|
9056
|
+
ck as suggestNearestComment,
|
|
8219
9057
|
suggestTokenProposalOnGitHub,
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
9058
|
+
e1 as suggestionAllowedRoots,
|
|
9059
|
+
e2 as suggestionAppliedComment,
|
|
9060
|
+
e3 as suggestionApplyTable,
|
|
9061
|
+
e4 as suggestionIsTokenValues,
|
|
9062
|
+
e5 as suggestionIssueTitle,
|
|
9063
|
+
e6 as suggestionKindFromLabels,
|
|
9064
|
+
e7 as suggestionLabelChanges,
|
|
9065
|
+
e8 as suggestionLabels,
|
|
9066
|
+
e9 as suggestionOverrideOf,
|
|
9067
|
+
ea as suggestionOverrideProblem,
|
|
9068
|
+
eb as suggestionPathProblems,
|
|
9069
|
+
ec as suggestionPayloadSha,
|
|
9070
|
+
ed as suggestionReviewState,
|
|
9071
|
+
ee as suggestionShapeProblems,
|
|
9072
|
+
ef as suggestionStalenessOnGitHub,
|
|
8233
9073
|
suggestionStateFromLabels,
|
|
8234
|
-
|
|
9074
|
+
eg as suggestionStateLabel,
|
|
8235
9075
|
suggestionStatus,
|
|
8236
|
-
|
|
8237
|
-
|
|
9076
|
+
eh as suggestionSubject,
|
|
9077
|
+
ei as suggestionTouches,
|
|
8238
9078
|
summarizeApprovals,
|
|
8239
9079
|
summarizeCi,
|
|
8240
9080
|
summarizeComponent,
|
|
8241
|
-
|
|
8242
|
-
|
|
9081
|
+
ej as summarizeSuggestionApprovals,
|
|
9082
|
+
cl as summarizeTokenChange,
|
|
8243
9083
|
switchToPublishedOnGitHub,
|
|
8244
|
-
|
|
9084
|
+
ek as syncSuggestionStateLabel,
|
|
8245
9085
|
takeMine,
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
9086
|
+
cm as tarballAssetName,
|
|
9087
|
+
cn as tarballUrl,
|
|
9088
|
+
co as teamKey,
|
|
9089
|
+
cp as teamKeyOf,
|
|
9090
|
+
cq as teamName,
|
|
9091
|
+
cr as teamUnreadableMessage,
|
|
9092
|
+
cs as teamsIn,
|
|
9093
|
+
thingOf,
|
|
8253
9094
|
threeWayDiff,
|
|
9095
|
+
tier1Domain,
|
|
9096
|
+
tier1PublishOf,
|
|
9097
|
+
tier1PublishReviewBody,
|
|
9098
|
+
tier1PublishSentence,
|
|
9099
|
+
tier1PublishedBy,
|
|
8254
9100
|
tintedNeutral,
|
|
8255
|
-
|
|
9101
|
+
el as titleOf,
|
|
8256
9102
|
titleWithJiraKey,
|
|
8257
|
-
|
|
8258
|
-
|
|
9103
|
+
ct as toAlias,
|
|
9104
|
+
em as toChangeSummary,
|
|
8259
9105
|
toConfigJson,
|
|
8260
9106
|
toHex,
|
|
8261
|
-
|
|
9107
|
+
cu as toMap,
|
|
8262
9108
|
toOklch,
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
9109
|
+
cv as toPlain,
|
|
9110
|
+
cw as tokenBump,
|
|
9111
|
+
en as tokenCssVar,
|
|
8266
9112
|
tokenFileChanges,
|
|
8267
9113
|
tokenFilesOf,
|
|
8268
|
-
|
|
8269
|
-
|
|
9114
|
+
eo as tokenFingerprint,
|
|
9115
|
+
ep as tokenHistory,
|
|
8270
9116
|
t as tokenLabel,
|
|
8271
|
-
|
|
9117
|
+
cx as tokenNode,
|
|
8272
9118
|
tokenPublishRoute,
|
|
8273
9119
|
tokenRepoFromMeasure,
|
|
8274
9120
|
tokenRowAxes,
|
|
@@ -8279,7 +9125,7 @@ export {
|
|
|
8279
9125
|
tokenSuggestionPayload,
|
|
8280
9126
|
tokenUsage,
|
|
8281
9127
|
tokenValueCss,
|
|
8282
|
-
|
|
9128
|
+
eq as tokenVariablePath,
|
|
8283
9129
|
tokensPath,
|
|
8284
9130
|
touchesLayout,
|
|
8285
9131
|
touchesPackages,
|
|
@@ -8288,50 +9134,53 @@ export {
|
|
|
8288
9134
|
tryInProductUrl,
|
|
8289
9135
|
tryTogetherUrl,
|
|
8290
9136
|
unpicked,
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
9137
|
+
cy as unreadableShipTeams,
|
|
9138
|
+
cz as unreadableTeams,
|
|
9139
|
+
er as unsafePathProblem,
|
|
8294
9140
|
updatePendingDrafts,
|
|
8295
9141
|
upsertToken,
|
|
8296
9142
|
useLibraryPreviewOnGitHub,
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
9143
|
+
es as utf8ToBase64,
|
|
9144
|
+
et as validateComponentSpec,
|
|
9145
|
+
eu as validateDocs,
|
|
8300
9146
|
validateDocsTask,
|
|
8301
9147
|
validateFigmaFixes,
|
|
8302
|
-
|
|
9148
|
+
ev as validateIntegrations,
|
|
8303
9149
|
validateLayered,
|
|
8304
9150
|
validatePendingDrafts,
|
|
8305
9151
|
validateProposal,
|
|
8306
9152
|
validateSet,
|
|
8307
|
-
|
|
8308
|
-
|
|
9153
|
+
cA as valueIn,
|
|
9154
|
+
cB as valueToCss,
|
|
8309
9155
|
valuesEqual,
|
|
8310
9156
|
variantCombosFromMapping,
|
|
8311
9157
|
variantDifferences,
|
|
8312
9158
|
variantKeyOf,
|
|
8313
|
-
|
|
9159
|
+
ew as variantName,
|
|
8314
9160
|
variantValuesFromMapping,
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
9161
|
+
ex as versionTimeline,
|
|
9162
|
+
cC as visualImpactProblems,
|
|
9163
|
+
cD as visualOffer,
|
|
9164
|
+
ey as webStorageEtagStore,
|
|
9165
|
+
whereItStands,
|
|
9166
|
+
cE as whoCanShipSentence,
|
|
9167
|
+
cF as whyItExists,
|
|
9168
|
+
ez as withAuthorLine,
|
|
9169
|
+
withDoneExploring,
|
|
8322
9170
|
withFigmaOrigin,
|
|
8323
|
-
|
|
9171
|
+
eA as withFigmaPushApprovals,
|
|
8324
9172
|
withLink,
|
|
8325
9173
|
withNeededBy,
|
|
8326
|
-
|
|
9174
|
+
eB as withOutdated,
|
|
8327
9175
|
withPiecesRecord,
|
|
8328
9176
|
withRepoTokenPaths,
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
9177
|
+
eC as withRequestExploration,
|
|
9178
|
+
eD as withSrgbColors,
|
|
9179
|
+
eE as withSuggestionApprovals,
|
|
9180
|
+
eF as withTokenValueApprovals,
|
|
8333
9181
|
withoutAddedWrappers,
|
|
8334
|
-
|
|
9182
|
+
eG as withoutJiraKey,
|
|
9183
|
+
wordsOf,
|
|
8335
9184
|
writeDraftResults,
|
|
8336
9185
|
writeTokenProposalOnGitHub
|
|
8337
9186
|
};
|