feat(studio): add offline manual theme designer
Quality / quality (ubuntu-latest) (push) Failing after 6m40s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-10-19 20:58:12 +05:30
1 parent 5259668006
commit ae35ac6714
7 files changed
+570 -8

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
import { expect, test } from "bun:test";
import {
CORE_DESIGN_SYSTEMS,
validateDesignSystem,
compileDesignSystem,
} from "@wrnexus/design-system";
import { manualDesign } from "../src/manual.ts";
import { StudioSession } from "../src/session.ts";
import { checkDecisionLocks } from "../src/decisions.ts";
test("manual colors update only their mode and maintain readable contrast and legacy bindings", () => {
const source = CORE_DESIGN_SYSTEMS.soft;
const original = JSON.stringify(source);
const next = manualDesign(source, {
kind: "color",
mode: "light",
name: "primary",
value: "#000000",
});
expect(next.modes!.light!["color.primary"]!.value).toBe("#000000");
expect(next.modes!.light!["color.primary-contrast"]!.value).toBe("#FFFFFF");
expect(next.modes!.light!["color.focus"]!.legacy).toBe("color-focus");
expect(next.modes!.dark).toEqual(source.modes!.dark);
expect(JSON.stringify(source)).toBe(original);
expect(validateDesignSystem(next)).toEqual([]);
expect(() => compileDesignSystem(next, { mode: "light" })).not.toThrow();
expect(() =>
manualDesign(source, {
kind: "color",
mode: "light",
name: "primary",
value: "red; display:none",
}),
).toThrow();
});
test("manual foundations are recoverable and decision locks still detect changes", () => {
const session = new StudioSession(CORE_DESIGN_SYSTEMS.soft);
const before = JSON.stringify(session.valid);
const next = manualDesign(session.valid, { kind: "token", name: "shape.control", value: "8px" });
expect(checkDecisionLocks(session.valid, next, ["/tokens/shape.control"])).not.toHaveLength(0);
session.replace(next, "Manual shape");
expect(session.valid.tokens["shape.control"]!.value).toBe("8px");
session.undo();
expect(JSON.stringify(session.valid)).toBe(before);
session.redo();
expect(session.valid.tokens["shape.control"]!.value).toBe("8px");
expect(() =>
manualDesign(next, { kind: "token", name: "shape.control", value: "-2px" }),
).toThrow();
});
test("manual typography, spacing, surfaces and semantic colors validate for all core themes", () => {
for (const original of Object.values(CORE_DESIGN_SYSTEMS)) {
let system = structuredClone(original);
for (const [name, value] of [
["type.body", "Arial, sans-serif"],
["type.heading", "Georgia, serif"],
["space.panel", "24px"],
["shape.panel", "12px"],
["elevation.panel", "none"],
])
system = manualDesign(system, { kind: "token", name: name!, value: value! });
for (const mode of ["light", "dark"])
for (const name of ["secondary", "success", "warning", "danger", "info"] as const)
system = manualDesign(system, { kind: "color", mode, name, value: "#00677B" });
expect(validateDesignSystem(system)).toEqual([]);
expect(() => compileDesignSystem(system, { mode: "dark" })).not.toThrow();
}
});
test("manual button styles, tilt, pill shapes, default mode and advanced colors compile", () => {
let system = manualDesign(CORE_DESIGN_SYSTEMS.soft, { kind: "buttonStyle", value: "outline" });
system = manualDesign(system, { kind: "tilt", value: "none" });
system = manualDesign(system, { kind: "token", name: "shape.control", value: "999px" });
system = manualDesign(system, { kind: "mode", value: "dark" });
system = manualDesign(system, {
kind: "color",
mode: "dark",
name: "primary-contrast",
value: "#FFFFFF",
});
expect(system.defaultMode).toBe("dark");
expect(system.recipes!.Button!.variants!.at(-1)!.parts.control!.backgroundColor).toBe(
"transparent",
);
expect(system.recipes!.Button!.base!.control!.rotate).toBe("0deg");
expect(() => compileDesignSystem(system)).not.toThrow();
});
test("manual form saves one atomic revision and rejects locked changes without corrupting drafts", async () => {
const { Window } = await import("happy-dom");
const { manualHtml, setupManual } = await import("../src/manual-editor.ts");
const win = new Window();
const names = ["document", "HTMLInputElement", "Option"] as const;
const previous = Object.fromEntries(names.map((name) => [name, (globalThis as any)[name]]));
try {
(globalThis as any).document = win.document;
(globalThis as any).HTMLInputElement = win.HTMLInputElement;
(globalThis as any).Option = win.Option;
win.document.body.innerHTML =
manualHtml + '<select id="preview-mode"><option value="light">light</option></select>';
const session = new StudioSession(CORE_DESIGN_SYSTEMS.soft);
let notice = "";
const editor = setupManual(session, {
changed: async () => {
editor.render();
},
message: (text) => {
notice = text;
},
mode: () => {},
});
editor.render();
const input = win.document.querySelector('[data-manual-token="shape.control"]') as any;
input.value = "12";
(win.document.querySelector('input[type=text][data-manual-color="primary"]') as any).value =
"#6336AD";
(win.document.querySelector("[data-manual-apply]") as any).click();
await Bun.sleep(10);
expect(session.valid.tokens["shape.control"]!.value).toBe("12px");
expect(session.valid.modes!.light!["color.primary"]!.value).toBe("#6336AD");
expect(session.history).toHaveLength(2);
session.setLocks(["/tokens/shape.control"]);
editor.render();
(win.document.querySelector('[data-manual-token="shape.control"]') as any).value = "24";
(win.document.querySelector("[data-manual-apply]") as any).click();
await Bun.sleep(10);
expect(notice).toContain("locked decision");
expect(session.valid.tokens["shape.control"]!.value).toBe("12px");
expect(session.diagnostics).toEqual([]);
} finally {
for (const name of names) (globalThis as any)[name] = previous[name];
await win.happyDOM.close();
}
});