@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/README.md +18 -7
- package/dist/build/preview.d.ts +19 -15
- package/dist/build.cjs.js +1 -1
- package/dist/build.cjs.js.map +1 -1
- package/dist/build.esm.js +1 -1
- package/dist/build.esm.js.map +1 -1
- package/dist/cli.js +779 -229
- package/dist/cli.js.map +1 -1
- package/dist/core/ThemeAdapter.d.ts +33 -0
- package/package.json +1 -1
- package/skills/build-config/SKILL.md +12 -1
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
|
|
6073
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
/**
|
|
6098
|
-
|
|
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
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
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 & 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
|
|
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
|
-
//
|
|
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
|
|
6161
|
-
*
|
|
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">→</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
|
-
/**
|
|
6175
|
-
function
|
|
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 =
|
|
6182
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6191
|
-
|
|
6192
|
-
`
|
|
6193
|
-
`
|
|
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
|
|
6545
|
+
const out = [];
|
|
6206
6546
|
for (const [group, recipes] of groups) {
|
|
6207
|
-
|
|
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
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
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("
|
|
6217
|
-
|
|
6218
|
-
|
|
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
|
|
6589
|
+
return out.join("");
|
|
6223
6590
|
}
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
function
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
for (const
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
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
|
|
6610
|
+
return "";
|
|
6240
6611
|
}
|
|
6241
6612
|
// ---------------------------------------------------------------------------
|
|
6242
6613
|
// Page chrome
|
|
6243
6614
|
// ---------------------------------------------------------------------------
|
|
6244
6615
|
/**
|
|
6245
|
-
* The preview's own styling.
|
|
6246
|
-
*
|
|
6247
|
-
*
|
|
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
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
.rfp-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
.rfp
|
|
6265
|
-
.rfp-
|
|
6266
|
-
.rfp-
|
|
6267
|
-
.rfp-
|
|
6268
|
-
|
|
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-
|
|
6271
|
-
.rfp-
|
|
6272
|
-
.rfp-
|
|
6273
|
-
|
|
6274
|
-
.rfp-
|
|
6275
|
-
.rfp-
|
|
6276
|
-
.rfp-
|
|
6277
|
-
.rfp-
|
|
6278
|
-
.rfp-
|
|
6279
|
-
|
|
6280
|
-
.rfp-
|
|
6281
|
-
|
|
6282
|
-
.rfp-
|
|
6283
|
-
|
|
6284
|
-
.rfp-
|
|
6285
|
-
|
|
6286
|
-
.rfp-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
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
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
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="./${
|
|
6352
|
-
: `<style data-rfp-source="${
|
|
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
|
|
6375
|
-
const
|
|
6376
|
-
|
|
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>${
|
|
6388
|
-
// Theme
|
|
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
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
`<div class="rfp-
|
|
6400
|
-
`<
|
|
6401
|
-
`<
|
|
6402
|
-
`<
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
`<
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
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
|
}
|