@theme-registry/refract 0.1.8 → 0.1.9

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/dist/cli.js CHANGED
@@ -6070,15 +6070,15 @@ const DEFAULT_FILE = "preview.html";
6070
6070
  // ---------------------------------------------------------------------------
6071
6071
  // Escaping — every value below originates in user-authored theme content
6072
6072
  // ---------------------------------------------------------------------------
6073
- const escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6073
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6074
6074
  /**
6075
6075
  * A token value going into a `style="…"` attribute. Declaration/rule terminators are dropped so a
6076
6076
  * value can never break out of its declaration, then the result is HTML-attribute escaped.
6077
6077
  */
6078
- const cssValue = (v) => escapeHtml(String(v).replace(/[;{}<>]/g, "").trim());
6078
+ const cssValue = (v) => esc(String(v).replace(/[;{}<>]/g, "").trim());
6079
6079
  /** Inline `<style>`/`<script>` bodies can't contain a literal `</` without ending the element early. */
6080
6080
  const escapeTextElement = (s) => s.replace(/<\/(?=[a-zA-Z])/g, "<\\/");
6081
- /** Collect every `$value` leaf, threading the nearest declared `$type` down the tree. */
6081
+ const bytes = (n) => (n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`);
6082
6082
  function collectLeaves(node, path, inherited, out) {
6083
6083
  if (!node || typeof node !== "object" || Array.isArray(node))
6084
6084
  return;
@@ -6094,45 +6094,9 @@ function collectLeaves(node, path, inherited, out) {
6094
6094
  collectLeaves(child, [...path, key], type, out);
6095
6095
  }
6096
6096
  }
6097
- /** Sample text for the plates that demonstrate a value by rendering type in it. */
6098
- const TYPE_SPECIMEN = "The quick brown fox";
6099
- /**
6100
- * The visual for one token. A `dimension` under `typography` is a font size (render the ramp);
6101
- * elsewhere it's a length (render a bar). Anything unrecognized falls back to its literal value,
6102
- * which is still the honest answer for a duration or a cubic-bezier.
6103
- */
6104
- function renderSwatch(leaf) {
6105
- const value = leaf.value;
6106
- const label = `<code class="rfp-val">${escapeHtml(String(value))}</code>`;
6107
- switch (leaf.type) {
6108
- case "color":
6109
- return `<span class="rfp-chip" style="background:${cssValue(value)}"></span>${label}`;
6110
- case "shadow":
6111
- return `<span class="rfp-box" style="box-shadow:${cssValue(value)}"></span>${label}`;
6112
- case "border":
6113
- return `<span class="rfp-box" style="border:${cssValue(value)}"></span>${label}`;
6114
- case "fontFamily":
6115
- return `<span class="rfp-specimen" style="font-family:${cssValue(value)}">${TYPE_SPECIMEN}</span>${label}`;
6116
- case "fontWeight":
6117
- return `<span class="rfp-specimen" style="font-weight:${cssValue(value)}">${TYPE_SPECIMEN}</span>${label}`;
6118
- case "dimension":
6119
- // `typography.fontSize.*` is the type ramp — show it as type. Every other dimension (spacing,
6120
- // radius, breakpoints, and typography's own `letterSpacing`) reads better as a length bar.
6121
- return leaf.path[0] === "typography" && leaf.path[1] === "fontSize"
6122
- ? `<span class="rfp-specimen" style="font-size:${cssValue(value)}">${TYPE_SPECIMEN}</span>${label}`
6123
- : `<span class="rfp-bar" style="width:${cssValue(value)}"></span>${label}`;
6124
- default:
6125
- return label;
6126
- }
6127
- }
6128
- /** One plate per top-level DTCG group (`color`, `spacing`, `shadow`, …), rows sorted by source order. */
6129
- function renderTokenPlates(tokens) {
6097
+ /** `<group>` its leaves, in source order. The group is the token's first path segment. */
6098
+ function groupLeaves(leaves) {
6130
6099
  var _a;
6131
- const leaves = [];
6132
- collectLeaves(tokens, [], undefined, leaves);
6133
- if (leaves.length === 0) {
6134
- return `<p class="rfp-note">This theme declares no tokens.</p>`;
6135
- }
6136
6100
  const groups = new Map();
6137
6101
  for (const leaf of leaves) {
6138
6102
  const key = (_a = leaf.path[0]) !== null && _a !== void 0 ? _a : "tokens";
@@ -6142,238 +6106,781 @@ function renderTokenPlates(tokens) {
6142
6106
  else
6143
6107
  groups.set(key, [leaf]);
6144
6108
  }
6145
- const sections = [];
6146
- for (const [group, rows] of groups) {
6147
- const items = rows
6148
- .map(leaf => `<li class="rfp-row"><span class="rfp-name">${escapeHtml(leaf.path.slice(1).join(".") || group)}</span>` +
6149
- `<span class="rfp-swatch">${renderSwatch(leaf)}</span></li>`)
6150
- .join("\n");
6151
- sections.push(`<section class="rfp-plate">\n<h3 class="rfp-plate-title">${escapeHtml(group)} ` +
6152
- `<span class="rfp-count">${rows.length}</span></h3>\n<ul class="rfp-rows">\n${items}\n</ul>\n</section>`);
6109
+ return groups;
6110
+ }
6111
+ const SECTIONS = [
6112
+ { id: "palette", title: "Colour", eyebrow: "Palette",
6113
+ note: "Every rung is a token you can reference. The marked rung is the family's <code>base</code>; click any identifier to copy it." },
6114
+ { id: "type", title: "Type scale", eyebrow: "Typography" },
6115
+ { id: "space", title: "Spacing and size", eyebrow: "Space",
6116
+ note: "There is no separate <code>padding</code> token — spacing <em>is</em> the padding scale, so it is shown both as a measure and as an applied inset." },
6117
+ { id: "shape", title: "Borders, radius and elevation", eyebrow: "Shape &amp; depth" },
6118
+ { id: "motion", title: "Transitions", eyebrow: "Motion" },
6119
+ { id: "layout", title: "Breakpoints", eyebrow: "Layout",
6120
+ note: "These drive the Width control at the top — pick one to reflow the whole specimen at that viewport." },
6121
+ { id: "other", title: "Other tokens", eyebrow: "Additional" },
6122
+ ];
6123
+ /** Group → section. Anything unmapped falls into `other`, so a new subsystem is never dropped. */
6124
+ const SECTION_OF = {
6125
+ color: "palette",
6126
+ typography: "type",
6127
+ spacing: "space", gutters: "space", sizes: "space", aspectRatio: "space",
6128
+ radius: "shape", borderWidth: "shape", borderStyle: "shape", outlineOffset: "shape",
6129
+ shadow: "shape", blur: "shape", opacity: "shape", zIndex: "shape",
6130
+ transition: "motion",
6131
+ breakpoint: "layout",
6132
+ };
6133
+ // ---------------------------------------------------------------------------
6134
+ // Specimen builders
6135
+ // ---------------------------------------------------------------------------
6136
+ const SPECIMEN_TEXT = "Precision is a design decision";
6137
+ const LEADING_TEXT = "Leading is the quiet half of a type scale. Two lines are the minimum needed to judge it, so this specimen wraps.";
6138
+ const idButton = (path, varName) => `<button class="rfp-id" type="button">${esc(path)}</button>` +
6139
+ (varName ? `<span class="rfp-var">${esc(varName)}</span>` : "");
6140
+ const leafPath = (leaf) => leaf.path.join(".");
6141
+ const leafLabel = (leaf) => leaf.path.slice(1).join(".") || leaf.path[0];
6142
+ /** A three-column row: identifier · specimen · value. */
6143
+ const rowOf = (id, specimen, value, centred = false) => `<div class="rfp-row${centred ? " rfp-centred" : ""}"><div>${id}</div>${specimen}` +
6144
+ `<div class="rfp-rowval">${esc(value)}</div></div>`;
6145
+ /** Is this colour-family child a numeric ladder rung (`50`…`900`)? */
6146
+ const isRung = (name) => /^\d+$/.test(name);
6147
+ /** Nearest px magnitude of a dimension value, for bar widths. `undefined` when it has no magnitude. */
6148
+ function pxOf(value) {
6149
+ const match = /^(-?[\d.]+)\s*(px|rem|em)?$/.exec(String(value).trim());
6150
+ if (!match)
6151
+ return undefined;
6152
+ const n = Number(match[1]);
6153
+ if (!Number.isFinite(n))
6154
+ return undefined;
6155
+ return match[2] === "rem" || match[2] === "em" ? n * 16 : n;
6156
+ }
6157
+ // ── Colour ─────────────────────────────────────────────────────────────────
6158
+ function renderPalette(leaves, tokenName) {
6159
+ var _a, _b, _c;
6160
+ // family → member → hex. `color.<family>.<member>`; a family may also be a bare leaf.
6161
+ const families = new Map();
6162
+ for (const leaf of leaves) {
6163
+ const family = (_a = leaf.path[1]) !== null && _a !== void 0 ? _a : "color";
6164
+ const member = leaf.path.slice(2).join(".") || "base";
6165
+ const bucket = families.get(family);
6166
+ if (bucket)
6167
+ bucket.push([member, String(leaf.value)]);
6168
+ else
6169
+ families.set(family, [[member, String(leaf.value)]]);
6170
+ }
6171
+ const ladders = [];
6172
+ const pairs = [];
6173
+ for (const [family, members] of families) {
6174
+ const rungs = members.filter(([name]) => isRung(name));
6175
+ const base = (_b = members.find(([name]) => name === "base")) === null || _b === void 0 ? void 0 : _b[1];
6176
+ if (rungs.length >= 3) {
6177
+ // A ladder: contiguous rungs read as a ramp in a way a row-per-token list never does.
6178
+ rungs.sort((a, b) => Number(a[0]) - Number(b[0]));
6179
+ const strip = rungs
6180
+ .map(([step, hex]) => {
6181
+ const isBase = base !== undefined && hex.toLowerCase() === base.toLowerCase();
6182
+ return (`<div class="rfp-rung"${isBase ? ' data-base="true"' : ""} title="colors.${esc(family)}.${esc(step)} · ${esc(hex)}">` +
6183
+ `<div class="rfp-rung-chip" data-hex="${esc(hex)}" style="background:${cssValue(hex)}"></div>` +
6184
+ `<div class="rfp-rung-foot">${esc(step)}</div></div>`);
6185
+ })
6186
+ .join("");
6187
+ ladders.push(`<div class="rfp-ladder"><div><div class="rfp-ladder-name">${esc(family)}</div>` +
6188
+ (base ? `<div class="rfp-ladder-base">base · ${esc(base)}</div>` : "") +
6189
+ idButton(`colors.${family}`, tokenName === null || tokenName === void 0 ? void 0 : tokenName(`colors.${family}`)) +
6190
+ `</div><div class="rfp-rungs">${strip}</div></div>`);
6191
+ continue;
6192
+ }
6193
+ // Not a ladder — render each member as a swatch card. When the family declares a `text`
6194
+ // pairing, the card renders that text ON the colour with a live contrast readout, which is the
6195
+ // same pairing `refract audit` scores.
6196
+ const text = (_c = members.find(([name]) => name === "text")) === null || _c === void 0 ? void 0 : _c[1];
6197
+ for (const [member, hex] of members) {
6198
+ if (member === "text")
6199
+ continue;
6200
+ const path = member === "base" ? `colors.${family}` : `colors.${family}.${member}`;
6201
+ pairs.push(`<div class="rfp-pair"><div class="rfp-pair-swatch" data-bg="${esc(hex)}"${text ? ` data-fg="${esc(text)}"` : ""}` +
6202
+ ` style="background:${cssValue(hex)}${text ? `;color:${cssValue(text)}` : ""}">` +
6203
+ `<span class="rfp-pair-sample">Aa</span>` +
6204
+ (text ? `<span class="rfp-pair-ratio"></span>` : "") +
6205
+ `</div><div class="rfp-pair-foot">${idButton(path, tokenName === null || tokenName === void 0 ? void 0 : tokenName(path))}` +
6206
+ `<span class="rfp-hex">${esc(hex)}${text ? ` on ${esc(text)}` : ""}</span></div></div>`);
6207
+ }
6208
+ }
6209
+ let html = "";
6210
+ if (ladders.length) {
6211
+ html += plate("Families", `${ladders.length} · ${leaves.length} tokens`, ladders.join(""));
6212
+ }
6213
+ if (pairs.length) {
6214
+ html += plate("Swatches", "contrast computed against each token's paired <code>text</code>", `<div class="rfp-pairs">${pairs.join("")}</div>`);
6215
+ }
6216
+ return html;
6217
+ }
6218
+ // ── Typography ─────────────────────────────────────────────────────────────
6219
+ function renderTypography(leaves, tokenName) {
6220
+ var _a;
6221
+ const byProp = new Map();
6222
+ for (const leaf of leaves) {
6223
+ const prop = (_a = leaf.path[1]) !== null && _a !== void 0 ? _a : "typography";
6224
+ const bucket = byProp.get(prop);
6225
+ if (bucket)
6226
+ bucket.push(leaf);
6227
+ else
6228
+ byProp.set(prop, [leaf]);
6229
+ }
6230
+ const out = [];
6231
+ for (const [prop, items] of byProp) {
6232
+ const rows = items
6233
+ .map(leaf => {
6234
+ const path = leafPath(leaf);
6235
+ const id = idButton(path, tokenName === null || tokenName === void 0 ? void 0 : tokenName(path));
6236
+ const value = String(leaf.value);
6237
+ if (prop === "fontSize") {
6238
+ return rowOf(id, `<div class="rfp-specimen" style="font-size:${cssValue(value)}">${SPECIMEN_TEXT}</div>`, value);
6239
+ }
6240
+ if (prop === "fontFamily") {
6241
+ return rowOf(id, `<div class="rfp-specimen" style="font-family:${cssValue(value)}">${SPECIMEN_TEXT}</div>`, value);
6242
+ }
6243
+ if (prop === "fontWeight") {
6244
+ return rowOf(id, `<div class="rfp-specimen" style="font-weight:${cssValue(value)}">${SPECIMEN_TEXT}</div>`, value);
6245
+ }
6246
+ if (prop === "lineHeight") {
6247
+ // One line tells you nothing about leading — the specimen has to wrap.
6248
+ return rowOf(id, `<div class="rfp-leading" style="line-height:${cssValue(value)}">${LEADING_TEXT}</div>`, value);
6249
+ }
6250
+ if (prop === "letterSpacing") {
6251
+ return rowOf(id, `<div class="rfp-specimen" style="font-size:22px;letter-spacing:${cssValue(value)}">${SPECIMEN_TEXT}</div>`, value);
6252
+ }
6253
+ return rowOf(id, `<div class="rfp-specimen">${esc(value)}</div>`, value);
6254
+ })
6255
+ .join("");
6256
+ out.push(plate(`typography.${prop}`, `${items.length} token(s)`, `<div class="rfp-rows">${rows}</div>`));
6257
+ }
6258
+ return out.join("");
6259
+ }
6260
+ // ── Space ──────────────────────────────────────────────────────────────────
6261
+ function renderSpace(groups, tokenName) {
6262
+ const out = [];
6263
+ for (const group of ["spacing", "gutters", "sizes", "aspectRatio"]) {
6264
+ const leaves = groups.get(group);
6265
+ if (!(leaves === null || leaves === void 0 ? void 0 : leaves.length))
6266
+ continue;
6267
+ if (group === "aspectRatio") {
6268
+ const tiles = leaves
6269
+ .map(leaf => {
6270
+ const path = leafPath(leaf);
6271
+ const ratio = String(leaf.value);
6272
+ return tile(`<div class="rfp-obj" style="width:84px;aspect-ratio:${cssValue(ratio)};height:auto;border-radius:4px"></div>`, idButton(path, tokenName === null || tokenName === void 0 ? void 0 : tokenName(path)), ratio);
6273
+ })
6274
+ .join("");
6275
+ out.push(plate(`layout.${group}`, `${leaves.length} token(s)`, `<div class="rfp-tiles">${tiles}</div>`));
6276
+ continue;
6277
+ }
6278
+ const max = Math.max(...leaves.map(l => { var _a; return (_a = pxOf(l.value)) !== null && _a !== void 0 ? _a : 0; }), 1);
6279
+ const rows = leaves
6280
+ .map(leaf => {
6281
+ const path = leafPath(leaf);
6282
+ const px = pxOf(leaf.value);
6283
+ const width = px === undefined ? "100%" : `${Math.max(1, (px / max) * 100)}%`;
6284
+ return rowOf(idButton(path, tokenName === null || tokenName === void 0 ? void 0 : tokenName(path)), `<div class="rfp-bar${group === "spacing" ? "" : " rfp-ghost"}" style="width:${width}"></div>`, String(leaf.value), true);
6285
+ })
6286
+ .join("");
6287
+ out.push(plate(`layout.${group}`, `${leaves.length} steps · measure`, `<div class="rfp-rows">${rows}</div>`));
6288
+ // Spacing gets two applied views on top of the measure: there is no `padding` token, so this is
6289
+ // the only place a reader can see what a step feels like as an inset or a gap.
6290
+ if (group === "spacing") {
6291
+ const applied = leaves.filter(l => { var _a; return ((_a = pxOf(l.value)) !== null && _a !== void 0 ? _a : 0) > 0; }).slice(0, 6);
6292
+ if (applied.length) {
6293
+ const insets = applied
6294
+ .map(leaf => {
6295
+ const path = leafPath(leaf);
6296
+ return (`<div class="rfp-tile"><div class="rfp-inset" style="padding:${cssValue(leaf.value)}">` +
6297
+ `<div class="rfp-inset-core">${esc(String(leaf.value))}</div></div>` +
6298
+ `<div>${idButton(path, tokenName === null || tokenName === void 0 ? void 0 : tokenName(path))}</div></div>`);
6299
+ })
6300
+ .join("");
6301
+ out.push(plate(`layout.${group}`, "applied as padding — hatching is the inset, solid is the content box", `<div class="rfp-tiles">${insets}</div>`));
6302
+ const gaps = applied
6303
+ .slice(0, 4)
6304
+ .map(leaf => rowOf(idButton(leafPath(leaf), tokenName === null || tokenName === void 0 ? void 0 : tokenName(leafPath(leaf))), `<div class="rfp-gap" style="gap:${cssValue(leaf.value)}"><i></i><i></i><i></i><i></i></div>`, String(leaf.value), true))
6305
+ .join("");
6306
+ out.push(plate(`layout.${group}`, "applied as gap", `<div class="rfp-rows">${gaps}</div>`));
6307
+ }
6308
+ }
6153
6309
  }
6154
- return sections.join("\n");
6310
+ return out.join("");
6155
6311
  }
6312
+ // ── Shape & depth ──────────────────────────────────────────────────────────
6313
+ function renderShape(groups, tokenName) {
6314
+ const out = [];
6315
+ const tilesFor = (group, build, sub) => {
6316
+ const leaves = groups.get(group);
6317
+ if (!(leaves === null || leaves === void 0 ? void 0 : leaves.length))
6318
+ return;
6319
+ const tiles = leaves
6320
+ .map(leaf => tile(build(leaf), idButton(leafPath(leaf), tokenName === null || tokenName === void 0 ? void 0 : tokenName(leafPath(leaf))), String(leaf.value)))
6321
+ .join("");
6322
+ out.push(plate(group, sub !== null && sub !== void 0 ? sub : `${leaves.length} token(s)`, `<div class="rfp-tiles">${tiles}</div>`));
6323
+ };
6324
+ tilesFor("radius", l => `<div class="rfp-obj" style="border-radius:${cssValue(l.value)}"></div>`);
6325
+ // The stroke is the subject here, so the box must NOT be filled.
6326
+ tilesFor("borderWidth", l => `<div class="rfp-obj rfp-outlined" style="border:${cssValue(l.value)} solid currentColor;border-radius:8px"></div>`);
6327
+ tilesFor("borderStyle", l => `<div class="rfp-obj rfp-outlined" style="border:3px ${cssValue(l.value)} currentColor;border-radius:8px"></div>`);
6328
+ tilesFor("outlineOffset", l => `<div class="rfp-obj rfp-outlined" style="border-radius:8px;outline:2px solid currentColor;outline-offset:${cssValue(l.value)}"></div>`);
6329
+ tilesFor("shadow", l => `<div class="rfp-obj rfp-raised" style="border-radius:8px;box-shadow:${cssValue(l.value)}"></div>`);
6330
+ // Blur and opacity need a saturated fill: a mid-grey at .38 is indistinguishable from the stage.
6331
+ tilesFor("blur", l => `<div class="rfp-obj rfp-accent" style="border-radius:8px;filter:blur(${cssValue(l.value)})"></div>`);
6332
+ tilesFor("opacity", l => `<div class="rfp-obj rfp-accent" style="border-radius:8px;opacity:${cssValue(l.value)}"></div>`);
6333
+ tilesFor("zIndex", l => `<span class="rfp-numeral">${esc(String(l.value))}</span>`);
6334
+ return out.join("");
6335
+ }
6336
+ // ── Motion ─────────────────────────────────────────────────────────────────
6337
+ function renderMotion(leaves, tokenName) {
6338
+ const rows = leaves
6339
+ .map(leaf => {
6340
+ const path = leafPath(leaf);
6341
+ const value = String(leaf.value);
6342
+ return rowOf(idButton(path, tokenName === null || tokenName === void 0 ? void 0 : tokenName(path)), `<div class="rfp-track"><i class="rfp-dot" style="transition:transform ${cssValue(value)}"></i></div>`, value, true);
6343
+ })
6344
+ .join("");
6345
+ return plate("effects.transitions", `${leaves.length} token(s)`, `<div class="rfp-rows">${rows}</div>`, `<button class="rfp-play" type="button" id="rfp-play">Play</button>`);
6346
+ }
6347
+ // ── Generic fallback (unknown groups keep their tokens visible) ─────────────
6348
+ function renderGeneric(group, leaves, tokenName) {
6349
+ const rows = leaves
6350
+ .map(leaf => rowOf(idButton(leafPath(leaf), tokenName === null || tokenName === void 0 ? void 0 : tokenName(leafPath(leaf))), `<div class="rfp-specimen">${esc(leafLabel(leaf))}</div>`, String(leaf.value)))
6351
+ .join("");
6352
+ return plate(group, `${leaves.length} token(s)`, `<div class="rfp-rows">${rows}</div>`);
6353
+ }
6354
+ // ── Shared bits ────────────────────────────────────────────────────────────
6355
+ const plate = (name, sub, body, action = "") => `<section class="rfp-plate"><div class="rfp-plate-head">` +
6356
+ `<span class="rfp-plate-name">${esc(name)}</span><span class="rfp-plate-sub">${sub}</span>${action}` +
6357
+ `</div>${body}</section>`;
6358
+ const tile = (stage, id, value) => `<div class="rfp-tile"><div class="rfp-stage">${stage}</div>` +
6359
+ `<div>${id}<span class="rfp-hex">${esc(value)}</span></div></div>`;
6156
6360
  // ---------------------------------------------------------------------------
6157
- // Recipe plates
6361
+ // Model-derived plates: modes, base elements
6158
6362
  // ---------------------------------------------------------------------------
6363
+ /** Distinct appearance modes across every property's `modes` list, in first-appearance order. */
6364
+ function collectModes(model) {
6365
+ var _a, _b;
6366
+ const modes = [];
6367
+ for (const subsystem of Object.values(model.subsystems)) {
6368
+ for (const property of Object.values((_a = subsystem.properties) !== null && _a !== void 0 ? _a : {})) {
6369
+ for (const override of (_b = property.modes) !== null && _b !== void 0 ? _b : []) {
6370
+ if (override.mode && !modes.includes(override.mode))
6371
+ modes.push(override.mode);
6372
+ }
6373
+ }
6374
+ }
6375
+ return modes;
6376
+ }
6377
+ /** A literal `Ref` reads straight off `value`; a derived/aliased one has to be skipped honestly. */
6378
+ const literalOf = (ref) => {
6379
+ if (!ref || ref.value === undefined || ref.value === null)
6380
+ return undefined;
6381
+ if (typeof ref.value === "object")
6382
+ return undefined;
6383
+ return String(ref.value);
6384
+ };
6159
6385
  /**
6160
- * Which element to render a recipe as, when the adapter didn't say. Presentation guesswork belongs
6161
- * here rather than in an adapter: it's about the specimen page, not about the output format.
6386
+ * Which tokens actually carry a mode override. The toggle shows the *result*; this shows the
6387
+ * *cause* and the tokens that DIDN'T override are usually the surprise worth catching.
6388
+ * Derived (non-literal) overrides are counted but not tabled, since there's no honest value to show.
6162
6389
  */
6390
+ function collectModeChanges(model) {
6391
+ var _a;
6392
+ const changes = [];
6393
+ let skipped = 0;
6394
+ const walk = (subsystem, name, property) => {
6395
+ var _a, _b, _c, _d, _e, _f, _g;
6396
+ for (const override of (_a = property.modes) !== null && _a !== void 0 ? _a : []) {
6397
+ if (!override.mode)
6398
+ continue;
6399
+ for (const [field, ref] of Object.entries((_b = override.overrides) !== null && _b !== void 0 ? _b : {})) {
6400
+ const to = literalOf(ref);
6401
+ const suffix = [override.target, field === "base" ? undefined : field].filter(Boolean).join(".");
6402
+ const path = `${subsystem}.${name}${suffix ? `.${suffix}` : ""}`;
6403
+ if (to === undefined) {
6404
+ skipped += 1;
6405
+ continue;
6406
+ }
6407
+ const from = field === "base" && !override.target
6408
+ ? literalOf(property.base)
6409
+ : (_d = literalOf((_c = property.extras) === null || _c === void 0 ? void 0 : _c[field])) !== null && _d !== void 0 ? _d : literalOf((_g = (_e = property.variants) === null || _e === void 0 ? void 0 : _e[(_f = override.target) !== null && _f !== void 0 ? _f : ""]) === null || _g === void 0 ? void 0 : _g.base);
6410
+ changes.push({ path, mode: override.mode, from, to });
6411
+ }
6412
+ }
6413
+ };
6414
+ for (const [subsystem, sub] of Object.entries(model.subsystems)) {
6415
+ for (const [name, property] of Object.entries((_a = sub.properties) !== null && _a !== void 0 ? _a : {}))
6416
+ walk(subsystem, name, property);
6417
+ }
6418
+ return { changes, skipped };
6419
+ }
6420
+ function renderModes(model, totalTokens, tokenName) {
6421
+ const modes = collectModes(model);
6422
+ if (!modes.length)
6423
+ return "";
6424
+ const { changes, skipped } = collectModeChanges(model);
6425
+ if (!changes.length && !skipped)
6426
+ return "";
6427
+ const rows = changes
6428
+ .map(c => `<tr><td>${idButton(c.path, tokenName === null || tokenName === void 0 ? void 0 : tokenName(c.path))}</td>` +
6429
+ `<td>${swatchCell(c.from)}</td><td class="rfp-arrow">&rarr;</td><td>${swatchCell(c.to)}</td></tr>`)
6430
+ .join("");
6431
+ const note = skipped
6432
+ ? `<p class="rfp-note-sm">${skipped} further override(s) resolve through a derivation, so they carry no single literal to show here.</p>`
6433
+ : "";
6434
+ return (`<section class="rfp-section" id="rfp-modes"><div class="rfp-section-head"><div>` +
6435
+ `<div class="rfp-eyebrow">Appearance</div><h2>What changes in ${esc(modes.join(" / "))}</h2></div>` +
6436
+ `<span class="rfp-tag">${modes.length} mode(s) declared</span></div>` +
6437
+ `<p class="rfp-note">Flipping the toggle shows you the result; this shows the cause. Only these tokens carry an override — everything else is inherited.</p>` +
6438
+ plate(`mode: ${modes.join(", ")}`, `${changes.length} of ${totalTokens} tokens overridden`, `<div class="rfp-scroll"><table class="rfp-diff"><thead><tr><th>Token</th><th>Base</th><th></th><th>Override</th></tr></thead>` +
6439
+ `<tbody>${rows}</tbody></table></div>${note}`) +
6440
+ `</section>`);
6441
+ }
6442
+ const swatchCell = (value) => {
6443
+ if (value === undefined)
6444
+ return `<span class="rfp-hex">—</span>`;
6445
+ const isColor = /^(#|rgb|hsl|oklch|color\()/i.test(value.trim());
6446
+ return (`<span class="rfp-swatch-cell">` +
6447
+ (isColor ? `<i class="rfp-chip" style="background:${cssValue(value)}"></i>` : "") +
6448
+ `<span class="rfp-hex">${esc(value)}</span></span>`);
6449
+ };
6450
+ /**
6451
+ * The `globals` subsystem themes bare elements — no class involved — so a prose specimen is the only
6452
+ * way to show them. The selectors come from the model; the emitted stylesheet does the styling.
6453
+ */
6454
+ function renderGlobals(model, live) {
6455
+ var _a, _b;
6456
+ const groups = (_a = model.subsystems.globals) === null || _a === void 0 ? void 0 : _a.ruleSets;
6457
+ if (!groups || !live)
6458
+ return "";
6459
+ const selectors = Object.keys((_b = groups.elements) !== null && _b !== void 0 ? _b : {});
6460
+ if (!selectors.length)
6461
+ return "";
6462
+ const has = (sel) => selectors.some(s => s === sel || s.startsWith(`${sel}.`) || s.startsWith(`${sel}:`));
6463
+ const parts = [];
6464
+ if (has("h1"))
6465
+ parts.push(`<h1>Shipping a theme</h1>`);
6466
+ parts.push(`<p>A theme compiles once and lowers to every format you target — the same source produces CSS custom ` +
6467
+ `properties, Sass partials, a JSON document, or styled-components modules` +
6468
+ (has("a") ? ` <a href="#rfp-recipes">without re-authoring a single value</a>` : "") +
6469
+ `.</p>`);
6470
+ if (has("h2"))
6471
+ parts.push(`<h2>Why bare elements matter</h2>`);
6472
+ parts.push(`<p>Content you don't control — a CMS body field, rendered markdown, a third-party embed — arrives ` +
6473
+ `without your class names. Theming the elements themselves is what keeps it consistent.</p>`);
6474
+ if (has("ul")) {
6475
+ parts.push(`<ul><li>Headings inherit the type scale and its derived leading</li>` +
6476
+ `<li>Links pick up the brand colour and its underline treatment</li>` +
6477
+ `<li>Variants emit only the delta from their base rule</li></ul>`);
6478
+ }
6479
+ if (has("blockquote")) {
6480
+ parts.push(`<blockquote>Structural devices should encode something true about the content, not decorate it.</blockquote>`);
6481
+ }
6482
+ if (has("hr"))
6483
+ parts.push(`<hr>`);
6484
+ if (has("h3"))
6485
+ parts.push(`<h3>Variants are structural deltas</h3>`);
6486
+ return (`<section class="rfp-section" id="rfp-globals"><div class="rfp-section-head"><div>` +
6487
+ `<div class="rfp-eyebrow">Base elements</div><h2>Unclassed markup</h2></div>` +
6488
+ `<span class="rfp-tag">globals.elements</span></div>` +
6489
+ `<p class="rfp-note">These style bare elements with no class involved, so plain HTML from a CMS or a markdown ` +
6490
+ `pipeline already looks right. Nothing else in this document renders without a class.</p>` +
6491
+ plate("globals.elements", `${selectors.length} selector(s) · ${esc(selectors.join(", "))}`, `<div class="rfp-prose">${parts.join("")}</div>`) +
6492
+ `</section>`);
6493
+ }
6494
+ // ---------------------------------------------------------------------------
6495
+ // Recipes
6496
+ // ---------------------------------------------------------------------------
6163
6497
  function inferTag(recipe) {
6164
6498
  const group = recipe.group.toLowerCase();
6165
6499
  if (group.includes("button") || group.includes("btn"))
6166
6500
  return "button";
6501
+ if (group.includes("input") || group.includes("field"))
6502
+ return "input";
6167
6503
  if (group.includes("link") || group.includes("anchor"))
6168
6504
  return "a";
6169
- if (group.includes("badge") || group.includes("chip") || group.includes("tag") || group.includes("label")) {
6505
+ if (group.includes("badge") || group.includes("chip") || group.includes("tag") || group.includes("label"))
6170
6506
  return "span";
6171
- }
6172
6507
  return "div";
6173
6508
  }
