@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.
- package/LICENSE +201 -0
- package/dist/block-error-boundary.d.ts +18 -0
- package/dist/block-error-boundary.js +36 -0
- package/dist/blocks/_base.css +108 -0
- package/dist/blocks/_layout.css +47 -0
- package/dist/blocks/_shared.d.ts +24 -0
- package/dist/blocks/_shared.js +153 -0
- package/dist/blocks/_tokens.css +127 -0
- package/dist/blocks/banner/renderer.d.ts +2 -0
- package/dist/blocks/banner/renderer.js +21 -0
- package/dist/blocks/banner/styles.css +72 -0
- package/dist/blocks/block-image.d.ts +14 -0
- package/dist/blocks/block-image.js +10 -0
- package/dist/blocks/block-types.d.ts +2 -0
- package/dist/blocks/block-types.js +22 -0
- package/dist/blocks/card/renderer.d.ts +1 -0
- package/dist/blocks/card/renderer.js +11 -0
- package/dist/blocks/card/styles.css +94 -0
- package/dist/blocks/card-grid/renderer.d.ts +1 -0
- package/dist/blocks/card-grid/renderer.js +17 -0
- package/dist/blocks/card-grid/styles.css +38 -0
- package/dist/blocks/carousel/init.d.ts +8 -0
- package/dist/blocks/carousel/init.js +35 -0
- package/dist/blocks/carousel/renderer.d.ts +2 -0
- package/dist/blocks/carousel/renderer.js +19 -0
- package/dist/blocks/carousel/styles.css +136 -0
- package/dist/blocks/cta/renderer.d.ts +1 -0
- package/dist/blocks/cta/renderer.js +7 -0
- package/dist/blocks/cta/styles.css +111 -0
- package/dist/blocks/embed/renderer.d.ts +2 -0
- package/dist/blocks/embed/renderer.js +16 -0
- package/dist/blocks/embed/styles.css +58 -0
- package/dist/blocks/faq-accordion/renderer.d.ts +1 -0
- package/dist/blocks/faq-accordion/renderer.js +11 -0
- package/dist/blocks/faq-accordion/styles.css +68 -0
- package/dist/blocks/feature-grid/renderer.d.ts +1 -0
- package/dist/blocks/feature-grid/renderer.js +12 -0
- package/dist/blocks/feature-grid/styles.css +62 -0
- package/dist/blocks/footer/renderer.d.ts +1 -0
- package/dist/blocks/footer/renderer.js +19 -0
- package/dist/blocks/footer/styles.css +64 -0
- package/dist/blocks/gallery/renderer.d.ts +2 -0
- package/dist/blocks/gallery/renderer.js +21 -0
- package/dist/blocks/gallery/styles.css +59 -0
- package/dist/blocks/hero/renderer.d.ts +1 -0
- package/dist/blocks/hero/renderer.js +20 -0
- package/dist/blocks/hero/styles.css +137 -0
- package/dist/blocks/index.d.ts +5 -0
- package/dist/blocks/index.js +47 -0
- package/dist/blocks/quote/renderer.d.ts +2 -0
- package/dist/blocks/quote/renderer.js +9 -0
- package/dist/blocks/quote/styles.css +97 -0
- package/dist/blocks/rich-text/renderer.d.ts +1 -0
- package/dist/blocks/rich-text/renderer.js +15 -0
- package/dist/blocks/rich-text/styles.css +64 -0
- package/dist/blocks/site-header/renderer.d.ts +1 -0
- package/dist/blocks/site-header/renderer.js +64 -0
- package/dist/blocks/site-header/styles.css +345 -0
- package/dist/blocks/stats/renderer.d.ts +1 -0
- package/dist/blocks/stats/renderer.js +13 -0
- package/dist/blocks/stats/styles.css +64 -0
- package/dist/blocks/styles.css +23 -0
- package/dist/blocks/table/renderer.d.ts +2 -0
- package/dist/blocks/table/renderer.js +14 -0
- package/dist/blocks/table/styles.css +87 -0
- package/dist/blocks/tabs/init.d.ts +5 -0
- package/dist/blocks/tabs/init.js +38 -0
- package/dist/blocks/tabs/renderer.d.ts +2 -0
- package/dist/blocks/tabs/renderer.js +30 -0
- package/dist/blocks/tabs/styles.css +147 -0
- package/dist/blocks/testimonials/renderer.d.ts +1 -0
- package/dist/blocks/testimonials/renderer.js +15 -0
- package/dist/blocks/testimonials/styles.css +92 -0
- package/dist/blocks/two-column/renderer.d.ts +1 -0
- package/dist/blocks/two-column/renderer.js +41 -0
- package/dist/blocks/two-column/styles.css +83 -0
- package/dist/blocks/video/renderer.d.ts +2 -0
- package/dist/blocks/video/renderer.js +52 -0
- package/dist/blocks/video/styles.css +84 -0
- package/dist/blocks-hydrator.d.ts +5 -0
- package/dist/blocks-hydrator.js +15 -0
- package/dist/catalogue.d.ts +1 -0
- package/dist/catalogue.js +741 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/renderer.d.ts +12 -0
- package/dist/renderer.js +35 -0
- 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
|
+
`;
|