161 lines
6.4 KiB
TypeScript
161 lines
6.4 KiB
TypeScript
import samples from "./gallery-samples.json";
|
|
import { resolveControlTemplate } from "@wrnexus/core";
|
|
|
|
const illustration = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360"><rect width="640" height="360" fill="#dbeafe"/><path d="M0 260L190 100l130 140 120-100 200 160v60H0Z" fill="#2563eb"/><circle cx="470" cy="80" r="36" fill="#f59e0b"/></svg>')}`;
|
|
const map = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360"><rect width="640" height="360" fill="#e2e8d5"/><path d="M0 140h640M200 0v360M440 0v360" stroke="#fff" stroke-width="32"/><path d="M0 140h640M200 0v360M440 0v360" stroke="#b7c2ca" stroke-width="2"/><rect x="260" y="210" width="120" height="100" rx="20" fill="#a4c7a1"/></svg>')}`;
|
|
|
|
function localAssets(value: unknown, key = ""): unknown {
|
|
if (typeof value === "string")
|
|
return /^(?:https?:\/\/|\/showcase-images\/)/.test(value) &&
|
|
/src|image|avatar|poster|logo/i.test(key)
|
|
? illustration
|
|
: /^https?:\/\//.test(value) && /href|action/i.test(key)
|
|
? "#sample"
|
|
: value;
|
|
if (Array.isArray(value)) return value.map((entry) => localAssets(entry, key));
|
|
if (value && typeof value === "object")
|
|
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, localAssets(v, k)]));
|
|
return value;
|
|
}
|
|
/** Fixed, populated sample content; never contains draft-supplied executable markup. */
|
|
export function gallerySample(name: string) {
|
|
const source = (samples as Record<string, { props: Record<string, unknown>; content: string }>)[
|
|
name
|
|
];
|
|
if (!source) throw Error(`Missing gallery sample: ${name}`);
|
|
const props = localAssets(structuredClone(source.props)) as Record<string, unknown>;
|
|
const field = [
|
|
"Input",
|
|
"Textarea",
|
|
"Select",
|
|
"Checkbox",
|
|
"Radio",
|
|
"Switch",
|
|
"AdvancedSelect",
|
|
"ComboBox",
|
|
"DatePicker",
|
|
"TimePicker",
|
|
"RangeSlider",
|
|
"InputNumber",
|
|
"PinInput",
|
|
"FileInput",
|
|
"StrongPassword",
|
|
"TogglePassword",
|
|
].includes(name);
|
|
if (Object.hasOwn(props, "id") || field) props.id = `sample-${name.toLowerCase()}`;
|
|
if (field && !props.name) props.name = `sample-${name.toLowerCase()}`;
|
|
props.class = "";
|
|
let content = source.content;
|
|
let hint = "Sample data. Interact with the controls to try this component.";
|
|
if (name === "Map") {
|
|
Object.assign(props, {
|
|
title: "Sample location map",
|
|
description: "Schematic sample: select a marker or use the zoom controls.",
|
|
imageSrc: map,
|
|
imageAlt: "Schematic sample map",
|
|
items: [
|
|
{ label: "Office", x: 32, y: 42 },
|
|
{ label: "Park", x: 68, y: 70 },
|
|
],
|
|
});
|
|
hint = "Local schematic sample; no map service or API key is needed.";
|
|
}
|
|
if (name === "DrawflowCanvas" || name === "FlowCanvas")
|
|
Object.assign(props, {
|
|
nodes: [
|
|
{
|
|
id: "start",
|
|
label: "Start",
|
|
x: 30,
|
|
y: 50,
|
|
input: false,
|
|
ports: [{ key: "next", label: "Next", target: "review" }],
|
|
},
|
|
{
|
|
id: "review",
|
|
label: "Review",
|
|
x: 260,
|
|
y: 100,
|
|
ports: [],
|
|
},
|
|
],
|
|
palette: [{ kind: "task", label: "Add task" }],
|
|
});
|
|
if (name === "Toaster")
|
|
content = '<button type="button" data-preview-toast>Show sample notification</button>';
|
|
if (name === "BackToTop") {
|
|
props.threshold = 0;
|
|
props.alwaysVisible = true;
|
|
props.showLabel = true;
|
|
hint = "Scroll this preview, then use the back-to-top control.";
|
|
}
|
|
if (name === "DeviceFrame") {
|
|
props.items = [];
|
|
content =
|
|
"<article><h2>Sample mobile page</h2><p>This local content sits inside the device frame.</p><h3>Account preferences</h3><p>Rotate the preview to compare portrait and landscape layouts.</p></article>";
|
|
}
|
|
if (name === "VisuallyHidden")
|
|
hint = "This accessibility component intentionally hides its content visually.";
|
|
if (name === "CustomScrollbar")
|
|
content = Array.from(
|
|
{ length: 12 },
|
|
(_, i) => `<p>Sample row ${i + 1} — scroll to see more content.</p>`,
|
|
).join("");
|
|
if (["AdvancedSelect", "Checkbox", "Select"].includes(name)) delete props.values;
|
|
if (["AuthForm", "AuthSplitLayout", "FileInput"].includes(name))
|
|
hint = "Preview only. Submissions and file uploads are not sent anywhere.";
|
|
return { props, content, hint };
|
|
}
|
|
|
|
export function previewSlots(html: string, content: string) {
|
|
const named = new Map<string, string>();
|
|
// Author-controlled sample slots are the only HTML accepted here.
|
|
let defaultContent = "";
|
|
let cursor = 0;
|
|
const slots = /<div data-slot="([\w-]+)">/g;
|
|
for (let match = slots.exec(content); match; match = slots.exec(content)) {
|
|
defaultContent += content.slice(cursor, match.index);
|
|
const tags = /<\/?div\b[^>]*>/g;
|
|
tags.lastIndex = slots.lastIndex;
|
|
let depth = 1;
|
|
let end = tags.lastIndex;
|
|
for (let tag = tags.exec(content); tag; tag = tags.exec(content)) {
|
|
depth += tag[0].startsWith("</") ? -1 : 1;
|
|
if (depth === 0) {
|
|
named.set(match[1]!, content.slice(slots.lastIndex, tag.index));
|
|
end = tags.lastIndex;
|
|
break;
|
|
}
|
|
}
|
|
if (depth !== 0) throw Error("Unbalanced sample slot markup.");
|
|
cursor = end;
|
|
slots.lastIndex = end;
|
|
}
|
|
defaultContent += content.slice(cursor);
|
|
return html.replace(/<slot\b([^>]*)>([\s\S]*?)<\/slot>/g, (all, attributes) => {
|
|
const name = /\bname=["']([^"']+)["']/.exec(attributes)?.[1];
|
|
const replacement = name ? named.get(name) : defaultContent;
|
|
return replacement ? `<slot${attributes}>${replacement}</slot>` : all;
|
|
});
|
|
}
|
|
|
|
/** Conditional/loop bodies are request-owned templates, and need the same slots as SSR. */
|
|
export function previewSampleMarkup(html: string, content: string) {
|
|
const seen = new Set<string>();
|
|
const visit = (markup: string): string => {
|
|
for (const match of markup.matchAll(/data-wrn-(?:if|each)="(@[^"]+)"/g)) {
|
|
if (seen.has(match[1]!)) continue;
|
|
seen.add(match[1]!);
|
|
const definition = resolveControlTemplate(match[1]!);
|
|
for (const entry of Array.isArray(definition) ? definition : [definition]) {
|
|
if (!entry || typeof entry !== "object") continue;
|
|
for (const key of ["body", "empty"]) {
|
|
if (typeof entry[key] === "string") entry[key] = visit(entry[key]);
|
|
}
|
|
}
|
|
}
|
|
return previewSlots(markup, content);
|
|
};
|
|
return visit(html);
|
|
}
|