@avocadostudio-ai/blocks 0.1.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.
Files changed (88) hide show
  1. package/LICENSE +201 -0
  2. package/dist/block-error-boundary.d.ts +18 -0
  3. package/dist/block-error-boundary.js +36 -0
  4. package/dist/blocks/_base.css +108 -0
  5. package/dist/blocks/_layout.css +47 -0
  6. package/dist/blocks/_shared.d.ts +24 -0
  7. package/dist/blocks/_shared.js +153 -0
  8. package/dist/blocks/_tokens.css +127 -0
  9. package/dist/blocks/banner/renderer.d.ts +2 -0
  10. package/dist/blocks/banner/renderer.js +21 -0
  11. package/dist/blocks/banner/styles.css +72 -0
  12. package/dist/blocks/block-image.d.ts +14 -0
  13. package/dist/blocks/block-image.js +10 -0
  14. package/dist/blocks/block-types.d.ts +2 -0
  15. package/dist/blocks/block-types.js +22 -0
  16. package/dist/blocks/card/renderer.d.ts +1 -0
  17. package/dist/blocks/card/renderer.js +11 -0
  18. package/dist/blocks/card/styles.css +94 -0
  19. package/dist/blocks/card-grid/renderer.d.ts +1 -0
  20. package/dist/blocks/card-grid/renderer.js +17 -0
  21. package/dist/blocks/card-grid/styles.css +38 -0
  22. package/dist/blocks/carousel/init.d.ts +8 -0
  23. package/dist/blocks/carousel/init.js +35 -0
  24. package/dist/blocks/carousel/renderer.d.ts +2 -0
  25. package/dist/blocks/carousel/renderer.js +19 -0
  26. package/dist/blocks/carousel/styles.css +136 -0
  27. package/dist/blocks/cta/renderer.d.ts +1 -0
  28. package/dist/blocks/cta/renderer.js +7 -0
  29. package/dist/blocks/cta/styles.css +111 -0
  30. package/dist/blocks/embed/renderer.d.ts +2 -0
  31. package/dist/blocks/embed/renderer.js +16 -0
  32. package/dist/blocks/embed/styles.css +58 -0
  33. package/dist/blocks/faq-accordion/renderer.d.ts +1 -0
  34. package/dist/blocks/faq-accordion/renderer.js +11 -0
  35. package/dist/blocks/faq-accordion/styles.css +68 -0
  36. package/dist/blocks/feature-grid/renderer.d.ts +1 -0
  37. package/dist/blocks/feature-grid/renderer.js +12 -0
  38. package/dist/blocks/feature-grid/styles.css +62 -0
  39. package/dist/blocks/footer/renderer.d.ts +1 -0
  40. package/dist/blocks/footer/renderer.js +19 -0
  41. package/dist/blocks/footer/styles.css +64 -0
  42. package/dist/blocks/gallery/renderer.d.ts +2 -0
  43. package/dist/blocks/gallery/renderer.js +21 -0
  44. package/dist/blocks/gallery/styles.css +59 -0
  45. package/dist/blocks/hero/renderer.d.ts +1 -0
  46. package/dist/blocks/hero/renderer.js +20 -0
  47. package/dist/blocks/hero/styles.css +137 -0
  48. package/dist/blocks/index.d.ts +5 -0
  49. package/dist/blocks/index.js +47 -0
  50. package/dist/blocks/quote/renderer.d.ts +2 -0
  51. package/dist/blocks/quote/renderer.js +9 -0
  52. package/dist/blocks/quote/styles.css +97 -0
  53. package/dist/blocks/rich-text/renderer.d.ts +1 -0
  54. package/dist/blocks/rich-text/renderer.js +15 -0
  55. package/dist/blocks/rich-text/styles.css +64 -0
  56. package/dist/blocks/site-header/renderer.d.ts +1 -0
  57. package/dist/blocks/site-header/renderer.js +64 -0
  58. package/dist/blocks/site-header/styles.css +345 -0
  59. package/dist/blocks/stats/renderer.d.ts +1 -0
  60. package/dist/blocks/stats/renderer.js +13 -0
  61. package/dist/blocks/stats/styles.css +64 -0
  62. package/dist/blocks/styles.css +23 -0
  63. package/dist/blocks/table/renderer.d.ts +2 -0
  64. package/dist/blocks/table/renderer.js +14 -0
  65. package/dist/blocks/table/styles.css +87 -0
  66. package/dist/blocks/tabs/init.d.ts +5 -0
  67. package/dist/blocks/tabs/init.js +38 -0
  68. package/dist/blocks/tabs/renderer.d.ts +2 -0
  69. package/dist/blocks/tabs/renderer.js +30 -0
  70. package/dist/blocks/tabs/styles.css +147 -0
  71. package/dist/blocks/testimonials/renderer.d.ts +1 -0
  72. package/dist/blocks/testimonials/renderer.js +15 -0
  73. package/dist/blocks/testimonials/styles.css +92 -0
  74. package/dist/blocks/two-column/renderer.d.ts +1 -0
  75. package/dist/blocks/two-column/renderer.js +41 -0
  76. package/dist/blocks/two-column/styles.css +83 -0
  77. package/dist/blocks/video/renderer.d.ts +2 -0
  78. package/dist/blocks/video/renderer.js +52 -0
  79. package/dist/blocks/video/styles.css +84 -0
  80. package/dist/blocks-hydrator.d.ts +5 -0
  81. package/dist/blocks-hydrator.js +15 -0
  82. package/dist/catalogue.d.ts +1 -0
  83. package/dist/catalogue.js +741 -0
  84. package/dist/index.d.ts +7 -0
  85. package/dist/index.js +5 -0
  86. package/dist/renderer.d.ts +12 -0
  87. package/dist/renderer.js +35 -0
  88. package/package.json +54 -0
