@marktiderman/genesis-ui 2.13.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +33 -0
- package/dist/{chunk-5OFA2CQR.js → chunk-DTQHAZWN.js} +3 -3
- package/dist/{chunk-5OFA2CQR.js.map → chunk-DTQHAZWN.js.map} +1 -1
- package/dist/chunk-SURORBV7.js +85 -0
- package/dist/chunk-SURORBV7.js.map +1 -0
- package/dist/chunk-TP6QNFDG.js +81 -0
- package/dist/chunk-TP6QNFDG.js.map +1 -0
- package/dist/chunk-ZEUUIJ57.js +605 -0
- package/dist/chunk-ZEUUIJ57.js.map +1 -0
- package/dist/components/data/index.js +10 -10
- package/dist/components/layout/builder-layout.d.ts +96 -0
- package/dist/components/layout/builder-layout.js +5 -0
- package/dist/components/layout/builder-layout.js.map +1 -0
- package/dist/components/layout/index.d.ts +249 -2
- package/dist/components/layout/index.js +315 -3
- package/dist/components/layout/index.js.map +1 -1
- package/dist/components/patterns/index.js +4 -4
- package/dist/components/resource/index.d.ts +234 -0
- package/dist/components/resource/index.js +21 -0
- package/dist/components/resource/index.js.map +1 -0
- package/dist/components/resource/rules.d.ts +74 -0
- package/dist/components/resource/rules.js +4 -0
- package/dist/components/resource/rules.js.map +1 -0
- package/dist/components/rich-text/rich-text-editor.js +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +67 -44
- package/dist/index.js.map +1 -1
- package/package.json +15 -3
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/components/resource/status.ts
|
|
2
|
+
var SAVE_STATE_LABEL = {
|
|
3
|
+
saved: "Changes saved",
|
|
4
|
+
saving: "Saving\u2026",
|
|
5
|
+
unsaved: "Unsaved changes",
|
|
6
|
+
failed: "Couldn't save; trying again",
|
|
7
|
+
stopped: "Couldn't save",
|
|
8
|
+
conflict: "Changed elsewhere; reload"
|
|
9
|
+
};
|
|
10
|
+
function readinessOf(findings, options = {}) {
|
|
11
|
+
const { emptyCode, pending } = options;
|
|
12
|
+
let errors = 0;
|
|
13
|
+
let warnings = 0;
|
|
14
|
+
for (const f of findings) {
|
|
15
|
+
if (emptyCode !== void 0 && f.code === emptyCode) return { state: "empty", count: 0 };
|
|
16
|
+
if (f.level === "error") errors++;
|
|
17
|
+
else if (f.level === "warning") warnings++;
|
|
18
|
+
}
|
|
19
|
+
if (errors) return { state: "problems", count: errors };
|
|
20
|
+
if (pending) return { state: "pending", count: 0 };
|
|
21
|
+
if (warnings) return { state: "warnings", count: warnings };
|
|
22
|
+
return { state: "ready", count: 0 };
|
|
23
|
+
}
|
|
24
|
+
function readinessChip(readiness, labels = {}) {
|
|
25
|
+
switch (readiness.state) {
|
|
26
|
+
case "ready":
|
|
27
|
+
return { label: labels.ready ?? "Ready to publish", tone: "success" };
|
|
28
|
+
case "empty":
|
|
29
|
+
return { label: labels.empty ?? "Nothing to publish yet", tone: "muted" };
|
|
30
|
+
case "pending":
|
|
31
|
+
return { label: labels.pending ?? "In progress", tone: "muted" };
|
|
32
|
+
case "problems":
|
|
33
|
+
return {
|
|
34
|
+
label: labels.problems?.(readiness.count) ?? `${readiness.count} ${readiness.count === 1 ? "problem" : "problems"} to fix`,
|
|
35
|
+
tone: "danger"
|
|
36
|
+
};
|
|
37
|
+
case "warnings":
|
|
38
|
+
return { label: labels.warnings?.(readiness.count) ?? `${readiness.count} to check`, tone: "warning" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/components/resource/days.ts
|
|
43
|
+
var MONTH_DAY = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
|
44
|
+
var MONTH_DAY_YEAR = new Intl.DateTimeFormat("en-US", {
|
|
45
|
+
month: "short",
|
|
46
|
+
day: "numeric",
|
|
47
|
+
year: "numeric",
|
|
48
|
+
timeZone: "UTC"
|
|
49
|
+
});
|
|
50
|
+
function dayLabel(day, today) {
|
|
51
|
+
const t = Date.parse(`${day}T00:00:00Z`);
|
|
52
|
+
if (Number.isNaN(t)) return day;
|
|
53
|
+
if (today) {
|
|
54
|
+
const diff = Math.round((Date.parse(`${today}T00:00:00Z`) - t) / 864e5);
|
|
55
|
+
if (diff === 0) return "Today";
|
|
56
|
+
if (diff === 1) return "Yesterday";
|
|
57
|
+
if (day.slice(0, 4) !== today.slice(0, 4)) return MONTH_DAY_YEAR.format(t);
|
|
58
|
+
}
|
|
59
|
+
return MONTH_DAY.format(t);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/components/resource/share.ts
|
|
63
|
+
var RELATIVE_BASE = "http://relative.invalid";
|
|
64
|
+
function prefilledUrl(url, values) {
|
|
65
|
+
let absolute = true;
|
|
66
|
+
let u;
|
|
67
|
+
try {
|
|
68
|
+
u = new URL(url);
|
|
69
|
+
} catch {
|
|
70
|
+
absolute = false;
|
|
71
|
+
u = new URL(url, RELATIVE_BASE);
|
|
72
|
+
}
|
|
73
|
+
for (const [k, v] of Object.entries(values)) if (v) u.searchParams.set(k, v);
|
|
74
|
+
if (absolute) return u.toString();
|
|
75
|
+
const path = url.startsWith("/") ? u.pathname : u.pathname.replace(/^\//, "");
|
|
76
|
+
return `${path}${u.search}${u.hash}`;
|
|
77
|
+
}
|
|
78
|
+
var attr = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
79
|
+
function embedSnippet(url, title) {
|
|
80
|
+
return `<iframe src="${attr(url)}" title="${attr(title)}" width="100%" height="720" style="border:0;max-width:100%" loading="lazy"></iframe>`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { SAVE_STATE_LABEL, dayLabel, embedSnippet, prefilledUrl, readinessChip, readinessOf };
|
|
84
|
+
//# sourceMappingURL=chunk-SURORBV7.js.map
|
|
85
|
+
//# sourceMappingURL=chunk-SURORBV7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/resource/status.ts","../src/components/resource/days.ts","../src/components/resource/share.ts"],"names":[],"mappings":"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","file":"chunk-SURORBV7.js","sourcesContent":["// Words for a resource page's chips (save state, readiness). Pure: no React, safe on a\n// server. Not about any one kind of resource: forms, courses, lessons and worksheets use\n// the same page pattern.\n\n/** Where the host's draft saving stands. */\nexport type SaveState = \"saved\" | \"saving\" | \"unsaved\" | \"failed\" | \"stopped\" | \"conflict\";\n\nexport const SAVE_STATE_LABEL: Record<SaveState, string> = {\n saved: \"Changes saved\",\n saving: \"Saving…\",\n unsaved: \"Unsaved changes\",\n failed: \"Couldn't save; trying again\",\n stopped: \"Couldn't save\",\n conflict: \"Changed elsewhere; reload\",\n};\n\n/** A chip's colour: the tinted, AA-safe Genesis badge tones. */\nexport type ChipTone = \"success\" | \"warning\" | \"danger\" | \"info\" | \"muted\";\n\n/** Whether something can be published, from an editor's checks. */\nexport interface Readiness {\n state: \"ready\" | \"problems\" | \"warnings\" | \"empty\" | \"pending\";\n /** Problems or warnings to look at (0 when ready, empty or pending). */\n count: number;\n}\n\n/** One of an editor's findings, as readinessOf reads it. */\nexport interface ReadinessFinding {\n level: \"error\" | \"warning\" | \"info\";\n code?: string | undefined;\n}\n\nexport interface ReadinessOptions {\n /** Errors are held back while something new is being written: \"pending\", not \"problems\". */\n pending?: boolean | undefined;\n /**\n * The finding code that means \"nothing to publish yet\" (for example a form builder's\n * `no-questions`): it makes the result \"empty\", not broken. None by default.\n */\n emptyCode?: string | undefined;\n}\n\n/**\n * Readiness from an editor's findings (any list of `{ level, code? }`): errors block\n * publishing, warnings ask for a look. One pass over the findings.\n */\nexport function readinessOf(\n findings: readonly ReadinessFinding[],\n options: ReadinessOptions = {},\n): Readiness {\n const { emptyCode, pending } = options;\n let errors = 0;\n let warnings = 0;\n for (const f of findings) {\n if (emptyCode !== undefined && f.code === emptyCode) return { state: \"empty\", count: 0 };\n if (f.level === \"error\") errors++;\n else if (f.level === \"warning\") warnings++;\n }\n if (errors) return { state: \"problems\", count: errors };\n if (pending) return { state: \"pending\", count: 0 };\n if (warnings) return { state: \"warnings\", count: warnings };\n return { state: \"ready\", count: 0 };\n}\n\n/** The readiness chip's words, each optional; the defaults are resource-neutral. */\nexport interface ReadinessLabels {\n /** Default \"Ready to publish\". */\n ready?: string | undefined;\n /** When there is nothing yet. Default \"Nothing to publish yet\"; a form says \"Add a question\". */\n empty?: string | undefined;\n /** Default \"In progress\". */\n pending?: string | undefined;\n /** Default \"1 problem to fix\" / \"3 problems to fix\". */\n problems?: ((count: number) => string) | undefined;\n /** Default \"2 to check\". */\n warnings?: ((count: number) => string) | undefined;\n}\n\n/** The readiness chip's label and tone. */\nexport function readinessChip(\n readiness: Readiness,\n labels: ReadinessLabels = {},\n): { label: string; tone: ChipTone } {\n switch (readiness.state) {\n case \"ready\":\n return { label: labels.ready ?? \"Ready to publish\", tone: \"success\" };\n case \"empty\":\n return { label: labels.empty ?? \"Nothing to publish yet\", tone: \"muted\" };\n case \"pending\":\n return { label: labels.pending ?? \"In progress\", tone: \"muted\" };\n case \"problems\":\n return {\n label:\n labels.problems?.(readiness.count) ??\n `${readiness.count} ${readiness.count === 1 ? \"problem\" : \"problems\"} to fix`,\n tone: \"danger\",\n };\n case \"warnings\":\n return { label: labels.warnings?.(readiness.count) ?? `${readiness.count} to check`, tone: \"warning\" };\n }\n}\n","// A day (YYYY-MM-DD) in plain words. Pure.\n\nconst MONTH_DAY = new Intl.DateTimeFormat(\"en-US\", { month: \"short\", day: \"numeric\", timeZone: \"UTC\" });\nconst MONTH_DAY_YEAR = new Intl.DateTimeFormat(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n timeZone: \"UTC\",\n});\n\n/** A day (YYYY-MM-DD) in words, given today on the same clock: \"Today\", \"Yesterday\", \"Oct 21\", \"Oct 21, 2025\". */\nexport function dayLabel(day: string, today?: string): string {\n const t = Date.parse(`${day}T00:00:00Z`);\n if (Number.isNaN(t)) return day;\n if (today) {\n const diff = Math.round((Date.parse(`${today}T00:00:00Z`) - t) / 86_400_000);\n if (diff === 0) return \"Today\";\n if (diff === 1) return \"Yesterday\";\n if (day.slice(0, 4) !== today.slice(0, 4)) return MONTH_DAY_YEAR.format(t);\n }\n return MONTH_DAY.format(t);\n}\n","// Sharing a published page (ShareDialog): prefilled links and the embed snippet. Pure.\n\n/** One search param a prefilled link may set, and the values staff pick from. */\nexport interface PrefillChoice {\n key: string;\n /** The picker's accessible name, e.g. \"Location\". */\n label: string;\n /** The picker's \"leave it out\" choice, e.g. \"Any location\". */\n anyLabel: string;\n options: readonly { value: string; label: string }[];\n}\n\n// Resolves a relative link without inventing an origin for it.\nconst RELATIVE_BASE = \"http://relative.invalid\";\n\n/**\n * `url` with `values` set as search params (empty values left out), keeping what it has.\n * An absolute link stays absolute; a relative one (`/f/a?x=1`) stays relative, so it never\n * throws while a page is still working out its origin.\n */\nexport function prefilledUrl(url: string, values: Record<string, string | undefined>): string {\n let absolute = true;\n let u: URL;\n try {\n u = new URL(url);\n } catch {\n absolute = false;\n u = new URL(url, RELATIVE_BASE);\n }\n for (const [k, v] of Object.entries(values)) if (v) u.searchParams.set(k, v);\n if (absolute) return u.toString();\n const path = url.startsWith(\"/\") ? u.pathname : u.pathname.replace(/^\\//, \"\");\n return `${path}${u.search}${u.hash}`;\n}\n\nconst attr = (s: string) =>\n s.replace(/&/g, \"&\").replace(/\"/g, \""\").replace(/</g, \"<\").replace(/>/g, \">\");\n\n/** An `<iframe>` that shows the page on another website, full width. */\nexport function embedSnippet(url: string, title: string): string {\n return `<iframe src=\"${attr(url)}\" title=\"${attr(title)}\" width=\"100%\" height=\"720\" style=\"border:0;max-width:100%\" loading=\"lazy\"></iframe>`;\n}\n"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { cn } from './chunk-NZ5WBA4R.js';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var GRID = {
|
|
6
|
+
full: "@min-[34rem]:grid-cols-[13rem_minmax(0,1fr)] @min-[56rem]:grid-cols-[13rem_minmax(0,1fr)_20rem] @min-[76rem]:grid-cols-[16rem_minmax(0,1fr)_24.5rem]",
|
|
7
|
+
outline: "@min-[34rem]:grid-cols-[13rem_minmax(0,1fr)] @min-[48rem]:grid-cols-[16rem_minmax(0,1fr)]",
|
|
8
|
+
preview: "@min-[56rem]:grid-cols-[minmax(0,1fr)_20rem] @min-[76rem]:grid-cols-[minmax(0,1fr)_24.5rem]",
|
|
9
|
+
editor: ""
|
|
10
|
+
};
|
|
11
|
+
var OUTLINE_STICKY = "@min-[34rem]:sticky @min-[34rem]:top-4";
|
|
12
|
+
var OUTLINE_SCROLL = "@min-[34rem]:max-h-[calc(100dvh-2rem)] @min-[34rem]:overflow-y-auto";
|
|
13
|
+
var PREVIEW_STICKY = "@min-[56rem]:sticky @min-[56rem]:top-4";
|
|
14
|
+
var PREVIEW_SCROLL = "@min-[56rem]:max-h-[calc(100dvh-2rem)] @min-[56rem]:overflow-y-auto";
|
|
15
|
+
var PREVIEW_UNDER = "@min-[34rem]:col-span-2 @min-[56rem]:col-span-1";
|
|
16
|
+
var BuilderLayout = React.forwardRef(
|
|
17
|
+
({
|
|
18
|
+
outline,
|
|
19
|
+
editor,
|
|
20
|
+
preview,
|
|
21
|
+
outlineLabel,
|
|
22
|
+
previewLabel,
|
|
23
|
+
previewHidden = false,
|
|
24
|
+
sticky = false,
|
|
25
|
+
scrollSticky = false,
|
|
26
|
+
outlineProps,
|
|
27
|
+
editorProps,
|
|
28
|
+
previewProps,
|
|
29
|
+
className,
|
|
30
|
+
...props
|
|
31
|
+
}, ref) => {
|
|
32
|
+
const hasOutline = outline != null && outline !== false;
|
|
33
|
+
const hasPreview = preview != null && preview !== false;
|
|
34
|
+
const showsPreview = hasPreview && !previewHidden;
|
|
35
|
+
const shape = hasOutline ? showsPreview ? "full" : "outline" : showsPreview ? "preview" : "editor";
|
|
36
|
+
const stickOutline = sticky === true || typeof sticky === "object" && !!sticky.outline;
|
|
37
|
+
const stickPreview = sticky === true || typeof sticky === "object" && !!sticky.preview;
|
|
38
|
+
const Outline = outlineLabel ? "nav" : "div";
|
|
39
|
+
const Preview = previewLabel ? "aside" : "div";
|
|
40
|
+
return /* @__PURE__ */ jsx("div", { ref, "data-slot": "builder-layout", className: cn("@container", className), ...props, children: /* @__PURE__ */ jsxs("div", { className: cn("grid items-start gap-6", GRID[shape]), children: [
|
|
41
|
+
hasOutline ? /* @__PURE__ */ jsx(
|
|
42
|
+
Outline,
|
|
43
|
+
{
|
|
44
|
+
"aria-label": outlineLabel,
|
|
45
|
+
...outlineProps,
|
|
46
|
+
"data-slot": "builder-layout-outline",
|
|
47
|
+
className: cn(
|
|
48
|
+
"min-w-0",
|
|
49
|
+
stickOutline && OUTLINE_STICKY,
|
|
50
|
+
stickOutline && scrollSticky && OUTLINE_SCROLL,
|
|
51
|
+
outlineProps?.className
|
|
52
|
+
),
|
|
53
|
+
children: outline
|
|
54
|
+
}
|
|
55
|
+
) : null,
|
|
56
|
+
/* @__PURE__ */ jsx("div", { ...editorProps, "data-slot": "builder-layout-editor", className: cn("min-w-0", editorProps?.className), children: editor }),
|
|
57
|
+
hasPreview ? /* @__PURE__ */ jsx(
|
|
58
|
+
Preview,
|
|
59
|
+
{
|
|
60
|
+
"aria-label": previewLabel,
|
|
61
|
+
...previewProps,
|
|
62
|
+
"data-slot": "builder-layout-preview",
|
|
63
|
+
className: cn(
|
|
64
|
+
"min-w-0",
|
|
65
|
+
hasOutline && PREVIEW_UNDER,
|
|
66
|
+
stickPreview && PREVIEW_STICKY,
|
|
67
|
+
stickPreview && scrollSticky && PREVIEW_SCROLL,
|
|
68
|
+
previewHidden && "hidden",
|
|
69
|
+
previewProps?.className
|
|
70
|
+
),
|
|
71
|
+
children: preview
|
|
72
|
+
}
|
|
73
|
+
) : null
|
|
74
|
+
] }) });
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
BuilderLayout.displayName = "BuilderLayout";
|
|
78
|
+
|
|
79
|
+
export { BuilderLayout };
|
|
80
|
+
//# sourceMappingURL=chunk-TP6QNFDG.js.map
|
|
81
|
+
//# sourceMappingURL=chunk-TP6QNFDG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/layout/builder-layout.tsx"],"names":[],"mappings":"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","file":"chunk-TP6QNFDG.js","sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils\";\n\n/**\n * Props passed through to one slot's wrapper element: a class, a test id,\n * any `data-*` or ARIA attribute.\n */\nexport type BuilderLayoutSlotProps = React.HTMLAttributes<HTMLElement> & {\n [attribute: `data-${string}`]: string | undefined;\n};\n\nexport interface BuilderLayoutProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Left column: the list of things being built (questions, lessons, steps).\n * Optional. Rendered as a `<nav>` when `outlineLabel` is set.\n */\n outline?: React.ReactNode;\n /** Centre column: the editor for the selected thing. Required. */\n editor: React.ReactNode;\n /**\n * Right column: a live preview (and anything that sits with it, such as\n * checks). Optional. Rendered as an `<aside>` when `previewLabel` is set.\n *\n * It keeps its place in the tree whatever `editor` and `outline` hold, so\n * swapping the editor (another section, another selected item) never\n * remounts it: the preview's own state (device, answers typed into it,\n * scroll) survives.\n */\n preview?: React.ReactNode;\n /** Accessible name for the outline; makes it a `navigation` landmark. */\n outlineLabel?: string | undefined;\n /** Accessible name for the preview; makes it a `complementary` landmark. */\n previewLabel?: string | undefined;\n /**\n * Keep the preview mounted but hidden, and give its width back to the\n * editor. For a section that needs the whole width (a host's own\n * settings layout) without losing the preview's state.\n */\n previewHidden?: boolean | undefined;\n /**\n * Pin the outline and/or the preview at the top while the editor scrolls,\n * once each sits in a column of its own (not while stacked). `true` pins\n * both. Default `false`.\n */\n sticky?: boolean | { outline?: boolean | undefined; preview?: boolean | undefined } | undefined;\n /**\n * With `sticky`, cap each pinned column at the viewport height and let it\n * scroll on its own, so a long outline or preview stays reachable.\n * Default `false`.\n */\n scrollSticky?: boolean | undefined;\n /** Props for the outline's wrapper (class, test id, data attributes). */\n outlineProps?: BuilderLayoutSlotProps | undefined;\n /** Props for the editor's wrapper. */\n editorProps?: BuilderLayoutSlotProps | undefined;\n /** Props for the preview's wrapper. */\n previewProps?: BuilderLayoutSlotProps | undefined;\n}\n\ntype Shape = \"full\" | \"outline\" | \"preview\" | \"editor\";\n\n/**\n * Grid tracks per slot combination. Container queries, so the columns follow\n * the space the builder has, not the window: a builder beside a sidebar gets\n * fewer columns than one on the whole screen. Written out literally because\n * Tailwind scans source text.\n *\n * Outline, editor and preview:\n * - under 34rem everything stacks (outline, editor, preview);\n * - from 34rem the outline (13rem) sits beside the editor, the preview under both;\n * - from 56rem three columns: 13rem, editor, 20rem;\n * - from 76rem the side columns widen: 16rem, editor, 24.5rem.\n * Outline and editor: 13rem from 34rem, 16rem from 48rem.\n * Editor and preview: preview 20rem from 56rem, 24.5rem from 76rem.\n */\nconst GRID: Record<Shape, string> = {\n full: \"@min-[34rem]:grid-cols-[13rem_minmax(0,1fr)] @min-[56rem]:grid-cols-[13rem_minmax(0,1fr)_20rem] @min-[76rem]:grid-cols-[16rem_minmax(0,1fr)_24.5rem]\",\n outline: \"@min-[34rem]:grid-cols-[13rem_minmax(0,1fr)] @min-[48rem]:grid-cols-[16rem_minmax(0,1fr)]\",\n preview: \"@min-[56rem]:grid-cols-[minmax(0,1fr)_20rem] @min-[76rem]:grid-cols-[minmax(0,1fr)_24.5rem]\",\n editor: \"\",\n};\n\n// The outline is a column from 34rem; the preview from 56rem.\nconst OUTLINE_STICKY = \"@min-[34rem]:sticky @min-[34rem]:top-4\";\nconst OUTLINE_SCROLL = \"@min-[34rem]:max-h-[calc(100dvh-2rem)] @min-[34rem]:overflow-y-auto\";\nconst PREVIEW_STICKY = \"@min-[56rem]:sticky @min-[56rem]:top-4\";\nconst PREVIEW_SCROLL = \"@min-[56rem]:max-h-[calc(100dvh-2rem)] @min-[56rem]:overflow-y-auto\";\n// With an outline, the preview spans both columns while it sits under them.\nconst PREVIEW_UNDER = \"@min-[34rem]:col-span-2 @min-[56rem]:col-span-1\";\n\n/**\n * BuilderLayout — the three columns of a builder: an outline of the parts on\n * the left, the editor for the selected part in the centre, a live preview\n * on the right.\n *\n * ```tsx\n * <BuilderLayout\n * outline={<QuestionList />}\n * outlineLabel=\"Outline\"\n * editor={<QuestionEditor key={selectedId} />}\n * preview={<LivePreview />}\n * previewLabel=\"Preview\"\n * sticky\n * />\n * ```\n *\n * Responsive by container query, so it needs no knowledge of the page around\n * it: all three columns from a 56rem container, wider side columns from\n * 76rem, the preview under the editor below 56rem and everything stacked\n * below 34rem (see `GRID` for each slot combination). The preview keeps its\n * place in the tree when the editor or outline changes, so it never\n * remounts; changing `previewLabel` between set and unset does remount it\n * (the element changes from `<div>` to `<aside>`).\n *\n * It positions the three parts and nothing else. It does **not** own tabs,\n * the app rail or a page header: those belong to the shell. Put a builder's\n * sections in `useAppShellSubNav` (or your own tabs inside `editor`) and its\n * title, status and actions in `AppShellPageHeader`. To keep one preview\n * across sections, render one `BuilderLayout` around the sections and swap\n * `editor`; nest a second one (outline and editor only) inside a section\n * that has an outline.\n *\n * @stability Beta\n */\nexport const BuilderLayout = React.forwardRef<HTMLDivElement, BuilderLayoutProps>(\n (\n {\n outline,\n editor,\n preview,\n outlineLabel,\n previewLabel,\n previewHidden = false,\n sticky = false,\n scrollSticky = false,\n outlineProps,\n editorProps,\n previewProps,\n className,\n ...props\n },\n ref,\n ) => {\n const hasOutline = outline != null && outline !== false;\n const hasPreview = preview != null && preview !== false;\n const showsPreview = hasPreview && !previewHidden;\n const shape: Shape = hasOutline ? (showsPreview ? \"full\" : \"outline\") : showsPreview ? \"preview\" : \"editor\";\n const stickOutline = sticky === true || (typeof sticky === \"object\" && !!sticky.outline);\n const stickPreview = sticky === true || (typeof sticky === \"object\" && !!sticky.preview);\n\n const Outline = outlineLabel ? \"nav\" : \"div\";\n const Preview = previewLabel ? \"aside\" : \"div\";\n\n return (\n <div ref={ref} data-slot=\"builder-layout\" className={cn(\"@container\", className)} {...props}>\n <div className={cn(\"grid items-start gap-6\", GRID[shape])}>\n {/* Every slot keeps its position (null when absent), so the preview never moves in the tree. */}\n {hasOutline ? (\n <Outline\n aria-label={outlineLabel}\n {...outlineProps}\n data-slot=\"builder-layout-outline\"\n className={cn(\n \"min-w-0\",\n stickOutline && OUTLINE_STICKY,\n stickOutline && scrollSticky && OUTLINE_SCROLL,\n outlineProps?.className,\n )}\n >\n {outline}\n </Outline>\n ) : null}\n <div {...editorProps} data-slot=\"builder-layout-editor\" className={cn(\"min-w-0\", editorProps?.className)}>\n {editor}\n </div>\n {hasPreview ? (\n <Preview\n aria-label={previewLabel}\n {...previewProps}\n data-slot=\"builder-layout-preview\"\n className={cn(\n \"min-w-0\",\n hasOutline && PREVIEW_UNDER,\n stickPreview && PREVIEW_STICKY,\n stickPreview && scrollSticky && PREVIEW_SCROLL,\n previewHidden && \"hidden\",\n previewProps?.className,\n )}\n >\n {preview}\n </Preview>\n ) : null}\n </div>\n </div>\n );\n },\n);\nBuilderLayout.displayName = \"BuilderLayout\";\n"]}
|