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
+329 -17

No files matched your search

+1 -1
View File
@@ -391,7 +391,7 @@
},
"packages/design-studio": {
"name": "@wrnexus/design-studio",
"version": "0.1.2",
"version": "0.1.3",
"dependencies": {
"@wrnexus/ai": "workspace:*",
"@wrnexus/compiler": "workspace:*",
@@ -0,0 +1,39 @@
# Studio 0.1.3: presets and proposal previews
The Studio now offers 13 presets in both its starting and comparison menus.
The three existing engine presets and public engine API remain unchanged. Ten
Studio choices add distinct palettes, fonts, corner shapes and panel treatments.
All 13 validate and compile in light and dark mode; the preview regression test
renders all 26 combinations using real contracted components.
The first valid generated alternative automatically appears in the main preview.
Review switches that preview without modifying the draft. Its label identifies
the reviewed proposal; Show current draft returns to the saved design. Accept
still creates an ordinary recoverable, undoable revision. New designs start from
the selected preset instead of an empty token map. Existing empty drafts remain
untouched and can still be edited or replaced explicitly.
Provider instructions now explicitly describe flat dotted token names, complete
typed token objects for additions, and mode overrides. A regression test rejects
nested color groups on an empty draft, then validates three repaired proposals.
Validation, lock checks, the single repair attempt and deadlines remain enforced.
## Live APIMart verification
On October 19, the deployed 60-second configuration returned three valid proposals
in one APIMart attempt using gpt-6-luna: Square — 0px, Rounded — 8px, Soft — 16px.
The brief requested a booking website and these three specific control shapes.
Their frames rendered in the deployed Studio. The exported generation report was
independently checked: three distinct canonical designs, zero schema diagnostics,
and successful compilation of every proposal in both light and dark modes.
No server key was copied or inspected.
Earlier broader requests in this session produced an invalid nested token group
and a provider failure. They preserved the draft and prior alternatives. The
successful constrained request establishes real provider connectivity and valid
output, not universal success for arbitrary briefs. The improved provider prompt
and main-preview behavior require deploying 0.1.3 before live verification.
Browser checks of the new local client cover automatic first-proposal preview,
switching reviewed proposals, acceptance and undo. HTTP success alone is not
treated as proof of visible preview rendering.
+11
View File
@@ -75,4 +75,15 @@ Run `bun run scripts/evaluate-design-agents.ts` for the six-page/twelve-preview
## Installed packages and upgrades
The preset and comparison menus offer 13 starting systems: Square, Soft, Angular,
Swiss, Forest, Ocean, Rose, Violet, Ember, Editorial, Terminal, Brutalist and
Contrast. Each has light and dark modes and uses the existing component contracts.
The original three engine presets and existing saved drafts retain their formats.
Create new seeds an editable design from the selected preset (Swiss when none is selected).
Generation automatically previews the first valid proposal in the large preview.
Reviewing another proposal switches that preview and labels it as an unchanged
draft review. Use **Show current draft** to return, or **Use reviewed proposal**
to save the design as an undoable draft revision. Export/apply remains separate.
The staged package ships an installed `wrnexus-design-studio` binary, a prebuilt browser client and explicit server/agent subpaths. MCP consumes Studio as an optional peer. See [RELEASE.md](RELEASE.md) for installed usage, verified shared design bundles, upgrade planning, migrations, recovery and the release gate. The new package/upgrade APIs are programmatic; existing Studio authoring controls and APIs remain available.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/design-studio",
"version": "0.1.2",
"version": "0.1.3",
"private": true,
"type": "module",
"main": "src/index.ts",
+23 -10
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
$("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
@@ -202,6 +202,51 @@ test("provider adapter uses bounded patches, compact capabilities and one repair
expect(context).toContain("Repair these diagnostics");
});
test("empty drafts require flat typed tokens and reject nested color groups before repair", async () => {
const system = {
schemaVersion: 1 as const,
id: "empty",
name: "Empty",
version: "1.0.0",
tokens: {},
};
let calls = 0;
const batch = await generateDesigns(await request("directions", [], system), {
provider: {
name: "fixture",
async generate(_prompt, options) {
calls++;
expect(options!.system).toContain("Token maps are FLAT");
expect(options!.system).toContain("COMPLETE token object");
return {
provider: "fixture",
value: JSON.stringify({
alternatives: ["#002FA7", "#00642C", "#A32957"].map((color, index) => ({
name: `Direction ${index}`,
rationale: "A distinct brand color.",
patch: [
calls === 1
? set("/tokens/color", { primary: { type: "color", value: color } })
: set("/tokens/color.primary", {
type: "color",
value: color,
legacy: "color-primary",
}),
],
})),
}),
};
},
},
});
expect(calls).toBe(2);
expect(batch.alternatives).toHaveLength(3);
expect(batch.diagnostics).toEqual([]);
for (const alternative of batch.alternatives)
expect(validateDesignSystem(alternative.system)).toEqual([]);
expect(system.tokens).toEqual({});
});
test("provider invalid parts and lock violations cannot survive bounded repair", async () => {
let calls = 0;
const provider: GenerationProvider = {
@@ -0,0 +1,35 @@
import { expect, test } from "bun:test";
import {
CORE_DESIGN_SYSTEMS,
validateDesignSystem,
compileDesignSystem,
} from "@wrnexus/design-system";
import { STUDIO_PRESETS } from "../src/presets.ts";
import { studioHtml } from "../src/shell.ts";
import { StudioSession } from "../src/session.ts";
test("thirteen distinct Studio presets validate and compile both modes without changing core presets", () => {
expect(Object.keys(STUDIO_PRESETS)).toHaveLength(13);
expect(Object.keys(CORE_DESIGN_SYSTEMS)).toEqual(["square", "soft", "angular"]);
const signatures = new Set<string>();
for (const [id, system] of Object.entries(STUDIO_PRESETS)) {
expect(system.id).toBe(id);
expect(validateDesignSystem(system)).toEqual([]);
expect(studioHtml).toContain(`<option value="${id}">${system.name}</option>`);
signatures.add(JSON.stringify([system.tokens, system.modes]));
for (const mode of ["light", "dark"])
expect(() => compileDesignSystem(system, { mode })).not.toThrow();
}
expect(signatures.size).toBe(13);
});
test("choosing a new preset stays undoable and never mutates the catalog", () => {
const session = new StudioSession(STUDIO_PRESETS.square);
const original = JSON.stringify(STUDIO_PRESETS.swiss);
session.replace(STUDIO_PRESETS.swiss!, "Selected Swiss");
session.edit(JSON.stringify({ ...session.valid, name: "My Swiss" }));
expect(JSON.stringify(STUDIO_PRESETS.swiss)).toBe(original);
session.undo();
session.undo();
expect(session.valid.id).toBe("square");
});
+2 -1
View File
@@ -8,6 +8,7 @@ import { CORE_DESIGN_SYSTEMS } from "../../design-system/src/index.ts";
import { resolveThemeConfig } from "../../styles/src/theme.ts";
import { designRevision } from "../src/decisions.ts";
import { DEFAULT_DESIGN_BRIEF } from "../src/generation.ts";
import { STUDIO_PRESETS } from "../src/presets.ts";
const root = mkdtempSync(join(tmpdir(), "wrn-studio-test-"));
afterAll(() => rmSync(root, { recursive: true, force: true }));
@@ -139,7 +140,7 @@ test("server serves a browser bundle, rejects cross-origin, invalid, oversized a
});
test("all presets generate isolated real component previews with light/dark and responsive/RTL controls", async () => {
for (const system of Object.values(CORE_DESIGN_SYSTEMS))
for (const system of Object.values(STUDIO_PRESETS))
for (const mode of ["light", "dark"]) {
const result = await send("/api/preview", { system, mode, direction: "rtl" });
expect(result.status).toBe(200);