@theme-registry/refract 0.1.6 → 0.1.8
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 +25 -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 +73 -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 +403 -8
- package/dist/cli.js.map +1 -1
- package/dist/core/ThemeAdapter.d.ts +58 -0
- package/package.json +2 -2
- package/skills/adapter-scaffold/SKILL.md +10 -0
- package/skills/build-config/SKILL.md +29 -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,361 @@ 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 escapeHtml = (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) => escapeHtml(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
|
+
/** Collect every `$value` leaf, threading the nearest declared `$type` down the tree. */
|
|
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
|
+
/** Sample text for the plates that demonstrate a value by rendering type in it. */
|
|
6098
|
+
const TYPE_SPECIMEN = "The quick brown fox";
|
|
6099
|
+
/**
|
|
6100
|
+
* The visual for one token. A `dimension` under `typography` is a font size (render the ramp);
|
|
6101
|
+
* elsewhere it's a length (render a bar). Anything unrecognized falls back to its literal value,
|
|
6102
|
+
* which is still the honest answer for a duration or a cubic-bezier.
|
|
6103
|
+
*/
|
|
6104
|
+
function renderSwatch(leaf) {
|
|
6105
|
+
const value = leaf.value;
|
|
6106
|
+
const label = `<code class="rfp-val">${escapeHtml(String(value))}</code>`;
|
|
6107
|
+
switch (leaf.type) {
|
|
6108
|
+
case "color":
|
|
6109
|
+
return `<span class="rfp-chip" style="background:${cssValue(value)}"></span>${label}`;
|
|
6110
|
+
case "shadow":
|
|
6111
|
+
return `<span class="rfp-box" style="box-shadow:${cssValue(value)}"></span>${label}`;
|
|
6112
|
+
case "border":
|
|
6113
|
+
return `<span class="rfp-box" style="border:${cssValue(value)}"></span>${label}`;
|
|
6114
|
+
case "fontFamily":
|
|
6115
|
+
return `<span class="rfp-specimen" style="font-family:${cssValue(value)}">${TYPE_SPECIMEN}</span>${label}`;
|
|
6116
|
+
case "fontWeight":
|
|
6117
|
+
return `<span class="rfp-specimen" style="font-weight:${cssValue(value)}">${TYPE_SPECIMEN}</span>${label}`;
|
|
6118
|
+
case "dimension":
|
|
6119
|
+
// `typography.fontSize.*` is the type ramp — show it as type. Every other dimension (spacing,
|
|
6120
|
+
// radius, breakpoints, and typography's own `letterSpacing`) reads better as a length bar.
|
|
6121
|
+
return leaf.path[0] === "typography" && leaf.path[1] === "fontSize"
|
|
6122
|
+
? `<span class="rfp-specimen" style="font-size:${cssValue(value)}">${TYPE_SPECIMEN}</span>${label}`
|
|
6123
|
+
: `<span class="rfp-bar" style="width:${cssValue(value)}"></span>${label}`;
|
|
6124
|
+
default:
|
|
6125
|
+
return label;
|
|
6126
|
+
}
|
|
6127
|
+
}
|
|
6128
|
+
/** One plate per top-level DTCG group (`color`, `spacing`, `shadow`, …), rows sorted by source order. */
|
|
6129
|
+
function renderTokenPlates(tokens) {
|
|
6130
|
+
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
|
+
const groups = new Map();
|
|
6137
|
+
for (const leaf of leaves) {
|
|
6138
|
+
const key = (_a = leaf.path[0]) !== null && _a !== void 0 ? _a : "tokens";
|
|
6139
|
+
const bucket = groups.get(key);
|
|
6140
|
+
if (bucket)
|
|
6141
|
+
bucket.push(leaf);
|
|
6142
|
+
else
|
|
6143
|
+
groups.set(key, [leaf]);
|
|
6144
|
+
}
|
|
6145
|
+
const sections = [];
|
|
6146
|
+
for (const [group, rows] of groups) {
|
|
6147
|
+
const items = rows
|
|
6148
|
+
.map(leaf => `<li class="rfp-row"><span class="rfp-name">${escapeHtml(leaf.path.slice(1).join(".") || group)}</span>` +
|
|
6149
|
+
`<span class="rfp-swatch">${renderSwatch(leaf)}</span></li>`)
|
|
6150
|
+
.join("\n");
|
|
6151
|
+
sections.push(`<section class="rfp-plate">\n<h3 class="rfp-plate-title">${escapeHtml(group)} ` +
|
|
6152
|
+
`<span class="rfp-count">${rows.length}</span></h3>\n<ul class="rfp-rows">\n${items}\n</ul>\n</section>`);
|
|
6153
|
+
}
|
|
6154
|
+
return sections.join("\n");
|
|
6155
|
+
}
|
|
6156
|
+
// ---------------------------------------------------------------------------
|
|
6157
|
+
// Recipe plates
|
|
6158
|
+
// ---------------------------------------------------------------------------
|
|
6159
|
+
/**
|
|
6160
|
+
* Which element to render a recipe as, when the adapter didn't say. Presentation guesswork belongs
|
|
6161
|
+
* here rather than in an adapter: it's about the specimen page, not about the output format.
|
|
6162
|
+
*/
|
|
6163
|
+
function inferTag(recipe) {
|
|
6164
|
+
const group = recipe.group.toLowerCase();
|
|
6165
|
+
if (group.includes("button") || group.includes("btn"))
|
|
6166
|
+
return "button";
|
|
6167
|
+
if (group.includes("link") || group.includes("anchor"))
|
|
6168
|
+
return "a";
|
|
6169
|
+
if (group.includes("badge") || group.includes("chip") || group.includes("tag") || group.includes("label")) {
|
|
6170
|
+
return "span";
|
|
6171
|
+
}
|
|
6172
|
+
return "div";
|
|
6173
|
+
}
|
|
6174
|
+
/** `<tag attr="…">Label</tag>` for one recipe, or `undefined` when the adapter offers no markup. */
|
|
6175
|
+
function renderRecipeSpecimen(recipe, descriptor) {
|
|
6176
|
+
var _a, _b;
|
|
6177
|
+
const markup = (_a = descriptor === null || descriptor === void 0 ? void 0 : descriptor.markup) === null || _a === void 0 ? void 0 : _a.call(descriptor, recipe);
|
|
6178
|
+
if (!markup)
|
|
6179
|
+
return undefined;
|
|
6180
|
+
const tag = (_b = markup.tag) !== null && _b !== void 0 ? _b : inferTag(recipe);
|
|
6181
|
+
const attrs = Object.entries(markup.attrs)
|
|
6182
|
+
.map(([key, value]) => ` ${escapeHtml(key)}="${escapeHtml(value)}"`)
|
|
6183
|
+
.join("");
|
|
6184
|
+
return `<${tag}${attrs}>${escapeHtml(recipe.variant)}</${tag}>`;
|
|
6185
|
+
}
|
|
6186
|
+
function renderRecipePlates(usage, descriptor, live) {
|
|
6187
|
+
var _a, _b;
|
|
6188
|
+
if (usage.recipes.length === 0) {
|
|
6189
|
+
// The scaffolder writes tokens and no recipes, so this is the FIRST thing a new user sees here.
|
|
6190
|
+
// Say what's missing and where to go, rather than showing an empty box.
|
|
6191
|
+
return (`<p class="rfp-note"><strong>No recipes yet — this theme is tokens only.</strong> ` +
|
|
6192
|
+
`Tokens are values; recipes are the rule-sets that turn them into styled components ` +
|
|
6193
|
+
`(a button, a card). Add a <code>recipes</code> block to a subsystem in your raw theme and ` +
|
|
6194
|
+
`rebuild — they'll render here.</p>`);
|
|
6195
|
+
}
|
|
6196
|
+
const groups = new Map();
|
|
6197
|
+
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 : "";
|
|
6199
|
+
const bucket = groups.get(key);
|
|
6200
|
+
if (bucket)
|
|
6201
|
+
bucket.push(recipe);
|
|
6202
|
+
else
|
|
6203
|
+
groups.set(key, [recipe]);
|
|
6204
|
+
}
|
|
6205
|
+
const sections = [];
|
|
6206
|
+
for (const [group, recipes] of groups) {
|
|
6207
|
+
const items = recipes
|
|
6208
|
+
.map(recipe => {
|
|
6209
|
+
const specimen = live ? renderRecipeSpecimen(recipe, descriptor) : undefined;
|
|
6210
|
+
const address = `${recipe.subsystem}.${recipe.group}.${recipe.variant}`;
|
|
6211
|
+
return (`<li class="rfp-recipe">` +
|
|
6212
|
+
(specimen ? `<div class="rfp-stage">${specimen}</div>` : "") +
|
|
6213
|
+
`<div class="rfp-meta"><span class="rfp-name">${escapeHtml(address)}</span>` +
|
|
6214
|
+
`<code class="rfp-val">${escapeHtml(recipe.name)}</code></div></li>`);
|
|
6215
|
+
})
|
|
6216
|
+
.join("\n");
|
|
6217
|
+
const title = group
|
|
6218
|
+
? `<h3 class="rfp-plate-title">${escapeHtml(group)} <span class="rfp-count">${recipes.length}</span></h3>\n`
|
|
6219
|
+
: "";
|
|
6220
|
+
sections.push(`<section class="rfp-plate">\n${title}<ul class="rfp-recipes">\n${items}\n</ul>\n</section>`);
|
|
6221
|
+
}
|
|
6222
|
+
return sections.join("\n");
|
|
6223
|
+
}
|
|
6224
|
+
// ---------------------------------------------------------------------------
|
|
6225
|
+
// Model-derived page controls
|
|
6226
|
+
// ---------------------------------------------------------------------------
|
|
6227
|
+
/** Distinct appearance modes across every property's `modes` list, in first-appearance order. */
|
|
6228
|
+
function collectModes(model) {
|
|
6229
|
+
var _a, _b;
|
|
6230
|
+
const modes = [];
|
|
6231
|
+
for (const subsystem of Object.values(model.subsystems)) {
|
|
6232
|
+
for (const property of Object.values((_a = subsystem.properties) !== null && _a !== void 0 ? _a : {})) {
|
|
6233
|
+
for (const override of (_b = property.modes) !== null && _b !== void 0 ? _b : []) {
|
|
6234
|
+
if (override.mode && !modes.includes(override.mode))
|
|
6235
|
+
modes.push(override.mode);
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
return modes;
|
|
6240
|
+
}
|
|
6241
|
+
// ---------------------------------------------------------------------------
|
|
6242
|
+
// Page chrome
|
|
6243
|
+
// ---------------------------------------------------------------------------
|
|
6244
|
+
/**
|
|
6245
|
+
* The preview's own styling. Class-only selectors (`.rfp-*`), so the theme's `globals` element rules
|
|
6246
|
+
* can never outrank it, and emitted LAST so an equal-specificity theme class still loses here. The
|
|
6247
|
+
* document body is deliberately left to the theme — a themed page background is part of the specimen.
|
|
6248
|
+
*/
|
|
6249
|
+
const CHROME_CSS = `
|
|
6250
|
+
.rfp-root{font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;color:#111;background:#fff;
|
|
6251
|
+
max-width:1100px;margin:0 auto;padding:24px;box-sizing:border-box}
|
|
6252
|
+
.rfp-head{display:flex;flex-wrap:wrap;gap:12px;align-items:baseline;justify-content:space-between;
|
|
6253
|
+
border-bottom:1px solid #e5e5e5;padding-bottom:12px;margin-bottom:20px}
|
|
6254
|
+
.rfp-title{font-size:20px;font-weight:600;margin:0}
|
|
6255
|
+
.rfp-sub{color:#666;font-size:12px}
|
|
6256
|
+
.rfp-controls{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px}
|
|
6257
|
+
.rfp-controls button{font:inherit;font-size:12px;padding:4px 10px;border:1px solid #ccc;border-radius:6px;
|
|
6258
|
+
background:#fafafa;color:#111;cursor:pointer}
|
|
6259
|
+
.rfp-controls button[aria-pressed="true"]{background:#111;border-color:#111;color:#fff}
|
|
6260
|
+
.rfp-group-label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#888;align-self:center;
|
|
6261
|
+
margin-right:4px}
|
|
6262
|
+
.rfp-section-title{font-size:15px;font-weight:600;margin:28px 0 10px}
|
|
6263
|
+
.rfp-plate{border:1px solid #e5e5e5;border-radius:8px;padding:14px 16px;margin-bottom:14px;background:#fff}
|
|
6264
|
+
.rfp-plate-title{font-size:13px;font-weight:600;margin:0 0 10px;color:#333}
|
|
6265
|
+
.rfp-count{color:#999;font-weight:400}
|
|
6266
|
+
.rfp-rows,.rfp-recipes{list-style:none;margin:0;padding:0}
|
|
6267
|
+
.rfp-row{display:grid;grid-template-columns:minmax(120px,220px) 1fr;gap:12px;align-items:center;
|
|
6268
|
+
padding:5px 0;border-top:1px solid #f2f2f2}
|
|
6269
|
+
.rfp-row:first-child{border-top:0}
|
|
6270
|
+
.rfp-name{font-size:12px;color:#444;overflow-wrap:anywhere}
|
|
6271
|
+
.rfp-swatch{display:flex;align-items:center;gap:10px;min-width:0;overflow:hidden}
|
|
6272
|
+
.rfp-val{font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;color:#666;background:#f6f6f6;
|
|
6273
|
+
padding:1px 5px;border-radius:4px;white-space:nowrap}
|
|
6274
|
+
.rfp-chip{width:28px;height:28px;border-radius:5px;border:1px solid rgba(0,0,0,.15);flex:none}
|
|
6275
|
+
.rfp-box{width:44px;height:28px;border-radius:5px;background:#fff;border:1px solid rgba(0,0,0,.08);flex:none}
|
|
6276
|
+
.rfp-bar{height:10px;background:#111;border-radius:2px;flex:none;max-width:100%}
|
|
6277
|
+
.rfp-specimen{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
|
6278
|
+
.rfp-recipes{display:flex;flex-wrap:wrap;gap:14px}
|
|
6279
|
+
.rfp-recipe{border:1px solid #eee;border-radius:8px;padding:12px;min-width:160px;background:#fff}
|
|
6280
|
+
.rfp-stage{display:flex;align-items:center;justify-content:center;min-height:56px;margin-bottom:8px}
|
|
6281
|
+
.rfp-meta{display:flex;flex-direction:column;gap:3px;align-items:flex-start}
|
|
6282
|
+
.rfp-note{font-size:13px;color:#555;background:#f8f8f8;border:1px solid #ececec;border-left:3px solid #bbb;
|
|
6283
|
+
border-radius:0 6px 6px 0;padding:10px 14px;margin:0 0 14px}
|
|
6284
|
+
.rfp-frame{margin:0 auto;transition:max-width .15s ease}
|
|
6285
|
+
.rfp-files{font-size:12px;color:#666;margin:0 0 16px}
|
|
6286
|
+
.rfp-files code{font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
6287
|
+
@media (prefers-color-scheme:dark){
|
|
6288
|
+
.rfp-root{color:#eee;background:#151515}
|
|
6289
|
+
.rfp-head{border-bottom-color:#2c2c2c}
|
|
6290
|
+
.rfp-plate,.rfp-recipe{background:#1c1c1c;border-color:#2c2c2c}
|
|
6291
|
+
.rfp-plate-title{color:#ddd}.rfp-name{color:#bbb}.rfp-sub,.rfp-files{color:#999}
|
|
6292
|
+
.rfp-val{background:#262626;color:#aaa}
|
|
6293
|
+
.rfp-controls button{background:#222;border-color:#3a3a3a;color:#eee}
|
|
6294
|
+
.rfp-controls button[aria-pressed="true"]{background:#eee;border-color:#eee;color:#111}
|
|
6295
|
+
.rfp-note{background:#1d1d1d;border-color:#2c2c2c;border-left-color:#555;color:#bbb}
|
|
6296
|
+
.rfp-row{border-top-color:#242424}
|
|
6297
|
+
.rfp-bar{background:#eee}
|
|
6298
|
+
}
|
|
6299
|
+
`.trim();
|
|
6300
|
+
/** Mode + breakpoint controls. Plain DOM, no dependencies — the page must work from `file://`. */
|
|
6301
|
+
const CHROME_JS = `
|
|
6302
|
+
(function(){
|
|
6303
|
+
var root=document.documentElement;
|
|
6304
|
+
function bind(sel,onPick){
|
|
6305
|
+
var btns=[].slice.call(document.querySelectorAll(sel));
|
|
6306
|
+
btns.forEach(function(b){
|
|
6307
|
+
b.addEventListener("click",function(){
|
|
6308
|
+
btns.forEach(function(o){o.setAttribute("aria-pressed",String(o===b));});
|
|
6309
|
+
onPick(b.getAttribute("data-value"));
|
|
6310
|
+
});
|
|
6311
|
+
});
|
|
6312
|
+
}
|
|
6313
|
+
bind("[data-rfp-mode]",function(v){
|
|
6314
|
+
var attr=root.getAttribute("data-rfp-mode-attr");
|
|
6315
|
+
if(!attr)return;
|
|
6316
|
+
if(v)root.setAttribute(attr,v);else root.removeAttribute(attr);
|
|
6317
|
+
});
|
|
6318
|
+
bind("[data-rfp-width]",function(v){
|
|
6319
|
+
var frame=document.getElementById("rfp-frame");
|
|
6320
|
+
if(frame)frame.style.maxWidth=v?v+"px":"";
|
|
6321
|
+
});
|
|
6322
|
+
})();
|
|
6323
|
+
`.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
|
+
/** `<style>`/`<link>` tags for the emitted artifacts, in the adapter's declared load order. */
|
|
6346
|
+
function renderThemeLinks(stylesheets, contents, inline) {
|
|
6347
|
+
return stylesheets
|
|
6348
|
+
.map(name => {
|
|
6349
|
+
const body = inline ? contents === null || contents === void 0 ? void 0 : contents[name] : undefined;
|
|
6350
|
+
return body === undefined
|
|
6351
|
+
? `<link rel="stylesheet" href="./${escapeHtml(name)}">`
|
|
6352
|
+
: `<style data-rfp-source="${escapeHtml(name)}">\n${escapeTextElement(body)}\n</style>`;
|
|
6353
|
+
})
|
|
6354
|
+
.join("\n");
|
|
6355
|
+
}
|
|
6356
|
+
// ---------------------------------------------------------------------------
|
|
6357
|
+
// Entry point
|
|
6358
|
+
// ---------------------------------------------------------------------------
|
|
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
|
+
function buildPreview(source, options) {
|
|
6367
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
6368
|
+
const { usage, preview, tokens, model } = source;
|
|
6369
|
+
const fileName = (_a = options.file) !== null && _a !== void 0 ? _a : DEFAULT_FILE;
|
|
6370
|
+
const inline = (_b = options.inline) !== null && _b !== void 0 ? _b : true;
|
|
6371
|
+
const written = new Set(options.files);
|
|
6372
|
+
const stylesheets = ((_c = preview === null || preview === void 0 ? void 0 : preview.stylesheets) !== null && _c !== void 0 ? _c : []).filter(name => written.has(name));
|
|
6373
|
+
const live = stylesheets.length > 0;
|
|
6374
|
+
const modes = collectModes(model);
|
|
6375
|
+
const breakpoints = (_d = model.breakpoints) !== null && _d !== void 0 ? _d : {};
|
|
6376
|
+
const title = (_e = options.title) !== null && _e !== void 0 ? _e : `${usage.format} theme preview`;
|
|
6377
|
+
const notes = [];
|
|
6378
|
+
if (!live) {
|
|
6379
|
+
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 ` +
|
|
6380
|
+
`directly — token values below are exact, but recipes are listed by name only.`);
|
|
6381
|
+
}
|
|
6382
|
+
for (const note of (_g = preview === null || preview === void 0 ? void 0 : preview.notes) !== null && _g !== void 0 ? _g : [])
|
|
6383
|
+
notes.push(note);
|
|
6384
|
+
const head = [
|
|
6385
|
+
`<meta charset="utf-8">`,
|
|
6386
|
+
`<meta name="viewport" content="width=device-width,initial-scale=1">`,
|
|
6387
|
+
`<title>${escapeHtml(title)}</title>`,
|
|
6388
|
+
// Theme first, chrome last: equal-specificity ties then resolve in the chrome's favor.
|
|
6389
|
+
renderThemeLinks(stylesheets, options.contents, inline),
|
|
6390
|
+
`<style>\n${CHROME_CSS}\n</style>`,
|
|
6391
|
+
]
|
|
6392
|
+
.filter(Boolean)
|
|
6393
|
+
.join("\n");
|
|
6394
|
+
const modeAttribute = preview === null || preview === void 0 ? void 0 : preview.modeAttribute;
|
|
6395
|
+
const fileList = options.files.length
|
|
6396
|
+
? `<p class="rfp-files">Built files: ${options.files.map(f => `<code>${escapeHtml(f)}</code>`).join(" · ")}</p>`
|
|
6397
|
+
: "";
|
|
6398
|
+
const body = [
|
|
6399
|
+
`<div class="rfp-root">`,
|
|
6400
|
+
`<header class="rfp-head">`,
|
|
6401
|
+
`<h1 class="rfp-title">${escapeHtml(title)}</h1>`,
|
|
6402
|
+
`<span class="rfp-sub">format <strong>${escapeHtml(usage.format)}</strong> · emit ` +
|
|
6403
|
+
`<strong>${escapeHtml(options.plan.type)}</strong> · ${usage.recipes.length} recipe(s)</span>`,
|
|
6404
|
+
`</header>`,
|
|
6405
|
+
fileList,
|
|
6406
|
+
notes.map(n => `<p class="rfp-note">${escapeHtml(n)}</p>`).join("\n"),
|
|
6407
|
+
renderControls(modes, modeAttribute, breakpoints),
|
|
6408
|
+
`<div class="rfp-frame" id="rfp-frame">`,
|
|
6409
|
+
`<h2 class="rfp-section-title">Tokens</h2>`,
|
|
6410
|
+
renderTokenPlates(tokens),
|
|
6411
|
+
`<h2 class="rfp-section-title">Recipes</h2>`,
|
|
6412
|
+
renderRecipePlates(usage, preview, live),
|
|
6413
|
+
`</div>`,
|
|
6414
|
+
`</div>`,
|
|
6415
|
+
`<script>\n${CHROME_JS}\n</script>`,
|
|
6416
|
+
]
|
|
6417
|
+
.filter(Boolean)
|
|
6418
|
+
.join("\n");
|
|
6419
|
+
const html = `<!doctype html>\n<html lang="en"${modeAttribute ? ` data-rfp-mode-attr="${escapeHtml(modeAttribute)}"` : ""}>\n` +
|
|
6420
|
+
`<head>\n${head}\n</head>\n<body>\n${body}\n</body>\n</html>\n`;
|
|
6421
|
+
return { files: { [fileName]: html } };
|
|
6422
|
+
}
|
|
6423
|
+
|
|
6048
6424
|
/**
|
|
6049
6425
|
* W3C Design Tokens Community Group (DTCG) format types.
|
|
6050
6426
|
* Based on Second Editors' Draft (2024).
|
|
@@ -7042,8 +7418,8 @@ const formatDimensionValue = (value, unit) => dimText(value, unit);
|
|
|
7042
7418
|
* source via `VENDOR_HELPERS`. Explicit opt-in — a target names which it wants; never auto-emitted.
|
|
7043
7419
|
*/
|
|
7044
7420
|
async function emitTheme(options) {
|
|
7045
|
-
var _a;
|
|
7046
|
-
const { raw, adapter, outDir, helpers = [], emit, media: mediaConfig, units, baseFontSize, guide } = options;
|
|
7421
|
+
var _a, _b;
|
|
7422
|
+
const { raw, adapter, outDir, helpers = [], emit, media: mediaConfig, units, baseFontSize, guide, preview } = options;
|
|
7047
7423
|
const theme = createTheme(raw, { adapter, media: mediaConfig, units, baseFontSize });
|
|
7048
7424
|
// Re-bind with the PLAIN core media descriptor: `emit()` isn't on the theme surface, and an
|
|
7049
7425
|
// adapter may decorate `theme.media` (SC → tagged templates), whereas emitted vendored helpers
|
|
@@ -7054,7 +7430,9 @@ async function emitTheme(options) {
|
|
|
7054
7430
|
if (!bound.emit) {
|
|
7055
7431
|
throw new Error(`Adapter "${adapter.name}" does not implement emit(); it cannot build to disk.`);
|
|
7056
7432
|
}
|
|
7057
|
-
|
|
7433
|
+
// Hoisted: `guide` and `preview` both describe THIS emit, and `describePreview` is handed the plan.
|
|
7434
|
+
const plan = resolveEmitPlan(emit);
|
|
7435
|
+
const emitted = bound.emit(plan);
|
|
7058
7436
|
const written = [];
|
|
7059
7437
|
node_fs.mkdirSync(outDir, { recursive: true });
|
|
7060
7438
|
const write = (name, contents) => {
|
|
@@ -7092,6 +7470,22 @@ async function emitTheme(options) {
|
|
|
7092
7470
|
for (const [name, contents] of Object.entries(built.files))
|
|
7093
7471
|
write(name, contents);
|
|
7094
7472
|
}
|
|
7473
|
+
// §20 — human-facing `preview.html`: the same theme rendered as a specimen page. `describePreview`
|
|
7474
|
+
// is OPTIONAL and gets the plan plus the REAL emitted names (`filename` is a user function in
|
|
7475
|
+
// subsystem/components mode, so the names cannot be re-derived from the plan). An adapter that
|
|
7476
|
+
// doesn't implement it yields a tokens-only page rather than an error.
|
|
7477
|
+
if (preview) {
|
|
7478
|
+
const cfg = preview === true ? {} : preview;
|
|
7479
|
+
const files = Object.keys(emitted.files);
|
|
7480
|
+
const built = buildPreview({
|
|
7481
|
+
usage: bound.describeUsage(),
|
|
7482
|
+
preview: (_b = bound.describePreview) === null || _b === void 0 ? void 0 : _b.call(bound, plan, files),
|
|
7483
|
+
tokens: toDTCG(theme),
|
|
7484
|
+
model: theme.model,
|
|
7485
|
+
}, { files, contents: emitted.files, plan, file: cfg.file, inline: cfg.inline, title: cfg.title });
|
|
7486
|
+
for (const [name, contents] of Object.entries(built.files))
|
|
7487
|
+
write(name, contents);
|
|
7488
|
+
}
|
|
7095
7489
|
return { outDir, files: written };
|
|
7096
7490
|
}
|
|
7097
7491
|
|
|
@@ -7160,6 +7554,7 @@ async function runBuild(options = {}) {
|
|
|
7160
7554
|
units: config.units,
|
|
7161
7555
|
baseFontSize: config.baseFontSize,
|
|
7162
7556
|
guide: target.guide,
|
|
7557
|
+
preview: target.preview,
|
|
7163
7558
|
});
|
|
7164
7559
|
summaries.push({
|
|
7165
7560
|
name: target.name,
|