@drawnagency/primitives 0.1.66 → 0.1.67
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/dist/chunk-4VMAZZXT.js +968 -0
- package/dist/{chunk-VEIDANEU.js → chunk-KM6HOW4H.js} +126 -39
- package/dist/{chunk-DQTKWKSK.js → chunk-WJ7RZ6CC.js} +11 -2
- package/dist/components/primitives/defineHeadingSection.d.ts +2 -0
- package/dist/components/primitives/defineHeadingSection.d.ts.map +1 -1
- package/dist/components/primitives/index.js +1 -1
- package/dist/components/sections/Button/index.d.ts.map +1 -1
- package/dist/components/sections/Colors/index.d.ts.map +1 -1
- package/dist/components/sections/Colors/schema.d.ts.map +1 -1
- package/dist/components/sections/Container/index.d.ts.map +1 -1
- package/dist/components/sections/DoDontMedia/index.d.ts.map +1 -1
- package/dist/components/sections/IconList/IconList.d.ts +2 -1
- package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
- package/dist/components/sections/IconList/IconListSettings.d.ts +2 -1
- package/dist/components/sections/IconList/IconListSettings.d.ts.map +1 -1
- package/dist/components/sections/IconList/index.d.ts +1 -0
- package/dist/components/sections/IconList/index.d.ts.map +1 -1
- package/dist/components/sections/LinkHeading/index.d.ts.map +1 -1
- package/dist/components/sections/Media/index.d.ts.map +1 -1
- package/dist/components/sections/Prose/index.d.ts.map +1 -1
- package/dist/components/sections/Spacer/index.d.ts.map +1 -1
- package/dist/components/sections/SubHeading/index.d.ts.map +1 -1
- package/dist/components/sections/SubSubHeading/index.d.ts.map +1 -1
- package/dist/components/sections/all-sections.d.ts +1 -0
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.d.ts +1 -0
- package/dist/components/sections/brand-guide.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +1 -1
- package/dist/components/sections/register-schemas.js +11 -941
- package/dist/components/sections/register.js +36 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/index.js +20 -20
- package/dist/lib/index.js +2 -2
- package/dist/lib/insert-menu.d.ts +19 -0
- package/dist/lib/insert-menu.d.ts.map +1 -0
- package/dist/lib/registry.d.ts +23 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/types/database.d.ts +248 -34
- package/dist/types/database.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/primitives/defineHeadingSection.tsx +18 -2
- package/src/components/sections/Button/index.tsx +10 -3
- package/src/components/sections/Colors/index.tsx +15 -7
- package/src/components/sections/Colors/schema.ts +5 -2
- package/src/components/sections/Container/index.tsx +19 -3
- package/src/components/sections/DoDontMedia/index.tsx +18 -7
- package/src/components/sections/IconList/IconList.tsx +43 -19
- package/src/components/sections/IconList/IconListSettings.tsx +58 -11
- package/src/components/sections/IconList/index.tsx +51 -7
- package/src/components/sections/LinkHeading/index.tsx +2 -0
- package/src/components/sections/Media/index.tsx +15 -6
- package/src/components/sections/Prose/index.tsx +9 -1
- package/src/components/sections/Spacer/index.tsx +2 -0
- package/src/components/sections/SubHeading/index.tsx +2 -0
- package/src/components/sections/SubSubHeading/index.tsx +2 -0
- package/src/components/shell/EditorShell.tsx +13 -19
- package/src/lib/insert-menu.ts +42 -0
- package/src/lib/registry.ts +24 -0
- package/src/types/database.ts +236 -36
- package/dist/{chunk-ZYXLNJ5M.js → chunk-AFK45HX2.js} +3 -3
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
import {
|
|
2
|
+
brandGuideSectionDefs,
|
|
3
|
+
pluralize,
|
|
4
|
+
stripHtmlToPlainText,
|
|
5
|
+
truncate
|
|
6
|
+
} from "./chunk-KM6HOW4H.js";
|
|
7
|
+
import {
|
|
8
|
+
DeleteButton,
|
|
9
|
+
DragHandle,
|
|
10
|
+
Popover,
|
|
11
|
+
PopoverItem,
|
|
12
|
+
SettingsButton,
|
|
13
|
+
SettingsForm,
|
|
14
|
+
buildBlockDragData,
|
|
15
|
+
buildBlockDropData,
|
|
16
|
+
canDropBlock,
|
|
17
|
+
isBlockDragData
|
|
18
|
+
} from "./chunk-2BHPWO7M.js";
|
|
19
|
+
import {
|
|
20
|
+
EditableRichText,
|
|
21
|
+
defineHeadingSection
|
|
22
|
+
} from "./chunk-WJ7RZ6CC.js";
|
|
23
|
+
import {
|
|
24
|
+
EditablePlainText,
|
|
25
|
+
MAX_CONTAINER_COLUMNS,
|
|
26
|
+
MediaBlock,
|
|
27
|
+
colSpanClass,
|
|
28
|
+
containerGridClass,
|
|
29
|
+
spacerHiddenClass
|
|
30
|
+
} from "./chunk-XNGG5AGI.js";
|
|
31
|
+
import {
|
|
32
|
+
DEFAULT_LINK,
|
|
33
|
+
LinkValueSchema
|
|
34
|
+
} from "./chunk-3DCG4DNU.js";
|
|
35
|
+
import {
|
|
36
|
+
sanitizeHtml
|
|
37
|
+
} from "./chunk-53DKVAB6.js";
|
|
38
|
+
import {
|
|
39
|
+
cn
|
|
40
|
+
} from "./chunk-7EOSDFS3.js";
|
|
41
|
+
import {
|
|
42
|
+
getSectionSchema
|
|
43
|
+
} from "./chunk-TZQPOR5A.js";
|
|
44
|
+
import {
|
|
45
|
+
SingleMediaReferenceSchema
|
|
46
|
+
} from "./chunk-TEYTLQVH.js";
|
|
47
|
+
import {
|
|
48
|
+
defineSection,
|
|
49
|
+
getSection
|
|
50
|
+
} from "./chunk-VKAGMEKE.js";
|
|
51
|
+
|
|
52
|
+
// src/components/sections/LinkHeading/index.tsx
|
|
53
|
+
import { Heading1 } from "lucide-react";
|
|
54
|
+
import { jsx } from "react/jsx-runtime";
|
|
55
|
+
var LinkHeading_default = defineHeadingSection({
|
|
56
|
+
type: "link_heading",
|
|
57
|
+
label: "Link Heading",
|
|
58
|
+
icon: /* @__PURE__ */ jsx(Heading1, { size: 18 }),
|
|
59
|
+
navRole: "h1",
|
|
60
|
+
tag: "h2",
|
|
61
|
+
placeholder: "Section heading",
|
|
62
|
+
defaultHeading: "New Section",
|
|
63
|
+
authoringNote: "A top-level section heading (renders an <h2>, reported to navigation as h1). Use it to begin a major named section of the guide; its text becomes a navigation entry."
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// src/components/sections/SubHeading/index.tsx
|
|
67
|
+
import { Heading2 } from "lucide-react";
|
|
68
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
69
|
+
var SubHeading_default = defineHeadingSection({
|
|
70
|
+
type: "sub_heading",
|
|
71
|
+
label: "Sub Heading",
|
|
72
|
+
icon: /* @__PURE__ */ jsx2(Heading2, { size: 18 }),
|
|
73
|
+
navRole: "h2",
|
|
74
|
+
tag: "h3",
|
|
75
|
+
placeholder: "Sub heading",
|
|
76
|
+
defaultHeading: "New Sub Heading",
|
|
77
|
+
navExclusion: true,
|
|
78
|
+
authoringNote: "A second-level heading (renders <h3>, nav level h2). Use under a Link Heading to divide a section; toggle Exclude from navigation to keep it out of the nav tree."
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// src/components/sections/SubSubHeading/index.tsx
|
|
82
|
+
import { Heading3 } from "lucide-react";
|
|
83
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
84
|
+
var SubSubHeading_default = defineHeadingSection({
|
|
85
|
+
type: "sub_sub_heading",
|
|
86
|
+
label: "Sub Sub Heading",
|
|
87
|
+
icon: /* @__PURE__ */ jsx3(Heading3, { size: 18 }),
|
|
88
|
+
navRole: "h3",
|
|
89
|
+
tag: "h4",
|
|
90
|
+
placeholder: "Sub sub heading",
|
|
91
|
+
defaultHeading: "New Sub Sub Heading",
|
|
92
|
+
navExclusion: true,
|
|
93
|
+
authoringNote: "A third-level heading (renders <h4>, nav level h3). Use for the finest-grained labeled subsection; toggle Exclude from navigation to omit it from the nav tree."
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// src/components/sections/Prose/index.tsx
|
|
97
|
+
import { z } from "zod";
|
|
98
|
+
import { AlignLeft } from "lucide-react";
|
|
99
|
+
|
|
100
|
+
// src/components/sections/Prose/Prose.tsx
|
|
101
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
102
|
+
function Prose({ body, onChange }) {
|
|
103
|
+
if (onChange) {
|
|
104
|
+
return /* @__PURE__ */ jsx4("div", { className: "prose-content", children: /* @__PURE__ */ jsx4(
|
|
105
|
+
EditableRichText,
|
|
106
|
+
{
|
|
107
|
+
value: body,
|
|
108
|
+
onChange: (html) => onChange({ type: "prose", content: { body: html } }),
|
|
109
|
+
isEditMode: true,
|
|
110
|
+
preset: "rich",
|
|
111
|
+
placeholder: "Start writing..."
|
|
112
|
+
}
|
|
113
|
+
) });
|
|
114
|
+
}
|
|
115
|
+
return /* @__PURE__ */ jsx4(
|
|
116
|
+
"div",
|
|
117
|
+
{
|
|
118
|
+
className: "prose-content",
|
|
119
|
+
dangerouslySetInnerHTML: { __html: sanitizeHtml(body) }
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/components/sections/Prose/index.tsx
|
|
125
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
126
|
+
var schema = z.object({
|
|
127
|
+
type: z.literal("prose"),
|
|
128
|
+
content: z.object({
|
|
129
|
+
body: z.string().describe(
|
|
130
|
+
"Rich-text HTML for the paragraph block. Author with the editor; stored as sanitized HTML."
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
var Prose_default = defineSection({
|
|
135
|
+
type: "prose",
|
|
136
|
+
label: "Prose",
|
|
137
|
+
icon: /* @__PURE__ */ jsx5(AlignLeft, { size: 18 }),
|
|
138
|
+
schema,
|
|
139
|
+
authoringNote: "A block of rich-text body copy (paragraphs, lists, inline emphasis, links). Use for any explanatory prose; author with the editor, not raw HTML.",
|
|
140
|
+
richTextFields: ["body"],
|
|
141
|
+
component: ({ content, onChange }) => /* @__PURE__ */ jsx5(Prose, { body: content.content.body, onChange: onChange ? (c) => onChange(c) : void 0 }),
|
|
142
|
+
defaults: () => ({ type: "prose", content: { body: "<p></p>" } }),
|
|
143
|
+
getLabel: (content) => truncate(stripHtmlToPlainText(content.content.body), 60)
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// src/components/sections/Media/index.tsx
|
|
147
|
+
import { z as z2 } from "zod";
|
|
148
|
+
import { Image as ImageIcon } from "lucide-react";
|
|
149
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
150
|
+
var schema2 = z2.object({
|
|
151
|
+
type: z2.literal("media"),
|
|
152
|
+
content: z2.object({
|
|
153
|
+
ref: SingleMediaReferenceSchema.describe(
|
|
154
|
+
"The image or video asset to display (a media reference by id). Required."
|
|
155
|
+
),
|
|
156
|
+
link: LinkValueSchema.describe(
|
|
157
|
+
"Optional link wrapping the media \u2014 an internal page or a safe external URL."
|
|
158
|
+
).optional()
|
|
159
|
+
}),
|
|
160
|
+
options: z2.object({
|
|
161
|
+
square: z2.boolean().describe("When true, crop the media to a 1:1 square aspect ratio.").optional(),
|
|
162
|
+
showCaption: z2.boolean().describe("When true, render the asset's caption beneath it.").optional(),
|
|
163
|
+
border: z2.boolean().describe("When true, draw a border around the media.").optional(),
|
|
164
|
+
objectFit: z2.enum(["cover", "contain"]).describe("How the image fills its box: 'contain' (fit whole image) or 'cover' (fill and crop).").optional()
|
|
165
|
+
}).default({})
|
|
166
|
+
});
|
|
167
|
+
var Media_default = defineSection({
|
|
168
|
+
type: "media",
|
|
169
|
+
label: "Media",
|
|
170
|
+
icon: /* @__PURE__ */ jsx6(ImageIcon, { size: 18 }),
|
|
171
|
+
schema: schema2,
|
|
172
|
+
authoringNote: "A single image or video with optional caption and link. Use to show one asset; for several assets side by side, place multiple Media blocks inside a Container.",
|
|
173
|
+
component: ({ content, options, onChange }) => /* @__PURE__ */ jsx6(
|
|
174
|
+
MediaBlock,
|
|
175
|
+
{
|
|
176
|
+
refValue: content.content.ref,
|
|
177
|
+
options: options ?? {},
|
|
178
|
+
link: content.content.link,
|
|
179
|
+
isEditMode: !!onChange,
|
|
180
|
+
onRefChange: onChange ? (ref) => onChange({ ...content, content: { ...content.content, ref } }) : void 0
|
|
181
|
+
}
|
|
182
|
+
),
|
|
183
|
+
defaults: () => ({
|
|
184
|
+
type: "media",
|
|
185
|
+
content: { ref: { type: "image", imageId: "" } },
|
|
186
|
+
options: {}
|
|
187
|
+
}),
|
|
188
|
+
getLabel: () => "Media",
|
|
189
|
+
getThumbnails: (content) => content.content.ref.imageId ? [{ type: "image", src: content.content.ref.imageId }] : [],
|
|
190
|
+
settings: {
|
|
191
|
+
square: { type: "checkbox", label: "Square aspect ratio", default: false },
|
|
192
|
+
showCaption: { type: "checkbox", label: "Show caption", default: false },
|
|
193
|
+
border: { type: "checkbox", label: "Border", default: false },
|
|
194
|
+
objectFit: {
|
|
195
|
+
type: "select",
|
|
196
|
+
label: "Image fit",
|
|
197
|
+
default: "contain",
|
|
198
|
+
options: [
|
|
199
|
+
{ label: "Contain", value: "contain" },
|
|
200
|
+
{ label: "Cover", value: "cover" }
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
link: { type: "link", label: "Link", default: DEFAULT_LINK, target: "content" }
|
|
204
|
+
},
|
|
205
|
+
inheritableSettings: ["square", "showCaption", "border", "objectFit"],
|
|
206
|
+
settingsTabs: [
|
|
207
|
+
{ label: "Display", fields: ["square", "showCaption", "border", "objectFit"] },
|
|
208
|
+
{ label: "Link", fields: ["link"] }
|
|
209
|
+
]
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// src/components/sections/Button/index.tsx
|
|
213
|
+
import { z as z3 } from "zod";
|
|
214
|
+
import { RectangleHorizontal } from "lucide-react";
|
|
215
|
+
|
|
216
|
+
// src/components/sections/Button/CTAButton.tsx
|
|
217
|
+
import { Download } from "lucide-react";
|
|
218
|
+
import { jsx as jsx7, jsxs } from "react/jsx-runtime";
|
|
219
|
+
function Button({ text, link, download, onChange }) {
|
|
220
|
+
const base = "inline-block rounded-md px-6 py-3 font-bold transition-colors";
|
|
221
|
+
const variant = "border-2 border-primary text-primary hover:bg-primary hover:text-primary-contrast";
|
|
222
|
+
if (onChange) {
|
|
223
|
+
return /* @__PURE__ */ jsxs("span", { className: cn(base, variant, "inline-flex items-center gap-2"), children: [
|
|
224
|
+
download && /* @__PURE__ */ jsx7(Download, { size: 16, className: "shrink-0" }),
|
|
225
|
+
/* @__PURE__ */ jsx7(
|
|
226
|
+
EditablePlainText,
|
|
227
|
+
{
|
|
228
|
+
tag: "span",
|
|
229
|
+
value: text,
|
|
230
|
+
onChange: (newText) => onChange({ type: "button", content: { text: newText, link, download } }),
|
|
231
|
+
isEditMode: true,
|
|
232
|
+
placeholder: "Button text"
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
] });
|
|
236
|
+
}
|
|
237
|
+
const href = link && link.kind === "external" ? link.href : "";
|
|
238
|
+
const target = link?.target;
|
|
239
|
+
if (href) {
|
|
240
|
+
return /* @__PURE__ */ jsxs(
|
|
241
|
+
"a",
|
|
242
|
+
{
|
|
243
|
+
href,
|
|
244
|
+
target,
|
|
245
|
+
download: download ? "" : void 0,
|
|
246
|
+
className: cn(base, variant, download && "inline-flex items-center gap-2"),
|
|
247
|
+
children: [
|
|
248
|
+
download && /* @__PURE__ */ jsx7(Download, { size: 16, className: "shrink-0" }),
|
|
249
|
+
text
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return /* @__PURE__ */ jsx7("button", { className: cn("cursor-pointer", base, variant), children: text });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/components/sections/Button/index.tsx
|
|
258
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
259
|
+
var schema3 = z3.object({
|
|
260
|
+
type: z3.literal("button"),
|
|
261
|
+
content: z3.object({
|
|
262
|
+
text: z3.string().describe("The button label. Plain text, short (a few words)."),
|
|
263
|
+
link: LinkValueSchema.describe(
|
|
264
|
+
"Where the button points \u2014 an internal page or a safe external URL."
|
|
265
|
+
).optional(),
|
|
266
|
+
download: z3.boolean().describe("When true, the link is treated as a file download rather than navigation.").optional()
|
|
267
|
+
})
|
|
268
|
+
});
|
|
269
|
+
var Button_default = defineSection({
|
|
270
|
+
type: "button",
|
|
271
|
+
label: "Button",
|
|
272
|
+
icon: /* @__PURE__ */ jsx8(RectangleHorizontal, { size: 18 }),
|
|
273
|
+
schema: schema3,
|
|
274
|
+
authoringNote: "A call-to-action button linking to an internal page or external URL, optionally as a file download. Use for a single prominent action.",
|
|
275
|
+
component: ({ content, onChange }) => /* @__PURE__ */ jsx8(
|
|
276
|
+
Button,
|
|
277
|
+
{
|
|
278
|
+
text: content.content.text,
|
|
279
|
+
link: content.content.link,
|
|
280
|
+
download: content.content.download,
|
|
281
|
+
onChange: onChange ? (c) => onChange(c) : void 0
|
|
282
|
+
}
|
|
283
|
+
),
|
|
284
|
+
defaults: () => ({ type: "button", content: { text: "Button" } }),
|
|
285
|
+
getLabel: (content) => content.content.text,
|
|
286
|
+
settings: {
|
|
287
|
+
link: { type: "link", label: "Link", default: DEFAULT_LINK, target: "content" },
|
|
288
|
+
download: { type: "checkbox", label: "Download link", default: false, target: "content" }
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// src/components/sections/Container/index.tsx
|
|
293
|
+
import { z as z4 } from "zod";
|
|
294
|
+
import { LayoutTemplate } from "lucide-react";
|
|
295
|
+
|
|
296
|
+
// src/components/sections/Container/Container.tsx
|
|
297
|
+
import "react";
|
|
298
|
+
|
|
299
|
+
// src/components/editor/ChildBlockWrapper.tsx
|
|
300
|
+
import { forwardRef } from "react";
|
|
301
|
+
import { CopyPlus } from "lucide-react";
|
|
302
|
+
|
|
303
|
+
// src/components/editor/ColSpanControl.tsx
|
|
304
|
+
import { useRef, useState } from "react";
|
|
305
|
+
import { Columns3 } from "lucide-react";
|
|
306
|
+
import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
307
|
+
function ColSpanControl({ colSpan, columns, onChange }) {
|
|
308
|
+
const buttonRef = useRef(null);
|
|
309
|
+
const [open, setOpen] = useState(false);
|
|
310
|
+
if (columns <= 1) return null;
|
|
311
|
+
const current = Math.min(Math.max(colSpan, 1), columns);
|
|
312
|
+
return /* @__PURE__ */ jsxs2("div", { className: "relative", children: [
|
|
313
|
+
/* @__PURE__ */ jsx9(
|
|
314
|
+
"button",
|
|
315
|
+
{
|
|
316
|
+
ref: buttonRef,
|
|
317
|
+
type: "button",
|
|
318
|
+
"aria-label": "Column span",
|
|
319
|
+
"aria-haspopup": "true",
|
|
320
|
+
"aria-expanded": open,
|
|
321
|
+
onClick: () => setOpen((v) => !v),
|
|
322
|
+
className: "pointer-events-auto inline-flex cursor-pointer items-center gap-1 rounded p-1 text-xs text-base-contrast-light hover:text-primary",
|
|
323
|
+
children: /* @__PURE__ */ jsx9(Columns3, { size: 14 })
|
|
324
|
+
}
|
|
325
|
+
),
|
|
326
|
+
/* @__PURE__ */ jsx9(Popover, { isOpen: open, onClose: () => setOpen(false), anchorRef: buttonRef, className: "min-w-28", children: /* @__PURE__ */ jsx9("ul", { role: "list", className: "py-1", children: Array.from({ length: columns }, (_, i) => i + 1).map((span) => /* @__PURE__ */ jsx9("li", { children: /* @__PURE__ */ jsxs2(
|
|
327
|
+
PopoverItem,
|
|
328
|
+
{
|
|
329
|
+
"aria-current": span === current,
|
|
330
|
+
onClick: () => {
|
|
331
|
+
onChange(span);
|
|
332
|
+
setOpen(false);
|
|
333
|
+
},
|
|
334
|
+
className: cn(span === current && "font-semibold text-primary"),
|
|
335
|
+
children: [
|
|
336
|
+
"Span ",
|
|
337
|
+
span
|
|
338
|
+
]
|
|
339
|
+
}
|
|
340
|
+
) }, span)) }) })
|
|
341
|
+
] });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/components/editor/ChildBlockWrapper.tsx
|
|
345
|
+
import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
346
|
+
var ChildBlockWrapper = forwardRef(
|
|
347
|
+
function ChildBlockWrapper2({
|
|
348
|
+
label,
|
|
349
|
+
columns,
|
|
350
|
+
colSpan,
|
|
351
|
+
closestEdge,
|
|
352
|
+
isDragging,
|
|
353
|
+
hasSettings,
|
|
354
|
+
dragHandleRef,
|
|
355
|
+
onDelete,
|
|
356
|
+
onDuplicate,
|
|
357
|
+
onColSpanChange,
|
|
358
|
+
onOpenSettings,
|
|
359
|
+
children
|
|
360
|
+
}, ref) {
|
|
361
|
+
return /* @__PURE__ */ jsxs3(
|
|
362
|
+
"div",
|
|
363
|
+
{
|
|
364
|
+
ref,
|
|
365
|
+
className: cn(
|
|
366
|
+
"group/childblock relative h-full rounded-sm",
|
|
367
|
+
isDragging && "opacity-50",
|
|
368
|
+
"hover:outline hover:outline-2 hover:outline-primary/30"
|
|
369
|
+
),
|
|
370
|
+
children: [
|
|
371
|
+
/* @__PURE__ */ jsxs3("div", { className: "absolute top-0 left-0 -translate-x-full z-20 flex flex-col items-center gap-1 opacity-0 transition-opacity group-hover/childblock:opacity-100 group-focus-within/childblock:opacity-100 pointer-events-none", children: [
|
|
372
|
+
/* @__PURE__ */ jsx10("span", { className: "sr-only", children: label }),
|
|
373
|
+
/* @__PURE__ */ jsx10("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx10(DragHandle, { ref: dragHandleRef }) }),
|
|
374
|
+
/* @__PURE__ */ jsx10("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx10(ColSpanControl, { colSpan, columns, onChange: onColSpanChange }) }),
|
|
375
|
+
hasSettings && onOpenSettings && /* @__PURE__ */ jsx10("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx10(SettingsButton, { onClick: onOpenSettings }) }),
|
|
376
|
+
/* @__PURE__ */ jsx10("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx10(
|
|
377
|
+
"button",
|
|
378
|
+
{
|
|
379
|
+
type: "button",
|
|
380
|
+
"aria-label": "Duplicate block",
|
|
381
|
+
onClick: onDuplicate,
|
|
382
|
+
className: "cursor-pointer rounded p-1 text-base-contrast-light hover:text-primary",
|
|
383
|
+
children: /* @__PURE__ */ jsx10(CopyPlus, { size: 16 })
|
|
384
|
+
}
|
|
385
|
+
) }),
|
|
386
|
+
/* @__PURE__ */ jsx10("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx10(DeleteButton, { onDelete }) })
|
|
387
|
+
] }),
|
|
388
|
+
closestEdge === "left" && /* @__PURE__ */ jsx10(
|
|
389
|
+
"div",
|
|
390
|
+
{
|
|
391
|
+
"data-drop-edge": "left",
|
|
392
|
+
className: "absolute bottom-0 left-0 top-0 z-10 w-0.5 -translate-x-1 bg-primary"
|
|
393
|
+
}
|
|
394
|
+
),
|
|
395
|
+
closestEdge === "right" && /* @__PURE__ */ jsx10(
|
|
396
|
+
"div",
|
|
397
|
+
{
|
|
398
|
+
"data-drop-edge": "right",
|
|
399
|
+
className: "absolute bottom-0 right-0 top-0 z-10 w-0.5 translate-x-1 bg-primary"
|
|
400
|
+
}
|
|
401
|
+
),
|
|
402
|
+
closestEdge === "top" && /* @__PURE__ */ jsx10("div", { "data-drop-edge": "top", className: "absolute left-0 right-0 top-0 z-10 h-0.5 -translate-y-1 bg-primary" }),
|
|
403
|
+
closestEdge === "bottom" && /* @__PURE__ */ jsx10("div", { "data-drop-edge": "bottom", className: "absolute left-0 right-0 bottom-0 z-10 h-0.5 translate-y-1 bg-primary" }),
|
|
404
|
+
children
|
|
405
|
+
]
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// src/hooks/useBlockDnd.ts
|
|
412
|
+
import { useEffect, useRef as useRef2, useState as useState2 } from "react";
|
|
413
|
+
function useBlockDnd({ blockId, containerId, index, isContainer, allowedEdges, enabled }) {
|
|
414
|
+
const dragRef = useRef2(null);
|
|
415
|
+
const handleRef = useRef2(null);
|
|
416
|
+
const [closestEdge, setClosestEdge] = useState2(null);
|
|
417
|
+
const [isDragging, setIsDragging] = useState2(false);
|
|
418
|
+
useEffect(() => {
|
|
419
|
+
const element = dragRef.current;
|
|
420
|
+
const handle = handleRef.current;
|
|
421
|
+
if (!element || !enabled) return;
|
|
422
|
+
let cleanup;
|
|
423
|
+
let cancelled = false;
|
|
424
|
+
Promise.all([
|
|
425
|
+
import("./adapter-HH47ZPGM.js"),
|
|
426
|
+
import("./closest-edge-EBOXL3YW.js")
|
|
427
|
+
]).then(([{ draggable, dropTargetForElements }, { attachClosestEdge, extractClosestEdge }]) => {
|
|
428
|
+
if (cancelled) return;
|
|
429
|
+
const cleanupDraggable = draggable({
|
|
430
|
+
element,
|
|
431
|
+
dragHandle: handle ?? void 0,
|
|
432
|
+
getInitialData: () => buildBlockDragData({ blockId, containerId, index, isContainer }),
|
|
433
|
+
onGenerateDragPreview: () => {
|
|
434
|
+
element.style.opacity = "0.4";
|
|
435
|
+
requestAnimationFrame(() => {
|
|
436
|
+
element.style.opacity = "";
|
|
437
|
+
});
|
|
438
|
+
},
|
|
439
|
+
onDragStart: () => setIsDragging(true),
|
|
440
|
+
onDrop: () => setIsDragging(false)
|
|
441
|
+
});
|
|
442
|
+
const cleanupDrop = dropTargetForElements({
|
|
443
|
+
element,
|
|
444
|
+
canDrop: ({ source }) => isBlockDragData(source.data) && canDropBlock(source.data, containerId),
|
|
445
|
+
getData: ({ input, element: el }) => attachClosestEdge(buildBlockDropData({ dropContainerId: containerId, index }), {
|
|
446
|
+
input,
|
|
447
|
+
element: el,
|
|
448
|
+
allowedEdges
|
|
449
|
+
}),
|
|
450
|
+
onDrag: ({ self, source, location }) => {
|
|
451
|
+
const innermost = location.current.dropTargets[0]?.element === self.element;
|
|
452
|
+
if (!innermost || !isBlockDragData(source.data) || source.data.blockId === blockId) {
|
|
453
|
+
setClosestEdge(null);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
setClosestEdge(extractClosestEdge(self.data));
|
|
457
|
+
},
|
|
458
|
+
onDragLeave: () => setClosestEdge(null),
|
|
459
|
+
onDrop: () => setClosestEdge(null)
|
|
460
|
+
});
|
|
461
|
+
cleanup = () => {
|
|
462
|
+
cleanupDraggable();
|
|
463
|
+
cleanupDrop();
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
return () => {
|
|
467
|
+
cancelled = true;
|
|
468
|
+
cleanup?.();
|
|
469
|
+
};
|
|
470
|
+
}, [blockId, containerId, index, isContainer, enabled, allowedEdges]);
|
|
471
|
+
return { dragRef, handleRef, closestEdge, isDragging };
|
|
472
|
+
}
|
|
473
|
+
function useBlockDropZone({ containerId, index, toColumn, allowedEdges, enabled }) {
|
|
474
|
+
const dropRef = useRef2(null);
|
|
475
|
+
const [closestEdge, setClosestEdge] = useState2(null);
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
const element = dropRef.current;
|
|
478
|
+
if (!element || !enabled) return;
|
|
479
|
+
let cleanup;
|
|
480
|
+
let cancelled = false;
|
|
481
|
+
Promise.all([
|
|
482
|
+
import("./adapter-HH47ZPGM.js"),
|
|
483
|
+
import("./closest-edge-EBOXL3YW.js")
|
|
484
|
+
]).then(([{ dropTargetForElements }, { attachClosestEdge, extractClosestEdge }]) => {
|
|
485
|
+
if (cancelled) return;
|
|
486
|
+
cleanup = dropTargetForElements({
|
|
487
|
+
element,
|
|
488
|
+
canDrop: ({ source }) => isBlockDragData(source.data) && canDropBlock(source.data, containerId),
|
|
489
|
+
getData: ({ input, element: el }) => attachClosestEdge(buildBlockDropData({ dropContainerId: containerId, index, toColumn }), {
|
|
490
|
+
input,
|
|
491
|
+
element: el,
|
|
492
|
+
allowedEdges
|
|
493
|
+
}),
|
|
494
|
+
onDrag: ({ self, source, location }) => {
|
|
495
|
+
const innermost = location.current.dropTargets[0]?.element === self.element;
|
|
496
|
+
if (!innermost || !isBlockDragData(source.data)) {
|
|
497
|
+
setClosestEdge(null);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
setClosestEdge(extractClosestEdge(self.data));
|
|
501
|
+
},
|
|
502
|
+
onDragLeave: () => setClosestEdge(null),
|
|
503
|
+
onDrop: () => setClosestEdge(null)
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
return () => {
|
|
507
|
+
cancelled = true;
|
|
508
|
+
cleanup?.();
|
|
509
|
+
};
|
|
510
|
+
}, [containerId, index, toColumn, enabled, allowedEdges]);
|
|
511
|
+
return { dropRef, closestEdge };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/lib/container-ops.ts
|
|
515
|
+
function getContainerChildren(block) {
|
|
516
|
+
const content = block.content;
|
|
517
|
+
return Array.isArray(content?.children) ? content.children : [];
|
|
518
|
+
}
|
|
519
|
+
function withChildren(container, children) {
|
|
520
|
+
return { ...container, content: { ...container.content, children } };
|
|
521
|
+
}
|
|
522
|
+
function containerColumns(container) {
|
|
523
|
+
const n = container.content?.columns;
|
|
524
|
+
return typeof n === "number" && n >= 1 ? n : 1;
|
|
525
|
+
}
|
|
526
|
+
function removeChildAt(container, index) {
|
|
527
|
+
const children = getContainerChildren(container);
|
|
528
|
+
if (index < 0 || index >= children.length) return { container, removed: null };
|
|
529
|
+
const next = [...children];
|
|
530
|
+
const [removed] = next.splice(index, 1);
|
|
531
|
+
return { container: withChildren(container, next), removed };
|
|
532
|
+
}
|
|
533
|
+
function duplicateChildAt(container, index, newId2) {
|
|
534
|
+
const children = getContainerChildren(container);
|
|
535
|
+
if (index < 0 || index >= children.length) return container;
|
|
536
|
+
const clone = { ...JSON.parse(JSON.stringify(children[index])), id: newId2() };
|
|
537
|
+
const next = [...children];
|
|
538
|
+
next.splice(index + 1, 0, clone);
|
|
539
|
+
return withChildren(container, next);
|
|
540
|
+
}
|
|
541
|
+
function setChildColSpan(container, index, span) {
|
|
542
|
+
const children = getContainerChildren(container);
|
|
543
|
+
if (index < 0 || index >= children.length) return container;
|
|
544
|
+
const clamped = Math.max(1, Math.min(span, containerColumns(container)));
|
|
545
|
+
const next = children.map((child, i) => {
|
|
546
|
+
if (i !== index) return child;
|
|
547
|
+
if (clamped <= 1) {
|
|
548
|
+
const { layout: _drop, ...rest } = child;
|
|
549
|
+
return rest;
|
|
550
|
+
}
|
|
551
|
+
return { ...child, layout: { ...child.layout, colSpan: clamped } };
|
|
552
|
+
});
|
|
553
|
+
return withChildren(container, next);
|
|
554
|
+
}
|
|
555
|
+
function effectiveSpan(child, columns) {
|
|
556
|
+
return isSpacer(child) ? 1 : Math.min(child.layout?.colSpan ?? 1, columns);
|
|
557
|
+
}
|
|
558
|
+
function isSpacer(child) {
|
|
559
|
+
return child.type === "spacer";
|
|
560
|
+
}
|
|
561
|
+
function occupiedColumns(children, columns) {
|
|
562
|
+
return children.reduce((sum, c) => sum + effectiveSpan(c, columns), 0);
|
|
563
|
+
}
|
|
564
|
+
function trimTrailingSpacers(container) {
|
|
565
|
+
const children = getContainerChildren(container);
|
|
566
|
+
let end = children.length;
|
|
567
|
+
while (end > 0 && isSpacer(children[end - 1])) end--;
|
|
568
|
+
if (end === children.length) return container;
|
|
569
|
+
return withChildren(container, children.slice(0, end));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/components/sections/Container/Container.tsx
|
|
573
|
+
import { jsx as jsx11, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
574
|
+
var ROW_EDGES = ["left", "right"];
|
|
575
|
+
var COL_EDGES = ["top", "bottom"];
|
|
576
|
+
function ContainerChild({
|
|
577
|
+
containerId,
|
|
578
|
+
index,
|
|
579
|
+
child,
|
|
580
|
+
columns,
|
|
581
|
+
flow,
|
|
582
|
+
cellClass,
|
|
583
|
+
label,
|
|
584
|
+
hasSettings,
|
|
585
|
+
mergedOptions,
|
|
586
|
+
isEditMode,
|
|
587
|
+
ChildComp,
|
|
588
|
+
onDelete,
|
|
589
|
+
onDuplicate,
|
|
590
|
+
onColSpanChange,
|
|
591
|
+
onOpenSettings,
|
|
592
|
+
onChildContentChange,
|
|
593
|
+
openModal
|
|
594
|
+
}) {
|
|
595
|
+
const isContainer = Array.isArray(child.content?.children);
|
|
596
|
+
const { dragRef, handleRef, closestEdge, isDragging } = useBlockDnd({
|
|
597
|
+
blockId: child.id ?? `child-${index}`,
|
|
598
|
+
containerId,
|
|
599
|
+
index,
|
|
600
|
+
isContainer,
|
|
601
|
+
allowedEdges: flow === "column" ? COL_EDGES : ROW_EDGES,
|
|
602
|
+
enabled: true
|
|
603
|
+
});
|
|
604
|
+
return /* @__PURE__ */ jsx11(
|
|
605
|
+
"div",
|
|
606
|
+
{
|
|
607
|
+
ref: dragRef,
|
|
608
|
+
className: cellClass,
|
|
609
|
+
"data-child-index": index,
|
|
610
|
+
"data-child-id": child.id,
|
|
611
|
+
children: /* @__PURE__ */ jsx11(
|
|
612
|
+
ChildBlockWrapper,
|
|
613
|
+
{
|
|
614
|
+
label,
|
|
615
|
+
columns,
|
|
616
|
+
colSpan: Math.min(child.layout?.colSpan ?? 1, columns),
|
|
617
|
+
closestEdge,
|
|
618
|
+
isDragging,
|
|
619
|
+
hasSettings,
|
|
620
|
+
dragHandleRef: handleRef,
|
|
621
|
+
onDelete,
|
|
622
|
+
onDuplicate,
|
|
623
|
+
onColSpanChange,
|
|
624
|
+
onOpenSettings,
|
|
625
|
+
children: /* @__PURE__ */ jsx11(
|
|
626
|
+
ChildComp,
|
|
627
|
+
{
|
|
628
|
+
content: child,
|
|
629
|
+
options: mergedOptions,
|
|
630
|
+
isEditMode,
|
|
631
|
+
onChange: onChildContentChange,
|
|
632
|
+
openModal
|
|
633
|
+
}
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
function EmptyColumnCell({
|
|
641
|
+
containerId,
|
|
642
|
+
targetColumn,
|
|
643
|
+
testId
|
|
644
|
+
}) {
|
|
645
|
+
const { dropRef, closestEdge } = useBlockDropZone({
|
|
646
|
+
containerId,
|
|
647
|
+
index: 0,
|
|
648
|
+
toColumn: targetColumn,
|
|
649
|
+
allowedEdges: ROW_EDGES,
|
|
650
|
+
enabled: true
|
|
651
|
+
});
|
|
652
|
+
const active = closestEdge !== null;
|
|
653
|
+
return /* @__PURE__ */ jsx11(
|
|
654
|
+
"div",
|
|
655
|
+
{
|
|
656
|
+
ref: dropRef,
|
|
657
|
+
"data-empty-dropzone": true,
|
|
658
|
+
"data-testid": testId,
|
|
659
|
+
"data-target-column": targetColumn,
|
|
660
|
+
className: cn(
|
|
661
|
+
"flex min-h-20 items-center justify-center rounded-md border-2 border-dashed text-xs text-base-contrast-light/50",
|
|
662
|
+
active ? "border-primary" : "border-base-200"
|
|
663
|
+
),
|
|
664
|
+
children: "Empty"
|
|
665
|
+
}
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
function newId() {
|
|
669
|
+
return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `child-${Math.random().toString(36).slice(2)}-${Date.now()}`;
|
|
670
|
+
}
|
|
671
|
+
function Container({ content, isEditMode, onChange, openModal }) {
|
|
672
|
+
const { columns, flow, childDefaults, children } = content.content;
|
|
673
|
+
const editable = isEditMode && !!onChange;
|
|
674
|
+
const container = {
|
|
675
|
+
id: content.id ?? "container",
|
|
676
|
+
type: "container",
|
|
677
|
+
content: content.content
|
|
678
|
+
};
|
|
679
|
+
const emit = (nextSection) => onChange?.({ ...content, content: nextSection.content });
|
|
680
|
+
const updateChild = (index, childContent) => {
|
|
681
|
+
const next = children.map((c, i) => i === index ? childContent : c);
|
|
682
|
+
onChange?.({ ...content, content: { ...content.content, children: next } });
|
|
683
|
+
};
|
|
684
|
+
const gridClass = cn(
|
|
685
|
+
"grid",
|
|
686
|
+
"gap-8",
|
|
687
|
+
containerGridClass[columns] ?? "grid-cols-1",
|
|
688
|
+
flow === "column" && "grid-flow-col"
|
|
689
|
+
);
|
|
690
|
+
if (!editable) {
|
|
691
|
+
return /* @__PURE__ */ jsx11("div", { className: "@container", children: /* @__PURE__ */ jsx11("div", { className: gridClass, children: children.map((child, index) => {
|
|
692
|
+
const def = getSection(child.type);
|
|
693
|
+
if (!def) return null;
|
|
694
|
+
const Child = def.component;
|
|
695
|
+
const span = child.layout?.colSpan ?? 1;
|
|
696
|
+
const childOptions = "options" in child ? child.options : void 0;
|
|
697
|
+
const mergedOptions = { ...childDefaults ?? {}, ...childOptions ?? {} };
|
|
698
|
+
const cellClass = cn(
|
|
699
|
+
colSpanClass[columns]?.[Math.min(span, columns)] || void 0,
|
|
700
|
+
child.type === "spacer" && spacerHiddenClass[columns]
|
|
701
|
+
);
|
|
702
|
+
return /* @__PURE__ */ jsx11("div", { className: cellClass, children: /* @__PURE__ */ jsx11(Child, { content: child, options: mergedOptions, isEditMode }) }, child.id ?? index);
|
|
703
|
+
}) }) });
|
|
704
|
+
}
|
|
705
|
+
function columnStartOf(index) {
|
|
706
|
+
let c = 0;
|
|
707
|
+
for (let i = 0; i < index; i++) {
|
|
708
|
+
c += isSpacer(children[i]) ? 1 : Math.min(children[i].layout?.colSpan ?? 1, columns);
|
|
709
|
+
}
|
|
710
|
+
return c + 1;
|
|
711
|
+
}
|
|
712
|
+
function renderEditChild(child, index) {
|
|
713
|
+
const def = getSection(child.type);
|
|
714
|
+
if (!def) return null;
|
|
715
|
+
if (isSpacer(child)) {
|
|
716
|
+
return /* @__PURE__ */ jsx11(
|
|
717
|
+
EmptyColumnCell,
|
|
718
|
+
{
|
|
719
|
+
containerId: container.id,
|
|
720
|
+
targetColumn: columnStartOf(index),
|
|
721
|
+
testId: "spacer-cell"
|
|
722
|
+
},
|
|
723
|
+
child.id ?? index
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
const span = child.layout?.colSpan ?? 1;
|
|
727
|
+
const childOptions = "options" in child ? child.options : void 0;
|
|
728
|
+
const mergedOptions = { ...childDefaults ?? {}, ...childOptions ?? {} };
|
|
729
|
+
const cellClass = colSpanClass[columns]?.[Math.min(span, columns)] || void 0;
|
|
730
|
+
const hasSettings = !!(def.settings || def.settingsForm) && !!openModal;
|
|
731
|
+
const onOpenSettings = hasSettings && openModal ? () => {
|
|
732
|
+
const childOpts = childOptions ?? {};
|
|
733
|
+
const childContent = child.content ?? {};
|
|
734
|
+
const seed = { ...childContent, ...childOpts };
|
|
735
|
+
const applyResult = (result) => updateChild(index, {
|
|
736
|
+
...child,
|
|
737
|
+
content: { ...child.content, ...result.content },
|
|
738
|
+
options: { ...childOpts, ...result.options }
|
|
739
|
+
});
|
|
740
|
+
if (def.settingsForm) {
|
|
741
|
+
const CustomForm = def.settingsForm;
|
|
742
|
+
openModal(
|
|
743
|
+
`${def.label} Settings`,
|
|
744
|
+
/* @__PURE__ */ jsx11(CustomForm, { ...seed, onChange: applyResult })
|
|
745
|
+
);
|
|
746
|
+
} else if (def.settings) {
|
|
747
|
+
openModal(
|
|
748
|
+
`${def.label} Settings`,
|
|
749
|
+
/* @__PURE__ */ jsx11(
|
|
750
|
+
SettingsForm,
|
|
751
|
+
{
|
|
752
|
+
schema: def.settings,
|
|
753
|
+
values: seed,
|
|
754
|
+
onChange: applyResult,
|
|
755
|
+
tabs: def.settingsTabs
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
} : void 0;
|
|
761
|
+
return /* @__PURE__ */ jsx11(
|
|
762
|
+
ContainerChild,
|
|
763
|
+
{
|
|
764
|
+
containerId: container.id,
|
|
765
|
+
index,
|
|
766
|
+
child,
|
|
767
|
+
columns,
|
|
768
|
+
flow,
|
|
769
|
+
cellClass,
|
|
770
|
+
label: def.label,
|
|
771
|
+
hasSettings,
|
|
772
|
+
mergedOptions,
|
|
773
|
+
isEditMode,
|
|
774
|
+
ChildComp: def.component,
|
|
775
|
+
onDelete: () => emit(trimTrailingSpacers(removeChildAt(container, index).container)),
|
|
776
|
+
onDuplicate: () => emit(duplicateChildAt(container, index, newId)),
|
|
777
|
+
onColSpanChange: (s) => emit(setChildColSpan(container, index, s)),
|
|
778
|
+
onOpenSettings,
|
|
779
|
+
onChildContentChange: (c) => updateChild(index, c),
|
|
780
|
+
openModal
|
|
781
|
+
},
|
|
782
|
+
child.id ?? index
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const occupied = occupiedColumns(children, columns);
|
|
786
|
+
const emptyCols = occupied < columns ? columns - occupied : 0;
|
|
787
|
+
return /* @__PURE__ */ jsx11("div", { className: "@container", children: /* @__PURE__ */ jsxs4("div", { className: gridClass, children: [
|
|
788
|
+
children.map(renderEditChild),
|
|
789
|
+
Array.from({ length: emptyCols }, (_, i) => /* @__PURE__ */ jsx11(
|
|
790
|
+
EmptyColumnCell,
|
|
791
|
+
{
|
|
792
|
+
containerId: container.id,
|
|
793
|
+
targetColumn: occupied + i + 1,
|
|
794
|
+
testId: "ghost-cell"
|
|
795
|
+
},
|
|
796
|
+
`ghost-${i}`
|
|
797
|
+
))
|
|
798
|
+
] }) });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/components/sections/Container/ContainerSettingsForm.tsx
|
|
802
|
+
import { useState as useState3 } from "react";
|
|
803
|
+
import { jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
804
|
+
function inheritableSchema(children) {
|
|
805
|
+
const out = {};
|
|
806
|
+
for (const child of children) {
|
|
807
|
+
const def = getSection(child.type);
|
|
808
|
+
if (!def?.settings || !def.inheritableSettings) continue;
|
|
809
|
+
for (const key of def.inheritableSettings) {
|
|
810
|
+
const field = def.settings[key];
|
|
811
|
+
if (field && !out[key]) out[key] = field;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return out;
|
|
815
|
+
}
|
|
816
|
+
function ContainerSettingsForm({
|
|
817
|
+
columns = 1,
|
|
818
|
+
flow = "row",
|
|
819
|
+
childDefaults = {},
|
|
820
|
+
children = [],
|
|
821
|
+
onChange
|
|
822
|
+
}) {
|
|
823
|
+
const [layout, setLayout] = useState3({ columns, flow });
|
|
824
|
+
const [defaults, setDefaults] = useState3(childDefaults);
|
|
825
|
+
const childSchema = inheritableSchema(children);
|
|
826
|
+
const hasChildDefaults = Object.keys(childSchema).length > 0;
|
|
827
|
+
function emit(nextLayout, nextDefaults) {
|
|
828
|
+
onChange({ content: { ...nextLayout, childDefaults: nextDefaults }, options: {} });
|
|
829
|
+
}
|
|
830
|
+
const layoutSchema = {
|
|
831
|
+
columns: {
|
|
832
|
+
type: "select",
|
|
833
|
+
label: "Columns",
|
|
834
|
+
default: String(layout.columns),
|
|
835
|
+
target: "content",
|
|
836
|
+
coerce: "number",
|
|
837
|
+
options: Array.from({ length: MAX_CONTAINER_COLUMNS }, (_, i) => ({
|
|
838
|
+
label: String(i + 1),
|
|
839
|
+
value: String(i + 1)
|
|
840
|
+
}))
|
|
841
|
+
},
|
|
842
|
+
flow: {
|
|
843
|
+
type: "select",
|
|
844
|
+
label: "Flow",
|
|
845
|
+
default: layout.flow,
|
|
846
|
+
target: "content",
|
|
847
|
+
options: [
|
|
848
|
+
{ label: "Rows (wrap across)", value: "row" },
|
|
849
|
+
{ label: "Columns (fill down)", value: "column" }
|
|
850
|
+
]
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-6", children: [
|
|
854
|
+
/* @__PURE__ */ jsxs5("fieldset", { className: "flex flex-col gap-4", children: [
|
|
855
|
+
/* @__PURE__ */ jsx12("legend", { className: "mb-2 text-sm font-semibold text-base-contrast", children: "Layout" }),
|
|
856
|
+
/* @__PURE__ */ jsx12(
|
|
857
|
+
SettingsForm,
|
|
858
|
+
{
|
|
859
|
+
schema: layoutSchema,
|
|
860
|
+
values: { columns: String(layout.columns), flow: layout.flow },
|
|
861
|
+
onChange: (result) => {
|
|
862
|
+
const next = {
|
|
863
|
+
columns: Number(result.content.columns ?? layout.columns),
|
|
864
|
+
flow: String(result.content.flow ?? layout.flow)
|
|
865
|
+
};
|
|
866
|
+
setLayout(next);
|
|
867
|
+
emit(next, defaults);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
)
|
|
871
|
+
] }),
|
|
872
|
+
hasChildDefaults && /* @__PURE__ */ jsxs5("fieldset", { className: "flex flex-col gap-4 border-t border-base-200 pt-4", children: [
|
|
873
|
+
/* @__PURE__ */ jsx12("legend", { className: "mb-2 text-sm font-semibold text-base-contrast", children: "Apply to all items" }),
|
|
874
|
+
/* @__PURE__ */ jsx12(
|
|
875
|
+
SettingsForm,
|
|
876
|
+
{
|
|
877
|
+
schema: childSchema,
|
|
878
|
+
values: defaults,
|
|
879
|
+
onChange: (result) => {
|
|
880
|
+
const next = { ...defaults, ...result.options };
|
|
881
|
+
setDefaults(next);
|
|
882
|
+
emit(layout, next);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
)
|
|
886
|
+
] })
|
|
887
|
+
] });
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/components/sections/Container/index.tsx
|
|
891
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
892
|
+
var schema4 = z4.object({
|
|
893
|
+
type: z4.literal("container"),
|
|
894
|
+
content: z4.object({
|
|
895
|
+
columns: z4.number().int().min(1).max(MAX_CONTAINER_COLUMNS).describe("Number of columns in the grid. Children flow across this many columns.").default(1),
|
|
896
|
+
flow: z4.enum(["row", "column"]).describe(
|
|
897
|
+
"Layout direction: 'row' (children flow left-to-right across columns) or 'column' (stacked)."
|
|
898
|
+
).default("row"),
|
|
899
|
+
childDefaults: z4.record(z4.string(), z4.unknown()).describe("Optional default option values applied to child sections (inheritable settings only).").optional(),
|
|
900
|
+
// Recursive: children are full blocks. z.lazy defers evaluation to parse
|
|
901
|
+
// time, by which point every section schema (incl. container) is registered.
|
|
902
|
+
children: z4.array(z4.lazy(() => getSectionSchema())).default([])
|
|
903
|
+
}).transform((c) => ({
|
|
904
|
+
...c,
|
|
905
|
+
children: c.children.map(
|
|
906
|
+
(child) => child.layout?.colSpan && child.layout.colSpan > c.columns ? { ...child, layout: { ...child.layout, colSpan: c.columns } } : child
|
|
907
|
+
)
|
|
908
|
+
}))
|
|
909
|
+
});
|
|
910
|
+
var Container_default = defineSection({
|
|
911
|
+
type: "container",
|
|
912
|
+
label: "Container",
|
|
913
|
+
icon: /* @__PURE__ */ jsx13(LayoutTemplate, { size: 18 }),
|
|
914
|
+
schema: schema4,
|
|
915
|
+
authoringNote: "A layout wrapper that arranges child sections into columns/rows. Use to place blocks side by side or build multi-column layouts; set columns and add children.",
|
|
916
|
+
component: ({ content, onChange, isEditMode, openModal }) => /* @__PURE__ */ jsx13(Container, { content, isEditMode: !!isEditMode, onChange, openModal }),
|
|
917
|
+
defaults: () => ({
|
|
918
|
+
type: "container",
|
|
919
|
+
content: { columns: 1, flow: "row", children: [] }
|
|
920
|
+
}),
|
|
921
|
+
getLabel: (content) => `Container (${pluralize(content.content.children.length, "item")})`,
|
|
922
|
+
settingsForm: ContainerSettingsForm
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
// src/components/sections/Spacer/index.tsx
|
|
926
|
+
import { z as z5 } from "zod";
|
|
927
|
+
import { SquareDashed } from "lucide-react";
|
|
928
|
+
|
|
929
|
+
// src/components/sections/Spacer/Spacer.tsx
|
|
930
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
931
|
+
function Spacer() {
|
|
932
|
+
return /* @__PURE__ */ jsx14("div", { "data-spacer": true, "aria-hidden": "true", className: "h-full" });
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/components/sections/Spacer/index.tsx
|
|
936
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
937
|
+
var schema5 = z5.object({
|
|
938
|
+
type: z5.literal("spacer"),
|
|
939
|
+
content: z5.object({}).default({})
|
|
940
|
+
});
|
|
941
|
+
var Spacer_default = defineSection({
|
|
942
|
+
type: "spacer",
|
|
943
|
+
label: "Spacer",
|
|
944
|
+
icon: /* @__PURE__ */ jsx15(SquareDashed, { size: 18 }),
|
|
945
|
+
schema: schema5,
|
|
946
|
+
authoringNote: "Vertical whitespace between sections. Use sparingly to add breathing room where default section spacing is insufficient.",
|
|
947
|
+
component: () => /* @__PURE__ */ jsx15(Spacer, {}),
|
|
948
|
+
defaults: () => ({ type: "spacer", content: {} })
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// src/components/sections/core-sections.ts
|
|
952
|
+
var coreSectionDefs = [
|
|
953
|
+
LinkHeading_default,
|
|
954
|
+
SubHeading_default,
|
|
955
|
+
SubSubHeading_default,
|
|
956
|
+
Prose_default,
|
|
957
|
+
Media_default,
|
|
958
|
+
Button_default,
|
|
959
|
+
Container_default,
|
|
960
|
+
Spacer_default
|
|
961
|
+
];
|
|
962
|
+
|
|
963
|
+
// src/components/sections/all-sections.ts
|
|
964
|
+
var allSectionDefs = [...coreSectionDefs, ...brandGuideSectionDefs];
|
|
965
|
+
|
|
966
|
+
export {
|
|
967
|
+
allSectionDefs
|
|
968
|
+
};
|