Files
WRNexusJS/packages/ui/test/design-system-fixture.ts
T

145 lines
6.6 KiB
TypeScript

import { readFileSync } from "node:fs";
import { renderComponent } from "../../test/src/index.ts";
import { CORE_COMPONENT_CONTRACTS, type DesignSystem } from "../../design-system/src/index.ts";
import { resolveThemeConfig, renderThemeCss } from "../../styles/src/theme.ts";
import { renderDocument } from "../../ssr/src/index.ts";
import { uiCss } from "../src/registry.ts";
import { getReactiveRuntime } from "../../csr/src/index.ts";
import { runWithRequestContext } from "../../core/src/index.ts";
let fixture: Promise<string> | undefined;
export function designFixtureBody(): Promise<string> {
return (fixture ??= (async () => {
const render = (
name: keyof typeof CORE_COMPONENT_CONTRACTS,
props: Record<string, unknown> = {},
) => {
let source = readFileSync(CORE_COMPONENT_CONTRACTS[name].source, "utf8");
// The isolated renderer has no parent slot distributor; provide authored trigger content.
if (name === "ContextMenu")
source = source.replace('<slot name="trigger"></slot>', "<span>More options</span>");
return renderComponent(source, props);
};
const [
nav,
heading,
input,
select,
textarea,
checkbox,
radio,
toggle,
save,
cancel,
loading,
disabled,
card,
alert,
modal,
menu,
] = await Promise.all([
render("Nav", {
items: [
{ label: "Profile", href: "#profile", value: "profile" },
{ label: "Preferences", href: "#preferences", value: "preferences" },
],
active: "profile",
collapsible: false,
}),
render("PageHeader", {
title: "Design system preview",
description: "The same components, content, and behavior in three visual systems.",
}),
render("Input", {
id: "display-name",
name: "displayName",
label: "Display name",
placeholder: "Your name",
}),
render("Select", {
id: "language",
name: "language",
label: "Language",
value: "en",
options: [
{ value: "en", label: "English" },
{ value: "hi", label: "Hindi" },
],
}),
render("Textarea", {
id: "bio",
name: "bio",
label: "About you",
placeholder: "Add a short introduction",
}),
render("Checkbox", { id: "remember", name: "remember", label: "Remember preferences" }),
render("Radio", {
name: "density",
label: "Page density",
options: [
{ value: "comfortable", label: "Comfortable" },
{ value: "compact", label: "Compact" },
],
value: "comfortable",
}),
render("Switch", { id: "updates", name: "updates", label: "Email updates" }),
render("Button", { label: "Save changes" }),
render("Button", { label: "Cancel", variant: "ghost" }),
render("Button", { label: "Save changes", loading: true }),
render("Button", { label: "Unavailable", disabled: true }),
render("Card", {
title: "Profile",
description: "Update your profile and choose how this application works for you.",
}),
render("Alert", {
title: "Preview only",
description: "Changes on this page are not sent to a server.",
color: "info",
showIcon: true,
dismissible: true,
}),
render("Modal", {
title: "Review changes",
description: "Check your preferences before continuing.",
triggerLabel: "Open dialog",
confirmLabel: "Continue",
}),
render("ContextMenu", {
trigger: "click",
label: "More options",
items: [
{ label: "View profile", value: "profile" },
{ label: "Reset preferences", value: "reset" },
],
}),
]);
return `${nav}${heading}<main id="profile" class="design-preview-grid"><section class="design-preview-form">${card}<form onsubmit="event.preventDefault()">${input}${select}${textarea}<div id="preferences">${toggle}${checkbox}${radio}</div><div class="design-preview-actions">${save}${cancel}</div></form></section><aside class="design-preview-aside">${alert}<section><h2>Interaction states</h2><div class="design-preview-actions">${loading}${disabled}</div></section><section><h2>Overlays</h2><div class="design-preview-actions">${modal}${menu}</div></section></aside></main>`;
})());
}
export const PREVIEW_CSS = `*{box-sizing:border-box}body{margin:0;padding:clamp(1rem,4vw,3rem);background:var(--wrn-color-bg);color:var(--wrn-color-text);font-family:var(--wrn-font-sans);line-height:1.5}body>#app{max-width:1120px;margin:auto}.design-preview-grid{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(0,1fr);gap:2rem}.design-preview-form,.design-preview-aside,form{display:grid;gap:1.25rem;align-content:start;min-width:0}.design-preview-actions{display:flex;flex-wrap:wrap;gap:1rem;align-items:center}h2{font-size:1.125rem;margin:0 0 1rem}.preview-controls{display:flex;flex-wrap:wrap;gap:1rem;align-items:center;margin-bottom:2rem;font:0.875rem system-ui}.preview-controls a{color:inherit;text-underline-offset:4px}@media(max-width:720px){.design-preview-grid{grid-template-columns:1fr}.design-preview-actions{gap:0.75rem}}`;
export async function designFixtureDocument(
system: DesignSystem,
mode = "light",
delivery: "inline" | "linked" = "inline",
): Promise<string> {
const theme = resolveThemeConfig({ designSystem: { system }, default: mode });
const themeCss = renderThemeCss(theme);
const themeHead =
delivery === "inline"
? `<style>${themeCss}</style>`
: `<link rel="stylesheet" href="/theme.css?system=${system.id}">`;
const controls = `<nav class="preview-controls" aria-label="Preview settings"><strong>Design systems</strong>${["square", "soft", "angular"].map((name) => `<a href="/?system=${name}&mode=${mode}&delivery=${delivery}">${name}</a>`).join("")}<a href="/?system=${system.id}&mode=${mode === "dark" ? "light" : "dark"}&delivery=${delivery}">Switch mode</a><a href="/?system=${system.id}&mode=${mode}&delivery=${delivery === "inline" ? "linked" : "inline"}">${delivery === "inline" ? "Linked CSS" : "Inline CSS"}</a></nav>`;
const body = controls + (await designFixtureBody());
return runWithRequestContext({} as never, () =>
renderDocument({
meta: { title: `${system.name} — Component preview` },
body,
htmlAttrs: ` data-theme="${mode}"`,
extraHead: `${themeHead}<style>${uiCss()}${PREVIEW_CSS}</style>`,
componentRecipeCss: theme.recipeCss,
styleNonce: "design-preview",
extraBody: `<script>${getReactiveRuntime()}</script>`,
}),
);
}