@bridge-ds/core 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -283,6 +283,22 @@ reviewSuggestionOnGitHub(gh, n, { event, role?, message?, payloadSha? }, gov); l
283
283
  declineSuggestionOnGitHub(gh, n, reason, by?); syncSuggestionStateLabel(gh, n, labels, status);
284
284
  ```
285
285
 
286
+ ## storybook (storybook.ts)
287
+
288
+ Reads a deployed or built Storybook without the repo: `index.json` (v4 for Storybook 7, v5 for 8 and later), else
289
+ `stories.json` (v3). Entries are grouped by title into components (name = last title segment); test entries stay out
290
+ of `stories`. A 404, an HTML answer from a host that falls back to `index.html`, or a body with no entries moves on to
291
+ the next file. Errors carry a code: `invalid-url`, `not-found`, `not-json`, `private` (sign-in wall, Chromatic login
292
+ answer, 401/403), `empty`, `http`, `network`. Pure, with an injected `fetch`; `@bridge-ds/visual` uses the parser for
293
+ `storybook-static/index.json` and the iframe URL builder for its captures.
294
+
295
+ ```ts
296
+ fetchStorybookIndex(base, { fetch?, signal? }): Promise<{ version; source: 'index.json' | 'stories.json'; entries; components }>;
297
+ parseStorybookIndex(json, source?); groupStorybookComponents(entries): { title; name; group; stories; docs; tags }[];
298
+ normalizeStorybookUrl(pasted): string; // https only (http on localhost), drops ?path=, /index.html, /iframe.html
299
+ storybookIframeUrl(base, id, { viewMode?, globals?, args? }); storybookManagerUrl(base, id, viewMode?); storybookIndexUrls(base);
300
+ ```
301
+
286
302
  ## history (history.ts, history-github.ts, history-demo.ts)
287
303
 
288
304
  What changed in the tokens, when, who changed it and why, and how to undo it safely.
package/dist/index.d.ts CHANGED
@@ -63,3 +63,4 @@ export * from './suggestions';
63
63
  export * from './suggestion-review';
64
64
  export * from './suggestions-github';
65
65
  export * from './suggestion-apply';
66
+ export * from './storybook';
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ function checkValue(set, owner, value, mode, where, issues) {
52
52
  });
53
53
  }
54
54
  }
55
- function validateSet(set, pairs) {
55
+ function validateSet(set, pairs2) {
56
56
  const issues = [];
57
57
  for (const p2 of set.problems) issues.push({ code: "file-problem", severity: "error", message: p2 });
58
58
  for (const t2 of set.tokens.values()) {
@@ -119,7 +119,7 @@ function validateSet(set, pairs) {
119
119
  }
120
120
  );
121
121
  }
122
- const contrast = evaluatePairs(set.byPath, pairs, contrastScope(set));
122
+ const contrast = evaluatePairs(set.byPath, pairs2, contrastScope(set));
123
123
  for (const r of contrast) {
124
124
  const path = `${r.pair.foreground} on ${r.pair.background}`;
125
125
  const mode = `${r.brand ?? ""}.${r.theme}`;
@@ -154,9 +154,9 @@ function shadowedEdits(set, edits) {
154
154
  }
155
155
  return out;
156
156
  }
157
- function validateProposal(proposed, head, pairs, typeChanges = []) {
158
- const after = validateSet(proposed, pairs);
159
- const before = validateSet(head, pairs);
157
+ function validateProposal(proposed, head, pairs2, typeChanges = []) {
158
+ const after = validateSet(proposed, pairs2);
159
+ const before = validateSet(head, pairs2);
160
160
  const existing = new Set(before.issues.map(issueKey$1));
161
161
  const blocking = [];
162
162
  const warnings = [];
@@ -1112,7 +1112,7 @@ const RADIUS_ROLES = ["inset", "control", "container", "overlay"];
1112
1112
  const RESERVED = /* @__PURE__ */ new Set(["default", "base", "all", "none", "new", "blank", "primitives", "semantic", "components", "density"]);
1113
1113
  const BRAND_SLUG_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1114
1114
  const DEFAULT_PRIMARY_CHROMA = 0.14;
1115
- const isObj = (x) => !!x && typeof x === "object" && !Array.isArray(x);
1115
+ const isObj$1 = (x) => !!x && typeof x === "object" && !Array.isArray(x);
1116
1116
  const fileMap = (f) => f instanceof Map ? new Map(f) : new Map(Object.entries(f));
1117
1117
  const brandDisplayName = (slug) => slug.split("-").filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
1118
1118
  function brandLayers(config) {
@@ -1133,27 +1133,27 @@ function setAt(doc, path, node) {
1133
1133
  const keys = path.split(".");
1134
1134
  let cur = doc;
1135
1135
  for (const k of keys.slice(0, -1)) {
1136
- if (!isObj(cur[k])) cur[k] = {};
1136
+ if (!isObj$1(cur[k])) cur[k] = {};
1137
1137
  cur = cur[k];
1138
1138
  }
1139
1139
  const last = keys[keys.length - 1];
1140
- const prev = isObj(cur[last]) ? cur[last] : {};
1140
+ const prev = isObj$1(cur[last]) ? cur[last] : {};
1141
1141
  cur[last] = { ...prev, ...node };
1142
1142
  }
1143
1143
  function getAt(doc, path) {
1144
1144
  let cur = doc;
1145
- for (const k of path.split(".")) cur = isObj(cur) ? cur[k] : void 0;
1146
- return isObj(cur) ? cur : void 0;
1145
+ for (const k of path.split(".")) cur = isObj$1(cur) ? cur[k] : void 0;
1146
+ return isObj$1(cur) ? cur : void 0;
1147
1147
  }
1148
1148
  function typeAt(doc, path) {
1149
1149
  let cur = doc;
1150
1150
  let type;
1151
1151
  for (const k of path.split(".")) {
1152
- if (!isObj(cur)) return type;
1152
+ if (!isObj$1(cur)) return type;
1153
1153
  if (typeof cur.$type === "string") type = cur.$type;
1154
1154
  cur = cur[k];
1155
1155
  }
1156
- return isObj(cur) && typeof cur.$type === "string" ? cur.$type : type;
1156
+ return isObj$1(cur) && typeof cur.$type === "string" ? cur.$type : type;
1157
1157
  }
1158
1158
  function rampFor(input, pinStep, kind) {
1159
1159
  if (typeof input === "number" || typeof input === "string" && /^\d+(\.\d+)?$/.test(input.trim())) {
@@ -1210,7 +1210,7 @@ function listBrands(source) {
1210
1210
  doc = JSON.parse(files.get(file) ?? "{}");
1211
1211
  } catch {
1212
1212
  }
1213
- const meta = isObj(doc.$extensions) && isObj(doc.$extensions["dev.bridge.brand"]) ? doc.$extensions["dev.bridge.brand"] : {};
1213
+ const meta = isObj$1(doc.$extensions) && isObj$1(doc.$extensions["dev.bridge.brand"]) ? doc.$extensions["dev.bridge.brand"] : {};
1214
1214
  const combo = set.normalizeCombo({ brand: slug });
1215
1215
  const primary = set.resolve("color.action.primary.bg", combo);
1216
1216
  const overrides = layers.override ? (config.axes[layers.overrideAxis]?.values ?? []).map((v) => fill(layers.override, { brand: slug, [layers.overrideAxis]: v })).filter((f) => files.has(f)) : [];
@@ -1300,7 +1300,7 @@ function planNewBrand(source, input) {
1300
1300
  for (const [path, defs] of before.view(before.defaultCombo)) {
1301
1301
  const m = /^radius\.([\w-]+)$/.exec(path);
1302
1302
  const v = defs[defs.length - 1].value;
1303
- if (m && isObj(v) && typeof v.value === "number" && v.unit === "px" && !getAt(doc, path)) scale.set(v.value, m[1]);
1303
+ if (m && isObj$1(v) && typeof v.value === "number" && v.unit === "px" && !getAt(doc, path)) scale.set(v.value, m[1]);
1304
1304
  }
1305
1305
  const radius = input.radius ?? (blank ? RADIUS_PRESETS[0].radius : void 0);
1306
1306
  if (radius) {
@@ -1339,9 +1339,9 @@ function planNewBrand(source, input) {
1339
1339
  }
1340
1340
  const palette = getAt(doc, "palette");
1341
1341
  for (const [role, group] of Object.entries(palette ?? {})) {
1342
- if (role.startsWith("$") || !isObj(group)) continue;
1342
+ if (role.startsWith("$") || !isObj$1(group)) continue;
1343
1343
  for (const [step, node] of Object.entries(group)) {
1344
- if (step.startsWith("$") || !isObj(node) || !("$value" in node) || typeof node.$value === "string") continue;
1344
+ if (step.startsWith("$") || !isObj$1(node) || !("$value" in node) || typeof node.$value === "string") continue;
1345
1345
  const ramp = `${slug}-${role}`;
1346
1346
  const entry = prims.get(ramp) ?? { description: `${name} ${role}. Exact values set when the brand was created.`, steps: /* @__PURE__ */ new Map() };
1347
1347
  entry.steps.set(Number(step), node.$value);
@@ -1354,7 +1354,7 @@ function planNewBrand(source, input) {
1354
1354
  if (prims.size && files.has(primFile)) issues.push({ level: "error", code: "slug-file-exists", message: `${tokensPath(config, primFile)} already exists. Pick another name.` });
1355
1355
  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.` });