6174
- /** `<tag attr="…">Label</tag>` for one recipe, or `undefined` when the adapter offers no markup. */
6175
- function renderRecipeSpecimen(recipe, descriptor) {
6176
- var _a, _b;
6509
+ /** One rendered specimen. `pin` adds the adapter's state-pinning class so a state can be shown at rest. */
6510
+ function specimen(recipe, descriptor, pin) {
6511
+ var _a, _b, _c;
6177
6512
  const markup = (_a = descriptor === null || descriptor === void 0 ? void 0 : descriptor.markup) === null || _a === void 0 ? void 0 : _a.call(descriptor, recipe);
6178
6513
  if (!markup)
6179
6514
  return undefined;
6180
6515
  const tag = (_b = markup.tag) !== null && _b !== void 0 ? _b : inferTag(recipe);
6181
- const attrs = Object.entries(markup.attrs)
6182
- .map(([key, value]) => ` ${escapeHtml(key)}="${escapeHtml(value)}"`)
6516
+ const attrs = { ...markup.attrs };
6517
+ if (pin)
6518
+ attrs.class = `${(_c = attrs.class) !== null && _c !== void 0 ? _c : ""} ${pin}`.trim();
6519
+ const rendered = Object.entries(attrs)
6520
+ .map(([k, v]) => ` ${esc(k)}="${esc(v)}"`)
6183
6521
  .join("");
6184
- return `<${tag}${attrs}>${escapeHtml(recipe.variant)}</${tag}>`;
6522
+ if (tag === "input")
6523
+ return `<input${rendered} value="${esc(recipe.variant)}" readonly aria-label="${esc(recipe.variant)}">`;
6524
+ return `<${tag}${rendered}>${esc(recipe.variant)}</${tag}>`;
6185
6525
  }
6186
- function renderRecipePlates(usage, descriptor, live) {
6526
+ function renderRecipes(usage, descriptor, live) {
6187
6527
  var _a, _b;
6188
6528
  if (usage.recipes.length === 0) {
6189
6529
  // The scaffolder writes tokens and no recipes, so this is the FIRST thing a new user sees here.
6190
- // Say what's missing and where to go, rather than showing an empty box.
6191
- return (`<p class="rfp-note"><strong>No recipes yet — this theme is tokens only.</strong> ` +
6192
- `Tokens are values; recipes are the rule-sets that turn them into styled components ` +
6193
- `(a button, a card). Add a <code>recipes</code> block to a subsystem in your raw theme and ` +
6194
- `rebuild — they'll render here.</p>`);
6530
+ return (`<div class="rfp-notice"><span class="rfp-notice-mark">Empty</span><div>` +
6531
+ `<p><strong>No recipes yet — this theme is tokens only.</strong> Tokens are values; recipes are the ` +
6532
+ `rule-sets that turn them into styled components. Add a <code>recipes</code> block to a subsystem in ` +
6533
+ `your raw theme and rebuild — they'll render here.</p></div></div>`);
6195
6534
  }
6535
+ // Group for layout: the adapter decides (subsystem, component file, …); default is by group.
6196
6536
  const groups = new Map();
6197
6537
  for (const recipe of usage.recipes) {
6198
- const key = (_b = (_a = descriptor === null || descriptor === void 0 ? void 0 : descriptor.groupBy) === null || _a === void 0 ? void 0 : _a.call(descriptor, recipe)) !== null && _b !== void 0 ? _b : "";
6538
+ const key = (_b = (_a = descriptor === null || descriptor === void 0 ? void 0 : descriptor.groupBy) === null || _a === void 0 ? void 0 : _a.call(descriptor, recipe)) !== null && _b !== void 0 ? _b : `${recipe.subsystem}.${recipe.group}`;
6199
6539
  const bucket = groups.get(key);
6200
6540
  if (bucket)
6201
6541
  bucket.push(recipe);
6202
6542
  else
6203
6543
  groups.set(key, [recipe]);
6204
6544
  }
6205
- const sections = [];
6545
+ const out = [];
6206
6546
  for (const [group, recipes] of groups) {
6207
- const items = recipes
6547
+ // A state matrix beats one specimen per card: states are the whole point of a component sheet,
6548
+ // and side by side is the only way to tell whether hover and active are distinguishable.
6549
+ const states = live
6550
+ ? [...new Set(recipes.flatMap(r => { var _a, _b; return (_b = (_a = descriptor === null || descriptor === void 0 ? void 0 : descriptor.states) === null || _a === void 0 ? void 0 : _a.call(descriptor, r)) !== null && _b !== void 0 ? _b : []; }))].filter(s => { var _a; return (_a = descriptor === null || descriptor === void 0 ? void 0 : descriptor.statePinClass) === null || _a === void 0 ? void 0 : _a.call(descriptor, s); })
6551
+ : [];
6552
+ if (states.length && (descriptor === null || descriptor === void 0 ? void 0 : descriptor.markup)) {
6553
+ const head = ["base", ...states].map(s => `<th>${esc(s)}</th>`).join("");
6554
+ const body = recipes
6555
+ .map(recipe => {
6556
+ var _a, _b;
6557
+ const own = (_b = (_a = descriptor.states) === null || _a === void 0 ? void 0 : _a.call(descriptor, recipe)) !== null && _b !== void 0 ? _b : [];
6558
+ const cells = ["base", ...states]
6559
+ .map(state => {
6560
+ var _a, _b;
6561
+ if (state !== "base" && !own.includes(state))
6562
+ return `<td class="rfp-none">—</td>`;
6563
+ const pin = state === "base" ? undefined : (_a = descriptor.statePinClass) === null || _a === void 0 ? void 0 : _a.call(descriptor, state);
6564
+ return `<td>${(_b = specimen(recipe, descriptor, pin)) !== null && _b !== void 0 ? _b : ""}</td>`;
6565
+ })
6566
+ .join("");
6567
+ return (`<tr><td><span class="rfp-addr">${esc(`${recipe.subsystem}.${recipe.group}.${recipe.variant}`)}</span>` +
6568
+ `<button class="rfp-id" type="button">${esc(recipe.name)}</button></td>${cells}</tr>`);
6569
+ })
6570
+ .join("");
6571
+ out.push(plate(group, `${recipes.length} variant(s) × ${states.length} state(s)`, `<div class="rfp-scroll"><table class="rfp-matrix"><thead><tr><th>Variant</th>${head}</tr></thead>` +
6572
+ `<tbody>${body}</tbody></table></div>`));
6573
+ out.push(renderComposition(recipes, descriptor));
6574
+ continue;
6575
+ }
6576
+ const cards = recipes
6208
6577
  .map(recipe => {
6209
- const specimen = live ? renderRecipeSpecimen(recipe, descriptor) : undefined;
6210
- const address = `${recipe.subsystem}.${recipe.group}.${recipe.variant}`;
6211
- return (`<li class="rfp-recipe">` +
6212
- (specimen ? `<div class="rfp-stage">${specimen}</div>` : "") +
6213
- `<div class="rfp-meta"><span class="rfp-name">${escapeHtml(address)}</span>` +
6214
- `<code class="rfp-val">${escapeHtml(recipe.name)}</code></div></li>`);
6578
+ const rendered = live ? specimen(recipe, descriptor) : undefined;
6579
+ return (`<div class="rfp-recipe">` +
6580
+ (rendered ? `<div class="rfp-recipe-stage">${rendered}</div>` : "") +
6581
+ `<div class="rfp-recipe-foot"><span class="rfp-addr">` +
6582
+ esc(`${recipe.subsystem}.${recipe.group}.${recipe.variant}`) +
6583
+ `</span><button class="rfp-id" type="button">${esc(recipe.name)}</button></div></div>`);
6215
6584
  })
6216
- .join("\n");
6217
- const title = group
6218
- ? `<h3 class="rfp-plate-title">${escapeHtml(group)} <span class="rfp-count">${recipes.length}</span></h3>\n`
6219
- : "";
6220
- sections.push(`<section class="rfp-plate">\n${title}<ul class="rfp-recipes">\n${items}\n</ul>\n</section>`);
6585
+ .join("");
6586
+ out.push(plate(group, `${recipes.length} variant(s)`, `<div class="rfp-recipes">${cards}</div>`));
6587
+ out.push(renderComposition(recipes, descriptor));
6221
6588
  }
6222
- return sections.join("\n");
6589
+ return out.join("");
6223
6590
  }
6224
- // ---------------------------------------------------------------------------
6225
- // Model-derived page controls
6226
- // ---------------------------------------------------------------------------
6227
- /** Distinct appearance modes across every property's `modes` list, in first-appearance order. */
6228
- function collectModes(model) {
6229
- var _a, _b;
6230
- const modes = [];
6231
- for (const subsystem of Object.values(model.subsystems)) {
6232
- for (const property of Object.values((_a = subsystem.properties) !== null && _a !== void 0 ? _a : {})) {
6233
- for (const override of (_b = property.modes) !== null && _b !== void 0 ? _b : []) {
6234
- if (override.mode && !modes.includes(override.mode))
6235
- modes.push(override.mode);
6236
- }
6237
- }
6591
+ /**
6592
+ * A composed recipe emits a class LIST, not one class — its referenced recipes plus its own delta.
6593
+ * Showing the string without saying why it's two classes is the kind of thing that reads as a bug.
6594
+ */
6595
+ function renderComposition(recipes, descriptor) {
6596
+ if (!(descriptor === null || descriptor === void 0 ? void 0 : descriptor.composition))
6597
+ return "";
6598
+ for (const recipe of recipes) {
6599
+ const parts = descriptor.composition(recipe);
6600
+ if (!parts || parts.length < 2)
6601
+ continue;
6602
+ const chips = parts
6603
+ .map((p, i) => (i ? `<span class="rfp-plus">+</span>` : "") +
6604
+ `<span class="rfp-cls"><b>${esc(p.className)}</b>` +
6605
+ `<span>${p.from ? ` · from ${esc(p.from)}` : " · own delta"}</span></span>`)
6606
+ .join("");
6607
+ return plate("Composition", `why <code>${esc(recipe.variant)}</code> carries ${parts.length} classes`, `<div class="rfp-compose">${chips}</div>` +
6608
+ `<p class="rfp-note-sm">Order matters — the delta lands last, so it wins on equal specificity.</p>`);
6238
6609
  }
6239
- return modes;
6610
+ return "";
6240
6611
  }
6241
6612
  // ---------------------------------------------------------------------------
6242
6613
  // Page chrome
6243
6614
  // ---------------------------------------------------------------------------
6244
6615
  /**
6245
- * The preview's own styling. Class-only selectors (`.rfp-*`), so the theme's `globals` element rules
6246
- * can never outrank it, and emitted LAST so an equal-specificity theme class still loses here. The
6247
- * document body is deliberately left to the theme — a themed page background is part of the specimen.
6616
+ * The preview's own styling. Two rules it exists to enforce:
6617
+ *
6618
+ * - **Class-only selectors**, all under `.rfp`, emitted AFTER the theme — so a themed `body`/`h1`
6619
+ * rule from the `globals` subsystem can never outrank the tool around it.
6620
+ * - **`--rfp-spec` for specimen geometry.** Filling a swatch with the page ground makes it vanish
6621
+ * once the ground and the stage converge, which is precisely what happens in dark mode; using
6622
+ * the ink instead makes light mode a wall of near-black blobs competing with the theme. A
6623
+ * dedicated mid-tone reads at the same weight on both grounds.
6248
6624
  */
6249
6625
  const CHROME_CSS = `
6250
- .rfp-root{font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;color:#111;background:#fff;
6251
- max-width:1100px;margin:0 auto;padding:24px;box-sizing:border-box}
6252
- .rfp-head{display:flex;flex-wrap:wrap;gap:12px;align-items:baseline;justify-content:space-between;
6253
- border-bottom:1px solid #e5e5e5;padding-bottom:12px;margin-bottom:20px}
6254
- .rfp-title{font-size:20px;font-weight:600;margin:0}
6255
- .rfp-sub{color:#666;font-size:12px}
6256
- .rfp-controls{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px}
6257
- .rfp-controls button{font:inherit;font-size:12px;padding:4px 10px;border:1px solid #ccc;border-radius:6px;
6258
- background:#fafafa;color:#111;cursor:pointer}
6259
- .rfp-controls button[aria-pressed="true"]{background:#111;border-color:#111;color:#fff}
6260
- .rfp-group-label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#888;align-self:center;
6261
- margin-right:4px}
6262
- .rfp-section-title{font-size:15px;font-weight:600;margin:28px 0 10px}
6263
- .rfp-plate{border:1px solid #e5e5e5;border-radius:8px;padding:14px 16px;margin-bottom:14px;background:#fff}
6264
- .rfp-plate-title{font-size:13px;font-weight:600;margin:0 0 10px;color:#333}
6265
- .rfp-count{color:#999;font-weight:400}
6266
- .rfp-rows,.rfp-recipes{list-style:none;margin:0;padding:0}
6267
- .rfp-row{display:grid;grid-template-columns:minmax(120px,220px) 1fr;gap:12px;align-items:center;
6268
- padding:5px 0;border-top:1px solid #f2f2f2}
6626
+ .rfp{--rfp-ground:#fff;--rfp-panel:#f7f8fa;--rfp-panel-2:#eef0f4;--rfp-ink:#14171c;--rfp-ink-2:#4d5563;
6627
+ --rfp-ink-3:#79818f;--rfp-rule:#e4e7ec;--rfp-rule-2:#d3d8e0;--rfp-focus:#3b4ea8;--rfp-live:#2f9e6d;
6628
+ --rfp-spec:#97a1b0;--rfp-spec-2:#c2c9d4;--rfp-hatch:#d7dce4;--rfp-stage:#f2f4f7;--rfp-raised:#fff;
6629
+ --rfp-accent:#6b7a99;
6630
+ --rfp-mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
6631
+ --rfp-ui:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
6632
+ font-family:var(--rfp-ui);font-size:14px;line-height:1.55;color:var(--rfp-ink);
6633
+ background:var(--rfp-ground);-webkit-font-smoothing:antialiased;
6634
+ display:grid;grid-template-columns:216px minmax(0,1fr);gap:56px;max-width:1260px;margin:0 auto;
6635
+ padding:0 32px 96px;align-items:start;box-sizing:border-box}
6636
+ @media (prefers-color-scheme:dark){.rfp{--rfp-ground:#0f1114;--rfp-panel:#1a1e24;--rfp-panel-2:#232830;
6637
+ --rfp-ink:#e9ebef;--rfp-ink-2:#a8b0bd;--rfp-ink-3:#7b8492;--rfp-rule:#2b313a;--rfp-rule-2:#3b434f;
6638
+ --rfp-focus:#8ea2ff;--rfp-live:#4ec48d;--rfp-spec:#8791a0;--rfp-spec-2:#4a5361;--rfp-hatch:#333b46;
6639
+ --rfp-stage:#14181d;--rfp-raised:#232830;--rfp-accent:#8e9dbb}}
6640
+ .rfp *{box-sizing:border-box}
6641
+ .rfp *:focus-visible{outline:2px solid var(--rfp-focus);outline-offset:2px;border-radius:3px}
6642
+ .rfp-rail{position:sticky;top:0;padding:32px 0;max-height:100vh;overflow-y:auto}
6643
+ .rfp-brand{font-family:var(--rfp-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;
6644
+ color:var(--rfp-ink-3);padding-bottom:14px;margin-bottom:14px;border-bottom:1px solid var(--rfp-rule)}
6645
+ .rfp-nav{display:flex;flex-direction:column;gap:1px}
6646
+ .rfp-nav a{display:flex;justify-content:space-between;align-items:baseline;gap:10px;padding:5px 8px;
6647
+ border-radius:4px;color:var(--rfp-ink-2);text-decoration:none;font-size:13px}
6648
+ .rfp-nav a:hover{background:var(--rfp-panel);color:var(--rfp-ink)}
6649
+ .rfp-nav a[aria-current="true"]{background:var(--rfp-ink);color:var(--rfp-ground)}
6650
+ .rfp-nav .rfp-n{font-family:var(--rfp-mono);font-size:11px;font-variant-numeric:tabular-nums;color:var(--rfp-ink-3)}
6651
+ .rfp-nav a[aria-current="true"] .rfp-n{color:var(--rfp-ground);opacity:.65}
6652
+ .rfp-main{padding-top:32px;min-width:0}
6653
+ .rfp-masthead{padding-bottom:28px;border-bottom:1px solid var(--rfp-rule)}
6654
+ .rfp-eyebrow{font-family:var(--rfp-mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--rfp-ink-3)}
6655
+ .rfp-masthead h1{font-size:34px;line-height:1.1;letter-spacing:-.025em;font-weight:640;margin:10px 0 0;text-wrap:balance}
6656
+ .rfp-meta{display:flex;flex-wrap:wrap;gap:6px 10px;margin-top:16px;font-family:var(--rfp-mono);font-size:11.5px;color:var(--rfp-ink-2)}
6657
+ .rfp-meta span{display:inline-flex;align-items:center;gap:6px}
6658
+ .rfp-meta span+span::before{content:"";width:1px;height:11px;background:var(--rfp-rule-2)}
6659
+ .rfp-meta b{font-weight:600;color:var(--rfp-ink)}
6660
+ .rfp-dot{width:6px;height:6px;border-radius:50%;background:var(--rfp-live)}
6661
+ .rfp-controls{display:flex;flex-wrap:wrap;gap:24px;padding:18px 0;border-bottom:1px solid var(--rfp-rule);
6662
+ position:sticky;top:0;background:var(--rfp-ground);z-index:5}
6663
+ .rfp-ctl{display:flex;align-items:center;gap:8px}
6664
+ .rfp-ctl-label{font-family:var(--rfp-mono);font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--rfp-ink-3)}
6665
+ .rfp-seg{display:flex;border:1px solid var(--rfp-rule-2);border-radius:6px;overflow:hidden}
6666
+ .rfp-seg button{font:inherit;font-family:var(--rfp-mono);font-size:11.5px;padding:4px 10px;border:0;
6667
+ border-left:1px solid var(--rfp-rule-2);background:var(--rfp-ground);color:var(--rfp-ink-2);cursor:pointer}
6668
+ .rfp-seg button:first-child{border-left:0}
6669
+ .rfp-seg button:hover{background:var(--rfp-panel);color:var(--rfp-ink)}
6670
+ .rfp-seg button[aria-pressed="true"]{background:var(--rfp-ink);color:var(--rfp-ground)}
6671
+ .rfp-section{padding-top:52px;scroll-margin-top:72px}
6672
+ .rfp-section-head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;flex-wrap:wrap}
6673
+ .rfp-section-head h2{font-size:21px;letter-spacing:-.018em;font-weight:620;margin:6px 0 0}
6674
+ .rfp-note{color:var(--rfp-ink-2);max-width:64ch;margin:8px 0 0;font-size:13.5px}
6675
+ .rfp-note-sm{color:var(--rfp-ink-3);font-size:12.5px;margin:10px 0 0;max-width:64ch}
6676
+ .rfp-tag{font-family:var(--rfp-mono);font-size:11px;color:var(--rfp-ink-3);border:1px solid var(--rfp-rule-2);border-radius:3px;padding:1px 6px}
6677
+ .rfp-plate{margin-top:22px;border-top:1px solid var(--rfp-rule);padding-top:18px}
6678
+ .rfp-plate-head{display:flex;align-items:baseline;gap:10px;margin-bottom:14px;flex-wrap:wrap}
6679
+ .rfp-plate-name{font-family:var(--rfp-mono);font-size:12.5px;font-weight:600;color:var(--rfp-ink)}
6680
+ .rfp-plate-sub{font-family:var(--rfp-mono);font-size:11px;color:var(--rfp-ink-3)}
6681
+ .rfp-id{font-family:var(--rfp-mono);font-size:11.5px;color:var(--rfp-ink-2);background:none;border:0;
6682
+ padding:1px 4px;margin-left:-4px;border-radius:3px;cursor:copy;text-align:left;display:inline-block}
6683
+ .rfp-id:hover{background:var(--rfp-panel-2);color:var(--rfp-ink)}
6684
+ .rfp-id.rfp-copied{background:var(--rfp-ink);color:var(--rfp-ground)}
6685
+ .rfp-var{font-family:var(--rfp-mono);font-size:10.5px;color:var(--rfp-ink-3);display:block;margin-top:1px}
6686
+ .rfp-hex{font-family:var(--rfp-mono);font-size:11px;color:var(--rfp-ink-3);font-variant-numeric:tabular-nums;display:block;margin-top:1px}
6687
+ .rfp-ladder{display:grid;grid-template-columns:150px minmax(0,1fr);gap:20px;align-items:start}
6688
+ .rfp-ladder+.rfp-ladder{margin-top:18px}
6689
+ .rfp-ladder-name{font-family:var(--rfp-mono);font-size:12.5px;font-weight:600}
6690
+ .rfp-ladder-base{font-family:var(--rfp-mono);font-size:11px;color:var(--rfp-ink-3);margin-top:2px}
6691
+ .rfp-rungs{display:flex;border-radius:6px;overflow:hidden;border:1px solid var(--rfp-rule)}
6692
+ .rfp-rung{flex:1 1 0;min-width:0}
6693
+ .rfp-rung-chip{height:52px}
6694
+ .rfp-rung[data-base="true"] .rfp-rung-foot{color:var(--rfp-ink);font-weight:600}
6695
+ .rfp-rung[data-base="true"] .rfp-rung-foot::before{content:"● "}
6696
+ .rfp-rung-foot{padding:5px 2px 6px;text-align:center;background:var(--rfp-panel);border-top:1px solid var(--rfp-rule);
6697
+ font-family:var(--rfp-mono);font-size:10px;font-variant-numeric:tabular-nums;color:var(--rfp-ink-2)}
6698
+ .rfp-pairs{display:grid;grid-template-columns:repeat(auto-fill,minmax(184px,1fr));gap:12px}
6699
+ .rfp-pair{border:1px solid var(--rfp-rule);border-radius:6px;overflow:hidden}
6700
+ .rfp-pair-swatch{padding:16px 14px 14px;display:flex;flex-direction:column;gap:10px;min-height:92px}
6701
+ .rfp-pair-sample{font-size:14px;font-weight:600}
6702
+ .rfp-pair-ratio{align-self:flex-start;font-family:var(--rfp-mono);font-size:10.5px;font-variant-numeric:tabular-nums;
6703
+ padding:1px 6px;border-radius:3px;border:1px solid currentColor}
6704
+ .rfp-pair-foot{padding:8px 12px;background:var(--rfp-panel);border-top:1px solid var(--rfp-rule)}
6705
+ .rfp-rows{display:flex;flex-direction:column}
6706
+ .rfp-row{display:grid;grid-template-columns:190px minmax(0,1fr) 108px;gap:20px;align-items:baseline;
6707
+ padding:11px 0;border-top:1px solid var(--rfp-rule)}
6269
6708
  .rfp-row:first-child{border-top:0}
6270
- .rfp-name{font-size:12px;color:#444;overflow-wrap:anywhere}
6271
- .rfp-swatch{display:flex;align-items:center;gap:10px;min-width:0;overflow:hidden}
6272
- .rfp-val{font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;color:#666;background:#f6f6f6;
6273
- padding:1px 5px;border-radius:4px;white-space:nowrap}
6274
- .rfp-chip{width:28px;height:28px;border-radius:5px;border:1px solid rgba(0,0,0,.15);flex:none}
6275
- .rfp-box{width:44px;height:28px;border-radius:5px;background:#fff;border:1px solid rgba(0,0,0,.08);flex:none}
6276
- .rfp-bar{height:10px;background:#111;border-radius:2px;flex:none;max-width:100%}
6277
- .rfp-specimen{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
6278
- .rfp-recipes{display:flex;flex-wrap:wrap;gap:14px}
6279
- .rfp-recipe{border:1px solid #eee;border-radius:8px;padding:12px;min-width:160px;background:#fff}
6280
- .rfp-stage{display:flex;align-items:center;justify-content:center;min-height:56px;margin-bottom:8px}
6281
- .rfp-meta{display:flex;flex-direction:column;gap:3px;align-items:flex-start}
6282
- .rfp-note{font-size:13px;color:#555;background:#f8f8f8;border:1px solid #ececec;border-left:3px solid #bbb;
6283
- border-radius:0 6px 6px 0;padding:10px 14px;margin:0 0 14px}
6284
- .rfp-frame{margin:0 auto;transition:max-width .15s ease}
6285
- .rfp-files{font-size:12px;color:#666;margin:0 0 16px}
6286
- .rfp-files code{font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace}
6287
- @media (prefers-color-scheme:dark){
6288
- .rfp-root{color:#eee;background:#151515}
6289
- .rfp-head{border-bottom-color:#2c2c2c}
6290
- .rfp-plate,.rfp-recipe{background:#1c1c1c;border-color:#2c2c2c}
6291
- .rfp-plate-title{color:#ddd}.rfp-name{color:#bbb}.rfp-sub,.rfp-files{color:#999}
6292
- .rfp-val{background:#262626;color:#aaa}
6293
- .rfp-controls button{background:#222;border-color:#3a3a3a;color:#eee}
6294
- .rfp-controls button[aria-pressed="true"]{background:#eee;border-color:#eee;color:#111}
6295
- .rfp-note{background:#1d1d1d;border-color:#2c2c2c;border-left-color:#555;color:#bbb}
6296
- .rfp-row{border-top-color:#242424}
6297
- .rfp-bar{background:#eee}
6709
+ .rfp-row.rfp-centred{align-items:center}
6710
+ .rfp-rowval{font-family:var(--rfp-mono);font-size:11px;color:var(--rfp-ink-3);font-variant-numeric:tabular-nums;text-align:right;overflow-wrap:anywhere}
6711
+ .rfp-specimen{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:-.015em}
6712
+ .rfp-leading{font-size:13px;color:var(--rfp-ink-2);max-width:52ch}
6713
+ .rfp-bar{height:12px;background:var(--rfp-spec);border-radius:2px;max-width:100%}
6714
+ .rfp-bar.rfp-ghost{background:var(--rfp-spec-2)}
6715
+ .rfp-gap{display:flex;align-items:stretch;border:1px dashed var(--rfp-rule-2);border-radius:6px;padding:10px}
6716
+ .rfp-gap>i{flex:1 1 0;height:30px;background:var(--rfp-spec-2);border-radius:3px}
6717
+ .rfp-inset{border:1px solid var(--rfp-rule);border-radius:6px;
6718
+ background:repeating-linear-gradient(-45deg,var(--rfp-hatch) 0 4px,transparent 4px 8px)}
6719
+ .rfp-inset-core{background:var(--rfp-spec);color:var(--rfp-ground);border-radius:3px;font-family:var(--rfp-mono);
6720
+ font-size:10.5px;font-weight:600;text-align:center;padding:8px 4px}
6721
+ .rfp-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px}
6722
+ .rfp-tile{display:flex;flex-direction:column;gap:10px}
6723
+ .rfp-stage{height:82px;display:grid;place-items:center;background:var(--rfp-stage);border-radius:6px;
6724
+ border:1px solid var(--rfp-rule);color:var(--rfp-spec)}
6725
+ .rfp-obj{width:62px;height:62px;background:var(--rfp-spec)}
6726
+ .rfp-obj.rfp-outlined{background:transparent}
6727
+ .rfp-obj.rfp-raised{background:var(--rfp-raised)}
6728
+ .rfp-obj.rfp-accent{background:var(--rfp-accent)}
6729
+ .rfp-numeral{font-family:var(--rfp-mono);font-size:20px;font-variant-numeric:tabular-nums;color:var(--rfp-ink-2)}
6730
+ .rfp-track{height:34px;border-radius:6px;background:var(--rfp-stage);border:1px solid var(--rfp-rule);position:relative;overflow:hidden}
6731
+ .rfp-dot{position:absolute;top:6px;left:6px;width:22px;height:22px;border-radius:5px;background:var(--rfp-spec)}
6732
+ .rfp-play{font:inherit;font-family:var(--rfp-mono);font-size:11.5px;padding:3px 10px;border-radius:4px;
6733
+ border:1px solid var(--rfp-rule-2);background:var(--rfp-panel);color:var(--rfp-ink);cursor:pointer;margin-left:auto}
6734
+ .rfp-scroll{overflow-x:auto}
6735
+ .rfp-diff{width:100%;border-collapse:collapse;font-size:13px}
6736
+ .rfp-diff th{text-align:left;font-family:var(--rfp-mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
6737
+ color:var(--rfp-ink-3);font-weight:500;padding:0 12px 8px 0;white-space:nowrap}
6738
+ .rfp-diff td{padding:7px 12px 7px 0;border-top:1px solid var(--rfp-rule);vertical-align:middle}
6739
+ .rfp-swatch-cell{display:flex;align-items:center;gap:8px}
6740
+ .rfp-chip{width:18px;height:18px;border-radius:4px;border:1px solid var(--rfp-rule-2);flex:none}
6741
+ .rfp-arrow{color:var(--rfp-ink-3);font-family:var(--rfp-mono)}
6742
+ .rfp-prose{border:1px solid var(--rfp-rule);border-radius:6px;padding:26px 28px;overflow:hidden}
6743
+ .rfp-matrix{border-collapse:collapse;min-width:560px;width:100%}
6744
+ .rfp-matrix th{font-family:var(--rfp-mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
6745
+ color:var(--rfp-ink-3);font-weight:500;text-align:center;padding:0 8px 10px}
6746
+ .rfp-matrix th:first-child{text-align:left}
6747
+ .rfp-matrix td{padding:12px 8px;border-top:1px solid var(--rfp-rule);text-align:center}
6748
+ .rfp-matrix td:first-child{text-align:left;width:210px}
6749
+ .rfp-none{font-family:var(--rfp-mono);font-size:11px;color:var(--rfp-ink-3);opacity:.6}
6750
+ .rfp-recipes{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:14px}
6751
+ .rfp-recipe{border:1px solid var(--rfp-rule);border-radius:6px;overflow:hidden}
6752
+ .rfp-recipe-stage{min-height:96px;display:grid;place-items:center;padding:20px 16px;
6753
+ background:linear-gradient(45deg,var(--rfp-panel) 25%,transparent 25%,transparent 75%,var(--rfp-panel) 75%),
6754
+ linear-gradient(45deg,var(--rfp-panel) 25%,transparent 25%,transparent 75%,var(--rfp-panel) 75%);
6755
+ background-size:14px 14px;background-position:0 0,7px 7px}
6756
+ .rfp-recipe-foot{padding:9px 12px;border-top:1px solid var(--rfp-rule);background:var(--rfp-panel)}
6757
+ .rfp-addr{font-family:var(--rfp-mono);font-size:11px;color:var(--rfp-ink-3);display:block}
6758
+ .rfp-compose{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
6759
+ .rfp-cls{font-family:var(--rfp-mono);font-size:11.5px;padding:3px 8px;border-radius:4px;
6760
+ border:1px solid var(--rfp-rule-2);background:var(--rfp-panel)}
6761
+ .rfp-cls b{font-weight:600;color:var(--rfp-ink)}
6762
+ .rfp-cls span{color:var(--rfp-ink-3)}
6763
+ .rfp-plus{color:var(--rfp-ink-3);font-family:var(--rfp-mono)}
6764
+ .rfp-notice{display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:start;padding:14px 16px;
6765
+ border:1px solid var(--rfp-rule-2);border-radius:6px;background:var(--rfp-panel);margin-top:18px}
6766
+ .rfp-notice-mark{font-family:var(--rfp-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;
6767
+ padding:2px 6px;border-radius:3px;background:var(--rfp-ink);color:var(--rfp-ground);white-space:nowrap}
6768
+ .rfp-notice p{margin:0;color:var(--rfp-ink-2);font-size:13px;max-width:68ch}
6769
+ .rfp-notice p+p{margin-top:6px}
6770
+ .rfp-notice strong{color:var(--rfp-ink);font-weight:600}
6771
+ .rfp-frame{margin:0 auto}
6772
+ @media (prefers-reduced-motion:reduce){.rfp *{transition:none!important;animation:none!important}}
6773
+ @media (max-width:900px){
6774
+ .rfp{grid-template-columns:minmax(0,1fr);gap:0;padding:0 20px 72px}
6775
+ .rfp-rail{position:static;max-height:none;padding:24px 0 0}
6776
+ .rfp-nav{flex-direction:row;overflow-x:auto;gap:4px;padding-bottom:4px}
6777
+ .rfp-nav a{white-space:nowrap}
6778
+ .rfp-ladder{grid-template-columns:minmax(0,1fr);gap:10px}
6779
+ .rfp-row{grid-template-columns:130px minmax(0,1fr)}
6780
+ .rfp-rowval{grid-column:1/-1;text-align:left}
6298
6781
  }
6299
6782
  `.trim();
6300
- /** Mode + breakpoint controls. Plain DOM, no dependencies — the page must work from `file://`. */
6301
6783
  const CHROME_JS = `
6302
6784
  (function(){
6303
- var root=document.documentElement;
6304
- function bind(sel,onPick){
6305
- var btns=[].slice.call(document.querySelectorAll(sel));
6306
- btns.forEach(function(b){
6307
- b.addEventListener("click",function(){
6308
- btns.forEach(function(o){o.setAttribute("aria-pressed",String(o===b));});
6309
- onPick(b.getAttribute("data-value"));
6310
- });
6311
- });
6312
- }
6313
- bind("[data-rfp-mode]",function(v){
6314
- var attr=root.getAttribute("data-rfp-mode-attr");
6315
- if(!attr)return;
6316
- if(v)root.setAttribute(attr,v);else root.removeAttribute(attr);
6317
- });
6318
- bind("[data-rfp-width]",function(v){
6319
- var frame=document.getElementById("rfp-frame");
6320
- if(frame)frame.style.maxWidth=v?v+"px":"";
6321
- });
6785
+ var root=document.documentElement;
6786
+ function srgb(c){c/=255;return c<=0.03928?c/12.92:Math.pow((c+0.055)/1.055,2.4);}
6787
+ function lum(hex){var m=/^#([0-9a-f]{6})$/i.exec(hex.trim());if(!m)return null;var n=parseInt(m[1],16);
6788
+ return 0.2126*srgb((n>>16)&255)+0.7152*srgb((n>>8)&255)+0.0722*srgb(n&255);}
6789
+ // Contrast is computed here rather than baked in, so it can never drift from the swatch beside it.
6790
+ document.querySelectorAll(".rfp-pair-swatch[data-fg]").forEach(function(el){
6791
+ var out=el.querySelector(".rfp-pair-ratio");if(!out)return;
6792
+ var a=lum(el.getAttribute("data-bg")||""),b=lum(el.getAttribute("data-fg")||"");
6793
+ if(a===null||b===null){out.remove();return;}
6794
+ var r=(Math.max(a,b)+0.05)/(Math.min(a,b)+0.05);
6795
+ out.textContent=r.toFixed(2)+":1 · "+(r>=7?"AAA":r>=4.5?"AA":r>=3?"AA large":"fail");
6796
+ });
6797
+ function bind(sel,onPick){
6798
+ var btns=[].slice.call(document.querySelectorAll(sel));
6799
+ btns.forEach(function(b){b.addEventListener("click",function(){
6800
+ btns.forEach(function(o){o.setAttribute("aria-pressed",String(o===b));});
6801
+ onPick(b.getAttribute("data-value"));});});
6802
+ }
6803
+ bind("[data-rfp-mode]",function(v){
6804
+ var attr=root.getAttribute("data-rfp-mode-attr");if(!attr)return;
6805
+ if(v)root.setAttribute(attr,v);else root.removeAttribute(attr);});
6806
+ bind("[data-rfp-width]",function(v){
6807
+ var frame=document.getElementById("rfp-frame");if(frame)frame.style.maxWidth=v?v+"px":"";});
6808
+ var play=document.getElementById("rfp-play"),running=false;
6809
+ if(play)play.addEventListener("click",function(){
6810
+ running=!running;play.textContent=running?"Reset":"Play";
6811
+ document.querySelectorAll(".rfp-dot").forEach(function(d){
6812
+ d.style.transform=running?"translateX("+(d.parentElement.clientWidth-34)+"px)":"translateX(0)";});});
6813
+ document.addEventListener("click",function(e){
6814
+ var btn=e.target.closest?e.target.closest(".rfp-id"):null;if(!btn)return;
6815
+ var text=btn.textContent.trim();
6816
+ var done=function(){var prev=btn.textContent;btn.classList.add("rfp-copied");btn.textContent="copied";
6817
+ setTimeout(function(){btn.classList.remove("rfp-copied");btn.textContent=prev;},900);};
6818
+ if(navigator.clipboard){navigator.clipboard.writeText(text).then(done,done);}else{done();}});
6819
+ var links=[].slice.call(document.querySelectorAll(".rfp-nav a"));
6820
+ if(window.IntersectionObserver){
6821
+ var io=new IntersectionObserver(function(es){es.forEach(function(en){if(!en.isIntersecting)return;
6822
+ links.forEach(function(a){a.setAttribute("aria-current",String(a.getAttribute("href")==="#"+en.target.id));});});},
6823
+ {rootMargin:"-10% 0px -80% 0px"});
6824
+ document.querySelectorAll(".rfp-section").forEach(function(s){io.observe(s);});
6825
+ }
6322
6826
  })();
6323
6827
  `.trim();
6324
- function renderControls(modes, modeAttribute, breakpoints) {
6325
- const parts = [];
6326
- // A mode toggle is only offered when the adapter told us HOW to switch — otherwise the buttons
6327
- // would be decorative. "Auto" clears the attribute and hands control back to the OS media query.
6328
- if (modeAttribute && modes.length > 0) {
6329
- const buttons = [`<button type="button" data-rfp-mode data-value="" aria-pressed="true">auto</button>`]
6330
- .concat(modes.map(mode => `<button type="button" data-rfp-mode data-value="${escapeHtml(mode)}" aria-pressed="false">` +
6331
- `${escapeHtml(mode)}</button>`))
6332
- .join("");
6333
- parts.push(`<div class="rfp-controls"><span class="rfp-group-label">mode</span>${buttons}</div>`);
6334
- }
6335
- const widths = Object.entries(breakpoints);
6336
- if (widths.length > 0) {
6337
- const buttons = [`<button type="button" data-rfp-width data-value="" aria-pressed="true">full</button>`]
6338
- .concat(widths.map(([name, px]) => `<button type="button" data-rfp-width data-value="${escapeHtml(String(px))}" aria-pressed="false">` +
6339
- `${escapeHtml(name)} · ${escapeHtml(String(px))}px</button>`))
6340
- .join("");
6341
- parts.push(`<div class="rfp-controls"><span class="rfp-group-label">width</span>${buttons}</div>`);
6342
- }
6343
- return parts.join("\n");
6344
- }
6345
6828
  /** `<style>`/`<link>` tags for the emitted artifacts, in the adapter's declared load order. */
6346
6829
  function renderThemeLinks(stylesheets, contents, inline) {
6347
6830
  return stylesheets
6348
6831
  .map(name => {
6349
6832
  const body = inline ? contents === null || contents === void 0 ? void 0 : contents[name] : undefined;
6350
6833
  return body === undefined
6351
- ? `<link rel="stylesheet" href="./${escapeHtml(name)}">`
6352
- : `<style data-rfp-source="${escapeHtml(name)}">\n${escapeTextElement(body)}\n</style>`;
6834
+ ? `<link rel="stylesheet" href="./${esc(name)}">`
6835
+ : `<style data-rfp-source="${esc(name)}">\n${escapeTextElement(body)}\n</style>`;
6353
6836
  })
6354
6837
  .join("\n");
6355
6838
  }
6356
6839
  // ---------------------------------------------------------------------------
6357
6840
  // Entry point
6358
6841
  // ---------------------------------------------------------------------------
6359
- /**
6360
- * Build the preview artifact. Pure — returns `filename → contents` for the build layer to write.
6361
- *
6362
- * The adapter's declared `stylesheets` are intersected with `options.files` (what was actually
6363
- * written), so a descriptor that drifts from `emit()` degrades to tokens-only instead of producing a
6364
- * page that silently references a file that isn't there.
6365
- */
6366
6842
  function buildPreview(source, options) {
6367
- var _a, _b, _c, _d, _e, _f, _g;
6843
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
6368
6844
  const { usage, preview, tokens, model } = source;
6369
6845
  const fileName = (_a = options.file) !== null && _a !== void 0 ? _a : DEFAULT_FILE;
6370
6846
  const inline = (_b = options.inline) !== null && _b !== void 0 ? _b : true;
6371
6847
  const written = new Set(options.files);
6372
6848
  const stylesheets = ((_c = preview === null || preview === void 0 ? void 0 : preview.stylesheets) !== null && _c !== void 0 ? _c : []).filter(name => written.has(name));
6373
6849
  const live = stylesheets.length > 0;
6374
- const modes = collectModes(model);
6375
- const breakpoints = (_d = model.breakpoints) !== null && _d !== void 0 ? _d : {};
6376
- const title = (_e = options.title) !== null && _e !== void 0 ? _e : `${usage.format} theme preview`;
6850
+ const tokenName = preview === null || preview === void 0 ? void 0 : preview.tokenName;
6851
+ const leaves = [];
6852
+ collectLeaves(tokens, [], undefined, leaves);
6853
+ const groups = groupLeaves(leaves);
6854
+ // Build each section from the groups that map to it; a section with no content is omitted
6855
+ // entirely, so a theme without shadows shows no elevation plate rather than an empty box.
6856
+ const bodies = new Map();
6857
+ const counts = new Map();
6858
+ for (const [group, items] of groups) {
6859
+ const section = (_d = SECTION_OF[group]) !== null && _d !== void 0 ? _d : "other";
6860
+ counts.set(section, ((_e = counts.get(section)) !== null && _e !== void 0 ? _e : 0) + items.length);
6861
+ }
6862
+ for (const section of SECTIONS) {
6863
+ const own = [...groups.entries()].filter(([g]) => { var _a; return ((_a = SECTION_OF[g]) !== null && _a !== void 0 ? _a : "other") === section.id; });
6864
+ if (!own.length)
6865
+ continue;
6866
+ let body = "";
6867
+ if (section.id === "palette")
6868
+ body = renderPalette(own.flatMap(([, l]) => l), tokenName);
6869
+ else if (section.id === "type")
6870
+ body = renderTypography(own.flatMap(([, l]) => l), tokenName);
6871
+ else if (section.id === "space")
6872
+ body = renderSpace(new Map(own), tokenName);
6873
+ else if (section.id === "shape")
6874
+ body = renderShape(new Map(own), tokenName);
6875
+ else if (section.id === "motion")
6876
+ body = renderMotion(own.flatMap(([, l]) => l), tokenName);
6877
+ else
6878
+ body = own.map(([g, l]) => renderGeneric(g, l, tokenName)).join("");
6879
+ if (body)
6880
+ bodies.set(section.id, body);
6881
+ }
6882
+ const modesHtml = renderModes(model, leaves.length, tokenName);
6883
+ const globalsHtml = renderGlobals(model, live);
6377
6884
  const notes = [];
6378
6885
  if (!live) {
6379
6886
  notes.push((_f = preview === null || preview === void 0 ? void 0 : preview.unavailable) !== null && _f !== void 0 ? _f : `This theme was built with the ${usage.format} adapter, whose output a browser can't load ` +
@@ -6381,42 +6888,85 @@ function buildPreview(source, options) {
6381
6888
  }
6382
6889
  for (const note of (_g = preview === null || preview === void 0 ? void 0 : preview.notes) !== null && _g !== void 0 ? _g : [])
6383
6890
  notes.push(note);
6891
+ // ── Rail ────────────────────────────────────────────────────────────────
6892
+ const navItems = [];
6893
+ for (const section of SECTIONS) {
6894
+ if (!bodies.has(section.id))
6895
+ continue;
6896
+ navItems.push(`<a href="#rfp-${section.id}">${section.eyebrow}<span class="rfp-n">${(_h = counts.get(section.id)) !== null && _h !== void 0 ? _h : 0}</span></a>`);
6897
+ }
6898
+ if (modesHtml)
6899
+ navItems.push(`<a href="#rfp-modes">Appearance</a>`);
6900
+ if (globalsHtml)
6901
+ navItems.push(`<a href="#rfp-globals">Base elements</a>`);
6902
+ navItems.push(`<a href="#rfp-recipes">Components<span class="rfp-n">${usage.recipes.length}</span></a>`);
6903
+ // ── Masthead ────────────────────────────────────────────────────────────
6904
+ const title = (_j = options.title) !== null && _j !== void 0 ? _j : `${usage.format} theme`;
6905
+ const totalBytes = options.files.reduce((sum, f) => { var _a, _b, _c; return sum + ((_c = (_b = (_a = options.contents) === null || _a === void 0 ? void 0 : _a[f]) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0); }, 0);
6906
+ const metaBits = [
6907
+ `<span><i class="rfp-dot"></i> ${esc(usage.format)} · ${live ? "live" : "tokens only"}</span>`,
6908
+ `<span>emit <b>${esc(options.plan.type)}</b></span>`,
6909
+ `<span>${leaves.length} tokens</span>`,
6910
+ `<span>${usage.recipes.length} recipes</span>`,
6911
+ totalBytes ? `<span>${options.files.length} file(s) · ${bytes(totalBytes)}</span>` : "",
6912
+ ].filter(Boolean);
6913
+ const modeAttribute = preview === null || preview === void 0 ? void 0 : preview.modeAttribute;
6914
+ const modes = collectModes(model);
6915
+ const breakpoints = (_k = model.breakpoints) !== null && _k !== void 0 ? _k : {};
6916
+ const controls = [];
6917
+ if (modeAttribute && modes.length) {
6918
+ const buttons = [`<button type="button" data-rfp-mode data-value="" aria-pressed="true">auto</button>`]
6919
+ .concat(modes.map(m => `<button type="button" data-rfp-mode data-value="${esc(m)}" aria-pressed="false">${esc(m)}</button>`))
6920
+ .join("");
6921
+ controls.push(`<div class="rfp-ctl"><span class="rfp-ctl-label">Appearance</span><div class="rfp-seg" role="group" aria-label="Appearance mode">${buttons}</div></div>`);
6922
+ }
6923
+ const widths = Object.entries(breakpoints);
6924
+ if (widths.length) {
6925
+ const buttons = [`<button type="button" data-rfp-width data-value="" aria-pressed="true">full</button>`]
6926
+ .concat(widths.map(([n, px]) => `<button type="button" data-rfp-width data-value="${esc(String(px))}" aria-pressed="false">${esc(n)}</button>`))
6927
+ .join("");
6928
+ controls.push(`<div class="rfp-ctl"><span class="rfp-ctl-label">Width</span><div class="rfp-seg" role="group" aria-label="Viewport width">${buttons}</div></div>`);
6929
+ }
6930
+ // ── Sections ────────────────────────────────────────────────────────────
6931
+ const sectionHtml = SECTIONS.filter(s => bodies.has(s.id))
6932
+ .map(s => `<section class="rfp-section" id="rfp-${s.id}"><div class="rfp-section-head"><div>` +
6933
+ `<div class="rfp-eyebrow">${s.eyebrow}</div><h2>${s.title}</h2></div></div>` +
6934
+ (s.note ? `<p class="rfp-note">${s.note}</p>` : "") +
6935
+ bodies.get(s.id) +
6936
+ `</section>`)
6937
+ .join("");
6384
6938
  const head = [
6385
6939
  `<meta charset="utf-8">`,
6386
6940
  `<meta name="viewport" content="width=device-width,initial-scale=1">`,
6387
- `<title>${escapeHtml(title)}</title>`,
6388
- // Theme first, chrome last: equal-specificity ties then resolve in the chrome's favor.
6941
+ `<title>${esc(title)}</title>`,
6942
+ // Theme FIRST, chrome last: equal-specificity ties then resolve in the chrome's favour, and a
6943
+ // themed element rule (`globals`) can never outrank a class-only chrome selector.
6389
6944
  renderThemeLinks(stylesheets, options.contents, inline),
6945
+ (preview === null || preview === void 0 ? void 0 : preview.statePinCss) && live ? `<style data-rfp-state-pins>\n${escapeTextElement(preview.statePinCss)}\n</style>` : "",
6390
6946
  `<style>\n${CHROME_CSS}\n</style>`,
6391
6947
  ]
6392
6948
  .filter(Boolean)
6393
6949
  .join("\n");
6394
- const modeAttribute = preview === null || preview === void 0 ? void 0 : preview.modeAttribute;
6395
- const fileList = options.files.length
6396
- ? `<p class="rfp-files">Built files: ${options.files.map(f => `<code>${escapeHtml(f)}</code>`).join(" · ")}</p>`
6397
- : "";
6398
- const body = [
6399
- `<div class="rfp-root">`,
6400
- `<header class="rfp-head">`,
6401
- `<h1 class="rfp-title">${escapeHtml(title)}</h1>`,
6402
- `<span class="rfp-sub">format <strong>${escapeHtml(usage.format)}</strong> · emit ` +
6403
- `<strong>${escapeHtml(options.plan.type)}</strong> · ${usage.recipes.length} recipe(s)</span>`,
6404
- `</header>`,
6405
- fileList,
6406
- notes.map(n => `<p class="rfp-note">${escapeHtml(n)}</p>`).join("\n"),
6407
- renderControls(modes, modeAttribute, breakpoints),
6408
- `<div class="rfp-frame" id="rfp-frame">`,
6409
- `<h2 class="rfp-section-title">Tokens</h2>`,
6410
- renderTokenPlates(tokens),
6411
- `<h2 class="rfp-section-title">Recipes</h2>`,
6412
- renderRecipePlates(usage, preview, live),
6413
- `</div>`,
6414
- `</div>`,
6415
- `<script>\n${CHROME_JS}\n</script>`,
6416
- ]
6417
- .filter(Boolean)
6418
- .join("\n");
6419
- const html = `<!doctype html>\n<html lang="en"${modeAttribute ? ` data-rfp-mode-attr="${escapeHtml(modeAttribute)}"` : ""}>\n` +
6950
+ const body = `<div class="rfp">` +
6951
+ `<aside class="rfp-rail"><div class="rfp-brand">refract preview</div>` +
6952
+ `<nav class="rfp-nav">${navItems.join("")}</nav></aside>` +
6953
+ `<main class="rfp-main">` +
6954
+ `<header class="rfp-masthead"><div class="rfp-eyebrow">Theme specimen</div>` +
6955
+ `<h1>${esc(title)}</h1><div class="rfp-meta">${metaBits.join("")}</div></header>` +
6956
+ (controls.length ? `<div class="rfp-controls">${controls.join("")}</div>` : "") +
6957
+ notes.map(n => `<p class="rfp-note">${esc(n)}</p>`).join("") +
6958
+ `<div class="rfp-frame" id="rfp-frame">` +
6959
+ sectionHtml +
6960
+ modesHtml +
6961
+ globalsHtml +
6962
+ `<section class="rfp-section" id="rfp-recipes"><div class="rfp-section-head"><div>` +
6963
+ `<div class="rfp-eyebrow">Components</div><h2>Recipes${live ? " and their states" : ""}</h2></div>` +
6964
+ `<span class="rfp-tag">${live ? "rendered live" : "names only"}</span></div>` +
6965
+ renderRecipes(usage, preview, live) +
6966
+ `</section>` +
6967
+ `</div></main></div>` +
6968
+ `<script>\n${CHROME_JS}\n</script>`;
6969
+ const html = `<!doctype html>\n<html lang="en"${modeAttribute ? ` data-rfp-mode-attr="${esc(modeAttribute)}"` : ""}>\n` +
6420
6970
  `<head>\n${head}\n</head>\n<body>\n${body}\n</body>\n</html>\n`;
6421
6971
  return { files: { [fileName]: html } };
6422
6972
  }