feat(studio): add offline manual theme designer
This commit is contained in:
1 parent
5259668006
commit
ae35ac6714
7 files changed
+570
-8
No files matched your search
@@ -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();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user