1356
1356
  doc.$description = `${name}: created with Bridge, started from ${blank ? "a blank brand" : brandDisplayName(from)}.`;
1357
- doc.$extensions = { ...isObj(doc.$extensions) ? doc.$extensions : {}, "dev.bridge.brand": { name, from, ...Object.keys(anchors).length ? { anchors } : {} } };
1357
+ doc.$extensions = { ...isObj$1(doc.$extensions) ? doc.$extensions : {}, "dev.bridge.brand": { name, from, ...Object.keys(anchors).length ? { anchors } : {} } };
1358
1358
  const { $description, $extensions, ...rest } = doc;
1359
1359
  const brandFile = fill(layers.brand, { brand: slug });
1360
1360
  const nextFiles = new Map(files);
@@ -1406,10 +1406,10 @@ function planNewBrand(source, input) {
1406
1406
  if (i.modes && !i.modes.some((m) => m.split(",").includes(brandKey)) && !created.includes(i.file ?? "")) continue;
1407
1407
  issues.push({ level: i.level, code: i.code, message: i.message, ...i.path ? { path: i.path } : {}, ...i.file ? { file: tokensPath(config, i.file) } : {} });
1408
1408
  }
1409
- const pairs = parseContrastChecks(source.contrastText);
1409
+ const pairs2 = parseContrastChecks(source.contrastText);
1410
1410
  const inGate = nextConfig.axes.brand?.values.includes(slug);
