@drawnagency/primitives 0.1.59 → 0.1.61

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 (95) hide show
  1. package/dist/auth/capabilities.d.ts +16 -0
  2. package/dist/auth/capabilities.d.ts.map +1 -0
  3. package/dist/auth/index.d.ts +2 -0
  4. package/dist/auth/index.d.ts.map +1 -1
  5. package/dist/auth/index.js +3 -1
  6. package/dist/auth/types.d.ts +26 -22
  7. package/dist/auth/types.d.ts.map +1 -1
  8. package/dist/chunk-4OX3Z7SB.js +1421 -0
  9. package/dist/{chunk-RFZNNCAS.js → chunk-53DKVAB6.js} +1 -61
  10. package/dist/chunk-677NTHAZ.js +27 -0
  11. package/dist/chunk-7EOSDFS3.js +10 -0
  12. package/dist/{chunk-NTGSA3TI.js → chunk-AM5LZD6Q.js} +7 -30
  13. package/dist/{chunk-ICLXLWQ5.js → chunk-BT7STGDW.js} +31 -23
  14. package/dist/{chunk-L2JJFOXD.js → chunk-CBWK3KPV.js} +5 -1
  15. package/dist/{chunk-V7JN2DDU.js → chunk-ODCJQOVO.js} +1 -1
  16. package/dist/chunk-TZQPOR5A.js +24 -0
  17. package/dist/{chunk-XTK4BR27.js → chunk-UNWNT52N.js} +13 -0
  18. package/dist/chunk-UQ4OYRW7.js +670 -0
  19. package/dist/chunk-WDSVHKAH.js +54 -0
  20. package/dist/chunk-WLMJCNU7.js +70 -0
  21. package/dist/chunk-ZGFAYZWB.js +8 -0
  22. package/dist/components/editor/DropEdgeIndicator.d.ts +8 -0
  23. package/dist/components/editor/DropEdgeIndicator.d.ts.map +1 -0
  24. package/dist/components/editor/PagesModal.d.ts.map +1 -1
  25. package/dist/components/editor/SectionOrderingModal.d.ts.map +1 -1
  26. package/dist/components/editor/index.js +640 -0
  27. package/dist/components/primitives/defineHeadingSection.d.ts +28 -0
  28. package/dist/components/primitives/defineHeadingSection.d.ts.map +1 -0
  29. package/dist/components/primitives/index.d.ts +10 -0
  30. package/dist/components/primitives/index.d.ts.map +1 -0
  31. package/dist/components/primitives/index.js +166 -0
  32. package/dist/components/sections/Colors/index.d.ts.map +1 -1
  33. package/dist/components/sections/Container/index.d.ts.map +1 -1
  34. package/dist/components/sections/IconList/IconList.d.ts +1 -6
  35. package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
  36. package/dist/components/sections/IconList/index.d.ts +11 -0
  37. package/dist/components/sections/IconList/index.d.ts.map +1 -1
  38. package/dist/components/sections/LinkHeading/index.d.ts.map +1 -1
  39. package/dist/components/sections/SubHeading/index.d.ts +0 -1
  40. package/dist/components/sections/SubHeading/index.d.ts.map +1 -1
  41. package/dist/components/sections/SubSubHeading/index.d.ts +0 -1
  42. package/dist/components/sections/SubSubHeading/index.d.ts.map +1 -1
  43. package/dist/components/sections/all-sections.d.ts +0 -2
  44. package/dist/components/sections/all-sections.d.ts.map +1 -1
  45. package/dist/components/sections/register-schemas.js +376 -2453
  46. package/dist/components/shell/BugReportFAB.d.ts.map +1 -1
  47. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  48. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  49. package/dist/hooks/useSortableRow.d.ts +25 -0
  50. package/dist/hooks/useSortableRow.d.ts.map +1 -0
  51. package/dist/index.js +46 -37
  52. package/dist/lib/image-refs.d.ts +21 -0
  53. package/dist/lib/image-refs.d.ts.map +1 -0
  54. package/dist/lib/index.js +21 -14
  55. package/dist/lib/platform-broker.d.ts +14 -0
  56. package/dist/lib/platform-broker.d.ts.map +1 -0
  57. package/dist/lib/platform-broker.js +60 -0
  58. package/dist/lib/reorder.d.ts +12 -0
  59. package/dist/lib/reorder.d.ts.map +1 -0
  60. package/dist/lib/text.d.ts +2 -0
  61. package/dist/lib/text.d.ts.map +1 -1
  62. package/dist/media/index.js +3 -2
  63. package/dist/media/utils.d.ts.map +1 -1
  64. package/dist/schemas/index.js +17 -17
  65. package/dist/schemas/link.js +13 -0
  66. package/dist/types/database.d.ts +46 -0
  67. package/dist/types/database.d.ts.map +1 -1
  68. package/package.json +14 -2
  69. package/src/auth/capabilities.ts +26 -0
  70. package/src/auth/index.ts +3 -0
  71. package/src/auth/types.ts +36 -26
  72. package/src/components/editor/DropEdgeIndicator.tsx +13 -0
  73. package/src/components/editor/PagesModal.tsx +12 -80
  74. package/src/components/editor/SectionOrderingModal.tsx +8 -78
  75. package/src/components/primitives/defineHeadingSection.tsx +56 -0
  76. package/src/components/primitives/index.ts +11 -0
  77. package/src/components/sections/Colors/index.tsx +2 -4
  78. package/src/components/sections/Container/index.tsx +2 -4
  79. package/src/components/sections/IconList/IconList.tsx +1 -7
  80. package/src/components/sections/IconList/index.tsx +11 -5
  81. package/src/components/sections/LinkHeading/index.tsx +5 -20
  82. package/src/components/sections/SubHeading/index.tsx +6 -23
  83. package/src/components/sections/SubSubHeading/index.tsx +6 -23
  84. package/src/components/shell/BugReportFAB.tsx +13 -9
  85. package/src/hooks/useEditorPublish.ts +71 -49
  86. package/src/hooks/useMediaPipeline.ts +11 -34
  87. package/src/hooks/useSortableRow.ts +99 -0
  88. package/src/lib/image-refs.ts +53 -0
  89. package/src/lib/platform-broker.ts +72 -0
  90. package/src/lib/reorder.ts +20 -0
  91. package/src/lib/text.ts +5 -0
  92. package/src/media/utils.ts +6 -1
  93. package/src/types/database.ts +41 -0
  94. package/dist/chunk-DKOUFIP6.js +0 -35
  95. /package/dist/{chunk-ZU2MKPTG.js → chunk-3DCG4DNU.js} +0 -0