@@ -0,0 +1,741 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState, useMemo, useCallback, useRef, useEffect } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { SharedBlockRenderer } from "./renderer.js";
6
+ import { initCarousels } from "./blocks/carousel/init.js";
7
+ import { initTabs } from "./blocks/tabs/init.js";
8
+ import { getAllBlockMeta, allowedBlockTypes, defaultPropsForType, getChromeTypes } from "@avocadostudio-ai/shared";
9
+ const CATEGORY_ORDER = ["content", "conversion", "layout", "navigation", "media"];
10
+ // Catalogue-only prop overrides: richer sample data than the block's insert
11
+ // defaults, so previews showcase the block's full capability without bloating
12
+ // what the AI planner seeds into real sites.
13
+ const CATALOGUE_FIXTURES = {
14
+ CardGrid: {
15
+ title: "Explore more",
16
+ subtitle: "A mix of getting-started guides, reference material, and product updates.",
17
+ cards: [
18
+ { title: "Fast setup", description: "Create and ship updates quickly.", ctaText: "Get started", ctaHref: "/" },
19
+ { title: "Safe updates", description: "Schema-validated edits reduce breakage.", ctaText: "See how", ctaHref: "/pricing" },
20
+ { title: "Team workflow", description: "Collaborate with clear, reviewable changes.", ctaText: "Read guide", ctaHref: "/" },
21
+ { title: "Live preview", description: "Every change renders instantly in the preview pane.", ctaText: "Try it", ctaHref: "/" },
22
+ { title: "AI assistance", description: "Describe what you want and let the planner draft it.", ctaText: "Learn more", ctaHref: "/" },
23
+ { title: "Rollbacks", description: "Revert to any previous published version in one click.", ctaText: "Explore", ctaHref: "/" },
24
+ ],
25
+ },
26
+ };
27
+ const CATEGORY_LABELS = {
28
+ content: "Content",
29
+ media: "Media",
30
+ navigation: "Navigation",
31
+ conversion: "Conversion",
32
+ layout: "Layout",
33
+ };
34
+ // ---------------------------------------------------------------------------
35
+ // Dummy image URL builder — replaces image props with placeholder images
36
+ // that respect the field's declared aspect ratio / dimensions.
37
+ // ---------------------------------------------------------------------------
38
+ function dummyImageUrl(spec) {
39
+ const w = spec?.width ?? 600;
40
+ const h = spec?.height ?? 400;
41
+ return `https://placehold.co/${w}x${h}/ab96ab/000000.jpg`;
42
+ }
43
+ function withDummyImages(props, meta) {
44
+ if (!meta)
45
+ return props;
46
+ const result = { ...props };
47
+ // Scalar image fields — replace existing or populate missing optional images
48
+ for (const [key, fm] of Object.entries(meta.fields)) {
49
+ if (fm.kind === "image" && (typeof result[key] === "string" || result[key] === undefined)) {
50
+ result[key] = dummyImageUrl(fm.imageSpec);
51
+ }
52
+ }
53
+ // List item image fields
54
+ if (meta.listFields) {
55
+ for (const [listKey, lm] of Object.entries(meta.listFields)) {
56
+ const imageItemKeys = Object.entries(lm.itemFields)
57
+ .filter(([, fm]) => fm.kind === "image");
58
+ if (imageItemKeys.length === 0)
59
+ continue;
60
+ const list = result[listKey];
61
+ if (!Array.isArray(list))
62
+ continue;
63
+ result[listKey] = list.map((item) => {
64
+ if (!item || typeof item !== "object")
65
+ return item;
66
+ const patched = { ...item };
67
+ for (const [itemKey, fm] of imageItemKeys) {
68
+ if (typeof patched[itemKey] === "string" || patched[itemKey] === undefined) {
69
+ patched[itemKey] = dummyImageUrl(fm.imageSpec);
70
+ }
71
+ }
72
+ return patched;
73
+ });
74
+ }
75
+ }
76
+ return result;
77
+ }
78
+ export function BlockCatalogue() {
79
+ const allTypes = useMemo(() => [...allowedBlockTypes, ...getChromeTypes()], []);
80
+ const [selectedType, setSelectedType] = useState(allTypes[0] ?? "");
81
+ const [liveProps, setLiveProps] = useState({});
82
+ const [darkMode, setDarkMode] = useState(() => {
83
+ if (typeof window === "undefined")
84
+ return false;
85
+ const stored = window.localStorage.getItem("site-theme-v1");
86
+ if (stored === "dark")
87
+ return true;
88
+ if (stored === "light")
89
+ return false;
90
+ return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ?? false;
91
+ });
92
+ // Sync dark mode to document and localStorage
93
+ useEffect(() => {
94
+ document.documentElement.classList.toggle("dark", darkMode);
95
+ window.localStorage.setItem("site-theme-v1", darkMode ? "dark" : "light");
96
+ }, [darkMode]);
97
+ // Listen for external theme changes (e.g. SiteHeader toggle)
98
+ useEffect(() => {
99
+ const onStorage = (e) => {
100
+ if (e.key === "site-theme-v1")
101
+ setDarkMode(e.newValue === "dark");
102
+ };
103
+ window.addEventListener("storage", onStorage);
104
+ // Also watch for classList changes on <html> (same-tab toggle from SiteHeader)
105
+ const observer = new MutationObserver(() => {
106
+ const hasDark = document.documentElement.classList.contains("dark");
107
+ setDarkMode((prev) => prev !== hasDark ? hasDark : prev);
108
+ });
109
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
110
+ return () => { window.removeEventListener("storage", onStorage); observer.disconnect(); };
111
+ }, []);
112
+ const [viewportWidth, setViewportWidth] = useState(1280);
113
+ const previewWrapRef = useRef(null);
114
+ const [previewScale, setPreviewScale] = useState(1);
115
+ const allMeta = getAllBlockMeta();
116
+ const blocks = useMemo(() => {
117
+ return allTypes.map((type) => ({
118
+ type,
119
+ meta: allMeta[type],
120
+ defaultProps: defaultPropsForType(type),
121
+ }));
122
+ }, [allMeta, allTypes]);
123
+ // Group blocks by category
124
+ const groupedBlocks = useMemo(() => {
125
+ const groups = {};
126
+ for (const b of blocks) {
127
+ const cat = b.meta?.category ?? "other";
128
+ (groups[cat] ??= []).push(b);
129
+ }
130
+ const sorted = [];
131
+ for (const cat of CATEGORY_ORDER) {
132
+ if (groups[cat])
133
+ sorted.push([cat, groups[cat]]);
134
+ }
135
+ for (const [cat, items] of Object.entries(groups)) {
136
+ if (!CATEGORY_ORDER.includes(cat))
137
+ sorted.push([cat, items]);
138
+ }
139
+ return sorted;
140
+ }, [blocks]);
141
+ const activeType = selectedType && blocks.some((b) => b.type === selectedType)
142
+ ? selectedType
143
+ : blocks[0]?.type ?? "";
144
+ const activeBlock = blocks.find((b) => b.type === activeType);
145
+ const activeMeta = activeBlock?.meta;
146
+ const activeDefaultProps = activeBlock?.defaultProps ?? {};
147
+ const activeFixture = CATALOGUE_FIXTURES[activeType] ?? {};
148
+ const activeRawProps = { ...activeDefaultProps, ...activeFixture, ...(liveProps[activeType] ?? {}) };
149
+ const activeProps = withDummyImages(activeRawProps, activeMeta);
150
+ // Build enum variants — for each enum field with options, render one preview per value
151
+ const enumVariants = useMemo(() => {
152
+ if (!activeMeta)
153
+ return [];
154
+ const enumFields = Object.entries(activeMeta.fields)
155
+ .filter(([, fm]) => fm.kind === "enum" && Array.isArray(fm.options) && fm.options.length > 1);
156
+ if (enumFields.length === 0)
157
+ return [];
158
+ // Use the first enum field that has visual variants (e.g. imagePosition)
159
+ const [fieldKey, fm] = enumFields[0];
160
+ return fm.options.map((option) => ({
161
+ label: `${fm.label ?? fieldKey}: ${option}`,
162
+ props: { ...activeProps, [fieldKey]: option },
163
+ }));
164
+ }, [activeMeta, activeProps]);
165
+ // Scale preview to fit container
166
+ useEffect(() => {
167
+ const wrap = previewWrapRef.current;
168
+ if (!wrap)
169
+ return;
170
+ const measure = () => {
171
+ const wrapW = wrap.clientWidth;
172
+ setPreviewScale(Math.min(1, wrapW / viewportWidth));
173
+ };
174
+ measure();
175
+ const ro = new ResizeObserver(measure);
176
+ ro.observe(wrap);
177
+ return () => ro.disconnect();
178
+ }, [activeType, viewportWidth]);
179
+ const updateProp = useCallback((key, value) => {
180
+ setLiveProps((prev) => ({
181
+ ...prev,
182
+ [activeType]: { ...(prev[activeType] ?? {}), [key]: value },
183
+ }));
184
+ }, [activeType]);
185
+ const updateListItemProp = useCallback((listKey, index, itemKey, value) => {
186
+ setLiveProps((prev) => {
187
+ const current = { ...(prev[activeType] ?? {}) };
188
+ const list = Array.isArray(current[listKey])
189
+ ? [...current[listKey]]
190
+ : Array.isArray(activeDefaultProps[listKey])
191
+ ? [...activeDefaultProps[listKey]]
192
+ : [];
193
+ if (list[index]) {
194
+ list[index] = { ...list[index], [itemKey]: value };
195
+ }
196
+ current[listKey] = list;
197
+ return { ...prev, [activeType]: current };
198
+ });
199
+ }, [activeType, activeDefaultProps]);
200
+ const resetProps = useCallback(() => {
201
+ setLiveProps((prev) => {
202
+ const next = { ...prev };
203
+ delete next[activeType];
204
+ return next;
205
+ });
206
+ }, [activeType]);
207
+ return (_jsxs("div", { className: `cat-root${darkMode ? " dark" : ""}`, children: [_jsx("style", { children: catalogueCSS }), _jsx("header", { className: "cat-header", children: _jsxs("div", { className: "cat-header-row", children: [_jsxs("div", { children: [_jsx("h1", { className: "cat-title", children: "Block Catalogue" }), _jsxs("p", { className: "cat-subtitle", children: [allTypes.length, " blocks \u00B7 rendered with live props"] })] }), _jsx("button", { onClick: () => setDarkMode((d) => !d), className: "cat-theme-toggle", title: darkMode ? "Switch to light mode" : "Switch to dark mode", "aria-label": darkMode ? "Switch to light mode" : "Switch to dark mode", children: darkMode ? (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "4" }), _jsx("path", { d: "M12 2v2" }), _jsx("path", { d: "M12 20v2" }), _jsx("path", { d: "m4.93 4.93 1.41 1.41" }), _jsx("path", { d: "m17.66 17.66 1.41 1.41" }), _jsx("path", { d: "M2 12h2" }), _jsx("path", { d: "M20 12h2" }), _jsx("path", { d: "m6.34 17.66-1.41 1.41" }), _jsx("path", { d: "m19.07 4.93-1.41 1.41" })] })) : (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" }) })) })] }) }), _jsxs("div", { className: "cat-layout", children: [_jsx("nav", { className: "cat-sidebar", children: groupedBlocks.map(([cat, items]) => (_jsxs("div", { className: "cat-sidebar-group", children: [_jsx("p", { className: "cat-sidebar-heading", children: CATEGORY_LABELS[cat] ?? cat }), items.map(({ type, meta }) => (_jsxs("button", { onClick: () => setSelectedType(type), className: `cat-sidebar-item${type === activeType ? " active" : ""}`, children: [meta?.displayName ?? type, meta?.chrome && _jsx("span", { className: "cat-chrome-badge", children: "chrome" })] }, type)))] }, cat))) }), _jsx("main", { className: "cat-main", children: activeBlock && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "cat-info-bar", children: [_jsx("h2", { className: "cat-block-name", children: activeMeta?.displayName ?? activeType }), activeMeta?.category && (_jsx("span", { className: "cat-badge", children: activeMeta.category })), activeMeta?.chrome && (_jsx("span", { className: "cat-badge cat-badge-chrome", children: "chrome" })), _jsx("code", { className: "cat-type-id", children: activeType }), _jsxs("div", { className: "cat-viewport-toggle", children: [_jsx("button", { className: `cat-viewport-btn${viewportWidth === 390 ? " active" : ""}`, onClick: () => setViewportWidth(390), title: "Mobile (390px)", "aria-label": "Mobile view", children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "5", y: "2", width: "14", height: "20", rx: "2", ry: "2" }), _jsx("path", { d: "M12 18h.01" })] }) }), _jsx("button", { className: `cat-viewport-btn${viewportWidth === 768 ? " active" : ""}`, onClick: () => setViewportWidth(768), title: "Tablet (768px)", "aria-label": "Tablet view", children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "4", y: "2", width: "16", height: "20", rx: "2", ry: "2" }), _jsx("path", { d: "M12 18h.01" })] }) }), _jsx("button", { className: `cat-viewport-btn${viewportWidth === 1280 ? " active" : ""}`, onClick: () => setViewportWidth(1280), title: "Desktop (1280px)", "aria-label": "Desktop view", children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2", ry: "2" }), _jsx("path", { d: "M8 21h8" }), _jsx("path", { d: "M12 17v4" })] }) })] })] }), activeMeta?.description && (_jsx("p", { className: "cat-desc", children: activeMeta.description })), enumVariants.length > 0 ? (enumVariants.map(({ label, props: variantProps }) => (_jsxs("div", { className: "cat-variant", children: [_jsx("p", { className: "cat-variant-label", children: label }), _jsx(CataloguePreview, { viewportWidth: viewportWidth, darkMode: darkMode, previewWrapRef: previewWrapRef, previewScale: previewScale, children: _jsx(SharedBlockRenderer, { block: { id: `cat-${activeType}-${label}`, type: activeType, props: variantProps } }) })] }, label)))) : (_jsx(CataloguePreview, { viewportWidth: viewportWidth, darkMode: darkMode, previewWrapRef: previewWrapRef, previewScale: previewScale, children: _jsx(SharedBlockRenderer, { block: { id: `cat-${activeType}`, type: activeType, props: activeProps } }) }))] })) }), _jsx("aside", { className: "cat-props", children: activeBlock && activeMeta && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "cat-props-header", children: [_jsx("h3", { className: "cat-props-title", children: "Props" }), _jsx("button", { onClick: resetProps, className: "cat-props-reset", children: "Reset" })] }), Object.entries(activeMeta.fields).map(([key, fm]) => (_jsx(PropField, { fieldKey: key, meta: fm, value: activeProps[key], onChange: (v) => updateProp(key, v) }, key))), activeMeta.listFields &&
208
+ Object.entries(activeMeta.listFields).map(([listKey, lm]) => {
209
+ const items = Array.isArray(activeProps[listKey])
210
+ ? activeProps[listKey]
211
+ : [];
212
+ return (_jsxs("div", { className: "cat-list-section", children: [_jsxs("p", { className: "cat-list-label", children: [lm.label ?? listKey, " ", _jsxs("span", { className: "cat-list-count", children: [items.length, " items"] })] }), items.map((item, idx) => (_jsxs("div", { className: "cat-list-item", children: [_jsxs("p", { className: "cat-list-item-idx", children: ["#", idx + 1] }), Object.entries(lm.itemFields).map(([itemKey, itemMeta]) => (_jsx(PropField, { fieldKey: itemKey, meta: itemMeta, value: item[itemKey], onChange: (v) => updateListItemProp(listKey, idx, itemKey, v) }, itemKey)))] }, idx)))] }, listKey));
213
+ })] })) })] })] }));
214
+ }
215
+ // ---------------------------------------------------------------------------
216
+ // Prop field editor component
217
+ // ---------------------------------------------------------------------------
218
+ function PropField({ fieldKey, meta, value, onChange, }) {
219
+ const label = meta.label ?? fieldKey;
220
+ if (meta.kind === "enum" && meta.options) {
221
+ return (_jsxs("div", { className: "cat-prop-row", children: [_jsxs("label", { className: "cat-prop-label", children: [label, _jsx("span", { className: "cat-prop-kind", children: meta.kind })] }), _jsx("select", { className: "cat-prop-select", value: String(value ?? ""), onChange: (e) => onChange(e.target.value), children: meta.options.map((opt) => (_jsx("option", { value: opt, children: opt }, opt))) })] }));
222
+ }
223
+ if (meta.kind === "number") {
224
+ return (_jsxs("div", { className: "cat-prop-row", children: [_jsxs("label", { className: "cat-prop-label", children: [label, _jsx("span", { className: "cat-prop-kind", children: meta.kind })] }), _jsx("input", { type: "number", className: "cat-prop-input", value: value != null ? String(value) : "", onChange: (e) => onChange(Number(e.target.value)) })] }));
225
+ }
226
+ if (meta.kind === "color") {
227
+ return (_jsxs("div", { className: "cat-prop-row", children: [_jsxs("label", { className: "cat-prop-label", children: [label, _jsx("span", { className: "cat-prop-kind", children: meta.kind })] }), _jsxs("div", { className: "cat-prop-color-wrap", children: [_jsx("input", { type: "color", className: "cat-prop-color", value: String(value ?? "#000000"), onChange: (e) => onChange(e.target.value) }), _jsx("input", { type: "text", className: "cat-prop-input cat-prop-color-text", value: String(value ?? ""), onChange: (e) => onChange(e.target.value) })] })] }));
228
+ }
229
+ const isMultiline = meta.multiline || meta.kind === "richtext";
230
+ return (_jsxs("div", { className: "cat-prop-row", children: [_jsxs("label", { className: "cat-prop-label", children: [label, _jsx("span", { className: "cat-prop-kind", children: meta.kind })] }), isMultiline ? (_jsx("textarea", { className: "cat-prop-textarea", value: String(value ?? ""), rows: 3, onChange: (e) => onChange(e.target.value) })) : (_jsx("input", { type: "text", className: "cat-prop-input", value: String(value ?? ""), onChange: (e) => onChange(e.target.value) }))] }));
231
+ }
232
+ // ---------------------------------------------------------------------------
233
+ // Catalogue preview — renders children inside an iframe so CSS media queries
234
+ // respond to the iframe viewport width, not the browser viewport.
235
+ // ---------------------------------------------------------------------------
236
+ function CataloguePreview({ viewportWidth, darkMode, previewWrapRef, previewScale, children }) {
237
+ const iframeRef = useRef(null);
238
+ const [mountNode, setMountNode] = useState(null);
239
+ const [contentHeight, setContentHeight] = useState(200);
240
+ // Initialize the iframe: copy parent styles, create mount node, observe height
241
+ useEffect(() => {
242
+ const iframe = iframeRef.current;
243
+ if (!iframe)
244
+ return;
245
+ const setup = () => {
246
+ const doc = iframe.contentDocument;
247
+ if (!doc)
248
+ return;
249
+ // Copy parent stylesheets into the iframe
250
+ doc.head.innerHTML = "";
251
+ for (const sheet of Array.from(document.styleSheets)) {
252
+ try {
253
+ if (sheet.href) {
254
+ const link = doc.createElement("link");
255
+ link.rel = "stylesheet";
256
+ link.href = sheet.href;
257
+ doc.head.appendChild(link);
258
+ }
259
+ else if (sheet.ownerNode instanceof HTMLStyleElement) {
260
+ const style = doc.createElement("style");
261
+ style.textContent = sheet.ownerNode.textContent;
262
+ doc.head.appendChild(style);
263
+ }
264
+ }
265
+ catch { /* cross-origin sheets — skip */ }
266
+ }
267
+ doc.body.style.margin = "0";
268
+ doc.body.style.overflow = "hidden";
269
+ doc.documentElement.classList.toggle("dark", darkMode);
270
+ let mount = doc.getElementById("cat-iframe-root");
271
+ if (!mount) {
272
+ mount = doc.createElement("div");
273
+ mount.id = "cat-iframe-root";
274
+ doc.body.appendChild(mount);
275
+ }
276
+ setMountNode(mount);
277
+ // Observe content height changes
278
+ const ro = new ResizeObserver(() => {
279
+ const h = mount.scrollHeight || doc.body.scrollHeight;
280
+ if (h > 0)
281
+ setContentHeight(h);
282
+ });
283
+ ro.observe(mount);
284
+ ro.observe(doc.body);
285
+ return () => ro.disconnect();
286
+ };
287
+ iframe.addEventListener("load", setup);
288
+ iframe.src = "about:blank";
289
+ return () => iframe.removeEventListener("load", setup);
290
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
291
+ // Sync dark mode into iframe
292
+ useEffect(() => {
293
+ const doc = iframeRef.current?.contentDocument;
294
+ if (doc)
295
+ doc.documentElement.classList.toggle("dark", darkMode);
296
+ }, [darkMode]);
297
+ // Re-sync parent styles into iframe when they change (HMR)
298
+ useEffect(() => {
299
+ const doc = iframeRef.current?.contentDocument;
300
+ if (!doc || !mountNode)
301
+ return;
302
+ const observer = new MutationObserver(() => {
303
+ const existingLinks = new Set();
304
+ doc.head.querySelectorAll("link[rel=stylesheet]").forEach((el) => existingLinks.add(el.href));
305
+ for (const sheet of Array.from(document.styleSheets)) {
306
+ try {
307
+ if (sheet.href && !existingLinks.has(sheet.href)) {
308
+ const link = doc.createElement("link");
309
+ link.rel = "stylesheet";
310
+ link.href = sheet.href;
311
+ doc.head.appendChild(link);
312
+ }
313
+ else if (sheet.ownerNode instanceof HTMLStyleElement) {
314
+ // Find matching style tag by data attribute or position
315
+ const id = sheet.ownerNode.dataset.n || sheet.ownerNode.dataset.href;
316
+ if (id) {
317
+ const existing = doc.head.querySelector(`style[data-cat-src="${id}"]`);
318
+ if (existing) {
319
+ existing.textContent = sheet.ownerNode.textContent;
320
+ }
321
+ else {
322
+ const style = doc.createElement("style");
323
+ style.dataset.catSrc = id;
324
+ style.textContent = sheet.ownerNode.textContent;
325
+ doc.head.appendChild(style);
326
+ }
327
+ }
328
+ }
329
+ }
330
+ catch { /* skip */ }
331
+ }
332
+ });
333
+ observer.observe(document.head, { childList: true, subtree: true });
334
+ return () => observer.disconnect();
335
+ }, [mountNode]);
336
+ // Initialize interactive blocks (carousel, etc.) after portal renders
337
+ useEffect(() => {
338
+ if (!mountNode)
339
+ return;
340
+ // Defer to let React finish flushing the portal content
341
+ const id = requestAnimationFrame(() => { initCarousels(mountNode); initTabs(mountNode); });
342
+ return () => cancelAnimationFrame(id);
343
+ });
344
+ const scaledHeight = contentHeight * previewScale;
345
+ return (_jsx("div", { className: "cat-preview-wrap", ref: previewWrapRef, style: { height: scaledHeight }, children: _jsxs("div", { className: "cat-preview-inner", style: {
346
+ width: viewportWidth,
347
+ transform: `scale(${previewScale})`,
348
+ transformOrigin: "top left",
349
+ }, children: [_jsx("iframe", { ref: iframeRef, title: "Block preview", style: { width: "100%", height: contentHeight, border: "none", display: "block" } }), mountNode && createPortal(children, mountNode)] }) }));
350
+ }
351
+ // ---------------------------------------------------------------------------
352
+ // Styles
353
+ // ---------------------------------------------------------------------------
354
+ const catalogueCSS = /* css */ `
355
+ .cat-root {
356
+ min-height: 100vh;
357
+ background: var(--bg-0);
358
+ overflow-x: hidden;
359
+ }
360
+ .cat-root * {
361
+ box-sizing: border-box;
362
+ }
363
+
364
+ /* ---- Header (non-sticky — site nav provides sticky chrome) ---- */
365
+ .cat-header {
366
+ border-bottom: 1px solid var(--surface-border);
367
+ background: var(--nav-bg, var(--bg-0));
368
+ padding: 12px 24px;
369
+ }
370
+ .cat-header-row {
371
+ display: flex;
372
+ align-items: center;
373
+ justify-content: space-between;
374
+ gap: 16px;
375
+ }
376
+ .cat-title {
377
+ margin: 0;
378
+ font-size: 1.125rem;
379
+ font-weight: 800;
380
+ color: var(--heading);
381
+ letter-spacing: -0.02em;
382
+ }
383
+ .cat-subtitle {
384
+ margin: 2px 0 0;
385
+ font-size: 0.75rem;
386
+ color: var(--caption);
387
+ }
388
+
389
+ /* ---- Theme toggle ---- */
390
+ .cat-theme-toggle {
391
+ display: inline-flex;
392
+ align-items: center;
393
+ justify-content: center;
394
+ padding: 6px;
395
+ width: 30px;
396
+ height: 30px;
397
+ border: 1px solid var(--surface-border);
398
+ border-radius: 8px;
399
+ background: var(--bg-100);
400
+ color: var(--text-300);
401
+ cursor: pointer;
402
+ line-height: 1;
403
+ transition: border-color 0.15s, color 0.15s;
404
+ }
405
+ .cat-theme-toggle svg {
406
+ width: 16px;
407
+ height: 16px;
408
+ }
409
+ .cat-theme-toggle:hover {
410
+ color: var(--text-100);
411
+ border-color: var(--brand);
412
+ }
413
+
414
+ /* ---- Three-column layout ---- */
415
+ .cat-layout {
416
+ display: grid;
417
+ grid-template-columns: 200px 1fr 320px;
418
+ height: calc(100vh - 110px);
419
+ overflow: hidden;
420
+ }
421
+
422
+ /* ---- Left sidebar: grouped block list ---- */
423
+ .cat-sidebar {
424
+ border-right: 1px solid var(--surface-border);
425
+ padding: 12px 0;
426
+ overflow-y: auto;
427
+ background: var(--bg-0);
428
+ }
429
+ .cat-sidebar-group {
430
+ margin-bottom: 4px;
431
+ }
432
+ .cat-sidebar-heading {
433
+ margin: 0;
434
+ padding: 8px 16px 4px;
435
+ font-size: 0.625rem;
436
+ font-weight: 700;
437
+ text-transform: uppercase;
438
+ letter-spacing: 0.08em;
439
+ color: var(--caption);
440
+ }
441
+ .cat-sidebar-item {
442
+ display: block;
443
+ width: 100%;
444
+ padding: 6px 16px 6px 24px;
445
+ border: none;
446
+ background: transparent;
447
+ color: var(--text-300);
448
+ font-size: 0.8125rem;
449
+ font-family: inherit;
450
+ cursor: pointer;
451
+ text-align: left;
452
+ transition: background 0.1s, color 0.1s;
453
+ }
454
+ .cat-sidebar-item:hover {
455
+ background: var(--bg-100);
456
+ color: var(--text-100);
457
+ }
458
+ .cat-sidebar-item.active {
459
+ background: var(--brand-subtle, var(--bg-200));
460
+ color: var(--brand);
461
+ font-weight: 600;
462
+ }
463
+
464
+ /* ---- Enum variant labels ---- */
465
+ .cat-variant {
466
+ margin-bottom: 20px;
467
+ }
468
+ .cat-variant:last-child {
469
+ margin-bottom: 0;
470
+ }
471
+ .cat-variant-label {
472
+ margin: 0 0 6px;
473
+ font-size: 0.6875rem;
474
+ font-weight: 700;
475
+ text-transform: uppercase;
476
+ letter-spacing: 0.06em;
477
+ color: var(--caption);
478
+ }
479
+
480
+ /* ---- Viewport toggle ---- */
481
+ .cat-viewport-toggle {
482
+ display: flex;
483
+ gap: 2px;
484
+ margin-left: 8px;
485
+ border: 1px solid var(--surface-border);
486
+ border-radius: 6px;
487
+ padding: 2px;
488
+ background: var(--bg-100);
489
+ }
490
+ .cat-viewport-btn {
491
+ display: flex;
492
+ align-items: center;
493
+ justify-content: center;
494
+ width: 28px;
495
+ height: 24px;
496
+ border: none;
497
+ border-radius: 4px;
498
+ background: transparent;
499
+ color: var(--text-400);
500
+ cursor: pointer;
501
+ padding: 0;
502
+ transition: all 0.15s;
503
+ }
504
+ .cat-viewport-btn svg {
505
+ width: 14px;
506
+ height: 14px;
507
+ }
508
+ .cat-viewport-btn:hover {
509
+ color: var(--text-100);
510
+ background: var(--bg-200);
511
+ }
512
+ .cat-viewport-btn.active {
513
+ color: var(--brand);
514
+ background: var(--bg-0);
515
+ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
516
+ }
517
+
518
+ /* ---- Chrome badge ---- */
519
+ .cat-chrome-badge {
520
+ display: inline-block;
521
+ font-size: 0.5rem;
522
+ font-weight: 700;
523
+ text-transform: uppercase;
524
+ letter-spacing: 0.06em;
525
+ padding: 1px 5px;
526
+ border-radius: 3px;
527
+ background: var(--surface-border);
528
+ color: var(--caption);
529
+ margin-left: 6px;
530
+ vertical-align: middle;
531
+ }
532
+ .cat-badge-chrome {
533
+ background: var(--surface-border);
534
+ color: var(--caption);
535
+ }
536
+
537
+ /* ---- Center: preview ---- */
538
+ .cat-main {
539
+ overflow-y: auto;
540
+ padding: 20px 24px;
541
+ min-width: 0;
542
+ }
543
+ .cat-info-bar {
544
+ display: flex;
545
+ align-items: baseline;
546
+ gap: 10px;
547
+ flex-wrap: wrap;
548
+ margin-bottom: 4px;
549
+ }
550
+ .cat-block-name {
551
+ margin: 0;
552
+ font-size: 1.125rem;
553
+ font-weight: 700;
554
+ color: var(--heading);
555
+ }
556
+ .cat-badge {
557
+ font-size: 0.625rem;
558
+ font-weight: 600;
559
+ text-transform: uppercase;
560
+ letter-spacing: 0.05em;
561
+ padding: 2px 8px;
562
+ border-radius: 4px;
563
+ background: var(--brand-subtle);
564
+ color: var(--brand);
565
+ white-space: nowrap;
566
+ }
567
+ .cat-type-id {
568
+ font-size: 0.75rem;
569
+ color: var(--text-400);
570
+ font-family: var(--font-mono, monospace);
571
+ margin-left: auto;
572
+ }
573
+ .cat-desc {
574
+ margin: 0 0 12px;
575
+ font-size: 0.8125rem;
576
+ color: var(--caption);
577
+ line-height: 1.5;
578
+ }
579
+
580
+ /* ---- Scaled preview wrapper ---- */
581
+ .cat-preview-wrap {
582
+ border: 1px solid var(--surface-border);
583
+ border-radius: 10px;
584
+ overflow: hidden;
585
+ background: var(--bg-0);
586
+ position: relative;
587
+ }
588
+
589
+ /* ---- Right: prop editor ---- */
590
+ .cat-props {
591
+ border-left: 1px solid var(--surface-border);
592
+ overflow-y: auto;
593
+ padding: 16px;
594
+ background: var(--bg-0);
595
+ }
596
+ .cat-props-header {
597
+ display: flex;
598
+ align-items: center;
599
+ justify-content: space-between;
600
+ margin-bottom: 16px;
601
+ padding-bottom: 10px;
602
+ border-bottom: 1px solid var(--surface-border);
603
+ }
604
+ .cat-props-title {
605
+ margin: 0;
606
+ font-size: 0.8125rem;
607
+ font-weight: 700;
608
+ text-transform: uppercase;
609
+ letter-spacing: 0.06em;
610
+ color: var(--caption);
611
+ }
612
+ .cat-props-reset {
613
+ padding: 3px 10px;
614
+ border: 1px solid var(--surface-border);
615
+ border-radius: 5px;
616
+ background: transparent;
617
+ color: var(--caption);
618
+ font-size: 0.6875rem;
619
+ font-family: inherit;
620
+ cursor: pointer;
621
+ transition: all 0.15s;
622
+ }
623
+ .cat-props-reset:hover {
624
+ border-color: var(--brand);
625
+ color: var(--text-100);
626
+ }
627
+
628
+ /* ---- Prop rows ---- */
629
+ .cat-prop-row {
630
+ margin-bottom: 12px;
631
+ }
632
+ .cat-prop-label {
633
+ display: flex;
634
+ align-items: center;
635
+ gap: 6px;
636
+ font-size: 0.75rem;
637
+ font-weight: 600;
638
+ color: var(--text-200);
639
+ margin-bottom: 4px;
640
+ }
641
+ .cat-prop-kind {
642
+ font-size: 0.5625rem;
643
+ text-transform: uppercase;
644
+ letter-spacing: 0.04em;
645
+ opacity: 0.45;
646
+ font-weight: 500;
647
+ }
648
+ .cat-prop-input,
649
+ .cat-prop-select,
650
+ .cat-prop-textarea {
651
+ width: 100%;
652
+ padding: 6px 8px;
653
+ border: 1px solid var(--surface-border);
654
+ border-radius: 6px;
655
+ background: var(--bg-100);
656
+ color: var(--text-100);
657
+ font-size: 0.75rem;
658
+ font-family: var(--font-mono, monospace);
659
+ outline: none;
660
+ transition: border-color 0.15s;
661
+ }
662
+ .cat-prop-input:focus,
663
+ .cat-prop-select:focus,
664
+ .cat-prop-textarea:focus {
665
+ border-color: var(--brand);
666
+ }
667
+ .cat-prop-textarea {
668
+ resize: vertical;
669
+ min-height: 60px;
670
+ }
671
+ .cat-prop-color-wrap {
672
+ display: flex;
673
+ gap: 6px;
674
+ align-items: center;
675
+ }
676
+ .cat-prop-color {
677
+ width: 32px;
678
+ height: 32px;
679
+ border: 1px solid var(--surface-border);
680
+ border-radius: 6px;
681
+ padding: 2px;
682
+ cursor: pointer;
683
+ background: transparent;
684
+ }
685
+ .cat-prop-color-text {
686
+ flex: 1;
687
+ }
688
+
689
+ /* ---- List sections ---- */
690
+ .cat-list-section {
691
+ margin-top: 16px;
692
+ padding-top: 12px;
693
+ border-top: 1px solid var(--surface-border);
694
+ }
695
+ .cat-list-label {
696
+ margin: 0 0 10px;
697
+ font-size: 0.8125rem;
698
+ font-weight: 700;
699
+ color: var(--heading);
700
+ display: flex;
701
+ align-items: center;
702
+ gap: 8px;
703
+ }
704
+ .cat-list-count {
705
+ font-size: 0.625rem;
706
+ font-weight: 500;
707
+ color: var(--caption);
708
+ text-transform: uppercase;
709
+ letter-spacing: 0.04em;
710
+ }
711
+ .cat-list-item {
712
+ margin-bottom: 14px;
713
+ padding: 10px;
714
+ border: 1px solid var(--surface-border);
715
+ border-radius: 8px;
716
+ background: var(--bg-100);
717
+ }
718
+ .cat-list-item-idx {
719
+ margin: 0 0 8px;
720
+ font-size: 0.625rem;
721
+ font-weight: 700;
722
+ color: var(--caption);
723
+ text-transform: uppercase;
724
+ }
725
+
726
+ /* ---- Responsive ---- */
727
+ @media (max-width: 1100px) {
728
+ .cat-layout {
729
+ grid-template-columns: 180px 1fr 260px;
730
+ }
731
+ }
732
+ @media (max-width: 900px) {
733
+ .cat-header { padding: 10px 16px; }
734
+ .cat-layout {
735
+ grid-template-columns: 1fr;
736
+ height: auto;
737
+ }
738
+ .cat-sidebar { display: none; }
739
+ .cat-props { border-left: none; border-top: 1px solid var(--surface-border); }
740
+ }
741
+ `;