1411
1411
  const combos = inGate ? after.combos(nextConfig.contrast?.modes).filter((c) => c.brand === slug) : [];
1412
- const results = combos.flatMap((c) => pairs.map((p2) => checkLayeredPair(after, p2, c)));
1412
+ const results = combos.flatMap((c) => pairs2.map((p2) => checkLayeredPair(after, p2, c)));
1413
1413
  const failing = results.filter((r) => !r.pass);
1414
1414
  const themes = [...new Set(combos.map((c) => c.theme).filter((t2) => !!t2))];
1415
1415
  const ramps = {};
@@ -1780,7 +1780,7 @@ function composePendingDrafts(input) {
1780
1780
  return p2;
1781
1781
  }
1782
1782
  const KINDS = new Set(REQUEST_KINDS.map((k) => k.value));
1783
- const str = (x) => typeof x === "string";
1783
+ const str$1 = (x) => typeof x === "string";
1784
1784
  const optStr = (x) => x === void 0 || typeof x === "string";
1785
1785
  function validatePendingDrafts(x) {
1786
1786
  const out = [];
@@ -1788,7 +1788,7 @@ function validatePendingDrafts(x) {
1788
1788
  const p2 = x;
1789
1789
  if (p2.version !== DRAFT_EXCHANGE_VERSION) out.push(`Unsupported draft session version ${String(p2.version)}: update Bridge.`);
1790
1790
  if (!isDraftSessionId(p2.sessionId)) out.push("The session id is missing or invalid.");
1791
- if (!str(p2.createdAt) || Number.isNaN(Date.parse(p2.createdAt))) out.push("createdAt must be an ISO date.");
1791
+ if (!str$1(p2.createdAt) || Number.isNaN(Date.parse(p2.createdAt))) out.push("createdAt must be an ISO date.");
1792
1792
  if (p2.status !== "pending" && p2.status !== "drafted") out.push('status must be "pending" or "drafted".');
1793
1793
  if (p2.source !== "figma" && p2.source !== "studio") out.push('source must be "figma" or "studio".');
1794
1794
  if (!Array.isArray(p2.items) || !p2.items.length) out.push("A draft session needs at least one item.");
@@ -1799,32 +1799,32 @@ function validatePendingDrafts(x) {
1799
1799
  out.push(`${at3} is not an object.`);
1800
1800
  continue;
1801
1801
  }
1802
- if (!str(i.id) || !i.id) out.push(`${at3} has no id.`);
1802
+ if (!str$1(i.id) || !i.id) out.push(`${at3} has no id.`);
1803
1803
  else if (ids.has(i.id)) out.push(`${at3} repeats the id "${i.id}".`);
1804
1804
  else ids.add(i.id);
1805
1805
  if (i.scope !== "request" && i.scope !== "push" && i.scope !== "docs") out.push(`${at3}: scope must be "request", "push" or "docs".`);
1806
1806
  if (i.scope === "docs") {
1807
1807
  const d = i.docs;
1808
- if (!d || typeof d !== "object" || !str(d.slug) || !d.slug || !str(d.prompt) || !d.prompt || !optStr(d.path)) out.push(`${at3}: a docs item needs docs {slug, task, prompt}.`);
1808
+ if (!d || typeof d !== "object" || !str$1(d.slug) || !d.slug || !str$1(d.prompt) || !d.prompt || !optStr(d.path)) out.push(`${at3}: a docs item needs docs {slug, task, prompt}.`);
1809
1809
  else out.push(...validateDocsTask(d.task).map((m) => `${at3}: ${m}`));
1810
1810
  } else if (i.docs !== void 0) out.push(`${at3}: only a docs item has docs.`);
1811
1811
  if (!optStr(i.component)) out.push(`${at3}: component must be text.`);
1812
1812
  if (i.kind !== void 0 && !KINDS.has(i.kind)) out.push(`${at3}: unknown kind "${String(i.kind)}".`);
1813
- if (!Array.isArray(i.changes) || i.changes.some((c) => !c || !str(c.field) || !optStr(c.before) || !optStr(c.after))) out.push(`${at3}: changes must be a list of {field, before, after}.`);
1814
- if (i.variables !== void 0 && (!Array.isArray(i.variables) || i.variables.some((v) => !v || !str(v.path)))) out.push(`${at3}: variables must be a list of {path, before, after}.`);
1813
+ if (!Array.isArray(i.changes) || i.changes.some((c) => !c || !str$1(c.field) || !optStr(c.before) || !optStr(c.after))) out.push(`${at3}: changes must be a list of {field, before, after}.`);
1814
+ if (i.variables !== void 0 && (!Array.isArray(i.variables) || i.variables.some((v) => !v || !str$1(v.path)))) out.push(`${at3}: variables must be a list of {path, before, after}.`);
1815
1815
  if (i.variables !== void 0 && i.scope !== "push") out.push(`${at3}: only a push item has variables.`);
1816
- if (i.images !== void 0 && (!Array.isArray(i.images) || i.images.some((m) => !m || m.label !== "before" && m.label !== "after" || !str(m.url)))) out.push(`${at3}: images must be a list of {label: before or after, url}.`);
1817
- if (!i.draft || !str(i.draft.title) || !str(i.draft.summary)) out.push(`${at3}: draft must hold a title and a summary.`);
1818
- if (i.lines !== void 0 && (!Array.isArray(i.lines) || i.lines.some((l) => !l || !str(l.id) || !l.id || !str(l.line) || !optStr(l.title) || !Array.isArray(l.changes)))) out.push(`${at3}: lines must be a list of {id, changes, line, title}.`);
1816
+ if (i.images !== void 0 && (!Array.isArray(i.images) || i.images.some((m) => !m || m.label !== "before" && m.label !== "after" || !str$1(m.url)))) out.push(`${at3}: images must be a list of {label: before or after, url}.`);
1817
+ if (!i.draft || !str$1(i.draft.title) || !str$1(i.draft.summary)) out.push(`${at3}: draft must hold a title and a summary.`);
1818
+ if (i.lines !== void 0 && (!Array.isArray(i.lines) || i.lines.some((l) => !l || !str$1(l.id) || !l.id || !str$1(l.line) || !optStr(l.title) || !Array.isArray(l.changes)))) out.push(`${at3}: lines must be a list of {id, changes, line, title}.`);
1819
1819
  }
1820
1820
  if (p2.results !== void 0) {
1821
1821
  if (!Array.isArray(p2.results)) out.push("results must be a list.");
1822
1822
  else
1823
1823
  for (const r of p2.results) {
1824
- if (!r || !str(r.id) || !str(r.title) || !str(r.summary)) out.push("Each result needs an id, a title and a summary.");
1824
+ if (!r || !str$1(r.id) || !str$1(r.title) || !str$1(r.summary)) out.push("Each result needs an id, a title and a summary.");
1825
1825
  else if (r.docs !== void 0 && (!r.docs || typeof r.docs !== "object" || Array.isArray(r.docs))) out.push("Result docs must be an object of sections.");
1826
1826
  else if (!optStr(r.replacement)) out.push("A result replacement must be text.");
1827
- else if (r.lines !== void 0 && (!Array.isArray(r.lines) || r.lines.some((l) => !l || !str(l.id) || !str(l.line) || !optStr(l.title)))) out.push("Result lines must be a list of {id, line, title}.");
1827
+ else if (r.lines !== void 0 && (!Array.isArray(r.lines) || r.lines.some((l) => !l || !str$1(l.id) || !str$1(l.line) || !optStr(l.title)))) out.push("Result lines must be a list of {id, line, title}.");
1828
1828
  }
1829
1829
  }
1830
1830
  return out;
@@ -3210,6 +3210,149 @@ async function figmaPushStatusesOnGitHub(gh, numbers) {
3210
3210
  );
3211
3211
  return out;
3212
3212
  }
