@theme-registry/refract 0.1.7 → 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 +36 -1
- package/dist/build/config.d.ts +9 -0
- package/dist/build/emitTheme.d.ts +3 -0
- package/dist/build/index.d.ts +1 -0
- package/dist/build/preview.d.ts +77 -0
- 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 +953 -8
- package/dist/cli.js.map +1 -1
- package/dist/core/ThemeAdapter.d.ts +91 -0
- package/package.json +2 -2
- package/skills/adapter-scaffold/SKILL.md +10 -0
- package/skills/build-config/SKILL.md +40 -2
- package/skills/consuming-the-output/SKILL.md +5 -0
package/dist/cli.js
CHANGED
|
@@ -63,18 +63,39 @@ const readVendorSource = (source) => {
|
|
|
63
63
|
* with the CSS `emit()` never needs it. It's pulled in ONLY on the two paths that actually transpile
|
|
64
64
|
* TS: loading a `.ts` config and vendoring a shared helper module. If it's absent when one of those
|
|
65
65
|
* runs, we throw a clear, actionable error instead of an opaque module-resolution failure.
|
|
66
|
-
|
|
66
|
+
*
|
|
67
|
+
* §19 — **resolving is not enough.** TypeScript 7 (the native port) resolves fine but its main entry
|
|
68
|
+
* exports only `{ version, versionMajorMinor }`; the compiler API moved behind `./unstable/*` with a
|
|
69
|
+
* different shape. Without the second check below, `transpileModule`/`createProgram` sail through as
|
|
70
|
+
* `undefined` and every `.ts` config dies three frames later on the opaque
|
|
71
|
+
* `Cannot read properties of undefined (reading 'ESNext')`. We do NOT support TS 7 (decided
|
|
72
|
+
* 2026-07-28: `./unstable/*` is explicitly not a stability promise), so this fails loud and names
|
|
73
|
+
* the way out instead.
|
|
74
|
+
*/
|
|
75
|
+
/** The compiler entry points refract actually drives — the surface a usable `typescript` must have. */
|
|
76
|
+
const REQUIRED_TS_API = ["transpileModule", "createProgram"];
|
|
67
77
|
async function loadTypescript() {
|
|
68
78
|
var _a;
|
|
79
|
+
let mod;
|
|
69
80
|
try {
|
|
70
|
-
|
|
71
|
-
return (_a = mod.default) !== null && _a !== void 0 ? _a : mod;
|
|
81
|
+
mod = (await import('typescript'));
|
|
72
82
|
}
|
|
73
83
|
catch {
|
|
74
84
|
throw new Error('refract: the optional peer dependency "typescript" is required to transpile a `.ts` ' +
|
|
75
85
|
"theme.config or vendor a shared helper module, but it could not be resolved. Install it " +
|
|
76
|
-
"(`npm i -D typescript`), or use a `.mjs`/`.js` config and avoid the `helpers` opt-in.");
|
|
77
|
-
}
|
|
86
|
+
"(`npm i -D typescript@5`), or use a `.mjs`/`.js` config and avoid the `helpers` opt-in.");
|
|
87
|
+
}
|
|
88
|
+
const tsc = (_a = mod.default) !== null && _a !== void 0 ? _a : mod;
|
|
89
|
+
const missing = REQUIRED_TS_API.filter(name => typeof (tsc === null || tsc === void 0 ? void 0 : tsc[name]) !== "function");
|
|
90
|
+
if (missing.length > 0) {
|
|
91
|
+
const version = tsc === null || tsc === void 0 ? void 0 : tsc.version;
|
|
92
|
+
throw new Error(`refract: the installed "typescript" (${version !== null && version !== void 0 ? version : "unknown version"}) does not expose the ` +
|
|
93
|
+
`compiler API refract needs (missing: ${missing.join(", ")}). TypeScript 7 moved these ` +
|
|
94
|
+
"behind `./unstable/*` subpaths, which are explicitly not a stability promise, so refract " +
|
|
95
|
+
"does not use them. Install `typescript@5` (`npm i -D typescript@5`), or switch to a " +
|
|
96
|
+
"`.mjs`/`.js` theme.config — those are imported directly and never load typescript.");
|
|
97
|
+
}
|
|
98
|
+
return tsc;
|
|
78
99
|
}
|
|
79
100
|
/**
|
|
80
101
|
* Type-strip a self-contained TS module to standalone ESM — exactly what the 10a gate proves the
|
|
@@ -6045,6 +6066,911 @@ function buildGuide(descriptor, tokens, options) {
|
|
|
6045
6066
|
return { files };
|
|
6046
6067
|
}
|
|
6047
6068
|
|
|
6069
|
+
const DEFAULT_FILE = "preview.html";
|
|
6070
|
+
// ---------------------------------------------------------------------------
|
|
6071
|
+
// Escaping — every value below originates in user-authored theme content
|
|
6072
|
+
// ---------------------------------------------------------------------------
|
|
6073
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6074
|
+
/**
|
|
6075
|
+
* A token value going into a `style="…"` attribute. Declaration/rule terminators are dropped so a
|
|
6076
|
+
* value can never break out of its declaration, then the result is HTML-attribute escaped.
|
|
6077
|
+
*/
|
|
6078
|
+
const cssValue = (v) => esc(String(v).replace(/[;{}<>]/g, "").trim());
|
|
6079
|
+
/** Inline `<style>`/`<script>` bodies can't contain a literal `</` without ending the element early. */
|
|
6080
|
+
const escapeTextElement = (s) => s.replace(/<\/(?=[a-zA-Z])/g, "<\\/");
|
|
6081
|
+
const bytes = (n) => (n < 1024 ? `${n} B` : `${(n / 1024).toFixed(1)} KB`);
|
|
6082
|
+
function collectLeaves(node, path, inherited, out) {
|
|
6083
|
+
if (!node || typeof node !== "object" || Array.isArray(node))
|
|
6084
|
+
return;
|
|
6085
|
+
const record = node;
|
|
6086
|
+
const type = typeof record.$type === "string" ? record.$type : inherited;
|
|
6087
|
+
if ("$value" in record) {
|
|
6088
|
+
out.push({ path, type, value: record.$value });
|
|
6089
|
+
return;
|
|
6090
|
+
}
|
|
6091
|
+
for (const [key, child] of Object.entries(record)) {
|
|
6092
|
+
if (key.startsWith("$"))
|
|
6093
|
+
continue;
|
|
6094
|
+
collectLeaves(child, [...path, key], type, out);
|
|
6095
|
+
}
|
|
6096
|
+
}
|
|
6097
|
+
/** `<group>` → its leaves, in source order. The group is the token's first path segment. */
|
|
6098
|
+
function groupLeaves(leaves) {
|
|
6099
|
+
var _a;
|
|
6100
|
+
const groups = new Map();
|
|
6101
|
+
for (const leaf of leaves) {
|
|
6102
|
+
const key = (_a = leaf.path[0]) !== null && _a !== void 0 ? _a : "tokens";
|
|
6103
|
+
const bucket = groups.get(key);
|
|
6104
|
+
if (bucket)
|
|
6105
|
+
bucket.push(leaf);
|
|
6106
|
+
else
|
|
6107
|
+
groups.set(key, [leaf]);
|
|
6108
|
+
}
|
|
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
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
return out.join("");
|
|
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>`;
|
|
6360
|
+
// ---------------------------------------------------------------------------
|
|
6361
|
+
// Model-derived plates: modes, base elements
|
|
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
|
+
};
|
|
6385
|
+
/**
|
|
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.
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
6497
|
+
function inferTag(recipe) {
|
|
6498
|
+
const group = recipe.group.toLowerCase();
|
|
6499
|
+
if (group.includes("button") || group.includes("btn"))
|
|
6500
|
+
return "button";
|
|
6501
|
+
if (group.includes("input") || group.includes("field"))
|
|
6502
|
+
return "input";
|
|
6503
|
+
if (group.includes("link") || group.includes("anchor"))
|
|
6504
|
+
return "a";
|
|
6505
|
+
if (group.includes("badge") || group.includes("chip") || group.includes("tag") || group.includes("label"))
|
|
6506
|
+
return "span";
|
|
6507
|
+
return "div";
|
|
6508
|
+
}
|
|
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;
|
|
6512
|
+
const markup = (_a = descriptor === null || descriptor === void 0 ? void 0 : descriptor.markup) === null || _a === void 0 ? void 0 : _a.call(descriptor, recipe);
|
|
6513
|
+
if (!markup)
|
|
6514
|
+
return undefined;
|
|
6515
|
+
const tag = (_b = markup.tag) !== null && _b !== void 0 ? _b : inferTag(recipe);
|
|
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)}"`)
|
|
6521
|
+
.join("");
|
|
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}>`;
|
|
6525
|
+
}
|
|
6526
|
+
function renderRecipes(usage, descriptor, live) {
|
|
6527
|
+
var _a, _b;
|
|
6528
|
+
if (usage.recipes.length === 0) {
|
|
6529
|
+
// The scaffolder writes tokens and no recipes, so this is the FIRST thing a new user sees here.
|
|
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>`);
|
|
6534
|
+
}
|
|
6535
|
+
// Group for layout: the adapter decides (subsystem, component file, …); default is by group.
|
|
6536
|
+
const groups = new Map();
|
|
6537
|
+
for (const recipe of usage.recipes) {
|
|
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}`;
|
|
6539
|
+
const bucket = groups.get(key);
|
|
6540
|
+
if (bucket)
|
|
6541
|
+
bucket.push(recipe);
|
|
6542
|
+
else
|
|
6543
|
+
groups.set(key, [recipe]);
|
|
6544
|
+
}
|
|
6545
|
+
const out = [];
|
|
6546
|
+
for (const [group, recipes] of groups) {
|
|
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
|
|
6577
|
+
.map(recipe => {
|
|
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>`);
|
|
6584
|
+
})
|
|
6585
|
+
.join("");
|
|
6586
|
+
out.push(plate(group, `${recipes.length} variant(s)`, `<div class="rfp-recipes">${cards}</div>`));
|
|
6587
|
+
out.push(renderComposition(recipes, descriptor));
|
|
6588
|
+
}
|
|
6589
|
+
return out.join("");
|
|
6590
|
+
}
|
|
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>`);
|
|
6609
|
+
}
|
|
6610
|
+
return "";
|
|
6611
|
+
}
|
|
6612
|
+
// ---------------------------------------------------------------------------
|
|
6613
|
+
// Page chrome
|
|
6614
|
+
// ---------------------------------------------------------------------------
|
|
6615
|
+
/**
|
|
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.
|
|
6624
|
+
*/
|
|
6625
|
+
const CHROME_CSS = `
|
|
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)}
|
|
6708
|
+
.rfp-row:first-child{border-top:0}
|
|
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}
|
|
6781
|
+
}
|
|
6782
|
+
`.trim();
|
|
6783
|
+
const CHROME_JS = `
|
|
6784
|
+
(function(){
|
|
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
|
+
}
|
|
6826
|
+
})();
|
|
6827
|
+
`.trim();
|
|
6828
|
+
/** `<style>`/`<link>` tags for the emitted artifacts, in the adapter's declared load order. */
|
|
6829
|
+
function renderThemeLinks(stylesheets, contents, inline) {
|
|
6830
|
+
return stylesheets
|
|
6831
|
+
.map(name => {
|
|
6832
|
+
const body = inline ? contents === null || contents === void 0 ? void 0 : contents[name] : undefined;
|
|
6833
|
+
return body === undefined
|
|
6834
|
+
? `<link rel="stylesheet" href="./${esc(name)}">`
|
|
6835
|
+
: `<style data-rfp-source="${esc(name)}">\n${escapeTextElement(body)}\n</style>`;
|
|
6836
|
+
})
|
|
6837
|
+
.join("\n");
|
|
6838
|
+
}
|
|
6839
|
+
// ---------------------------------------------------------------------------
|
|
6840
|
+
// Entry point
|
|
6841
|
+
// ---------------------------------------------------------------------------
|
|
6842
|
+
function buildPreview(source, options) {
|
|
6843
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
6844
|
+
const { usage, preview, tokens, model } = source;
|
|
6845
|
+
const fileName = (_a = options.file) !== null && _a !== void 0 ? _a : DEFAULT_FILE;
|
|
6846
|
+
const inline = (_b = options.inline) !== null && _b !== void 0 ? _b : true;
|
|
6847
|
+
const written = new Set(options.files);
|
|
6848
|
+
const stylesheets = ((_c = preview === null || preview === void 0 ? void 0 : preview.stylesheets) !== null && _c !== void 0 ? _c : []).filter(name => written.has(name));
|
|
6849
|
+
const live = stylesheets.length > 0;
|
|
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);
|
|
6884
|
+
const notes = [];
|
|
6885
|
+
if (!live) {
|
|
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 ` +
|
|
6887
|
+
`directly — token values below are exact, but recipes are listed by name only.`);
|
|
6888
|
+
}
|
|
6889
|
+
for (const note of (_g = preview === null || preview === void 0 ? void 0 : preview.notes) !== null && _g !== void 0 ? _g : [])
|
|
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("");
|
|
6938
|
+
const head = [
|
|
6939
|
+
`<meta charset="utf-8">`,
|
|
6940
|
+
`<meta name="viewport" content="width=device-width,initial-scale=1">`,
|
|
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.
|
|
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>` : "",
|
|
6946
|
+
`<style>\n${CHROME_CSS}\n</style>`,
|
|
6947
|
+
]
|
|
6948
|
+
.filter(Boolean)
|
|
6949
|
+
.join("\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` +
|
|
6970
|
+
`<head>\n${head}\n</head>\n<body>\n${body}\n</body>\n</html>\n`;
|
|
6971
|
+
return { files: { [fileName]: html } };
|
|
6972
|
+
}
|
|
6973
|
+
|
|
6048
6974
|
/**
|
|
6049
6975
|
* W3C Design Tokens Community Group (DTCG) format types.
|
|
6050
6976
|
* Based on Second Editors' Draft (2024).
|
|
@@ -7042,8 +7968,8 @@ const formatDimensionValue = (value, unit) => dimText(value, unit);
|
|
|
7042
7968
|
* source via `VENDOR_HELPERS`. Explicit opt-in — a target names which it wants; never auto-emitted.
|
|
7043
7969
|
*/
|
|
7044
7970
|
async function emitTheme(options) {
|
|
7045
|
-
var _a;
|
|
7046
|
-
const { raw, adapter, outDir, helpers = [], emit, media: mediaConfig, units, baseFontSize, guide } = options;
|
|
7971
|
+
var _a, _b;
|
|
7972
|
+
const { raw, adapter, outDir, helpers = [], emit, media: mediaConfig, units, baseFontSize, guide, preview } = options;
|
|
7047
7973
|
const theme = createTheme(raw, { adapter, media: mediaConfig, units, baseFontSize });
|
|
7048
7974
|
// Re-bind with the PLAIN core media descriptor: `emit()` isn't on the theme surface, and an
|
|
7049
7975
|
// adapter may decorate `theme.media` (SC → tagged templates), whereas emitted vendored helpers
|
|
@@ -7054,7 +7980,9 @@ async function emitTheme(options) {
|
|
|
7054
7980
|
if (!bound.emit) {
|
|
7055
7981
|
throw new Error(`Adapter "${adapter.name}" does not implement emit(); it cannot build to disk.`);
|
|
7056
7982
|
}
|
|
7057
|
-
|
|
7983
|
+
// Hoisted: `guide` and `preview` both describe THIS emit, and `describePreview` is handed the plan.
|
|
7984
|
+
const plan = resolveEmitPlan(emit);
|
|
7985
|
+
const emitted = bound.emit(plan);
|
|
7058
7986
|
const written = [];
|
|
7059
7987
|
node_fs.mkdirSync(outDir, { recursive: true });
|
|
7060
7988
|
const write = (name, contents) => {
|
|
@@ -7092,6 +8020,22 @@ async function emitTheme(options) {
|
|
|
7092
8020
|
for (const [name, contents] of Object.entries(built.files))
|
|
7093
8021
|
write(name, contents);
|
|
7094
8022
|
}
|
|
8023
|
+
// §20 — human-facing `preview.html`: the same theme rendered as a specimen page. `describePreview`
|
|
8024
|
+
// is OPTIONAL and gets the plan plus the REAL emitted names (`filename` is a user function in
|
|
8025
|
+
// subsystem/components mode, so the names cannot be re-derived from the plan). An adapter that
|
|
8026
|
+
// doesn't implement it yields a tokens-only page rather than an error.
|
|
8027
|
+
if (preview) {
|
|
8028
|
+
const cfg = preview === true ? {} : preview;
|
|
8029
|
+
const files = Object.keys(emitted.files);
|
|
8030
|
+
const built = buildPreview({
|
|
8031
|
+
usage: bound.describeUsage(),
|
|
8032
|
+
preview: (_b = bound.describePreview) === null || _b === void 0 ? void 0 : _b.call(bound, plan, files),
|
|
8033
|
+
tokens: toDTCG(theme),
|
|
8034
|
+
model: theme.model,
|
|
8035
|
+
}, { files, contents: emitted.files, plan, file: cfg.file, inline: cfg.inline, title: cfg.title });
|
|
8036
|
+
for (const [name, contents] of Object.entries(built.files))
|
|
8037
|
+
write(name, contents);
|
|
8038
|
+
}
|
|
7095
8039
|
return { outDir, files: written };
|
|
7096
8040
|
}
|
|
7097
8041
|
|
|
@@ -7160,6 +8104,7 @@ async function runBuild(options = {}) {
|
|
|
7160
8104
|
units: config.units,
|
|
7161
8105
|
baseFontSize: config.baseFontSize,
|
|
7162
8106
|
guide: target.guide,
|
|
8107
|
+
preview: target.preview,
|
|
7163
8108
|
});
|
|
7164
8109
|
summaries.push({
|
|
7165
8110
|
name: target.name,
|