feat(studio): add thirteen presets and preview reviewed proposals
This commit is contained in:
1 parent
ba752e556d
commit
c765d76a06
12 files changed
+329
-17
No files matched your search
@@ -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.
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/design-studio",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
};
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user