3213
+ class StorybookIndexError extends Error {
3214
+ constructor(code2, message, status) {
3215
+ super(message);
3216
+ this.code = code2;
3217
+ this.status = status;
3218
+ this.name = "StorybookIndexError";
3219
+ }
3220
+ code;
3221
+ status;
3222
+ }
3223
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3224
+ const str = (v) => typeof v === "string" && v.trim() ? v : void 0;
3225
+ function entryFrom(key, raw, v3) {
3226
+ if (!isObj(raw)) return null;
3227
+ const id = str(raw.id) ?? key;
3228
+ const title = str(raw.title) ?? str(raw.kind);
3229
+ const name = str(raw.name) ?? str(raw.story);
3230
+ if (!id || !title || !name) return null;
3231
+ const params = isObj(raw.parameters) ? raw.parameters : {};
3232
+ const type = raw.type === "docs" || raw.type === "story" ? raw.type : v3 && params.docsOnly === true ? "docs" : "story";
3233
+ const e = {
3234
+ id,
3235
+ title,
3236
+ name,
3237
+ type,
3238
+ importPath: str(raw.importPath) ?? str(params.fileName) ?? "",
3239
+ tags: Array.isArray(raw.tags) ? raw.tags.filter((t2) => typeof t2 === "string") : []
3240
+ };
3241
+ const subtype = str(raw.subtype);
3242
+ if (subtype) e.subtype = subtype;
3243
+ const exportName2 = str(raw.exportName);
3244
+ if (exportName2) e.exportName = exportName2;
3245
+ const componentPath = str(raw.componentPath);
3246
+ if (componentPath) e.componentPath = componentPath;
3247
+ return e;
3248
+ }
3249
+ function groupStorybookComponents(entries) {
3250
+ const byTitle = /* @__PURE__ */ new Map();
3251
+ for (const e of entries) {
3252
+ let c = byTitle.get(e.title);
3253
+ if (!c) {
3254
+ const parts = e.title.split("/").map((p2) => p2.trim()).filter(Boolean);
3255
+ c = { title: e.title, name: parts[parts.length - 1] ?? e.title, group: parts.slice(0, -1), stories: [], docs: [], tags: [] };
3256
+ byTitle.set(e.title, c);
3257
+ }
3258
+ if (e.type === "docs") c.docs.push(e);
3259
+ else if (e.subtype !== "test") c.stories.push(e);
3260
+ for (const t2 of e.tags) if (!c.tags.includes(t2)) c.tags.push(t2);
3261
+ }
3262
+ const out = [...byTitle.values()].filter((c) => c.stories.length || c.docs.length);
3263
+ for (const c of out) c.tags.sort();
3264
+ return out.sort((a2, b) => a2.title.localeCompare(b.title));
3265
+ }
3266
+ function parseStorybookIndex(json, source) {
3267
+ if (!isObj(json)) throw new StorybookIndexError("not-json", "The Storybook index is not a JSON object.");
3268
+ if (typeof json.loginUrl === "string" && !("entries" in json) && !("stories" in json)) {
3269
+ throw new StorybookIndexError("private", "This Storybook is private: it asks for a sign-in before showing its index.");
3270
+ }
3271
+ const map = isObj(json.entries) ? json.entries : isObj(json.stories) ? json.stories : null;
3272
+ if (!map) throw new StorybookIndexError("empty", "This file has no Storybook entries.");
3273
+ const v3 = !isObj(json.entries);
3274
+ const entries = Object.entries(map).map(([k, raw]) => entryFrom(k, raw, v3)).filter((e) => e !== null);
3275
+ if (!entries.length) throw new StorybookIndexError("empty", "This Storybook index lists no stories or docs.");
3276
+ const version = typeof json.v === "number" ? json.v : v3 ? 3 : 4;
3277
+ return { version, source: source ?? (v3 ? "stories.json" : "index.json"), entries, components: groupStorybookComponents(entries) };
3278
+ }
3279
+ function normalizeStorybookUrl(input) {
3280
+ let u;
3281
+ try {
3282
+ u = new URL(input.trim());
3283
+ } catch {
3284
+ throw new StorybookIndexError("invalid-url", "Enter the full Storybook address, for example https://storybook.example.com");
3285
+ }
3286
+ const local = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
3287
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) {
3288
+ throw new StorybookIndexError("invalid-url", "The Storybook address must start with https://");
3289
+ }
3290
+ if (u.username || u.password) throw new StorybookIndexError("invalid-url", "Remove the user name and password from the address.");
3291
+ const path = u.pathname.replace(/\/(index\.html|iframe\.html|index\.json|stories\.json)$/i, "/").replace(/\/+$/, "");
3292
+ return `${u.protocol}//${u.host}${path}`;
3293
+ }
3294
+ const storybookIndexUrls = (base) => {
3295
+ const b = base.replace(/\/+$/, "");
3296
+ return [
3297
+ { url: `${b}/index.json`, source: "index.json" },
3298
+ { url: `${b}/stories.json`, source: "stories.json" }
3299
+ ];
3300
+ };
3301
+ const pairs = (o) => Object.entries(o).map(([k, v]) => `${encodeURIComponent(k)}:${encodeURIComponent(v)}`).join(";");
3302
+ function storybookIframeUrl(base, id, o = {}) {
3303
+ let url = `${base.replace(/\/+$/, "")}/iframe.html?id=${encodeURIComponent(id)}&viewMode=${o.viewMode ?? "story"}`;
3304
+ if (o.globals && Object.keys(o.globals).length) url += `&globals=${pairs(o.globals)}`;
3305
+ if (o.args && Object.keys(o.args).length) url += `&args=${pairs(o.args)}`;
3306
+ return url;
3307
+ }
3308
+ function storybookManagerUrl(base, id, viewMode = "story") {
3309
+ return `${base.replace(/\/+$/, "")}/?path=/${viewMode}/${encodeURIComponent(id)}`;
3310
+ }
3311
+ async function fetchStorybookIndex(base, o = {}) {
3312
+ const doFetch = o.fetch ?? ((url, init) => fetch(url, init));
3313
+ const rank = { private: 6, http: 5, network: 4, empty: 3, "not-json": 2, "not-found": 1, "invalid-url": 0 };
3314
+ let worst = null;
3315
+ const keep = (e) => {
3316
+ if (!worst || rank[e.code] > rank[worst.code]) worst = e;
3317
+ };
3318
+ for (const { url, source } of storybookIndexUrls(base)) {
3319
+ let res;
3320
+ try {
3321
+ res = await doFetch(url, { headers: { accept: "application/json" }, ...o.signal ? { signal: o.signal } : {} });
3322
+ } catch (err) {
3323
+ if (o.signal?.aborted) throw err;
3324
+ keep(new StorybookIndexError("network", `Could not reach ${url}. The host may be down or may not allow reading from another site.`));
3325
+ continue;
3326
+ }
3327
+ if (res.status === 404 || res.status === 410) {
3328
+ keep(new StorybookIndexError("not-found", `No Storybook index at ${base}: neither index.json nor stories.json was found.`, res.status));
3329
+ continue;
3330
+ }
3331
+ if (res.status === 401 || res.status === 403) {
3332
+ keep(new StorybookIndexError("private", "This Storybook is private: it asks for a sign-in before showing its index.", res.status));
3333
+ continue;
3334
+ }
3335
+ if (!res.ok) {
3336
+ keep(new StorybookIndexError("http", `The Storybook host answered ${res.status} for ${url}.`, res.status));
3337
+ continue;
3338
+ }
3339
+ const text = await res.text();
3340
+ let json;
3341
+ try {
3342
+ json = JSON.parse(text);
3343
+ } catch {
3344
+ keep(new StorybookIndexError("not-json", `${url} is not a Storybook index (the host answered with a web page).`));
3345
+ continue;
3346
+ }
3347
+ try {
3348
+ return parseStorybookIndex(json, source);
3349
+ } catch (err) {
3350
+ if (err instanceof StorybookIndexError) keep(err);
3351
+ else throw err;
3352
+ }
3353
+ }
3354
+ throw worst ?? new StorybookIndexError("not-found", `No Storybook index at ${base}.`);
3355
+ }
3213
3356
  export {
3214
3357
  af as AAA_MIN,
3215
3358
  ag as AA_MIN,
@@ -3326,6 +3469,7 @@ export {
3326
3469
  aI2 as SUGGESTION_STATE_LABELS,
3327
3470
  aJ2 as SUGGESTION_STATUS_LABEL,
3328
3471
  aK2 as SUGGESTION_TOO_LARGE,
3472
+ StorybookIndexError,
3329
3473
  aL2 as SuggestionError,
3330
3474
  TIER_AXIS,
3331
3475
  TIER_NAMES,
@@ -3515,6 +3659,7 @@ export {
3515
3659
  explorationTestBuildOnGitHub,
3516
3660
  explorationTryIt,
3517
3661
  explorationTryItOnGitHub,
3662
+ fetchStorybookIndex,
3518
3663
  fidelityComponentLine,
3519
3664
  fidelityVerdict,
3520
3665
  bb as fieldChanges,
@@ -3560,6 +3705,7 @@ export {
3560
3705
  gitBlobSha,
3561
3706
  gitHubHistoryBackend,
3562
3707
  bl as globToRegExp,
3708
+ groupStorybookComponents,
3563
3709
  bm as groupsPhrase,
3564
3710
  bL2 as hasSuggestionBlock,
3565
3711
  bM2 as hasWriteAccess,
@@ -3647,6 +3793,7 @@ export {
3647
3793
  newDraftSessionId,
3648
3794
  bC as nodesEqual,
3649
3795
  bD as normalizeRights,
3796
+ normalizeStorybookUrl,
3650
3797
  oklch,
3651
3798
  oklchToLinear,
3652
3799
  onlyTokens,
@@ -3683,6 +3830,7 @@ export {
3683
3830
  parsePiecesRecord,
3684
3831
  cd2 as parseRepo,
3685
3832
  ce2 as parseRequestIssue,
3833
+ parseStorybookIndex,
3686
3834
  cf2 as parseStructureChange,
3687
3835
  cg2 as parseSuggestionAppliedComment,
3688
3836
  ch2 as parseSuggestionIssue,
@@ -3813,6 +3961,9 @@ export {
3813
3961
  stableCommand,
3814
3962
  startExplorationOnGitHub,
3815
3963
  stateSentence,
3964
+ storybookIframeUrl,
3965
+ storybookIndexUrls,
3966
+ storybookManagerUrl,
3816
3967
  c4 as storybookUrl,
3817
3968
  c5 as suggestNearestComment,
3818
3969
  suggestTokenProposalOnGitHub,