@@ -0,0 +1,1421 @@
1
+ import {
2
+ AddIcon,
3
+ DeleteIcon,
4
+ DragHandle,
5
+ IconButton,
6
+ SettingsIcon,
7
+ usePages
8
+ } from "./chunk-WLMJCNU7.js";
9
+ import {
10
+ getMediaProvider
11
+ } from "./chunk-CBWK3KPV.js";
12
+ import {
13
+ sanitizeHtml,
14
+ toSectionId
15
+ } from "./chunk-53DKVAB6.js";
16
+ import {
17
+ cn
18
+ } from "./chunk-7EOSDFS3.js";
19
+ import {
20
+ defineSection
21
+ } from "./chunk-VKAGMEKE.js";
22
+
23
+ // src/components/primitives/EditablePlainText.tsx
24
+ import {
25
+ useRef as useRef2,
26
+ useEffect as useEffect2,
27
+ useCallback as useCallback2,
28
+ createElement
29
+ } from "react";
30
+
31
+ // src/components/primitives/useEditablePlainText.ts
32
+ import { useState, useEffect, useRef, useCallback } from "react";
33
+ function useEditablePlainText({
34
+ value: propValue,
35
+ onChange,
36
+ multiline = false
37
+ }) {
38
+ const [value, setValue] = useState(propValue);
39
+ const lastPropValue = useRef(propValue);
40
+ useEffect(() => {
41
+ if (propValue !== lastPropValue.current) {
42
+ setValue(propValue);
43
+ lastPropValue.current = propValue;
44
+ }
45
+ }, [propValue]);
46
+ const handleInput = useCallback((text) => {
47
+ setValue(text);
48
+ }, []);
49
+ const handleBlur = useCallback(() => {
50
+ if (value !== propValue) {
51
+ onChange(value);
52
+ lastPropValue.current = value;
53
+ }
54
+ }, [value, propValue, onChange]);
55
+ return {
56
+ value,
57
+ handleInput,
58
+ handleBlur,
59
+ shouldPreventEnter: !multiline
60
+ };
61
+ }
62
+
63
+ // src/components/primitives/EditablePlainText.tsx
64
+ function EditablePlainText({
65
+ tag,
66
+ value,
67
+ onChange,
68
+ isEditMode,
69
+ multiline = false,
70
+ placeholder,
71
+ className,
72
+ ...htmlAttrs
73
+ }) {
74
+ const ref = useRef2(null);
75
+ const {
76
+ value: localValue,
77
+ handleInput,
78
+ handleBlur,
79
+ shouldPreventEnter
80
+ } = useEditablePlainText({ value, onChange, multiline });
81
+ useEffect2(() => {
82
+ if (ref.current && ref.current.textContent !== localValue) {
83
+ ref.current.textContent = localValue;
84
+ }
85
+ }, [localValue]);
86
+ const onInput = useCallback2(() => {
87
+ if (ref.current) {
88
+ handleInput(ref.current.textContent || "");
89
+ }
90
+ }, [handleInput]);
91
+ const onKeyDown = useCallback2(
92
+ (e) => {
93
+ if (e.key === "Enter" && shouldPreventEnter) {
94
+ e.preventDefault();
95
+ }
96
+ },
97
+ [shouldPreventEnter]
98
+ );
99
+ const onPaste = useCallback2(
100
+ (e) => {
101
+ e.preventDefault();
102
+ const text = e.clipboardData.getData("text/plain");
103
+ const selection = window.getSelection();
104
+ if (selection && selection.rangeCount > 0) {
105
+ const range = selection.getRangeAt(0);
106
+ range.deleteContents();
107
+ range.insertNode(document.createTextNode(text));
108
+ range.collapse(false);
109
+ selection.removeAllRanges();
110
+ selection.addRange(range);
111
+ }
112
+ if (ref.current) {
113
+ handleInput(ref.current.textContent || "");
114
+ }
115
+ },
116
+ [handleInput]
117
+ );
118
+ return createElement(
119
+ tag,
120
+ {
121
+ ref,
122
+ className: isEditMode && placeholder ? `editable-placeholder ${className ?? ""}` : className,
123
+ contentEditable: isEditMode ? "true" : void 0,
124
+ suppressContentEditableWarning: isEditMode || void 0,
125
+ onInput: isEditMode ? onInput : void 0,
126
+ onBlur: isEditMode ? handleBlur : void 0,
127
+ onKeyDown: isEditMode ? onKeyDown : void 0,
128
+ onPaste: isEditMode ? onPaste : void 0,
129
+ "data-placeholder": isEditMode && placeholder ? placeholder : void 0,
130
+ ...htmlAttrs
131
+ },
132
+ value
133
+ );
134
+ }
135
+
136
+ // src/components/primitives/defineHeadingSection.tsx
137
+ import { z } from "zod";
138
+
139
+ // src/components/primitives/HeadingSection.tsx
140
+ import { jsx } from "react/jsx-runtime";
141
+ function HeadingSection({ heading, tag, placeholder, className, onChange }) {
142
+ return /* @__PURE__ */ jsx(
143
+ EditablePlainText,
144
+ {
145
+ tag,
146
+ value: heading,
147
+ onChange: onChange ?? (() => {
148
+ }),
149
+ isEditMode: !!onChange,
150
+ placeholder,
151
+ id: toSectionId(heading),
152
+ className: cn("scroll-mt-20", className)
153
+ }
154
+ );
155
+ }
156
+
157
+ // src/components/primitives/defineHeadingSection.tsx
158
+ import { jsx as jsx2 } from "react/jsx-runtime";
159
+ function defineHeadingSection(config) {
160
+ const content = config.navExclusion ? z.object({ heading: z.string(), excludeFromNav: z.boolean().optional() }) : z.object({ heading: z.string() });
161
+ const schema = z.object({ type: z.literal(config.type), content });
162
+ return defineSection({
163
+ type: config.type,
164
+ label: config.label,
165
+ icon: config.icon,
166
+ schema,
167
+ navRole: config.navRole,
168
+ component: ({ content: content2, onChange }) => /* @__PURE__ */ jsx2(
169
+ HeadingSection,
170
+ {
171
+ heading: content2.content.heading,
172
+ tag: config.tag,
173
+ placeholder: config.placeholder,
174
+ onChange: onChange ? (heading) => onChange({ ...content2, content: { ...content2.content, heading } }) : void 0
175
+ }
176
+ ),
177
+ defaults: () => ({ type: config.type, content: { heading: config.defaultHeading } }),
178
+ getLabel: (content2) => content2.content.heading,
179
+ ...config.navExclusion ? {
180
+ settings: {
181
+ excludeFromNav: { type: "checkbox", label: "Exclude from navigation", default: false, target: "content" }
182
+ }
183
+ } : {}
184
+ });
185
+ }
186
+
187
+ // src/components/primitives/LinkPopover.tsx
188
+ import { useState as useState2 } from "react";
189
+
190
+ // src/components/shared/Button.tsx
191
+ import { forwardRef } from "react";
192
+ import { jsx as jsx3 } from "react/jsx-runtime";
193
+ var boxSize = {
194
+ sm: "rounded px-3 py-1.5 text-xs font-medium",
195
+ md: "rounded px-4 py-2 text-sm font-medium"
196
+ };
197
+ var ghostSize = {
198
+ sm: "text-xs font-medium",
199
+ md: "text-sm font-medium"
200
+ };
201
+ var variantClasses = {
202
+ primary: "bg-base-contrast text-base-accent hover:bg-base-contrast/90",
203
+ brand: "bg-primary text-primary-contrast hover:opacity-90",
204
+ secondary: "border border-base-200 text-base-contrast hover:bg-base-accent",
205
+ destructive: "border border-red-600 text-red-600 hover:bg-red-600 hover:text-white"
206
+ };
207
+ var ghostToneClasses = {
208
+ neutral: "text-base-contrast-light hover:text-base-contrast",
209
+ destructive: "text-red-600 hover:text-red-800"
210
+ };
211
+ var Button = forwardRef(function Button2({
212
+ variant = "primary",
213
+ size = "sm",
214
+ tone = "neutral",
215
+ fullWidth = false,
216
+ isLoading = false,
217
+ loadingLabel,
218
+ disabled,
219
+ className,
220
+ children,
221
+ ...rest
222
+ }, ref) {
223
+ const isGhost = variant === "ghost";
224
+ const resolvedDisabled = disabled || isLoading;
225
+ const label = isLoading && loadingLabel ? loadingLabel : children;
226
+ return /* @__PURE__ */ jsx3(
227
+ "button",
228
+ {
229
+ ref,
230
+ disabled: resolvedDisabled,
231
+ className: cn(
232
+ "inline-flex items-center justify-center transition-colors",
233
+ isGhost ? ghostSize[size] : boxSize[size],
234
+ isGhost ? ghostToneClasses[tone] : variantClasses[variant],
235
+ fullWidth && "w-full",
236
+ resolvedDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
237
+ className
238
+ ),
239
+ ...rest,
240
+ children: label
241
+ }
242
+ );
243
+ });
244
+
245
+ // src/lib/links.ts
246
+ function internalHref(pageId, anchorSectionId) {
247
+ return `page://${pageId}${anchorSectionId ? `#${anchorSectionId}` : ""}`;
248
+ }
249
+ function parseInternalHref(href) {
250
+ const m = /^page:\/\/([^#]+)(?:#(.+))?$/.exec(href);
251
+ return m ? { pageId: m[1], anchorSectionId: m[2] ?? null } : null;
252
+ }
253
+
254
+ // src/components/primitives/LinkPopover.tsx
255
+ import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
256
+ var selectClasses = "w-full rounded border border-base-contrast/20 bg-base px-2 py-1 text-sm text-base-contrast focus:outline-none focus:ring-1 focus:ring-primary";
257
+ function LinkPopover({ editor, onClose }) {
258
+ const { pages, getPageHeadings } = usePages();
259
+ const existingAttrs = editor.getAttributes("link");
260
+ const existingInternal = parseInternalHref(existingAttrs.href ?? "");
261
+ const [mode, setMode] = useState2(existingInternal ? "internal" : "external");
262
+ const [url, setUrl] = useState2(existingInternal ? "" : existingAttrs.href ?? "");
263
+ const [pageId, setPageId] = useState2(existingInternal?.pageId ?? "");
264
+ const [anchorId, setAnchorId] = useState2(existingInternal?.anchorSectionId ?? "");
265
+ const [openInNewTab, setOpenInNewTab] = useState2(
266
+ existingAttrs.target === "_blank"
267
+ );
268
+ const hasLink = Boolean(existingAttrs.href);
269
+ const headings = pageId ? getPageHeadings(pageId) : [];
270
+ const handleApply = () => {
271
+ if (mode === "internal") {
272
+ if (pageId) {
273
+ editor.chain().focus().extendMarkRange("link").setLink({ href: internalHref(pageId, anchorId || null), target: null }).run();
274
+ }
275
+ } else if (url.trim()) {
276
+ editor.chain().focus().extendMarkRange("link").setLink({ href: url.trim(), target: openInNewTab ? "_blank" : null }).run();
277
+ }
278
+ onClose();
279
+ };
280
+ const handleRemove = () => {
281
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
282
+ onClose();
283
+ };
284
+ const handleKeyDown = (e) => {
285
+ if (e.key === "Enter") {
286
+ e.preventDefault();
287
+ handleApply();
288
+ } else if (e.key === "Escape") {
289
+ e.preventDefault();
290
+ onClose();
291
+ }
292
+ };
293
+ return /* @__PURE__ */ jsxs(
294
+ "div",
295
+ {
296
+ className: "flex w-64 flex-col gap-2 rounded bg-base-accent p-3 shadow-lg",
297
+ onMouseDown: (e) => e.stopPropagation(),
298
+ onKeyDown: handleKeyDown,
299
+ children: [
300
+ /* @__PURE__ */ jsx4("div", { className: "flex gap-2", role: "tablist", children: ["external", "internal"].map((kind) => /* @__PURE__ */ jsx4(
301
+ "button",
302
+ {
303
+ type: "button",
304
+ role: "tab",
305
+ "aria-selected": mode === kind,
306
+ onClick: () => setMode(kind),
307
+ className: cn(
308
+ "cursor-pointer rounded border px-2 py-0.5 text-xs",
309
+ mode === kind ? "border-primary bg-primary text-primary-contrast" : "border-base-200 text-base-contrast"
310
+ ),
311
+ children: kind === "external" ? "External URL" : "Internal page"
312
+ },
313
+ kind
314
+ )) }),
315
+ mode === "external" ? /* @__PURE__ */ jsxs(Fragment, { children: [
316
+ /* @__PURE__ */ jsx4(
317
+ "input",
318
+ {
319
+ autoFocus: true,
320
+ type: "url",
321
+ value: url,
322
+ onChange: (e) => setUrl(e.target.value),
323
+ placeholder: "https://example.com",
324
+ className: "rounded border border-base-contrast/20 bg-base px-2 py-1 text-sm text-base-contrast placeholder:text-base-contrast/40 focus:outline-none focus:ring-1 focus:ring-primary"
325
+ }
326
+ ),
327
+ /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm text-base-contrast", children: [
328
+ /* @__PURE__ */ jsx4(
329
+ "input",
330
+ {
331
+ type: "checkbox",
332
+ checked: openInNewTab,
333
+ onChange: (e) => setOpenInNewTab(e.target.checked),
334
+ className: "accent-primary"
335
+ }
336
+ ),
337
+ "Open in new tab"
338
+ ] })
339
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
340
+ /* @__PURE__ */ jsxs(
341
+ "select",
342
+ {
343
+ autoFocus: true,
344
+ "aria-label": "Page",
345
+ value: pageId,
346
+ onChange: (e) => {
347
+ setPageId(e.target.value);
348
+ setAnchorId("");
349
+ },
350
+ className: selectClasses,
351
+ children: [
352
+ /* @__PURE__ */ jsx4("option", { value: "", children: "Select a page\u2026" }),
353
+ pages.map((p) => /* @__PURE__ */ jsx4("option", { value: p.id, children: p.title }, p.id))
354
+ ]
355
+ }
356
+ ),
357
+ /* @__PURE__ */ jsxs(
358
+ "select",
359
+ {
360
+ "aria-label": "Section",
361
+ value: anchorId,
362
+ disabled: !pageId,
363
+ onChange: (e) => setAnchorId(e.target.value),
364
+ className: cn(selectClasses, !pageId && "cursor-not-allowed opacity-50"),
365
+ children: [
366
+ /* @__PURE__ */ jsx4("option", { value: "", children: "None (top of page)" }),
367
+ headings.map((h) => /* @__PURE__ */ jsx4("option", { value: h.id, children: h.label }, h.id))
368
+ ]
369
+ }
370
+ )
371
+ ] }),
372
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
373
+ /* @__PURE__ */ jsx4(
374
+ Button,
375
+ {
376
+ variant: "brand",
377
+ size: "sm",
378
+ onMouseDown: (e) => {
379
+ e.preventDefault();
380
+ handleApply();
381
+ },
382
+ children: "Apply"
383
+ }
384
+ ),
385
+ hasLink && /* @__PURE__ */ jsx4(
386
+ Button,
387
+ {
388
+ variant: "secondary",
389
+ size: "sm",
390
+ onMouseDown: (e) => {
391
+ e.preventDefault();
392
+ handleRemove();
393
+ },
394
+ children: "Remove"
395
+ }
396
+ ),
397
+ /* @__PURE__ */ jsx4(
398
+ Button,
399
+ {
400
+ variant: "secondary",
401
+ size: "sm",
402
+ onMouseDown: (e) => {
403
+ e.preventDefault();
404
+ onClose();
405
+ },
406
+ children: "Cancel"
407
+ }
408
+ )
409
+ ] })
410
+ ]
411
+ }
412
+ );
413
+ }
414
+
415
+ // src/components/primitives/EditableRichText.tsx
416
+ import { useRef as useRef4, useCallback as useCallback4, useEffect as useEffect4 } from "react";
417
+ import { EditorContent } from "@tiptap/react";
418
+ import { BubbleMenu } from "@tiptap/react/menus";
419
+
420
+ // src/components/primitives/useEditableRichText.ts
421
+ import { useState as useState3, useRef as useRef3, useCallback as useCallback3, useEffect as useEffect3 } from "react";
422
+ import { Editor } from "@tiptap/core";
423
+ import Placeholder from "@tiptap/extension-placeholder";
424
+
425
+ // src/components/primitives/tiptap-presets.ts
426
+ import StarterKit from "@tiptap/starter-kit";
427
+ import Underline from "@tiptap/extension-underline";
428
+ import Link from "@tiptap/extension-link";
429
+
430
+ // ../../node_modules/.pnpm/@tiptap+extension-paragraph@3.22.4_@tiptap+core@3.22.4_@tiptap+pm@3.22.4_/node_modules/@tiptap/extension-paragraph/dist/index.js
431
+ import { mergeAttributes, Node } from "@tiptap/core";
432
+ var EMPTY_PARAGRAPH_MARKDOWN = " ";
433
+ var NBSP_CHAR = "\xA0";
434
+ var Paragraph = Node.create({
435
+ name: "paragraph",
436
+ priority: 1e3,
437
+ addOptions() {
438
+ return {
439
+ HTMLAttributes: {}
440
+ };
441
+ },
442
+ group: "block",
443
+ content: "inline*",
444
+ parseHTML() {
445
+ return [{ tag: "p" }];
446
+ },
447
+ renderHTML({ HTMLAttributes }) {
448
+ return ["p", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
449
+ },
450
+ parseMarkdown: (token, helpers) => {
451
+ const tokens = token.tokens || [];
452
+ if (tokens.length === 1 && tokens[0].type === "image") {
453
+ return helpers.parseChildren([tokens[0]]);
454
+ }
455
+ const content = helpers.parseInline(tokens);
456
+ const hasExplicitEmptyParagraphMarker = tokens.length === 1 && tokens[0].type === "text" && (tokens[0].raw === EMPTY_PARAGRAPH_MARKDOWN || tokens[0].text === EMPTY_PARAGRAPH_MARKDOWN || tokens[0].raw === NBSP_CHAR || tokens[0].text === NBSP_CHAR);
457
+ if (hasExplicitEmptyParagraphMarker && content.length === 1 && content[0].type === "text" && (content[0].text === EMPTY_PARAGRAPH_MARKDOWN || content[0].text === NBSP_CHAR)) {
458
+ return helpers.createNode("paragraph", void 0, []);
459
+ }
460
+ return helpers.createNode("paragraph", void 0, content);
461
+ },
462
+ renderMarkdown: (node, h, ctx) => {
463
+ var _a, _b;
464
+ if (!node) {
465
+ return "";
466
+ }
467
+ const content = Array.isArray(node.content) ? node.content : [];
468
+ if (content.length === 0) {
469
+ const previousContent = Array.isArray((_a = ctx == null ? void 0 : ctx.previousNode) == null ? void 0 : _a.content) ? ctx.previousNode.content : [];
470
+ const previousNodeIsEmptyParagraph = ((_b = ctx == null ? void 0 : ctx.previousNode) == null ? void 0 : _b.type) === "paragraph" && previousContent.length === 0;
471
+ return previousNodeIsEmptyParagraph ? EMPTY_PARAGRAPH_MARKDOWN : "";
472
+ }
473
+ return h.renderChildren(content);
474
+ },
475
+ addCommands() {
476
+ return {
477
+ setParagraph: () => ({ commands }) => {
478
+ return commands.setNode(this.name);
479
+ }
480
+ };
481
+ },
482
+ addKeyboardShortcuts() {
483
+ return {
484
+ "Mod-Alt-0": () => this.editor.commands.setParagraph()
485
+ };
486
+ }
487
+ });
488
+ var index_default = Paragraph;
489
+
490
+ // src/components/primitives/CustomParagraph.ts
491
+ var ALLOWED_CLASSES = ["large", "lead-in"];
492
+ var CustomParagraph = index_default.extend({
493
+ addAttributes() {
494
+ return {
495
+ ...this.parent?.(),
496
+ class: {
497
+ default: null,
498
+ parseHTML: (element) => {
499
+ const cls = element.getAttribute("class");
500
+ if (cls && ALLOWED_CLASSES.includes(cls)) {
501
+ return cls;
502
+ }
503
+ return null;
504
+ },
505
+ renderHTML: (attributes) => {
506
+ if (!attributes.class) return {};
507
+ return { class: attributes.class };
508
+ }
509
+ }
510
+ };
511
+ }
512
+ });
513
+
514
+ // src/components/primitives/tiptap-presets.ts
515
+ var basic = [
516
+ StarterKit.configure({
517
+ paragraph: false,
518
+ heading: false,
519
+ blockquote: false,
520
+ bulletList: false,
521
+ orderedList: false,
522
+ listItem: false,
523
+ codeBlock: false,
524
+ horizontalRule: false,
525
+ code: false,
526
+ strike: false
527
+ }),
528
+ CustomParagraph,
529
+ Underline
530
+ ];
531
+ var rich = [
532
+ StarterKit.configure({
533
+ paragraph: false,
534
+ heading: { levels: [3, 4] },
535
+ blockquote: false,
536
+ codeBlock: false,
537
+ horizontalRule: false,
538
+ code: false,
539
+ strike: false
540
+ }),
541
+ CustomParagraph,
542
+ Underline,
543
+ // `page` protocol: stable internal-page hrefs (page://{pageId}#{sectionId})
544
+ // resolved to real routes at SSR — see lib/links.ts.
545
+ Link.configure({ openOnClick: false, protocols: ["page"] })
546
+ ];
547
+ var presets = { basic, rich };
548
+
549
+ // src/components/primitives/useEditableRichText.ts
550
+ function useEditableRichText({
551
+ value,
552
+ onChange,
553
+ preset,
554
+ placeholder
555
+ }) {
556
+ const editorRef = useRef3(null);
557
+ const [editor, setEditor] = useState3(null);
558
+ const valueRef = useRef3(value);
559
+ const onChangeRef = useRef3(onChange);
560
+ useEffect3(() => {
561
+ valueRef.current = value;
562
+ }, [value]);
563
+ useEffect3(() => {
564
+ onChangeRef.current = onChange;
565
+ }, [onChange]);
566
+ const activate = useCallback3(() => {
567
+ if (editorRef.current) return;
568
+ const extensions = placeholder ? [...presets[preset], Placeholder.configure({ placeholder })] : presets[preset];
569
+ const ed = new Editor({
570
+ extensions,
571
+ content: valueRef.current,
572
+ // Commit edits to onChange on every transaction, not only on deactivate().
573
+ // Without this, content lives only inside TipTap until blur; clicking a
574
+ // Save button while the editor is still focused flushes persistence before
575
+ // the (150ms-delayed) blur->deactivate commit fires, silently dropping the
576
+ // in-progress edit. Live-committing keeps persistence in sync continuously.
577
+ onUpdate: ({ editor: updated }) => {
578
+ const html = updated.getHTML();
579
+ if (html !== valueRef.current) {
580
+ valueRef.current = html;
581
+ onChangeRef.current(html);
582
+ }
583
+ }
584
+ });
585
+ editorRef.current = ed;
586
+ setEditor(ed);
587
+ }, [preset, placeholder]);
588
+ const deactivate = useCallback3(() => {
589
+ const ed = editorRef.current;
590
+ if (!ed) return;
591
+ const html = ed.getHTML();
592
+ ed.destroy();
593
+ editorRef.current = null;
594
+ setEditor(null);
595
+ if (html !== valueRef.current) {
596
+ onChangeRef.current(html);
597
+ }
598
+ }, []);
599
+ useEffect3(() => {
600
+ return () => {
601
+ editorRef.current?.destroy();
602
+ editorRef.current = null;
603
+ };
604
+ }, []);
605
+ return {
606
+ isEditorActive: editor !== null,
607
+ editor,
608
+ activate,
609
+ deactivate
610
+ };
611
+ }
612
+
613
+ // src/components/primitives/RichTextToolbar.tsx
614
+ import { useState as useState4 } from "react";
615
+ import { useEditorState } from "@tiptap/react";
616
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
617
+ function ToolbarButton({ isActive, onClick, title, children }) {
618
+ return /* @__PURE__ */ jsx5(
619
+ "button",
620
+ {
621
+ type: "button",
622
+ title,
623
+ "aria-label": title,
624
+ "aria-pressed": isActive,
625
+ onMouseDown: (e) => {
626
+ e.preventDefault();
627
+ onClick();
628
+ },
629
+ className: cn(
630
+ "cursor-pointer rounded px-2 py-1 text-sm font-medium transition-colors",
631
+ isActive ? "bg-primary text-primary-contrast" : "bg-base-accent text-base-contrast hover:opacity-80"
632
+ ),
633
+ children
634
+ }
635
+ );
636
+ }
637
+ function Separator() {
638
+ return /* @__PURE__ */ jsx5("div", { className: "mx-1 h-5 w-px bg-base-contrast/20" });
639
+ }
640
+ function RichTextToolbar({ editor, preset }) {
641
+ const [showLinkPopover, setShowLinkPopover] = useState4(false);
642
+ const state = useEditorState({
643
+ editor,
644
+ selector: ({ editor: ed }) => ({
645
+ isBold: ed.isActive("bold"),
646
+ isItalic: ed.isActive("italic"),
647
+ isUnderline: ed.isActive("underline"),
648
+ isLink: ed.isActive("link"),
649
+ isBulletList: ed.isActive("bulletList"),
650
+ isOrderedList: ed.isActive("orderedList"),
651
+ isH3: ed.isActive("heading", { level: 3 }),
652
+ isH4: ed.isActive("heading", { level: 4 }),
653
+ isLarge: ed.isActive("paragraph", { class: "large" }),
654
+ isLeadIn: ed.isActive("paragraph", { class: "lead-in" })
655
+ })
656
+ });
657
+ const toggleParagraphVariant = (cls, isCurrentlyActive) => {
658
+ if (isCurrentlyActive) {
659
+ editor.chain().focus().setNode("paragraph").run();
660
+ } else {
661
+ editor.chain().focus().setNode("paragraph", { class: cls }).run();
662
+ }
663
+ };
664
+ return /* @__PURE__ */ jsxs2(
665
+ "div",
666
+ {
667
+ "data-testid": "rich-text-toolbar",
668
+ className: "flex items-center gap-1 rounded border border-base-contrast/10 bg-base-accent p-1 shadow-md",
669
+ children: [
670
+ /* @__PURE__ */ jsx5(
671
+ ToolbarButton,
672
+ {
673
+ isActive: state.isBold,
674
+ onClick: () => editor.chain().focus().toggleBold().run(),
675
+ title: "Bold",
676
+ children: /* @__PURE__ */ jsx5("strong", { children: "B" })
677
+ }
678
+ ),
679
+ /* @__PURE__ */ jsx5(
680
+ ToolbarButton,
681
+ {
682
+ isActive: state.isItalic,
683
+ onClick: () => editor.chain().focus().toggleItalic().run(),
684
+ title: "Italic",
685
+ children: /* @__PURE__ */ jsx5("em", { children: "I" })
686
+ }
687
+ ),
688
+ /* @__PURE__ */ jsx5(
689
+ ToolbarButton,
690
+ {
691
+ isActive: state.isUnderline,
692
+ onClick: () => editor.chain().focus().toggleUnderline().run(),
693
+ title: "Underline",
694
+ children: /* @__PURE__ */ jsx5("span", { className: "underline", children: "U" })
695
+ }
696
+ ),
697
+ preset === "rich" && /* @__PURE__ */ jsxs2(Fragment2, { children: [
698
+ /* @__PURE__ */ jsx5(Separator, {}),
699
+ /* @__PURE__ */ jsxs2("div", { className: "relative", children: [
700
+ /* @__PURE__ */ jsx5(
701
+ ToolbarButton,
702
+ {
703
+ isActive: state.isLink || showLinkPopover,
704
+ onClick: () => setShowLinkPopover((prev) => !prev),
705
+ title: "Link",
706
+ children: "\u{1F517}"
707
+ }
708
+ ),
709
+ showLinkPopover && /* @__PURE__ */ jsx5("div", { className: "absolute left-0 top-full z-50 mt-1", children: /* @__PURE__ */ jsx5(
710
+ LinkPopover,
711
+ {
712
+ editor,
713
+ onClose: () => setShowLinkPopover(false)
714
+ }
715
+ ) })
716
+ ] }),
717
+ /* @__PURE__ */ jsx5(Separator, {}),
718
+ /* @__PURE__ */ jsx5(
719
+ ToolbarButton,
720
+ {
721
+ isActive: state.isBulletList,
722
+ onClick: () => editor.chain().focus().toggleBulletList().run(),
723
+ title: "Bullet List",
724
+ children: "\u2022\u2261"
725
+ }
726
+ ),
727
+ /* @__PURE__ */ jsx5(
728
+ ToolbarButton,
729
+ {
730
+ isActive: state.isOrderedList,
731
+ onClick: () => editor.chain().focus().toggleOrderedList().run(),
732
+ title: "Ordered List",
733
+ children: "1\u2261"
734
+ }
735
+ ),
736
+ /* @__PURE__ */ jsx5(Separator, {}),
737
+ /* @__PURE__ */ jsx5(
738
+ ToolbarButton,
739
+ {
740
+ isActive: state.isH3,
741
+ onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
742
+ title: "Heading 3",
743
+ children: "H3"
744
+ }
745
+ ),
746
+ /* @__PURE__ */ jsx5(
747
+ ToolbarButton,
748
+ {
749
+ isActive: state.isH4,
750
+ onClick: () => editor.chain().focus().toggleHeading({ level: 4 }).run(),
751
+ title: "Heading 4",
752
+ children: "H4"
753
+ }
754
+ ),
755
+ /* @__PURE__ */ jsx5(
756
+ ToolbarButton,
757
+ {
758
+ isActive: state.isLarge,
759
+ onClick: () => toggleParagraphVariant("large", state.isLarge),
760
+ title: "Large Paragraph",
761
+ children: "Lg"
762
+ }
763
+ ),
764
+ /* @__PURE__ */ jsx5(
765
+ ToolbarButton,
766
+ {
767
+ isActive: state.isLeadIn,
768
+ onClick: () => toggleParagraphVariant("lead-in", state.isLeadIn),
769
+ title: "Lead-In",
770
+ children: "Li"
771
+ }
772
+ )
773
+ ] })
774
+ ]
775
+ }
776
+ );
777
+ }
778
+
779
+ // src/components/primitives/EditableRichText.tsx
780
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
781
+ function isContentEmpty(html) {
782
+ return html.replace(/<[^>]*>/g, "").trim().length === 0;
783
+ }
784
+ var DEFAULT_PLACEHOLDER = "Click to edit\u2026";
785
+ function EditableRichText({
786
+ value,
787
+ onChange,
788
+ isEditMode,
789
+ preset = "rich",
790
+ className,
791
+ placeholder = DEFAULT_PLACEHOLDER
792
+ }) {
793
+ const { isEditorActive, editor, activate, deactivate } = useEditableRichText({ value, onChange, preset, placeholder });
794
+ const wrapperRef = useRef4(null);
795
+ const blurTimeoutRef = useRef4(null);
796
+ const clickCoordsRef = useRef4(null);
797
+ useEffect4(() => {
798
+ return () => {
799
+ if (blurTimeoutRef.current) {
800
+ clearTimeout(blurTimeoutRef.current);
801
+ }
802
+ };
803
+ }, []);
804
+ useEffect4(() => {
805
+ if (!isEditorActive || !editor) return;
806
+ requestAnimationFrame(() => {
807
+ if (editor.isDestroyed) return;
808
+ const coords = clickCoordsRef.current;
809
+ if (coords) {
810
+ try {
811
+ const pos = editor.view.posAtCoords(coords);
812
+ if (pos) {
813
+ editor.commands.focus();
814
+ editor.commands.setTextSelection(pos.pos);
815
+ } else {
816
+ editor.commands.focus("end");
817
+ }
818
+ } catch {
819
+ editor.commands.focus("end");
820
+ } finally {
821
+ clickCoordsRef.current = null;
822
+ }
823
+ } else {
824
+ editor.commands.focus("end");
825
+ }
826
+ });
827
+ }, [isEditorActive, editor]);
828
+ const handleClick = useCallback4((e) => {
829
+ if (isEditMode && !isEditorActive) {
830
+ clickCoordsRef.current = { left: e.clientX, top: e.clientY };
831
+ activate();
832
+ }
833
+ }, [isEditMode, isEditorActive, activate]);
834
+ const handleFocus = useCallback4(() => {
835
+ if (blurTimeoutRef.current) {
836
+ clearTimeout(blurTimeoutRef.current);
837
+ blurTimeoutRef.current = null;
838
+ }
839
+ }, []);
840
+ const handleBlur = useCallback4(() => {
841
+ blurTimeoutRef.current = setTimeout(() => {
842
+ if (wrapperRef.current && !wrapperRef.current.contains(document.activeElement)) {
843
+ deactivate();
844
+ }
845
+ }, 150);
846
+ }, [deactivate]);
847
+ if (!isEditMode) {
848
+ return /* @__PURE__ */ jsx6(
849
+ "div",
850
+ {
851
+ className,
852
+ dangerouslySetInnerHTML: { __html: sanitizeHtml(value) }
853
+ }
854
+ );
855
+ }
856
+ if (!isEditorActive || !editor) {
857
+ const empty = isContentEmpty(value);
858
+ if (empty) {
859
+ return /* @__PURE__ */ jsx6("div", { className, onClick: handleClick, children: /* @__PURE__ */ jsx6("p", { className: "tiptap-placeholder", children: placeholder }) });
860
+ }
861
+ return /* @__PURE__ */ jsx6(
862
+ "div",
863
+ {
864
+ className,
865
+ onClick: handleClick,
866
+ dangerouslySetInnerHTML: { __html: sanitizeHtml(value) }
867
+ }
868
+ );
869
+ }
870
+ return /* @__PURE__ */ jsxs3(
871
+ "div",
872
+ {
873
+ ref: wrapperRef,
874
+ className,
875
+ onFocus: handleFocus,
876
+ onBlur: handleBlur,
877
+ children: [
878
+ /* @__PURE__ */ jsx6(
879
+ BubbleMenu,
880
+ {
881
+ editor,
882
+ className: "z-50",
883
+ shouldShow: ({ editor: ed, state }) => {
884
+ const { from, to } = state.selection;
885
+ return ed.isFocused && from !== to;
886
+ },
887
+ children: /* @__PURE__ */ jsx6(RichTextToolbar, { editor, preset })
888
+ }
889
+ ),
890
+ /* @__PURE__ */ jsx6(EditorContent, { editor })
891
+ ]
892
+ }
893
+ );
894
+ }
895
+
896
+ // src/components/primitives/ResolvedMedia.tsx
897
+ import { ImageIcon } from "lucide-react";
898
+
899
+ // src/hooks/useResolvedMedia.ts
900
+ import { useMemo } from "react";
901
+
902
+ // src/components/shell/MediaLibraryContext.tsx
903
+ import { createContext, useContext } from "react";
904
+ var MediaLibraryContext = createContext(null);
905
+ function useMediaLibrary() {
906
+ return useContext(MediaLibraryContext);
907
+ }
908
+
909
+ // src/hooks/useResolvedMedia.ts
910
+ function useResolvedMedia(imageId) {
911
+ const ctx = useMediaLibrary();
912
+ return useMemo(() => {
913
+ if (!imageId || !ctx) {
914
+ return { src: void 0, srcset: void 0, poster: void 0, alt: "", kind: "image" };
915
+ }
916
+ const { manifest, pendingItems, pendingLocalUrls, siteConfig } = ctx;
917
+ const sizes = siteConfig?.media?.sizes ?? [640, 1080, 1920];
918
+ const manifestItem = manifest.images[imageId];
919
+ const pendingItem = pendingItems.find((i) => i.id === imageId);
920
+ const item = manifestItem ?? pendingItem;
921
+ const alt = item?.alt ?? "";
922
+ const kind = item?.kind ?? "image";
923
+ const localUrl = pendingLocalUrls[imageId];
924
+ if (localUrl) {
925
+ return { src: localUrl, srcset: void 0, poster: void 0, alt, kind };
926
+ }
927
+ if (manifestItem) {
928
+ const provider = getMediaProvider();
929
+ const resolved = provider.resolve(manifestItem, sizes);
930
+ if (resolved && resolved.tag === "img") {
931
+ return {
932
+ src: resolved.src,
933
+ srcset: "srcSet" in resolved ? resolved.srcSet : void 0,
934
+ poster: void 0,
935
+ alt,
936
+ kind
937
+ };
938
+ }
939
+ if (resolved && resolved.tag === "video") {
940
+ return { src: resolved.src, srcset: void 0, poster: resolved.poster, alt, kind };
941
+ }
942
+ }
943
+ return { src: void 0, srcset: void 0, poster: void 0, alt, kind };
944
+ }, [imageId, ctx]);
945
+ }
946
+
947
+ // src/components/primitives/ResolvedMedia.tsx
948
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
949
+ function ResolvedMedia({
950
+ imageId,
951
+ src: propSrc,
952
+ srcset: propSrcset,
953
+ alt: propAlt,
954
+ className,
955
+ imgClassName,
956
+ invertFrom
957
+ }) {
958
+ const resolved = useResolvedMedia(imageId);
959
+ const src = propSrc || resolved.src;
960
+ const srcset = propSrcset || resolved.srcset;
961
+ const poster = resolved.poster;
962
+ const alt = propAlt ?? resolved.alt;
963
+ const kind = resolved.kind;
964
+ const invertClass = invertFrom === "light" ? "invert dark:invert-0" : invertFrom === "dark" ? "dark:invert" : void 0;
965
+ const mediaClass = cn("h-full w-full", imgClassName, invertClass);
966
+ return /* @__PURE__ */ jsx7("div", { className, children: src ? kind === "video" ? /* @__PURE__ */ jsx7("video", { src, poster, muted: true, playsInline: true, loop: true, autoPlay: true, className: mediaClass }) : /* @__PURE__ */ jsx7("img", { src, srcSet: srcset, alt, className: mediaClass }) : /* @__PURE__ */ jsxs4("div", { className: "flex aspect-video h-full w-full flex-col items-center justify-center gap-1 rounded bg-base-accent text-base-contrast-light/50", children: [
967
+ /* @__PURE__ */ jsx7(ImageIcon, { size: 24 }),
968
+ /* @__PURE__ */ jsx7("span", { className: "text-sm", children: "No Image" })
969
+ ] }) });
970
+ }
971
+
972
+ // src/components/primitives/ImageDropZone.tsx
973
+ import { useState as useState5 } from "react";
974
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
975
+ var ACCEPTED = /^(image|video)\//;
976
+ function ImageDropZone({ onImageSelected, className, children }) {
977
+ const mediaLibrary = useMediaLibrary();
978
+ const [dragging, setDragging] = useState5(false);
979
+ if (!mediaLibrary) return /* @__PURE__ */ jsx8(Fragment3, { children });
980
+ const maxFileSize = mediaLibrary.siteConfig?.media.maxFileSize;
981
+ const hasFiles = (e) => e.dataTransfer.types.includes("Files");
982
+ const handleDragOver = (e) => {
983
+ if (!hasFiles(e)) return;
984
+ e.preventDefault();
985
+ e.dataTransfer.dropEffect = "copy";
986
+ setDragging(true);
987
+ };
988
+ const handleDragLeave = (e) => {
989
+ if (!hasFiles(e)) return;
990
+ const related = e.relatedTarget;
991
+ if (related && e.currentTarget.contains(related)) return;
992
+ setDragging(false);
993
+ };
994
+ const handleDrop = (e) => {
995
+ if (!hasFiles(e)) return;
996
+ e.preventDefault();
997
+ e.stopPropagation();
998
+ setDragging(false);
999
+ const file = e.dataTransfer.files[0];
1000
+ if (!file) return;
1001
+ if (!ACCEPTED.test(file.type)) {
1002
+ mediaLibrary.reportRejectedUploads([{ name: file.name, reason: "type" }]);
1003
+ return;
1004
+ }
1005
+ if (maxFileSize && file.size > maxFileSize) {
1006
+ mediaLibrary.reportRejectedUploads([{ name: file.name, reason: "size" }]);
1007
+ return;
1008
+ }
1009
+ mediaLibrary.uploadFileWithCallback(file, onImageSelected);
1010
+ };
1011
+ return /* @__PURE__ */ jsxs5(
1012
+ "div",
1013
+ {
1014
+ className: cn("relative", className),
1015
+ onDragOver: handleDragOver,
1016
+ onDragLeave: handleDragLeave,
1017
+ onDrop: handleDrop,
1018
+ children: [
1019
+ children,
1020
+ dragging && /* @__PURE__ */ jsx8("div", { className: "pointer-events-none absolute inset-0 z-10 rounded-md border-2 border-primary bg-primary/5" })
1021
+ ]
1022
+ }
1023
+ );
1024
+ }
1025
+
1026
+ // src/components/sections/Media/MediaBlock.tsx
1027
+ import { Check, X } from "lucide-react";
1028
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1029
+ function MediaBlock({ refValue, options, link, dodont, isEditMode, onRefChange }) {
1030
+ const mediaLibrary = useMediaLibrary();
1031
+ const fit = refValue.objectFit ?? options.objectFit;
1032
+ const fitClass = fit === "cover" ? "object-cover" : "object-contain";
1033
+ const showBorder = refValue.border ?? options.border;
1034
+ const captionStr = refValue.caption ? Array.isArray(refValue.caption) ? refValue.caption.join("\n") : refValue.caption : "";
1035
+ const replace = onRefChange ? (imageId) => onRefChange({ ...refValue, imageId }) : void 0;
1036
+ const resolved = /* @__PURE__ */ jsx9(
1037
+ ResolvedMedia,
1038
+ {
1039
+ imageId: refValue.imageId || void 0,
1040
+ className: "h-full w-full",
1041
+ imgClassName: fitClass,
1042
+ invertFrom: refValue.invertFrom
1043
+ }
1044
+ );
1045
+ let media = isEditMode && replace ? /* @__PURE__ */ jsx9(ImageDropZone, { onImageSelected: replace, className: "h-full w-full", children: resolved }) : resolved;
1046
+ if (!isEditMode && link && link.kind === "external" && link.href) {
1047
+ media = /* @__PURE__ */ jsx9("a", { href: link.href, target: link.target, className: "group block", children: media });
1048
+ }
1049
+ const onImageClick = isEditMode && replace && mediaLibrary ? () => mediaLibrary.openSelectModal((imageId) => replace(imageId)) : void 0;
1050
+ return /* @__PURE__ */ jsxs6("figure", { children: [
1051
+ dodont && /* @__PURE__ */ jsx9("div", { className: "flex h-10 items-center justify-center pb-1", children: dodont === "do" ? /* @__PURE__ */ jsx9(Check, { size: 28, className: "text-green-600" }) : /* @__PURE__ */ jsx9(X, { size: 28, className: "text-red-600" }) }),
1052
+ /* @__PURE__ */ jsx9(
1053
+ "div",
1054
+ {
1055
+ "data-testid": "media-open-surface",
1056
+ className: cn(
1057
+ "overflow-hidden rounded-md",
1058
+ showBorder && "border border-base-200",
1059
+ options.square && "aspect-square",
1060
+ onImageClick && "cursor-pointer"
1061
+ ),
1062
+ onClick: onImageClick,
1063
+ onPointerDown: onImageClick ? (e) => e.stopPropagation() : void 0,
1064
+ children: media
1065
+ }
1066
+ ),
1067
+ options.showCaption && /* @__PURE__ */ jsx9("figcaption", { className: "mt-2 min-h-[1em] text-sm text-base-contrast-light", children: isEditMode && onRefChange ? /* @__PURE__ */ jsx9(
1068
+ EditablePlainText,
1069
+ {
1070
+ tag: "span",
1071
+ value: captionStr,
1072
+ onChange: (caption) => onRefChange({ ...refValue, caption: caption || void 0 }),
1073
+ isEditMode: true,
1074
+ placeholder: "Caption",
1075
+ className: "block min-h-[1em]"
1076
+ }
1077
+ ) : captionStr || " " })
1078
+ ] });
1079
+ }
1080
+
1081
+ // src/components/primitives/EditableGrid.tsx
1082
+ import { useRef as useRef6, useEffect as useEffect6, useState as useState7 } from "react";
1083
+ import { ImageIcon as ImageIcon2 } from "lucide-react";
1084
+
1085
+ // src/lib/container-grid.ts
1086
+ var MAX_CONTAINER_COLUMNS = 6;
1087
+ var containerGridClass = {
1088
+ 1: "grid-cols-1",
1089
+ 2: "grid-cols-1 @sm:grid-cols-2",
1090
+ 3: "grid-cols-1 @sm:grid-cols-2 @lg:grid-cols-3",
1091
+ 4: "grid-cols-1 @xs:grid-cols-2 @lg:grid-cols-4",
1092
+ 5: "grid-cols-1 @xs:grid-cols-2 @md:grid-cols-3 @xl:grid-cols-5",
1093
+ 6: "grid-cols-1 @xs:grid-cols-2 @md:grid-cols-3 @2xl:grid-cols-6"
1094
+ };
1095
+ var colSpanClass = {
1096
+ 1: { 1: "" },
1097
+ 2: { 1: "", 2: "col-span-1 @sm:col-span-2" },
1098
+ 3: { 1: "", 2: "col-span-1 @sm:col-span-2", 3: "col-span-1 @sm:col-span-2 @lg:col-span-3" },
1099
+ 4: { 1: "", 2: "col-span-1 @xs:col-span-2", 3: "col-span-1 @xs:col-span-2 @lg:col-span-3", 4: "col-span-1 @xs:col-span-2 @lg:col-span-4" },
1100
+ 5: { 1: "", 2: "col-span-1 @xs:col-span-2", 3: "col-span-1 @xs:col-span-2 @md:col-span-3", 4: "col-span-1 @xs:col-span-2 @md:col-span-3 @xl:col-span-4", 5: "col-span-1 @xs:col-span-2 @md:col-span-3 @xl:col-span-5" },
1101
+ 6: { 1: "", 2: "col-span-1 @xs:col-span-2", 3: "col-span-1 @xs:col-span-2 @md:col-span-3", 4: "col-span-1 @xs:col-span-2 @md:col-span-3 @2xl:col-span-4", 5: "col-span-1 @xs:col-span-2 @md:col-span-3 @2xl:col-span-5", 6: "col-span-1 @xs:col-span-2 @md:col-span-3 @2xl:col-span-6" }
1102
+ };
1103
+ var spacerHiddenClass = {
1104
+ 1: "hidden",
1105
+ 2: "hidden @sm:block",
1106
+ 3: "hidden @sm:block",
1107
+ 4: "hidden @xs:block",
1108
+ 5: "hidden @xs:block",
1109
+ 6: "hidden @xs:block"
1110
+ };
1111
+
1112
+ // src/components/primitives/useEditableCollection.ts
1113
+ import { useState as useState6, useCallback as useCallback5, useRef as useRef5, useEffect as useEffect5 } from "react";
1114
+ var nextId = 0;
1115
+ function uid() {
1116
+ return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `_id_${nextId++}_${Date.now()}`;
1117
+ }
1118
+ function wrapItems(items) {
1119
+ return items.map((data) => ({ id: uid(), data }));
1120
+ }
1121
+ function useEditableCollection({
1122
+ items,
1123
+ onChange,
1124
+ createItem
1125
+ }) {
1126
+ const [wrappedItems, setWrappedItems] = useState6(
1127
+ () => wrapItems(items)
1128
+ );
1129
+ const prevItemsRef = useRef5(items);
1130
+ useEffect5(() => {
1131
+ if (items === prevItemsRef.current) return;
1132
+ prevItemsRef.current = items;
1133
+ setWrappedItems((prev) => {
1134
+ return items.map((data, i) => {
1135
+ if (i < prev.length) {
1136
+ return { id: prev[i].id, data };
1137
+ }
1138
+ return { id: uid(), data };
1139
+ });
1140
+ });
1141
+ }, [items]);
1142
+ const onReorder = useCallback5(
1143
+ (fromIndex, toIndex) => {
1144
+ setWrappedItems((prev) => {
1145
+ const next = [...prev];
1146
+ const [moved] = next.splice(fromIndex, 1);
1147
+ next.splice(toIndex, 0, moved);
1148
+ onChange(next.map((w) => w.data));
1149
+ return next;
1150
+ });
1151
+ },
1152
+ [onChange]
1153
+ );
1154
+ const onAdd = useCallback5(() => {
1155
+ const newItem = createItem();
1156
+ setWrappedItems((prev) => {
1157
+ const next = [...prev, { id: uid(), data: newItem }];
1158
+ onChange(next.map((w) => w.data));
1159
+ return next;
1160
+ });
1161
+ }, [createItem, onChange]);
1162
+ const onInsert = useCallback5(
1163
+ (index) => {
1164
+ const newItem = createItem();
1165
+ setWrappedItems((prev) => {
1166
+ const next = [...prev];
1167
+ next.splice(index, 0, { id: uid(), data: newItem });
1168
+ onChange(next.map((w) => w.data));
1169
+ return next;
1170
+ });
1171
+ },
1172
+ [createItem, onChange]
1173
+ );
1174
+ const onRemove = useCallback5(
1175
+ (id) => {
1176
+ setWrappedItems((prev) => {
1177
+ const next = prev.filter((w) => w.id !== id);
1178
+ onChange(next.map((w) => w.data));
1179
+ return next;
1180
+ });
1181
+ },
1182
+ [onChange]
1183
+ );
1184
+ return { wrappedItems, onReorder, onAdd, onInsert, onRemove };
1185
+ }
1186
+
1187
+ // src/components/primitives/EditableGrid.tsx
1188
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1189
+ function EditableGrid({
1190
+ items,
1191
+ columns,
1192
+ onChange,
1193
+ createItem,
1194
+ isEditMode,
1195
+ renderItem,
1196
+ onItemSettings,
1197
+ onItemImageClick,
1198
+ chromeTopClass,
1199
+ className
1200
+ }) {
1201
+ const { wrappedItems, onReorder, onAdd, onInsert, onRemove } = useEditableCollection({
1202
+ items,
1203
+ onChange,
1204
+ createItem
1205
+ });
1206
+ const [dragState, setDragState] = useState7({ sourceId: null, closestEdge: null });
1207
+ const [isDragging, setIsDragging] = useState7(false);
1208
+ useEffect6(() => {
1209
+ let cleanup;
1210
+ let cancelled = false;
1211
+ import("./adapter-HH47ZPGM.js").then(({ monitorForElements }) => {
1212
+ if (cancelled) return;
1213
+ cleanup = monitorForElements({
1214
+ onDragStart: ({ source }) => {
1215
+ if (source.data.dragType === "grid-cell") setIsDragging(true);
1216
+ },
1217
+ onDrop: ({ source }) => {
1218
+ if (source.data.dragType === "grid-cell") setIsDragging(false);
1219
+ }
1220
+ });
1221
+ });
1222
+ return () => {
1223
+ cancelled = true;
1224
+ cleanup?.();
1225
+ };
1226
+ }, []);
1227
+ if (!isEditMode) {
1228
+ return /* @__PURE__ */ jsx10("div", { className: "@container", children: /* @__PURE__ */ jsx10("div", { className: cn("grid gap-4", containerGridClass[columns] || containerGridClass[1], className), children: wrappedItems.map((wrapped, index) => /* @__PURE__ */ jsx10("div", { children: renderItem(wrapped.data, { isEditMode: false, index }) }, wrapped.id)) }) });
1229
+ }
1230
+ return /* @__PURE__ */ jsx10("div", { className: "@container", children: /* @__PURE__ */ jsxs7("div", { className: cn("group/grid relative grid gap-4", containerGridClass[columns] || containerGridClass[1], className), children: [
1231
+ wrappedItems.map((wrapped, index) => /* @__PURE__ */ jsx10(
1232
+ GridCell,
1233
+ {
1234
+ id: wrapped.id,
1235
+ index,
1236
+ isLast: index === wrappedItems.length - 1,
1237
+ isDragging,
1238
+ onReorder,
1239
+ onRemove,
1240
+ onInsert,
1241
+ onSettings: onItemSettings ? () => onItemSettings(index) : void 0,
1242
+ onImageClick: onItemImageClick ? () => onItemImageClick(index) : void 0,
1243
+ chromeTopClass,
1244
+ dragState,
1245
+ setDragState,
1246
+ children: renderItem(wrapped.data, { isEditMode: true, index })
1247
+ },
1248
+ wrapped.id
1249
+ )),
1250
+ /* @__PURE__ */ jsx10(
1251
+ IconButton,
1252
+ {
1253
+ icon: /* @__PURE__ */ jsx10(AddIcon, { size: 16 }),
1254
+ label: "Add item",
1255
+ size: "lg",
1256
+ intent: "primary",
1257
+ onClick: onAdd,
1258
+ className: "absolute -bottom-6 left-1/2 z-20 -translate-x-1/2 rounded-full border border-base-200 bg-base opacity-0 transition-opacity group-hover/grid:opacity-100"
1259
+ }
1260
+ )
1261
+ ] }) });
1262
+ }
1263
+ function GridCell({
1264
+ id,
1265
+ index,
1266
+ isLast,
1267
+ isDragging,
1268
+ onReorder,
1269
+ onRemove,
1270
+ onInsert,
1271
+ onSettings,
1272
+ onImageClick,
1273
+ chromeTopClass,
1274
+ dragState,
1275
+ setDragState,
1276
+ children
1277
+ }) {
1278
+ const cellRef = useRef6(null);
1279
+ const handleRef = useRef6(null);
1280
+ useEffect6(() => {
1281
+ const cell = cellRef.current;
1282
+ const handle = handleRef.current;
1283
+ if (!cell || !handle) return;
1284
+ let cleanup;
1285
+ let cancelled = false;
1286
+ Promise.all([
1287
+ import("./adapter-HH47ZPGM.js"),
1288
+ import("./closest-edge-EBOXL3YW.js")
1289
+ ]).then(([{ draggable, dropTargetForElements }, { attachClosestEdge, extractClosestEdge }]) => {
1290
+ if (cancelled) return;
1291
+ const cleanupDraggable = draggable({
1292
+ element: handle,
1293
+ getInitialData: () => ({ dragType: "grid-cell", id, index }),
1294
+ onDragStart: () => setDragState({ sourceId: id, closestEdge: null }),
1295
+ onDrop: () => setDragState({ sourceId: null, closestEdge: null })
1296
+ });
1297
+ const cleanupDropTarget = dropTargetForElements({
1298
+ element: cell,
1299
+ canDrop: ({ source }) => source.data.dragType === "grid-cell",
1300
+ getData: ({ input, element }) => attachClosestEdge(
1301
+ { id, index },
1302
+ { input, element, allowedEdges: ["left", "right"] }
1303
+ ),
1304
+ onDragEnter: ({ self }) => {
1305
+ const edge = extractClosestEdge(self.data);
1306
+ if (edge) setDragState((prev) => ({ ...prev, closestEdge: { id, edge } }));
1307
+ },
1308
+ onDrag: ({ self }) => {
1309
+ const edge = extractClosestEdge(self.data);
1310
+ if (edge) setDragState((prev) => ({ ...prev, closestEdge: { id, edge } }));
1311
+ },
1312
+ onDragLeave: () => {
1313
+ setDragState((prev) => prev.closestEdge?.id === id ? { ...prev, closestEdge: null } : prev);
1314
+ },
1315
+ onDrop: ({ source, self }) => {
1316
+ const fromIndex = source.data.index;
1317
+ const edge = extractClosestEdge(self.data);
1318
+ let toIndex = index;
1319
+ if (edge === "right") toIndex = index + 1;
1320
+ if (fromIndex < toIndex) toIndex--;
1321
+ if (fromIndex !== toIndex) {
1322
+ onReorder(fromIndex, toIndex);
1323
+ }
1324
+ }
1325
+ });
1326
+ cleanup = () => {
1327
+ cleanupDraggable();
1328
+ cleanupDropTarget();
1329
+ };
1330
+ });
1331
+ return () => {
1332
+ cancelled = true;
1333
+ cleanup?.();
1334
+ };
1335
+ }, [id, index, onReorder, setDragState]);
1336
+ const isCellDragging = dragState.sourceId === id;
1337
+ const showLeftEdge = dragState.closestEdge?.id === id && dragState.closestEdge.edge === "left" && dragState.sourceId !== id;
1338
+ const showRightEdge = dragState.closestEdge?.id === id && dragState.closestEdge.edge === "right" && dragState.sourceId !== id;
1339
+ return /* @__PURE__ */ jsxs7(
1340
+ "div",
1341
+ {
1342
+ ref: cellRef,
1343
+ className: cn(
1344
+ "group/cell relative",
1345
+ isCellDragging && "opacity-50"
1346
+ ),
1347
+ children: [
1348
+ showLeftEdge && /* @__PURE__ */ jsx10("div", { className: "absolute top-0 bottom-0 left-0 z-10 w-0.5 -translate-x-2 bg-primary" }),
1349
+ showRightEdge && /* @__PURE__ */ jsx10("div", { className: "absolute top-0 right-0 bottom-0 z-10 w-0.5 translate-x-2 bg-primary" }),
1350
+ !isDragging && !isLast && /* @__PURE__ */ jsx10(
1351
+ IconButton,
1352
+ {
1353
+ icon: /* @__PURE__ */ jsx10(AddIcon, { size: 12 }),
1354
+ label: "Insert item",
1355
+ size: "sm",
1356
+ intent: "primary",
1357
+ onClick: () => onInsert(index + 1),
1358
+ className: "absolute top-1/2 -right-2 z-20 -translate-y-1/2 translate-x-1/2 rounded-full border border-base-200 bg-base opacity-0 group-hover/cell:opacity-100"
1359
+ }
1360
+ ),
1361
+ /* @__PURE__ */ jsx10(
1362
+ IconButton,
1363
+ {
1364
+ ref: handleRef,
1365
+ icon: /* @__PURE__ */ jsx10(DragHandle, { size: 16 }),
1366
+ label: "Drag to reorder",
1367
+ className: cn("absolute left-1.5 z-10 cursor-grab bg-base/80 opacity-0 shadow-sm group-hover/cell:opacity-100 no-hover:opacity-100", chromeTopClass || "top-1.5"),
1368
+ tabIndex: -1
1369
+ }
1370
+ ),
1371
+ /* @__PURE__ */ jsxs7("div", { className: cn("absolute right-1.5 z-10 flex items-center gap-1 opacity-0 group-hover/cell:opacity-100 no-hover:opacity-100", chromeTopClass || "top-1.5"), children: [
1372
+ onImageClick && /* @__PURE__ */ jsx10(
1373
+ IconButton,
1374
+ {
1375
+ icon: /* @__PURE__ */ jsx10(ImageIcon2, { size: 16 }),
1376
+ label: "Change image",
1377
+ onClick: onImageClick,
1378
+ className: "bg-base/80 shadow-sm"
1379
+ }
1380
+ ),
1381
+ onSettings && /* @__PURE__ */ jsx10(
1382
+ IconButton,
1383
+ {
1384
+ icon: /* @__PURE__ */ jsx10(SettingsIcon, { size: 16 }),
1385
+ label: "Item settings",
1386
+ onClick: onSettings,
1387
+ className: "bg-base/80 shadow-sm"
1388
+ }
1389
+ ),
1390
+ /* @__PURE__ */ jsx10(
1391
+ IconButton,
1392
+ {
1393
+ icon: /* @__PURE__ */ jsx10(DeleteIcon, { size: 16 }),
1394
+ label: "Delete item",
1395
+ intent: "destructive",
1396
+ onClick: () => onRemove(id),
1397
+ className: "bg-base/80 shadow-sm"
1398
+ }
1399
+ )
1400
+ ] }),
1401
+ children
1402
+ ]
1403
+ }
1404
+ );
1405
+ }
1406
+
1407
+ export {
1408
+ EditablePlainText,
1409
+ defineHeadingSection,
1410
+ LinkPopover,
1411
+ EditableRichText,
1412
+ ResolvedMedia,
1413
+ ImageDropZone,
1414
+ MediaBlock,
1415
+ MAX_CONTAINER_COLUMNS,
1416
+ containerGridClass,
1417
+ colSpanClass,
1418
+ spacerHiddenClass,
1419
+ useEditableCollection,
1420
+ EditableGrid
1421
+ };