fix(studio): render complete interactive gallery previews
Quality / quality (ubuntu-latest) (push) Failing after 10m12s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-10-19 17:40:50 +05:30
1 parent c765d76a06
commit 9520651330
26 files changed
+5761 -39

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
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);
}