145 lines
6.6 KiB
TypeScript
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>`,
|
|
}),
|
|
);
|
|
}
|