feat(studio): add thirteen presets and preview reviewed proposals
Quality / quality (ubuntu-latest) (push) Failing after 10m52s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-10-19 14:51:03 +05:30
1 parent ba752e556d
commit c765d76a06
12 files changed
+331 -19

No files matched your search

+25 -12
View File
@@ -1,8 +1,8 @@
import { STUDIO_PRESETS } from "./presets.ts";
import { setupGuidance } from "./guidance.ts";
import { StudioSession, importDesign, exportDesign, reviewDesign } from "./session.ts";
import {
COMPONENT_CONTRACTS,
CORE_DESIGN_SYSTEMS,
TOKEN_TYPES,
type DesignSystem,
type ComponentDesignContract,
@@ -26,6 +26,7 @@ let application: { enabled?: boolean; revision: string; previous?: string; direc
};
let page = 0;
let previewGeneration = 0;
let reviewed: { system: DesignSystem; name: string } | undefined;
let activeTab = "foundations";
let noticeTimer: ReturnType<typeof setTimeout>;
function message(text: string) {
@@ -192,7 +193,7 @@ function renderProperties() {
typeof raw === "object" ? "@" + raw.ref : String(raw ?? "");
}
function render() {
$<HTMLSelectElement>("preset").value = Object.hasOwn(CORE_DESIGN_SYSTEMS, session.valid.id)
$<HTMLSelectElement>("preset").value = Object.hasOwn(STUDIO_PRESETS, session.valid.id)
? session.valid.id
: "";
$("system-name").setAttribute("value", session.valid.name);
@@ -254,12 +255,12 @@ async function previews() {
const comparison = value("compare");
$("comparison").hidden = !comparison;
const systems = [
session.valid,
reviewed?.system ?? session.valid,
...(comparison
? [
comparison === "applied"
? session.applied
: CORE_DESIGN_SYSTEMS[comparison as keyof typeof CORE_DESIGN_SYSTEMS],
: STUDIO_PRESETS[comparison as keyof typeof STUDIO_PRESETS],
]
: []),
];
@@ -285,11 +286,15 @@ async function previews() {
$<HTMLIFrameElement>("preview-frame").src = result[0].url;
if (comparison) $<HTMLIFrameElement>("compare-frame").src = result[1].url;
for (const id of ["preview-frame", "compare-frame"]) $(id).style.width = value("preview-width");
$("preview-label").textContent = session.diagnostics.length
? "Last valid draft · source has errors"
: "Current draft";
$("preview-label").textContent = reviewed
? `Reviewing ${reviewed.name} · draft unchanged`
: session.diagnostics.length
? "Last valid draft · source has errors"
: "Current draft";
$("show-draft").hidden = !reviewed;
}
async function changed() {
reviewed = undefined;
render();
await previews();
}
@@ -328,10 +333,10 @@ on("redo", async () => {
await changed();
});
on("new", async () => {
session.replace(
{ schemaVersion: 1, id: "my-design", name: "My design", version: "1.0.0", tokens: {} },
"Created new system",
);
const system = structuredClone(STUDIO_PRESETS[value("preset")] ?? STUDIO_PRESETS.swiss!);
system.id = "my-design";
system.name = "My design";
session.replace(system, "Created new system");
await changed();
});
on(
@@ -339,7 +344,7 @@ on(
async () => {
if (!value("preset")) return;
session.replace(
CORE_DESIGN_SYSTEMS[value("preset") as keyof typeof CORE_DESIGN_SYSTEMS],
STUDIO_PRESETS[value("preset") as keyof typeof STUDIO_PRESETS],
"Selected " + value("preset"),
);
await changed();
@@ -641,6 +646,10 @@ for (let i = 0; i < 7; i++) {
}
$("preview-content").closest(".preview-controls")!.after(pagination);
on("refresh-preview", previews);
on("show-draft", async () => {
reviewed = undefined;
await previews();
});
document.addEventListener("keydown", (event) => {
if (
(event.ctrlKey || event.metaKey) &&
@@ -666,6 +675,10 @@ window.addEventListener("beforeunload", () => {
const guidance = setupGuidance(session, {
message,
changed,
review: async (system, name) => {
reviewed = system ? { system, name: name ?? system.name } : undefined;
await previews();
},
preview: async (system) => (await request("/api/preview", { system, content: "composed" })).url,
});
render();
+1 -1
View File
@@ -479,7 +479,7 @@ export async function generateDesigns(
capabilities,
};
const rules =
'Return JSON only: {"alternatives":[{"name":"...","rationale":"...","patch":[{"op":"set","path":"/tokens/shape.control/value","value":"0"}]}]}. Return exactly three DISTINCT alternatives for directions, one for edit. Capability part values index propertyProfiles; states list accepted state names. Paths are JSON pointers, escape dots as literal characters (no escape needed). Only set/remove existing schema decisions: tokens, modes, densities, recipes, id/name/version/defaultMode. Never change locked raw or effective values, including token aliases. Use only declared component parts/properties/axes. No tools, code execution, remote assets or new schema fields. Do not embed secrets. A set operation has a value. Preserve source identity unless explicitly requested. Each alternative has at most 48 operations. Respect the brief and explain its tradeoffs.';
'Return JSON only: {"alternatives":[{"name":"...","rationale":"...","patch":[{"op":"set","path":"/tokens/shape.control/value","value":"0"}]}]}. Return exactly three DISTINCT alternatives for directions, one for edit. Capability part values index propertyProfiles; states list accepted state names. Paths are JSON pointers, escape dots as literal characters (no escape needed). Token maps are FLAT: color.primary is ONE key; never use /tokens/color/primary. Change an existing token with /tokens/color.primary/value. When adding a missing token, set /tokens/color.primary to a COMPLETE token object {"type":"color","value":"#002FA7","legacy":"color-primary"}, never a bare string. Allowed token types are color, dimension, number, fontFamily, fontWeight, shadow, duration, colorScheme. Use the same flat keys in /modes/light/color.primary/value and /modes/dark/color.primary/value. Existing mode overrides win over base tokens: update the active and other applicable modes when changing colors. Only set/remove schema decisions: tokens, modes, densities, recipes, id/name/version/defaultMode. Never change locked raw or effective values, including token aliases. Use only declared component parts/properties/axes. No tools, code execution, remote assets or new schema fields. Do not embed secrets. A set operation has a value. Preserve source identity unless explicitly requested. Each alternative has at most 48 operations. Respect the brief and explain its tradeoffs.';
let repair = "";
for (let attempt = 0; attempt < 2; attempt++) {
signal.throwIfAborted();
+10 -1
View File
@@ -78,6 +78,7 @@ export function setupGuidance(
message: (text: string) => void;
changed: () => Promise<void>;
preview: (system: DesignSystem) => Promise<string>;
review?: (system: DesignSystem | undefined, name?: string) => Promise<void>;
},
) {
const $ = <T extends HTMLElement = HTMLElement>(id: string) => document.getElementById(id) as T;
@@ -197,6 +198,7 @@ export function setupGuidance(
)
.join("\n\n") || "No changes.";
$("accept-proposal").removeAttribute("disabled");
hooks.review?.(proposal.system, proposal.name).catch((e) => hooks.message(e.message));
}
function render() {
document.querySelectorAll<HTMLButtonElement>("[data-lock-group]").forEach((button) => {
@@ -277,6 +279,7 @@ export function setupGuidance(
status = "Generating proposals. Your draft is unchanged.";
render();
try {
await hooks.review?.(undefined);
const response = await fetch(new URL("./api/generate", document.baseURI), {
method: "POST",
headers: { "content-type": "application/json" },
@@ -294,7 +297,9 @@ export function setupGuidance(
if (!response.ok) throw Error(result.error ?? "Generation failed.");
if (current !== epoch || signal.aborted) return;
if (result.diagnostics.length) {
status = result.diagnostics.map((d: { message: string }) => d.message).join("\n");
status =
"No valid proposals returned. Your draft and previous alternatives are unchanged.\n" +
result.diagnostics.map((d: { message: string }) => d.message).join("\n");
return;
}
batch = result;
@@ -305,6 +310,10 @@ export function setupGuidance(
)
status =
"Proposals arrived after the draft or locks changed. They are preserved for inspection; regenerate before accepting.";
else if (batch!.alternatives[0]) {
review(batch!.alternatives[0]);
status += " The first proposal is shown in the main preview; your draft is unchanged.";
}
save();
} catch (error) {
if (current === epoch)
+146
View File
@@ -0,0 +1,146 @@
import { CORE_DESIGN_SYSTEMS, type DesignSystem, type TokenMap } from "@wrnexus/design-system";
type Palette = [string, string, string, string, string, string, string, string];
function palette(tokens: TokenMap, values: Palette) {
const [bg, surface, muted, text, subtle, border, primary, contrast] = values;
const colors: Record<string, string> = {
bg,
background: bg,
surface,
"surface-2": muted,
text,
foreground: text,
"text-muted": subtle,
border,
primary,
"primary-hover": primary,
"primary-active": primary,
"primary-contrast": contrast,
"primary-text": primary,
"primary-soft": muted,
"surface-raised": surface,
"surface-muted": muted,
"surface-soft": muted,
"text-subtle": subtle,
"border-strong": border,
"primary-muted": muted,
};
for (const [name, value] of Object.entries(colors))
tokens[`color.${name}`] = { ...tokens[`color.${name}`]!, value };
}
function direction(
id: string,
name: string,
light: Palette,
dark: Palette,
radius: string,
font: string,
shadow = "none",
mode = "light",
): DesignSystem {
const system = structuredClone(CORE_DESIGN_SYSTEMS.soft);
system.id = id;
system.name = name;
system.defaultMode = mode;
palette(system.tokens, mode === "dark" ? dark : light);
palette(system.modes!.light!, light);
palette(system.modes!.dark!, dark);
for (const name of ["shape.control", "shape.panel", "shape.dialog", "shape.base"])
system.tokens[name] = { ...system.tokens[name]!, value: radius };
for (const name of ["type.body", "type.heading"])
system.tokens[name] = { ...system.tokens[name]!, value: font };
system.tokens["elevation.panel"] = { ...system.tokens["elevation.panel"]!, value: shadow };
return system;
}
const sans = '"Helvetica Neue", Arial, sans-serif';
const serif = 'Georgia, "Times New Roman", serif';
const mono = '"Courier New", monospace';
/** Studio choices extend the original three without changing the engine's core preset API. */
export const STUDIO_PRESETS: Record<string, DesignSystem> = {
...CORE_DESIGN_SYSTEMS,
swiss: direction(
"swiss",
"Swiss / Blue",
["#FFFFFF", "#FFFFFF", "#F7F7F8", "#101114", "#53565E", "#C9CBD1", "#002FA7", "#FFFFFF"],
["#101114", "#181A20", "#252832", "#F7F7F8", "#BABEC9", "#51596B", "#91B2FF", "#101114"],
"0",
sans,
),
forest: direction(
"forest",
"Forest / Sage",
["#E8DCC7", "#D4B895", "#C7CCB5", "#283320", "#48543E", "#7B866A", "#43552D", "#E8DCC7"],
["#20291C", "#2C3726", "#394530", "#E8DCC7", "#BDC5A5", "#718060", "#B7D092", "#20291C"],
"24px",
serif,
),
ocean: direction(
"ocean",
"Ocean / Teal",
["#F0FAFC", "#FFFFFF", "#DDF0F4", "#103846", "#42636E", "#99C4CE", "#00677B", "#FFFFFF"],
["#091F29", "#10303D", "#194452", "#E4F5FA", "#A3C5CE", "#477783", "#60D5DF", "#091F29"],
"8px",
sans,
),
rose: direction(
"rose",
"Rose / Rounded",
["#FFF3F5", "#FFFFFF", "#FADFE7", "#421E2C", "#76505F", "#D9A4B5", "#A32957", "#FFFFFF"],
["#29131E", "#3D202F", "#512C3D", "#FFEAF1", "#DBB2C2", "#955A71", "#FF9BBC", "#29131E"],
"20px",
sans,
),
violet: direction(
"violet",
"Violet / Studio",
["#F5F3FF", "#FFFFFF", "#E7E1FA", "#282044", "#625775", "#B9ADD4", "#6336AD", "#FFFFFF"],
["#1B142C", "#2A203E", "#3B2B54", "#F3EDFF", "#C2B2D8", "#7C649E", "#C8ABFF", "#1B142C"],
"12px",
sans,
"0 8px 24px #00000018",
),
ember: direction(
"ember",
"Ember / Terracotta",
["#E8DCC7", "#D4B895", "#CEB094", "#3D241C", "#694837", "#A7795A", "#8E3C21", "#E8DCC7"],
["#2B1B15", "#3B2820", "#50362B", "#E8DCC7", "#CEB094", "#9C7057", "#F4AE80", "#2B1B15"],
"16px",
serif,
),
editorial: direction(
"editorial",
"Editorial / Ink",
["#FFFFFF", "#FFFFFF", "#F0F0F0", "#171717", "#555555", "#A3A3A3", "#171717", "#FFFFFF"],
["#101010", "#1C1C1C", "#2B2B2B", "#FAFAFA", "#BCBCBC", "#747474", "#FAFAFA", "#101010"],
"0",
serif,
),
terminal: direction(
"terminal",
"Terminal / Green",
["#FFFFFF", "#FFFFFF", "#E4F7EB", "#001C0D", "#375D44", "#609B77", "#00642C", "#FFFFFF"],
["#000000", "#0B0C0A", "#102318", "#E1FFE9", "#9DCDA9", "#3A7550", "#00E676", "#000000"],
"0",
mono,
"none",
"dark",
),
brutalist: direction(
"brutalist",
"Brutalist / Primary",
["#FFFFFF", "#FFFF00", "#FFFFFF", "#000000", "#000000", "#000000", "#0000FF", "#FFFFFF"],
["#000000", "#000000", "#202020", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#FFFF00", "#000000"],
"0",
mono,
"4px 4px 0 #000000",
),
contrast: direction(
"contrast",
"Contrast / Clear",
["#FFFFFF", "#FFFFFF", "#E6E6E6", "#000000", "#333333", "#000000", "#002FA7", "#FFFFFF"],
["#000000", "#000000", "#1B1B1B", "#FFFFFF", "#E0E0E0", "#FFFFFF", "#FFFF00", "#000000"],
"4px",
sans,
),
};
+15 -2
View File
@@ -1,7 +1,14 @@
import { STUDIO_PRESETS } from "./presets.ts";
import { guidanceHtml, guidanceCss } from "./guidance-shell.ts";
export const studioHtml = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>WrNexus Design Studio</title><link rel="stylesheet" href="/studio.css"><script type="module" src="/studio.js"></script></head><body>
<header><a class="brand" href="/">WrNexus <strong>Design Studio</strong></a><span id="save-state" role="status">Local workspace</span><button id="undo">Undo</button><button id="redo">Redo</button><button id="export">Export JSON</button><button id="review" class="primary">Review changes</button></header>
<main><aside><p class="eyebrow">Workspace</p><h1>Your system.<br>Your components.</h1><p class="intro">Edit one design system and preview it on the existing component library.</p><label>Start from a preset<select id="preset"><option value="">Choose a preset</option><option value="square">Square / Industrial</option><option value="soft">Soft / Organic</option><option value="angular">Angular / Lo-Fi</option></select></label><div class="row"><button id="new">Create new</button><button id="duplicate">Duplicate</button></div><label>System name<input id="system-name"></label><label>System ID<input id="system-id"></label>
<main><aside><p class="eyebrow">Workspace</p><h1>Your system.<br>Your components.</h1><p class="intro">Edit one design system and preview it on the existing component library.</p><label>Start from a preset<select id="preset"><option value="">Choose a preset</option>${Object.entries(
STUDIO_PRESETS,
)
.map(([id, system]) => `<option value="${id}">${system.name}</option>`)
.join(
"",
)}</select></label><div class="row"><button id="new">Create new</button><button id="duplicate">Duplicate</button></div><label>System name<input id="system-name"></label><label>System ID<input id="system-id"></label>
<nav aria-label="Editors"><button data-tab="guided">Guided design</button><button data-tab="foundations" aria-current="page">Foundations</button><button data-tab="components">Components</button><button data-tab="source">Source & import</button><button data-tab="history">History</button><button data-tab="review">Review & apply</button></nav><p class="footnote">Local drafts stay in this browser. Export a file to keep a portable copy.</p></aside>
<section class="workspace"><div class="workspace-title"><div><p class="eyebrow">Live authoring</p><h2 id="editor-title">Foundations</h2></div><span id="validation-state" class="badge">Checking</span></div><div id="diagnostics" role="status" aria-live="polite"></div>
${guidanceHtml}
@@ -10,6 +17,12 @@ ${guidanceHtml}
<section id="source" class="editor" hidden><label>Canonical design system JSON<textarea id="source-text" rows="22" spellcheck="false"></textarea></label><button id="save-source">Update draft</button><button id="download-draft">Export raw draft</button><details open><summary>Import a design</summary><label>Format<select id="import-format"><option value="canonical">Canonical design system JSON</option><option value="legacy">WrNexus theme JSON</option><option value="tokens">Exchange tokens: literal scalar / alias subset</option></select></label><label>Choose a local JSON file<input id="import-file" type="file" accept=".json,application/json"></label><label>Or paste JSON<textarea id="import-text" rows="7" spellcheck="false"></textarea></label><button id="inspect-import">Inspect import</button><pre id="import-report"></pre><button id="accept-import" disabled>Use imported design</button></details></section>
<section id="history" class="editor" hidden><p>Up to 25 local revisions, including incomplete source edits. Undo and redo also restore invalid drafts.</p><ol id="revisions"></ol></section>
<section id="review-panel" class="editor" hidden><h3>Review before applying</h3><p id="target"></p><p>Apply writes a validated system, compiled CSS and theme configuration to the configured local application folder. Existing application configuration is preserved. Import the generated theme configuration to use it.</p><pre id="changes"></pre><div class="row"><button id="apply" class="primary">Apply to local folder</button><button id="rollback">Rollback last apply</button></div><h3>Portable exports</h3><div class="row"><button data-export="canonical">Canonical JSON</button><button data-export="css">Compiled CSS</button><button data-export="theme">Theme config</button><button data-export="tokens">Exchange tokens</button><button data-export="report">Compatibility report</button></div><p class="help">CSS and theme configuration are alternative integration paths. Publication and deployment are separate.</p><pre id="integration"></pre></section>
</section><section class="preview"><div class="preview-heading"><div><p class="eyebrow">Same content, different decisions</p><h2>Preview</h2></div><button id="refresh-preview">Refresh</button></div><div class="preview-controls"><label>Mode<select id="preview-mode"></select></label><label>Density<select id="preview-density"></select></label><label>Width<select id="preview-width"><option value="100%">Responsive</option><option value="390px">390 px</option><option value="768px">768 px</option><option value="1280px">1280 px</option></select></label><label>Direction<select id="preview-direction"><option value="ltr">LTR</option><option value="rtl">RTL</option></select></label><label>Content<select id="preview-content"><option value="composed">Composed page</option><option value="catalog">Component gallery</option></select></label><label>Compare<select id="compare"><option value="">No comparison</option><option value="applied">Last applied</option><option value="square">Square</option><option value="soft">Soft</option><option value="angular">Angular</option></select></label></div><p class="help">Previews use isolated frames. Invalid drafts keep the last valid design. Gallery shows default component states; provider regions retain their own styling.</p><div class="frames"><div><p id="preview-label">Current draft</p><div class="frame-scroll"><iframe id="preview-frame" title="Current design preview" sandbox="allow-scripts"></iframe></div></div><div id="comparison" hidden><p>Comparison</p><div class="frame-scroll"><iframe id="compare-frame" title="Comparison design preview" sandbox="allow-scripts"></iframe></div></div></div></section></main><div id="message" role="status" aria-live="polite"></div></body></html>`;
</section><section class="preview"><div class="preview-heading"><div><p class="eyebrow">Same content, different decisions</p><h2>Preview</h2></div><div class="row"><button id="show-draft" hidden>Show current draft</button><button id="refresh-preview">Refresh</button></div></div><div class="preview-controls"><label>Mode<select id="preview-mode"></select></label><label>Density<select id="preview-density"></select></label><label>Width<select id="preview-width"><option value="100%">Responsive</option><option value="390px">390 px</option><option value="768px">768 px</option><option value="1280px">1280 px</option></select></label><label>Direction<select id="preview-direction"><option value="ltr">LTR</option><option value="rtl">RTL</option></select></label><label>Content<select id="preview-content"><option value="composed">Composed page</option><option value="catalog">Component gallery</option></select></label><label>Compare<select id="compare"><option value="">No comparison</option><option value="applied">Last applied</option>${Object.entries(
STUDIO_PRESETS,
)
.map(([id, system]) => `<option value="${id}">${system.name}</option>`)
.join(
"",
)}</select></label></div><p class="help">Previews use isolated frames. Invalid drafts keep the last valid design. Gallery shows default component states; provider regions retain their own styling.</p><div class="frames"><div><p id="preview-label">Current draft</p><div class="frame-scroll"><iframe id="preview-frame" title="Current design preview" sandbox="allow-scripts"></iframe></div></div><div id="comparison" hidden><p>Comparison</p><div class="frame-scroll"><iframe id="compare-frame" title="Comparison design preview" sandbox="allow-scripts"></iframe></div></div></div></section></main><div id="message" role="status" aria-live="polite"></div></body></html>`;
export const studioCss = `${guidanceCss}:root{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;color:#171719;background:#fff;line-height:1.45;font-size:14px;--accent:#002fa7}*{box-sizing:border-box}body{margin:0}header{display:flex;align-items:center;gap:10px;min-height:72px;padding:16px 24px;border-bottom:1px solid #d9d9de;position:sticky;top:0;background:white;z-index:2}.brand{text-decoration:none;color:inherit;font-size:17px;letter-spacing:-.5px;margin-right:auto}.brand strong{margin-left:8px}#save-state{font-size:12px;color:#555;max-width:230px}button,input,select,textarea{font:inherit}button{padding:9px 12px;border:1px solid #c8c8d0;background:#fff;color:#171719;cursor:pointer;border-radius:0}button:hover{border-color:var(--accent);color:var(--accent)}button.primary{background:var(--accent);color:white;border-color:var(--accent)}button:disabled{opacity:.45;cursor:default}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,a:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px}main{display:grid;grid-template-columns:235px minmax(340px,.9fr) minmax(420px,1.1fr);min-height:calc(100vh - 72px)}aside{border-right:1px solid #d9d9de;padding:28px 20px}.eyebrow{font-size:11px;letter-spacing:1.2px;text-transform:uppercase;color:#666;margin:0 0 12px}h1{font-size:31px;line-height:1.08;letter-spacing:-1.4px;font-weight:500;margin:0 0 16px}h2{font-size:23px;line-height:1.2;letter-spacing:-.7px;font-weight:500;margin:0}h3{font-weight:500}.intro,.help,.footnote{color:#63636d;font-size:12px}.intro{margin-bottom:28px;max-width:28em}.footnote{margin-top:32px}label{display:block;font-size:12px;margin:12px 0;color:#3d3d45}input,select,textarea{display:block;width:100%;margin-top:6px;padding:9px;border:1px solid #c8c8d0;border-radius:0;background:white;color:#171719;min-width:0}textarea{font-family:Consolas,monospace;font-size:12px;tab-size:2;resize:vertical}nav{display:grid;margin:28px -20px 0}nav button{text-align:left;border:0;border-top:1px solid #e2e2e7;padding:15px 20px}nav button[aria-current=page]{border-left:3px solid var(--accent);padding-left:17px;color:var(--accent);background:#f7f7f8}.workspace{min-width:0;padding:28px 24px;border-right:1px solid #d9d9de}.workspace-title,.preview-heading{display:flex;justify-content:space-between;gap:12px;align-items:start;margin-bottom:24px}.badge{font-size:11px;border:1px solid #c8c8d0;padding:5px 8px;white-space:nowrap}.row{display:flex;gap:10px;align-items:end;flex-wrap:wrap}.row>label{flex:1;min-width:100px}.row>button{margin-bottom:12px}.token-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:0 18px}.token{border-top:1px solid #e2e2e7;padding:10px 0;min-width:0}.token label{margin:0}.token small{font-size:11px;display:block;overflow-wrap:anywhere;color:#686870;margin-top:7px}.token .token-name{font-weight:600;font-size:12px}.token-actions{display:flex;gap:8px}.token-actions input{flex:1}.swatch{width:18px;height:18px;border:1px solid #999;display:inline-block;vertical-align:middle;margin-right:6px}.preview{min-width:0;padding:28px 24px;background:#f7f7f8;align-self:start;position:sticky;top:72px;max-height:calc(100vh - 72px);overflow:auto}.preview-controls{display:flex;flex-wrap:wrap;gap:10px}.preview-controls label{flex:1;min-width:100px;margin-top:0}.frames{display:grid;gap:16px}.frames:has(#comparison:not([hidden])){grid-template-columns:1fr 1fr}.frames>div{min-width:0}.frame-scroll{overflow:auto;background:white;border:1px solid #d9d9de}iframe{border:0;width:100%;height:680px;display:block;background:white}.frames p{font-size:12px}details{border-top:1px solid #d9d9de;padding-top:16px;margin-top:24px}summary{cursor:pointer}pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:11px;background:#f7f7f8;padding:12px;max-height:350px;overflow:auto}#diagnostics:not(:empty){border-left:3px solid #b72032;padding:8px 12px;margin-bottom:20px;color:#8c1526;font-size:12px}#message:not(:empty){position:fixed;bottom:16px;left:24px;max-width:min(650px,calc(100vw - 48px));padding:14px 20px;background:#171719;color:white;z-index:4;white-space:pre-wrap}#contract{font-size:12px}.editor[hidden],[hidden]{display:none!important}#revisions li{margin-bottom:10px}#revisions button{text-align:left;width:100%}@media(min-width:1600px){main{grid-template-columns:250px minmax(470px,.8fr) minmax(600px,1.2fr)}}@media(max-width:1150px){main{grid-template-columns:210px minmax(0,1fr)}.preview{grid-column:2;position:static;max-height:none}.workspace{border-right:0}aside{grid-row:span 2}}@media(max-width:650px){header{flex-wrap:wrap;padding:12px}.brand{width:100%}#save-state{margin-right:auto}main{display:block}aside{border-right:0;padding:24px}h1 br{display:none}nav{display:flex;overflow:auto;margin:24px -24px 0}nav button{white-space:nowrap;padding:12px}.workspace,.preview{padding:24px}aside .intro,aside .footnote{display:none}.frames:has(#comparison:not([hidden])){grid-template-columns:1fr}.token- Line truncated