@nextlyhq/plugin-page-builder 0.0.2-alpha.31

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.
@@ -0,0 +1,2463 @@
1
+ import {
2
+ EDITOR_MODE_FIELD,
3
+ PAGE_BUILDER_CONTENT_FIELD,
4
+ PAGE_BUILDER_TYPE
5
+ } from "../chunk-EGU6QGUC.js";
6
+ import {
7
+ QUERY_LOOP_TYPE,
8
+ RenderNode,
9
+ loopGridStyle
10
+ } from "../chunk-R7YHOSL2.js";
11
+ import {
12
+ DEFAULT_SLOT,
13
+ compileDocumentCss,
14
+ compileTokensCss,
15
+ defaultBlockRegistry,
16
+ defaultControlRegistry,
17
+ duplicateNode,
18
+ findNode,
19
+ getPath,
20
+ insertNode,
21
+ makeNode,
22
+ moveNode,
23
+ nodeClass,
24
+ removeNode,
25
+ updateNode
26
+ } from "../chunk-ABMSYCSD.js";
27
+ import {
28
+ BlockErrorBoundary
29
+ } from "../chunk-E4G7NNXW.js";
30
+
31
+ // src/admin/index.ts
32
+ import {
33
+ registerComponents,
34
+ registerKnownPlugin
35
+ } from "@nextlyhq/plugin-sdk/admin";
36
+
37
+ // src/admin/controls/registerDefaultControls.tsx
38
+ import { createElement } from "react";
39
+
40
+ // src/admin/controls/ColorControl.tsx
41
+ import { Input as Input2 } from "@nextlyhq/ui";
42
+
43
+ // src/admin/icons.tsx
44
+ import {
45
+ ArrowDown,
46
+ ArrowUp,
47
+ ChevronDown,
48
+ ChevronRight,
49
+ Copy,
50
+ GripVertical,
51
+ Heading,
52
+ Image,
53
+ LayoutGrid,
54
+ Link2,
55
+ Monitor,
56
+ MousePointerClick,
57
+ Plus,
58
+ Repeat,
59
+ Search,
60
+ Smartphone,
61
+ Square,
62
+ Tablet,
63
+ Trash2,
64
+ Type,
65
+ Video,
66
+ X
67
+ } from "lucide-react";
68
+ var BLOCK_ICONS = {
69
+ Heading,
70
+ Image,
71
+ LayoutGrid,
72
+ MousePointerClick,
73
+ Repeat,
74
+ Square,
75
+ Type,
76
+ Video
77
+ };
78
+ function blockIcon(name) {
79
+ return name && BLOCK_ICONS[name] || Square;
80
+ }
81
+
82
+ // src/admin/controls/primitives.tsx
83
+ import {
84
+ Input,
85
+ Label,
86
+ Select,
87
+ SelectContent,
88
+ SelectItem,
89
+ SelectTrigger,
90
+ SelectValue,
91
+ Switch,
92
+ Textarea
93
+ } from "@nextlyhq/ui";
94
+ import { useId } from "react";
95
+ import { jsx, jsxs } from "react/jsx-runtime";
96
+ var UNITS = ["px", "%", "rem", "em", "vw", "vh"];
97
+ function ControlRow({
98
+ label,
99
+ htmlFor,
100
+ children
101
+ }) {
102
+ return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4, marginBottom: 10 }, children: [
103
+ label ? /* @__PURE__ */ jsx(Label, { htmlFor, className: "nx-pb-control-label", children: label }) : null,
104
+ children
105
+ ] });
106
+ }
107
+ var str = (v) => typeof v === "string" ? v : "";
108
+ function TextControl({ value, onChange, label, field }) {
109
+ const id = useId();
110
+ return /* @__PURE__ */ jsx(ControlRow, { label, htmlFor: id, children: /* @__PURE__ */ jsx(
111
+ Input,
112
+ {
113
+ id,
114
+ value: str(value),
115
+ placeholder: field?.placeholder,
116
+ onChange: (e) => onChange(e.target.value)
117
+ }
118
+ ) });
119
+ }
120
+ function TextareaControl({
121
+ value,
122
+ onChange,
123
+ label,
124
+ field
125
+ }) {
126
+ const id = useId();
127
+ return /* @__PURE__ */ jsx(ControlRow, { label, htmlFor: id, children: /* @__PURE__ */ jsx(
128
+ Textarea,
129
+ {
130
+ id,
131
+ rows: 3,
132
+ value: str(value),
133
+ placeholder: field?.placeholder,
134
+ onChange: (e) => onChange(e.target.value)
135
+ }
136
+ ) });
137
+ }
138
+ function NumberControl({ value, onChange, label }) {
139
+ const id = useId();
140
+ return /* @__PURE__ */ jsx(ControlRow, { label, htmlFor: id, children: /* @__PURE__ */ jsx(
141
+ Input,
142
+ {
143
+ id,
144
+ type: "number",
145
+ value: typeof value === "number" ? value : str(value),
146
+ onChange: (e) => onChange(e.target.value === "" ? void 0 : Number(e.target.value))
147
+ }
148
+ ) });
149
+ }
150
+ function BooleanControl({ value, onChange, label }) {
151
+ const id = useId();
152
+ return /* @__PURE__ */ jsxs(
153
+ "div",
154
+ {
155
+ style: {
156
+ display: "flex",
157
+ alignItems: "center",
158
+ justifyContent: "space-between",
159
+ marginBottom: 10
160
+ },
161
+ children: [
162
+ /* @__PURE__ */ jsx(Label, { htmlFor: id, className: "nx-pb-control-label", children: label }),
163
+ /* @__PURE__ */ jsx(
164
+ Switch,
165
+ {
166
+ id,
167
+ checked: value === true,
168
+ onCheckedChange: (checked) => onChange(checked)
169
+ }
170
+ )
171
+ ]
172
+ }
173
+ );
174
+ }
175
+ function SelectControl({ value, onChange, label, field }) {
176
+ const options = field?.options ?? [];
177
+ return /* @__PURE__ */ jsx(ControlRow, { label, children: /* @__PURE__ */ jsxs(Select, { value: str(value), onValueChange: (v) => onChange(v), children: [
178
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select\u2026" }) }),
179
+ /* @__PURE__ */ jsx(SelectContent, { children: options.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o.value, children: o.label }, o.value)) })
180
+ ] }) });
181
+ }
182
+ var ALIGN = [
183
+ { value: "left", label: "Left" },
184
+ { value: "center", label: "Center" },
185
+ { value: "right", label: "Right" },
186
+ { value: "justify", label: "Justify" }
187
+ ];
188
+ function AlignControl({ value, onChange, label }) {
189
+ return /* @__PURE__ */ jsx(ControlRow, { label, children: /* @__PURE__ */ jsx("div", { className: "nx-pb-choice", role: "group", "aria-label": label, children: ALIGN.map((a) => /* @__PURE__ */ jsx(
190
+ "button",
191
+ {
192
+ type: "button",
193
+ className: "nx-pb-choice-btn",
194
+ "aria-pressed": value === a.value,
195
+ onClick: () => onChange(value === a.value ? void 0 : a.value),
196
+ children: a.label
197
+ },
198
+ a.value
199
+ )) }) });
200
+ }
201
+ function splitLength(v) {
202
+ const s = str(v).trim();
203
+ const m = /^(-?\d*\.?\d+)(px|%|rem|em|vw|vh)?$/.exec(s);
204
+ if (!m) return { n: "", unit: "px" };
205
+ return { n: m[1], unit: m[2] ?? "px" };
206
+ }
207
+ function DimensionControl({ value, onChange, label }) {
208
+ const { n, unit } = splitLength(value);
209
+ const emit = (nextN, nextUnit) => onChange(nextN === "" ? void 0 : `${nextN}${nextUnit}`);
210
+ return /* @__PURE__ */ jsx(ControlRow, { label, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 4 }, children: [
211
+ /* @__PURE__ */ jsx(
212
+ Input,
213
+ {
214
+ type: "number",
215
+ value: n,
216
+ style: { flex: 1 },
217
+ onChange: (e) => emit(e.target.value, unit)
218
+ }
219
+ ),
220
+ /* @__PURE__ */ jsxs(Select, { value: unit, onValueChange: (u) => emit(n, u), children: [
221
+ /* @__PURE__ */ jsx(SelectTrigger, { style: { width: 80 }, children: /* @__PURE__ */ jsx(SelectValue, {}) }),
222
+ /* @__PURE__ */ jsx(SelectContent, { children: UNITS.map((u) => /* @__PURE__ */ jsx(SelectItem, { value: u, children: u }, u)) })
223
+ ] })
224
+ ] }) });
225
+ }
226
+ function LinkControl({ value, onChange, label }) {
227
+ const v = value ?? {};
228
+ const id = useId();
229
+ return /* @__PURE__ */ jsxs(ControlRow, { label, htmlFor: id, children: [
230
+ /* @__PURE__ */ jsx(
231
+ Input,
232
+ {
233
+ id,
234
+ value: str(v.href),
235
+ placeholder: "https://\u2026",
236
+ onChange: (e) => onChange({ ...v, href: e.target.value })
237
+ }
238
+ ),
239
+ /* @__PURE__ */ jsxs(
240
+ "label",
241
+ {
242
+ className: "nx-pb-control-label",
243
+ style: {
244
+ display: "flex",
245
+ alignItems: "center",
246
+ gap: 6,
247
+ marginTop: 4
248
+ },
249
+ children: [
250
+ /* @__PURE__ */ jsx(
251
+ "input",
252
+ {
253
+ type: "checkbox",
254
+ checked: v.target === "_blank",
255
+ onChange: (e) => onChange({ ...v, target: e.target.checked ? "_blank" : void 0 })
256
+ }
257
+ ),
258
+ "Open in new tab"
259
+ ]
260
+ }
261
+ )
262
+ ] });
263
+ }
264
+
265
+ // src/admin/controls/ColorControl.tsx
266
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
267
+ function isToken(v) {
268
+ return typeof v === "object" && v !== null && "token" in v;
269
+ }
270
+ function ColorControl({ value, onChange, label, tokens }) {
271
+ const token = isToken(value) ? value.token : void 0;
272
+ const hex = typeof value === "string" ? value : "";
273
+ return /* @__PURE__ */ jsxs2(ControlRow, { label, children: [
274
+ /* @__PURE__ */ jsxs2("div", { className: "nx-pb-color-row", children: [
275
+ /* @__PURE__ */ jsx2(
276
+ "input",
277
+ {
278
+ type: "color",
279
+ className: "nx-pb-color-swatch",
280
+ "aria-label": `${label ?? "color"} picker`,
281
+ value: /^#[0-9a-fA-F]{6}$/.test(hex) ? hex : "#000000",
282
+ onChange: (e) => onChange(e.target.value)
283
+ }
284
+ ),
285
+ /* @__PURE__ */ jsx2(
286
+ Input2,
287
+ {
288
+ value: token ? `token:${token}` : hex,
289
+ placeholder: "#000000",
290
+ onChange: (e) => onChange(e.target.value),
291
+ style: { flex: 1 }
292
+ }
293
+ ),
294
+ value !== void 0 ? /* @__PURE__ */ jsx2(
295
+ "button",
296
+ {
297
+ type: "button",
298
+ className: "nx-pb-icon-btn",
299
+ "aria-label": "Clear color",
300
+ onClick: () => onChange(void 0),
301
+ style: { padding: "6px 8px" },
302
+ children: /* @__PURE__ */ jsx2(X, { size: 14, "aria-hidden": true })
303
+ }
304
+ ) : null
305
+ ] }),
306
+ tokens && tokens.length > 0 ? /* @__PURE__ */ jsx2("div", { className: "nx-pb-color-tokens", children: tokens.map((t) => /* @__PURE__ */ jsx2(
307
+ "button",
308
+ {
309
+ type: "button",
310
+ className: "nx-pb-color-token",
311
+ "data-active": token === t.name || void 0,
312
+ title: t.label,
313
+ "aria-label": t.label,
314
+ onClick: () => onChange({ token: t.name }),
315
+ style: { background: t.preview }
316
+ },
317
+ t.name
318
+ )) }) : null
319
+ ] });
320
+ }
321
+
322
+ // src/admin/controls/MediaControl.tsx
323
+ import { MediaPickerDialog } from "@nextlyhq/admin";
324
+ import { Button, Input as Input3 } from "@nextlyhq/ui";
325
+ import { useState } from "react";
326
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
327
+ var str2 = (v) => typeof v === "string" ? v : "";
328
+ function MediaControl({ value, onChange, label }) {
329
+ const v = value ?? {};
330
+ const [open, setOpen] = useState(false);
331
+ const onSelect = (media) => {
332
+ const m = media[0];
333
+ if (m) {
334
+ onChange({
335
+ mediaId: m.id,
336
+ url: m.url,
337
+ alt: m.altText ?? "",
338
+ width: m.width ?? void 0,
339
+ height: m.height ?? void 0
340
+ });
341
+ }
342
+ setOpen(false);
343
+ };
344
+ return /* @__PURE__ */ jsxs3(ControlRow, { label, children: [
345
+ v.url ? /* @__PURE__ */ jsx3(
346
+ "img",
347
+ {
348
+ src: v.url,
349
+ alt: v.alt ?? "",
350
+ style: {
351
+ maxWidth: "100%",
352
+ maxHeight: 120,
353
+ borderRadius: "var(--radius)",
354
+ border: "1px solid hsl(var(--border))",
355
+ marginBottom: 6
356
+ }
357
+ }
358
+ ) : null,
359
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 6, marginBottom: 4 }, children: [
360
+ /* @__PURE__ */ jsx3(Button, { variant: "outline", onClick: () => setOpen(true), children: v.url ? "Replace" : "Choose from library" }),
361
+ v.url ? /* @__PURE__ */ jsx3(Button, { variant: "ghost", onClick: () => onChange(void 0), children: "Remove" }) : null
362
+ ] }),
363
+ /* @__PURE__ */ jsx3(
364
+ Input3,
365
+ {
366
+ value: str2(v.url),
367
+ placeholder: "\u2026or paste an image URL",
368
+ "aria-label": `${label ?? "media"} url`,
369
+ onChange: (e) => onChange({ ...v, url: e.target.value })
370
+ }
371
+ ),
372
+ /* @__PURE__ */ jsx3(
373
+ Input3,
374
+ {
375
+ value: str2(v.alt),
376
+ placeholder: "Alt text (describe the image)",
377
+ "aria-label": `${label ?? "media"} alt text`,
378
+ style: { marginTop: 4 },
379
+ onChange: (e) => onChange({ ...v, alt: e.target.value })
380
+ }
381
+ ),
382
+ /* @__PURE__ */ jsx3(
383
+ MediaPickerDialog,
384
+ {
385
+ mode: "single",
386
+ open,
387
+ onOpenChange: setOpen,
388
+ onSelect,
389
+ accept: "image/*",
390
+ title: "Select media"
391
+ }
392
+ )
393
+ ] });
394
+ }
395
+
396
+ // src/admin/controls/SpacingControl.tsx
397
+ import { Input as Input4 } from "@nextlyhq/ui";
398
+ import { useState as useState2 } from "react";
399
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
400
+ var SIDES = [
401
+ { key: "top", label: "T" },
402
+ { key: "right", label: "R" },
403
+ { key: "bottom", label: "B" },
404
+ { key: "left", label: "L" }
405
+ ];
406
+ function num(v) {
407
+ if (!v) return "";
408
+ const m = /^(-?\d*\.?\d+)/.exec(v);
409
+ return m ? m[1] : "";
410
+ }
411
+ function SpacingControl({ value, onChange, label }) {
412
+ const sides = value ?? {};
413
+ const [linked, setLinked] = useState2(false);
414
+ const emit = (side, raw) => {
415
+ if (linked) {
416
+ if (raw === "") return onChange(void 0);
417
+ const all = `${raw}px`;
418
+ return onChange({ top: all, right: all, bottom: all, left: all });
419
+ }
420
+ const next = { ...sides };
421
+ if (raw === "") delete next[side];
422
+ else next[side] = `${raw}px`;
423
+ onChange(Object.keys(next).length ? next : void 0);
424
+ };
425
+ return /* @__PURE__ */ jsx4(ControlRow, { label, children: /* @__PURE__ */ jsxs4("div", { className: "nx-pb-box", children: [
426
+ SIDES.map((s) => /* @__PURE__ */ jsxs4("div", { className: "nx-pb-box-side", children: [
427
+ /* @__PURE__ */ jsx4(
428
+ Input4,
429
+ {
430
+ type: "number",
431
+ "aria-label": `${label ?? "spacing"} ${s.key}`,
432
+ value: num(sides[s.key]),
433
+ onChange: (e) => emit(s.key, e.target.value)
434
+ }
435
+ ),
436
+ /* @__PURE__ */ jsx4("span", { children: s.label })
437
+ ] }, s.key)),
438
+ /* @__PURE__ */ jsx4(
439
+ "button",
440
+ {
441
+ type: "button",
442
+ className: "nx-pb-box-link",
443
+ "aria-label": "Link all sides",
444
+ "aria-pressed": linked,
445
+ title: "Edit all sides together",
446
+ onClick: () => setLinked((l) => !l),
447
+ children: /* @__PURE__ */ jsx4(Link2, { size: 15, "aria-hidden": true })
448
+ }
449
+ )
450
+ ] }) });
451
+ }
452
+
453
+ // src/admin/controls/registerDefaultControls.tsx
454
+ var CONTROLS = {
455
+ text: TextControl,
456
+ textarea: TextareaControl,
457
+ number: NumberControl,
458
+ boolean: BooleanControl,
459
+ select: SelectControl,
460
+ align: AlignControl,
461
+ dimension: DimensionControl,
462
+ link: LinkControl,
463
+ spacing: SpacingControl,
464
+ color: ColorControl,
465
+ media: MediaControl
466
+ };
467
+ var registered = false;
468
+ function registerDefaultControls() {
469
+ if (registered) return;
470
+ registered = true;
471
+ for (const [type, Component] of Object.entries(CONTROLS)) {
472
+ defaultControlRegistry.register({ type, Component });
473
+ }
474
+ }
475
+ function renderControl(type, props) {
476
+ const def = defaultControlRegistry.get(type);
477
+ const Component = def?.Component ?? TextControl;
478
+ return createElement(Component, props);
479
+ }
480
+
481
+ // src/admin/EditorSurface.tsx
482
+ import { DragDropProvider, DragOverlay } from "@dnd-kit/react";
483
+
484
+ // src/admin/store/EditorProvider.tsx
485
+ import {
486
+ createContext,
487
+ useContext,
488
+ useEffect,
489
+ useReducer,
490
+ useRef
491
+ } from "react";
492
+
493
+ // src/admin/store/editorStore.ts
494
+ var HISTORY_LIMIT = 50;
495
+ var BASE_BREAKPOINT = "base";
496
+ function initialState(document) {
497
+ return {
498
+ document,
499
+ selectedId: null,
500
+ activeBreakpoint: BASE_BREAKPOINT,
501
+ past: [],
502
+ future: [],
503
+ dirty: false
504
+ };
505
+ }
506
+ function keepValidSelection(document, selectedId) {
507
+ if (!selectedId) return null;
508
+ return findNode(document.root, selectedId) ? selectedId : null;
509
+ }
510
+ function createNodeFromType(nodeType) {
511
+ const def = defaultBlockRegistry.get(nodeType);
512
+ const props = def ? structuredClone(def.defaultProps) : {};
513
+ const style = def?.defaultStyle ? structuredClone(def.defaultStyle) : void 0;
514
+ const slots = def?.isContainer ? { default: [] } : void 0;
515
+ return makeNode(nodeType, props, style, slots);
516
+ }
517
+ function commit(state, root, selectedId = state.selectedId) {
518
+ const past = [...state.past, state.document].slice(-HISTORY_LIMIT);
519
+ return {
520
+ ...state,
521
+ document: { ...state.document, root },
522
+ past,
523
+ future: [],
524
+ dirty: true,
525
+ selectedId
526
+ };
527
+ }
528
+ function editorReducer(state, action) {
529
+ const root = state.document.root;
530
+ switch (action.type) {
531
+ case "SELECT":
532
+ return { ...state, selectedId: action.id };
533
+ case "SET_BREAKPOINT":
534
+ return { ...state, activeBreakpoint: action.breakpoint };
535
+ case "ADD": {
536
+ const node = createNodeFromType(action.nodeType);
537
+ return commit(
538
+ state,
539
+ insertNode(root, action.parentId, action.slot, node, action.index),
540
+ node.id
541
+ );
542
+ }
543
+ case "MOVE":
544
+ return commit(
545
+ state,
546
+ moveNode(root, action.id, action.parentId, action.slot, action.index)
547
+ );
548
+ case "REMOVE": {
549
+ const next = removeNode(root, action.id);
550
+ return { ...commit(state, next), selectedId: null };
551
+ }
552
+ case "DUPLICATE":
553
+ return commit(state, duplicateNode(root, action.id));
554
+ case "UPDATE_PROPS": {
555
+ const node = findNode(root, action.id);
556
+ const props = { ...node?.props ?? {}, ...action.props };
557
+ return commit(state, updateNode(root, action.id, { props }));
558
+ }
559
+ case "UPDATE_STYLE": {
560
+ const node = findNode(root, action.id);
561
+ const key = action.styleState === "hover" ? "styleHover" : "style";
562
+ const style = { ...node?.[key] ?? {} };
563
+ style[action.breakpoint] = {
564
+ ...style[action.breakpoint] ?? {},
565
+ ...action.style
566
+ };
567
+ return commit(state, updateNode(root, action.id, { [key]: style }));
568
+ }
569
+ case "SET_BINDING": {
570
+ const node = findNode(root, action.id);
571
+ const bindings = { ...node?.bindings ?? {} };
572
+ if (action.binding) bindings[action.prop] = action.binding;
573
+ else delete bindings[action.prop];
574
+ return commit(state, updateNode(root, action.id, { bindings }));
575
+ }
576
+ case "SET_CUSTOM_CLASS": {
577
+ const customClass = action.customClass.trim() || void 0;
578
+ return commit(state, updateNode(root, action.id, { customClass }));
579
+ }
580
+ case "REPLACE":
581
+ return {
582
+ ...initialState(action.document),
583
+ activeBreakpoint: state.activeBreakpoint
584
+ };
585
+ case "MARK_SAVED":
586
+ return { ...state, dirty: false };
587
+ case "UNDO": {
588
+ if (!state.past.length) return state;
589
+ const prev = state.past[state.past.length - 1];
590
+ return {
591
+ ...state,
592
+ document: prev,
593
+ selectedId: keepValidSelection(prev, state.selectedId),
594
+ past: state.past.slice(0, -1),
595
+ future: [state.document, ...state.future],
596
+ dirty: true
597
+ };
598
+ }
599
+ case "REDO": {
600
+ if (!state.future.length) return state;
601
+ const next = state.future[0];
602
+ return {
603
+ ...state,
604
+ document: next,
605
+ selectedId: keepValidSelection(next, state.selectedId),
606
+ past: [...state.past, state.document],
607
+ future: state.future.slice(1),
608
+ dirty: true
609
+ };
610
+ }
611
+ default:
612
+ return state;
613
+ }
614
+ }
615
+
616
+ // src/admin/store/EditorProvider.tsx
617
+ import { jsx as jsx5 } from "react/jsx-runtime";
618
+ var EditorContext = createContext(null);
619
+ function useEditor() {
620
+ const ctx = useContext(EditorContext);
621
+ if (!ctx) throw new Error("useEditor must be used inside <EditorProvider>");
622
+ return ctx;
623
+ }
624
+ function draftKeyFor(collectionSlug, entryId) {
625
+ return `nx-pb-draft:${collectionSlug}:${entryId ?? "new"}`;
626
+ }
627
+ function EditorProvider({
628
+ document: doc,
629
+ draftKey,
630
+ onDocumentChange,
631
+ children
632
+ }) {
633
+ const [state, dispatch] = useReducer(editorReducer, doc, initialState);
634
+ const timer = useRef(null);
635
+ const firstRender = useRef(true);
636
+ const onDocumentChangeRef = useRef(onDocumentChange);
637
+ onDocumentChangeRef.current = onDocumentChange;
638
+ useEffect(() => {
639
+ if (firstRender.current) {
640
+ firstRender.current = false;
641
+ return;
642
+ }
643
+ onDocumentChangeRef.current?.(state.document);
644
+ }, [state.document]);
645
+ useEffect(() => {
646
+ if (!state.dirty) return;
647
+ if (timer.current) clearTimeout(timer.current);
648
+ timer.current = setTimeout(() => {
649
+ try {
650
+ localStorage.setItem(draftKey, JSON.stringify(state.document));
651
+ } catch {
652
+ }
653
+ }, 800);
654
+ return () => {
655
+ if (timer.current) clearTimeout(timer.current);
656
+ };
657
+ }, [state.document, state.dirty, draftKey]);
658
+ return /* @__PURE__ */ jsx5(EditorContext.Provider, { value: { state, dispatch }, children });
659
+ }
660
+
661
+ // src/admin/canvas/CanvasNode.tsx
662
+ import { useDraggable, useDroppable as useDroppable2 } from "@dnd-kit/react";
663
+ import {
664
+ cloneElement,
665
+ isValidElement
666
+ } from "react";
667
+
668
+ // src/admin/logic/dragSensors.ts
669
+ import {
670
+ KeyboardSensor,
671
+ PointerActivationConstraints,
672
+ PointerSensor
673
+ } from "@dnd-kit/dom";
674
+ var dragSensors = [
675
+ PointerSensor.configure({
676
+ activationConstraints: [
677
+ new PointerActivationConstraints.Distance({ value: 4 })
678
+ ]
679
+ }),
680
+ KeyboardSensor
681
+ ];
682
+
683
+ // src/admin/canvas/CanvasQueryLoop.tsx
684
+ import { useEffect as useEffect2, useState as useState3 } from "react";
685
+
686
+ // src/admin/api/collectionsApi.ts
687
+ var BASE = "/admin/api";
688
+ var asArray = (v) => Array.isArray(v) ? v : [];
689
+ var str3 = (v, fallback = "") => typeof v === "string" ? v : fallback;
690
+ function normalizeCollections(body) {
691
+ const items = asArray(body?.items);
692
+ const out = [];
693
+ for (const c of items) {
694
+ const admin = c.admin;
695
+ if (admin?.hidden === true) continue;
696
+ const slug = str3(c.name) || str3(c.slug);
697
+ if (!slug) continue;
698
+ const labels = c.labels;
699
+ const label = str3(c.label) || str3(labels?.plural) || slug;
700
+ out.push({ slug, label });
701
+ }
702
+ return out;
703
+ }
704
+ function normalizeFields(body) {
705
+ const fields = asArray(body?.fields);
706
+ const out = [];
707
+ for (const f of fields) {
708
+ const name = str3(f.name);
709
+ if (!name) continue;
710
+ out.push({ name, type: str3(f.type, "text"), label: str3(f.label) || name });
711
+ }
712
+ return out;
713
+ }
714
+ async function getJson(url) {
715
+ const res = await fetch(url, {
716
+ credentials: "same-origin",
717
+ headers: { Accept: "application/json" }
718
+ });
719
+ if (!res.ok) throw new Error(`Request failed (${res.status})`);
720
+ return res.json();
721
+ }
722
+ async function listCollections() {
723
+ return normalizeCollections(await getJson(`${BASE}/collections`));
724
+ }
725
+ async function getCollectionFields(slug) {
726
+ if (!slug) return [];
727
+ return normalizeFields(
728
+ await getJson(`${BASE}/collections/schema/${encodeURIComponent(slug)}`)
729
+ );
730
+ }
731
+ async function getSampleEntries(slug, opts = {}) {
732
+ if (!slug) return [];
733
+ const params = new URLSearchParams();
734
+ params.set("limit", String(opts.limit ?? 5));
735
+ params.set("depth", "1");
736
+ if (opts.sort) params.set("sort", opts.sort);
737
+ if (opts.where !== void 0 && opts.where !== null) {
738
+ params.set("where", JSON.stringify(opts.where));
739
+ }
740
+ const body = await getJson(
741
+ `${BASE}/collections/${encodeURIComponent(slug)}/entries?${params.toString()}`
742
+ );
743
+ return asArray(body?.items);
744
+ }
745
+
746
+ // src/admin/canvas/CanvasQueryLoop.tsx
747
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
748
+ var PREVIEW_ROWS = 4;
749
+ function QueryLoopSamplePreview({ node }) {
750
+ const collection = typeof node.props.collection === "string" ? node.props.collection : "";
751
+ const sort = typeof node.props.sort === "string" ? node.props.sort : void 0;
752
+ const where = node.props.where;
753
+ const [rows, setRows] = useState3(null);
754
+ const [error, setError] = useState3(false);
755
+ useEffect2(() => {
756
+ let alive = true;
757
+ if (!collection) {
758
+ setRows(null);
759
+ return;
760
+ }
761
+ setError(false);
762
+ getSampleEntries(collection, { limit: PREVIEW_ROWS, sort, where }).then((r) => alive && setRows(r)).catch(() => alive && setError(true));
763
+ return () => {
764
+ alive = false;
765
+ };
766
+ }, [collection, sort, where]);
767
+ if (!collection) return null;
768
+ const template = node.slots?.[DEFAULT_SLOT] ?? [];
769
+ return /* @__PURE__ */ jsxs5("div", { style: { gridColumn: "1 / -1", pointerEvents: "none" }, "aria-hidden": true, children: [
770
+ /* @__PURE__ */ jsx6("div", { style: labelStyle, children: error ? "Preview unavailable" : rows == null ? "Loading sample preview\u2026" : rows.length === 0 ? "No entries found" : `Live preview \u2014 ${collection}` }),
771
+ rows && rows.length > 0 ? /* @__PURE__ */ jsx6("div", { style: loopGridStyle(node.props) ?? { display: "grid", gap: 12 }, children: rows.map((item, i) => /* @__PURE__ */ jsx6("div", { "data-nx-loop-item": i, children: template.map((child) => /* @__PURE__ */ jsx6(
772
+ RenderNode,
773
+ {
774
+ node: child,
775
+ registry: defaultBlockRegistry,
776
+ item
777
+ },
778
+ child.id
779
+ )) }, i)) }) : null
780
+ ] });
781
+ }
782
+ var labelStyle = {
783
+ fontSize: 11,
784
+ fontWeight: 600,
785
+ letterSpacing: "0.04em",
786
+ textTransform: "uppercase",
787
+ color: "#94a3b8",
788
+ margin: "10px 0 6px",
789
+ borderTop: "1px dashed #cbd5e1",
790
+ paddingTop: 8
791
+ };
792
+
793
+ // src/admin/canvas/DropZone.tsx
794
+ import { useDragDropMonitor, useDroppable } from "@dnd-kit/react";
795
+ import { useState as useState4 } from "react";
796
+ import { jsx as jsx7 } from "react/jsx-runtime";
797
+ var BLOCK_TYPE = "nx-block";
798
+ function DropZone({
799
+ parentId,
800
+ slot,
801
+ index,
802
+ empty = false
803
+ }) {
804
+ const [dragging, setDragging] = useState4(false);
805
+ useDragDropMonitor({
806
+ onDragStart() {
807
+ setDragging(true);
808
+ },
809
+ onDragEnd() {
810
+ setDragging(false);
811
+ }
812
+ });
813
+ const { ref, isDropTarget } = useDroppable({
814
+ id: `dz:${parentId}:${slot}:${index}`,
815
+ type: BLOCK_TYPE,
816
+ accept: BLOCK_TYPE,
817
+ data: { kind: "dropzone", parentId, slot, index }
818
+ });
819
+ if (empty) {
820
+ return /* @__PURE__ */ jsx7(
821
+ "div",
822
+ {
823
+ ref,
824
+ className: "nx-pb-dropzone-empty",
825
+ "data-active": isDropTarget || void 0,
826
+ children: "Drop a block here"
827
+ }
828
+ );
829
+ }
830
+ return /* @__PURE__ */ jsx7(
831
+ "div",
832
+ {
833
+ ref,
834
+ className: "nx-pb-dropzone",
835
+ "data-drag": dragging || void 0,
836
+ "data-active": isDropTarget || void 0
837
+ }
838
+ );
839
+ }
840
+
841
+ // src/admin/canvas/CanvasNode.tsx
842
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
843
+ var BLOCK_TYPE2 = "nx-block";
844
+ var placeholderStyle = {
845
+ padding: "14px 16px",
846
+ fontSize: 13,
847
+ color: "#6b7280",
848
+ border: "1px dashed #cbd5e1",
849
+ borderRadius: 6,
850
+ textAlign: "center",
851
+ background: "#f8fafc"
852
+ };
853
+ function isHorizontal(node) {
854
+ return node.type === "core/grid";
855
+ }
856
+ function mergeRefs(...refs) {
857
+ return (el) => {
858
+ for (const r of refs) r?.(el);
859
+ };
860
+ }
861
+ function classFor(node, selected, extra = []) {
862
+ return [
863
+ nodeClass(node.id),
864
+ node.customClass,
865
+ selected && "nx-pb-selected",
866
+ ...extra
867
+ ].filter(Boolean).join(" ");
868
+ }
869
+ function renderSlot(node, slotName) {
870
+ const children = node.slots?.[slotName] ?? [];
871
+ if (children.length === 0) {
872
+ return /* @__PURE__ */ jsx8(
873
+ DropZone,
874
+ {
875
+ parentId: node.id,
876
+ slot: slotName,
877
+ index: 0,
878
+ empty: true
879
+ },
880
+ "dz-empty"
881
+ );
882
+ }
883
+ if (isHorizontal(node)) {
884
+ return children.map((child, i) => /* @__PURE__ */ jsx8(
885
+ DraggableNode,
886
+ {
887
+ node: child,
888
+ parentId: node.id,
889
+ slot: slotName,
890
+ index: i,
891
+ dropBeforeIndex: i
892
+ },
893
+ child.id
894
+ ));
895
+ }
896
+ const out = [];
897
+ children.forEach((child, i) => {
898
+ out.push(
899
+ /* @__PURE__ */ jsx8(DropZone, { parentId: node.id, slot: slotName, index: i }, `dz-${i}`)
900
+ );
901
+ out.push(
902
+ /* @__PURE__ */ jsx8(
903
+ DraggableNode,
904
+ {
905
+ node: child,
906
+ parentId: node.id,
907
+ slot: slotName,
908
+ index: i
909
+ },
910
+ child.id
911
+ )
912
+ );
913
+ });
914
+ out.push(
915
+ /* @__PURE__ */ jsx8(
916
+ DropZone,
917
+ {
918
+ parentId: node.id,
919
+ slot: slotName,
920
+ index: children.length
921
+ },
922
+ `dz-${children.length}`
923
+ )
924
+ );
925
+ return out;
926
+ }
927
+ function buildSlots(node) {
928
+ const slots = {};
929
+ if (node.slots) {
930
+ for (const name of Object.keys(node.slots)) {
931
+ slots[name] = renderSlot(node, name);
932
+ }
933
+ }
934
+ return slots;
935
+ }
936
+ function CanvasNode({ node }) {
937
+ const { state } = useEditor();
938
+ const def = defaultBlockRegistry.get(node.type);
939
+ const selected = state.selectedId === node.id;
940
+ const className = classFor(node, selected);
941
+ if (!def) {
942
+ return /* @__PURE__ */ jsx8(
943
+ "div",
944
+ {
945
+ "data-nx-id": node.id,
946
+ "data-nx-unknown": node.type,
947
+ className
948
+ }
949
+ );
950
+ }
951
+ const element = def.render({
952
+ props: node.props,
953
+ node,
954
+ slots: buildSlots(node),
955
+ className
956
+ });
957
+ if (!isValidElement(element)) {
958
+ return /* @__PURE__ */ jsxs6("div", { className, "data-nx-id": node.id, style: placeholderStyle, children: [
959
+ def.label,
960
+ " \u2014 click to configure"
961
+ ] });
962
+ }
963
+ return cloneElement(element, {
964
+ "data-nx-id": node.id
965
+ });
966
+ }
967
+ function DraggableNode({
968
+ node,
969
+ parentId,
970
+ slot,
971
+ index,
972
+ dropBeforeIndex
973
+ }) {
974
+ const { state } = useEditor();
975
+ const def = defaultBlockRegistry.get(node.type);
976
+ const selected = state.selectedId === node.id;
977
+ const { ref: dragRef, isDragging } = useDraggable({
978
+ id: node.id,
979
+ type: BLOCK_TYPE2,
980
+ data: { kind: "node", nodeId: node.id, parentId, slot, index },
981
+ sensors: dragSensors
982
+ });
983
+ const before = useDroppable2({
984
+ id: `before:${node.id}`,
985
+ type: BLOCK_TYPE2,
986
+ accept: BLOCK_TYPE2,
987
+ disabled: dropBeforeIndex == null,
988
+ data: { kind: "dropzone", parentId, slot, index: dropBeforeIndex ?? 0 }
989
+ });
990
+ const grid = isHorizontal(node);
991
+ const appendIndex = node.slots?.default?.length ?? 0;
992
+ const append = useDroppable2({
993
+ id: `append:${node.id}`,
994
+ type: BLOCK_TYPE2,
995
+ accept: BLOCK_TYPE2,
996
+ disabled: !grid,
997
+ data: {
998
+ kind: "dropzone",
999
+ parentId: node.id,
1000
+ slot: "default",
1001
+ index: appendIndex
1002
+ }
1003
+ });
1004
+ const className = classFor(node, selected, [
1005
+ isDragging && "nx-pb-dragging",
1006
+ before.isDropTarget && "nx-pb-drop-before",
1007
+ append.isDropTarget && "nx-pb-drop-append"
1008
+ ]);
1009
+ const ref = mergeRefs(dragRef, before.ref, grid ? append.ref : void 0);
1010
+ if (!def) {
1011
+ return /* @__PURE__ */ jsx8(
1012
+ "div",
1013
+ {
1014
+ ref,
1015
+ "data-nx-id": node.id,
1016
+ "data-nx-unknown": node.type,
1017
+ className
1018
+ }
1019
+ );
1020
+ }
1021
+ const element = def.render({
1022
+ props: node.props,
1023
+ node,
1024
+ slots: buildSlots(node),
1025
+ className
1026
+ });
1027
+ if (!isValidElement(element)) {
1028
+ return /* @__PURE__ */ jsxs6(
1029
+ "div",
1030
+ {
1031
+ ref,
1032
+ className,
1033
+ "data-nx-id": node.id,
1034
+ style: placeholderStyle,
1035
+ children: [
1036
+ def.label,
1037
+ " \u2014 click to configure"
1038
+ ]
1039
+ }
1040
+ );
1041
+ }
1042
+ const el = element;
1043
+ const augmented = node.type === QUERY_LOOP_TYPE ? cloneElement(
1044
+ el,
1045
+ { "data-nx-id": node.id, ref },
1046
+ el.props.children,
1047
+ /* @__PURE__ */ jsx8(QueryLoopSamplePreview, { node }, "__nx-sample")
1048
+ ) : cloneElement(el, { "data-nx-id": node.id, ref });
1049
+ return /* @__PURE__ */ jsx8(
1050
+ BlockErrorBoundary,
1051
+ {
1052
+ fallback: /* @__PURE__ */ jsxs6(
1053
+ "div",
1054
+ {
1055
+ ref,
1056
+ "data-nx-id": node.id,
1057
+ className,
1058
+ style: {
1059
+ padding: 8,
1060
+ fontSize: 12,
1061
+ color: "#b91c1c",
1062
+ border: "1px dashed #fca5a5",
1063
+ borderRadius: 6
1064
+ },
1065
+ children: [
1066
+ def.label,
1067
+ " failed to render."
1068
+ ]
1069
+ }
1070
+ ),
1071
+ children: augmented
1072
+ }
1073
+ );
1074
+ }
1075
+
1076
+ // src/admin/canvas/IframeCanvas.tsx
1077
+ import { useEffect as useEffect3, useRef as useRef2, useState as useState5 } from "react";
1078
+ import { createPortal } from "react-dom";
1079
+
1080
+ // src/core/responsive.ts
1081
+ var BREAKPOINT_WIDTHS = {
1082
+ base: 1280,
1083
+ tablet: 768,
1084
+ mobile: 375
1085
+ };
1086
+ function readStyleValue(style, styleKey, breakpoint) {
1087
+ return style?.[breakpoint]?.[styleKey];
1088
+ }
1089
+
1090
+ // src/admin/canvas/IframeCanvas.tsx
1091
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1092
+ var OVERLAY_CSS = [
1093
+ "body{margin:0;font-family:system-ui,-apple-system,sans-serif}",
1094
+ // Blocks are grabbable; hovering hints the boundary (Elementor-like).
1095
+ "[data-nx-id]{cursor:grab}",
1096
+ "[data-nx-id]:active{cursor:grabbing}",
1097
+ "[data-nx-id]:hover{outline:1px dashed #a5b4fc;outline-offset:-1px}",
1098
+ // Selected block: solid ring + a small grip badge (top-left) as a grab cue.
1099
+ ".nx-pb-selected,[data-nx-id].nx-pb-selected:hover{outline:2px solid #6366f1;outline-offset:-2px;position:relative}",
1100
+ ".nx-pb-selected::before{content:'\\283F';position:absolute;top:-2px;left:-2px;transform:translateY(-100%);font-size:12px;line-height:1;padding:2px 5px;background:#6366f1;color:#fff;border-radius:4px 4px 0 0;pointer-events:none;z-index:2}",
1101
+ ".nx-pb-dragging{opacity:.4}",
1102
+ ".nx-pb-empty{color:#9ca3af;padding:32px;text-align:center;font-size:14px}",
1103
+ // Between-item drop zones: collapsed at rest, a hint while dragging, a bold blue
1104
+ // insertion bar when they are the active drop target.
1105
+ ".nx-pb-dropzone{height:0;border-radius:3px;transition:height .1s ease,background .1s ease}",
1106
+ ".nx-pb-dropzone[data-drag]{height:6px;margin:3px 0;background:rgba(99,102,241,.12)}",
1107
+ ".nx-pb-dropzone[data-active]{height:6px;margin:4px 0;background:#4f46e5;box-shadow:0 0 0 4px rgba(79,70,229,.15)}",
1108
+ // Empty-container placeholder.
1109
+ ".nx-pb-dropzone-empty{border:2px dashed #c7d2fe;border-radius:8px;padding:20px 12px;margin:6px;text-align:center;color:#6366f1;font-size:13px;background:#f8f9ff}",
1110
+ ".nx-pb-dropzone-empty[data-active]{border-color:#4f46e5;background:#eef2ff;color:#4338ca}",
1111
+ // Grid drop targets (layout-safe: inset shadow / outline, no box).
1112
+ ".nx-pb-drop-before{box-shadow:inset 3px 0 0 #4f46e5}",
1113
+ ".nx-pb-drop-append{outline:2px dashed #4f46e5;outline-offset:-2px}"
1114
+ ].join("");
1115
+ function IframeCanvas({ children }) {
1116
+ const { state, dispatch } = useEditor();
1117
+ const ref = useRef2(null);
1118
+ const [body, setBody] = useState5(null);
1119
+ const width = state.activeBreakpoint === "base" ? 0 : BREAKPOINT_WIDTHS[state.activeBreakpoint] || 0;
1120
+ const attach = () => {
1121
+ const doc = ref.current?.contentDocument;
1122
+ if (doc?.body) setBody(doc.body);
1123
+ };
1124
+ useEffect3(() => {
1125
+ attach();
1126
+ }, []);
1127
+ useEffect3(() => {
1128
+ const doc = ref.current?.contentDocument;
1129
+ if (!doc?.head) return;
1130
+ let overlay = doc.getElementById("nx-pb-overlay");
1131
+ if (!overlay) {
1132
+ overlay = doc.createElement("style");
1133
+ overlay.id = "nx-pb-overlay";
1134
+ overlay.textContent = OVERLAY_CSS;
1135
+ doc.head.appendChild(overlay);
1136
+ }
1137
+ let pageStyle = doc.getElementById(
1138
+ "nx-pb-style"
1139
+ );
1140
+ if (!pageStyle) {
1141
+ pageStyle = doc.createElement("style");
1142
+ pageStyle.id = "nx-pb-style";
1143
+ doc.head.appendChild(pageStyle);
1144
+ }
1145
+ pageStyle.textContent = compileTokensCss("nx-pb-page") + "\n" + compileDocumentCss(state.document);
1146
+ }, [state.document, body]);
1147
+ useEffect3(() => {
1148
+ const doc = ref.current?.contentDocument;
1149
+ if (!doc) return;
1150
+ const onClick = (e) => {
1151
+ const target = e.target;
1152
+ const el = target?.closest?.("[data-nx-id]") ?? null;
1153
+ dispatch({ type: "SELECT", id: el?.getAttribute("data-nx-id") ?? null });
1154
+ };
1155
+ doc.addEventListener("click", onClick);
1156
+ return () => doc.removeEventListener("click", onClick);
1157
+ }, [body, dispatch]);
1158
+ return /* @__PURE__ */ jsxs7(
1159
+ "div",
1160
+ {
1161
+ style: {
1162
+ display: "flex",
1163
+ // "safe center" centers the device frame but falls back to the start edge when it
1164
+ // would overflow — so a narrow pane scrolls from the left instead of clipping it.
1165
+ justifyContent: "safe center",
1166
+ height: "100%",
1167
+ background: "hsl(var(--muted))",
1168
+ overflow: "auto",
1169
+ padding: width ? 16 : 0
1170
+ },
1171
+ children: [
1172
+ /* @__PURE__ */ jsx9(
1173
+ "iframe",
1174
+ {
1175
+ ref,
1176
+ title: "Page preview",
1177
+ onLoad: attach,
1178
+ style: {
1179
+ border: "none",
1180
+ background: "#fff",
1181
+ height: "100%",
1182
+ width: width ? `${width}px` : "100%",
1183
+ // A fixed device width must NOT be capped to the pane — the canvas scrolls
1184
+ // instead, so the preview stays a faithful WYSIWYG at that width.
1185
+ maxWidth: width ? "none" : "100%",
1186
+ flexShrink: 0,
1187
+ boxShadow: width ? "0 0 0 1px #e5e7eb" : "none",
1188
+ borderRadius: width ? 8 : 0
1189
+ }
1190
+ }
1191
+ ),
1192
+ body ? createPortal(/* @__PURE__ */ jsx9("div", { className: "nx-pb-page", children }), body) : null
1193
+ ]
1194
+ }
1195
+ );
1196
+ }
1197
+
1198
+ // src/admin/canvas/Canvas.tsx
1199
+ import { jsx as jsx10 } from "react/jsx-runtime";
1200
+ function Canvas() {
1201
+ const { state } = useEditor();
1202
+ return /* @__PURE__ */ jsx10(IframeCanvas, { children: /* @__PURE__ */ jsx10(CanvasNode, { node: state.document.root }) });
1203
+ }
1204
+
1205
+ // src/admin/logic/dragLabel.ts
1206
+ function dragLabel(data, root, registry) {
1207
+ if (data.kind === "library" && data.blockType) {
1208
+ return registry.get(data.blockType)?.label ?? data.blockType;
1209
+ }
1210
+ if (data.kind === "node" && data.nodeId) {
1211
+ const node = findNode(root, data.nodeId);
1212
+ const label = node ? registry.get(node.type)?.label : void 0;
1213
+ return label ?? "Block";
1214
+ }
1215
+ return "Block";
1216
+ }
1217
+
1218
+ // src/admin/logic/dropRules.ts
1219
+ function canDrop(parentType, slotName, childType, registry) {
1220
+ const parent = registry.get(parentType);
1221
+ if (!parent) return { ok: false, reason: "unknown-parent" };
1222
+ if (!parent.isContainer) return { ok: false, reason: "not-a-container" };
1223
+ const slot = (parent.slots ?? []).find((s) => s.name === slotName);
1224
+ if (!slot) return { ok: false, reason: "unknown-slot" };
1225
+ if (slot.allowedBlocks && !slot.allowedBlocks.includes(childType)) {
1226
+ return { ok: false, reason: "not-allowed-in-slot" };
1227
+ }
1228
+ return { ok: true };
1229
+ }
1230
+
1231
+ // src/admin/logic/locate.ts
1232
+ function locateNode(root, id) {
1233
+ const stack = [root];
1234
+ while (stack.length) {
1235
+ const node = stack.pop();
1236
+ if (node.slots) {
1237
+ for (const [slot, children] of Object.entries(node.slots)) {
1238
+ const index = children.findIndex((c) => c.id === id);
1239
+ if (index !== -1) {
1240
+ return { parentId: node.id, slot, index, count: children.length };
1241
+ }
1242
+ stack.push(...children);
1243
+ }
1244
+ }
1245
+ }
1246
+ return null;
1247
+ }
1248
+
1249
+ // src/admin/logic/dropPlan.ts
1250
+ function planDrop(source, target, root, registry) {
1251
+ if (target.kind !== "dropzone" || target.parentId == null || target.slot == null) {
1252
+ return null;
1253
+ }
1254
+ const parent = findNode(root, target.parentId);
1255
+ if (!parent) return null;
1256
+ const index = target.index ?? 0;
1257
+ if (source.kind === "library" && source.blockType) {
1258
+ if (!canDrop(parent.type, target.slot, source.blockType, registry).ok)
1259
+ return null;
1260
+ return {
1261
+ type: "ADD",
1262
+ parentId: target.parentId,
1263
+ slot: target.slot,
1264
+ nodeType: source.blockType,
1265
+ index
1266
+ };
1267
+ }
1268
+ if (source.kind === "node" && source.nodeId) {
1269
+ const moving = findNode(root, source.nodeId);
1270
+ if (!moving) return null;
1271
+ if (findNode(moving, target.parentId)) return null;
1272
+ if (!canDrop(parent.type, target.slot, moving.type, registry).ok)
1273
+ return null;
1274
+ let toIndex = index;
1275
+ const loc = locateNode(root, source.nodeId);
1276
+ if (loc && loc.parentId === target.parentId && loc.slot === target.slot) {
1277
+ if (index === loc.index || index === loc.index + 1) return null;
1278
+ if (index > loc.index) toIndex = index - 1;
1279
+ }
1280
+ return {
1281
+ type: "MOVE",
1282
+ id: source.nodeId,
1283
+ parentId: target.parentId,
1284
+ slot: target.slot,
1285
+ index: toIndex
1286
+ };
1287
+ }
1288
+ return null;
1289
+ }
1290
+
1291
+ // src/admin/panels/BlockLibrary.tsx
1292
+ import { useDraggable as useDraggable2 } from "@dnd-kit/react";
1293
+ import { useMemo, useState as useState6 } from "react";
1294
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1295
+ var CATEGORY_ORDER = ["basic", "layout", "media", "dynamic"];
1296
+ function LibraryItem({ def }) {
1297
+ const { state, dispatch } = useEditor();
1298
+ const { ref, isDragging } = useDraggable2({
1299
+ id: `lib:${def.type}`,
1300
+ type: "nx-block",
1301
+ data: { kind: "library", blockType: def.type },
1302
+ sensors: dragSensors
1303
+ });
1304
+ const Icon = blockIcon(def.icon);
1305
+ const insert = () => {
1306
+ const root = state.document.root;
1307
+ const selected = state.selectedId ? findNode(root, state.selectedId) : void 0;
1308
+ const container = selected && defaultBlockRegistry.get(selected.type)?.isContainer ? selected : root;
1309
+ const slot = DEFAULT_SLOT;
1310
+ const index = container.slots?.[slot]?.length ?? 0;
1311
+ dispatch({
1312
+ type: "ADD",
1313
+ parentId: container.id,
1314
+ slot,
1315
+ nodeType: def.type,
1316
+ index
1317
+ });
1318
+ };
1319
+ return /* @__PURE__ */ jsxs8(
1320
+ "div",
1321
+ {
1322
+ ref,
1323
+ className: "nx-pb-lib-item",
1324
+ "data-dragging": isDragging || void 0,
1325
+ title: `Drag ${def.label} onto the canvas`,
1326
+ children: [
1327
+ /* @__PURE__ */ jsx11(Icon, { "aria-hidden": true }),
1328
+ /* @__PURE__ */ jsx11("span", { className: "nx-pb-lib-item-label", children: def.label }),
1329
+ /* @__PURE__ */ jsx11(
1330
+ "button",
1331
+ {
1332
+ type: "button",
1333
+ className: "nx-pb-lib-item-insert",
1334
+ onClick: insert,
1335
+ "aria-label": `Insert ${def.label}`,
1336
+ children: "Insert"
1337
+ }
1338
+ )
1339
+ ]
1340
+ }
1341
+ );
1342
+ }
1343
+ function Category({ name, defs }) {
1344
+ const [open, setOpen] = useState6(true);
1345
+ const Chevron = open ? ChevronDown : ChevronRight;
1346
+ return /* @__PURE__ */ jsxs8("div", { className: "nx-pb-lib-cat", children: [
1347
+ /* @__PURE__ */ jsxs8(
1348
+ "button",
1349
+ {
1350
+ type: "button",
1351
+ className: "nx-pb-lib-cat-btn",
1352
+ "aria-expanded": open,
1353
+ onClick: () => setOpen((o) => !o),
1354
+ children: [
1355
+ /* @__PURE__ */ jsx11(Chevron, { "aria-hidden": true }),
1356
+ name
1357
+ ]
1358
+ }
1359
+ ),
1360
+ open ? /* @__PURE__ */ jsx11("div", { className: "nx-pb-lib-grid", children: defs.map((def) => /* @__PURE__ */ jsx11(LibraryItem, { def }, def.type)) }) : null
1361
+ ] });
1362
+ }
1363
+ function BlockLibrary() {
1364
+ const [query, setQuery] = useState6("");
1365
+ const categories = useMemo(() => {
1366
+ const q = query.trim().toLowerCase();
1367
+ const byCategory = /* @__PURE__ */ new Map();
1368
+ for (const def of defaultBlockRegistry.all()) {
1369
+ if (q && !def.label.toLowerCase().includes(q)) continue;
1370
+ const list = byCategory.get(def.category) ?? [];
1371
+ list.push(def);
1372
+ byCategory.set(def.category, list);
1373
+ }
1374
+ return [...byCategory.keys()].sort((a, b) => CATEGORY_ORDER.indexOf(a) - CATEGORY_ORDER.indexOf(b)).map((name) => ({ name, defs: byCategory.get(name) }));
1375
+ }, [query]);
1376
+ return /* @__PURE__ */ jsxs8("div", { children: [
1377
+ /* @__PURE__ */ jsx11("div", { className: "nx-pb-pane-header", children: "Blocks" }),
1378
+ /* @__PURE__ */ jsxs8("div", { className: "nx-pb-lib-search", children: [
1379
+ /* @__PURE__ */ jsx11(Search, { "aria-hidden": true }),
1380
+ /* @__PURE__ */ jsx11(
1381
+ "input",
1382
+ {
1383
+ type: "search",
1384
+ value: query,
1385
+ placeholder: "Search blocks",
1386
+ "aria-label": "Search blocks",
1387
+ onChange: (e) => setQuery(e.target.value)
1388
+ }
1389
+ )
1390
+ ] }),
1391
+ categories.length === 0 ? /* @__PURE__ */ jsxs8("div", { className: "nx-pb-lib-empty", children: [
1392
+ "No blocks match \u201C",
1393
+ query,
1394
+ "\u201D."
1395
+ ] }) : categories.map(({ name, defs }) => (
1396
+ // Remount per query so a filtered category always shows expanded.
1397
+ /* @__PURE__ */ jsx11(
1398
+ Category,
1399
+ {
1400
+ name,
1401
+ defs
1402
+ },
1403
+ `${name}:${query ? "q" : ""}`
1404
+ )
1405
+ ))
1406
+ ] });
1407
+ }
1408
+
1409
+ // src/admin/panels/Inspector.tsx
1410
+ import {
1411
+ Input as Input6,
1412
+ Label as Label2,
1413
+ Select as Select3,
1414
+ SelectContent as SelectContent3,
1415
+ SelectItem as SelectItem3,
1416
+ SelectTrigger as SelectTrigger3,
1417
+ SelectValue as SelectValue3,
1418
+ Switch as Switch2,
1419
+ Tabs,
1420
+ TabsContent,
1421
+ TabsList,
1422
+ TabsTrigger
1423
+ } from "@nextlyhq/ui";
1424
+ import { useEffect as useEffect5, useState as useState8 } from "react";
1425
+
1426
+ // src/admin/content/contentFields.ts
1427
+ var TYPES = /* @__PURE__ */ new Set([
1428
+ "text",
1429
+ "textarea",
1430
+ "select",
1431
+ "number",
1432
+ "boolean",
1433
+ "link",
1434
+ "media"
1435
+ ]);
1436
+ function narrowContentFields(fields) {
1437
+ if (!Array.isArray(fields)) return [];
1438
+ const out = [];
1439
+ for (const f of fields) {
1440
+ if (!f || typeof f !== "object") continue;
1441
+ const r = f;
1442
+ if (typeof r.name !== "string") continue;
1443
+ if (typeof r.type !== "string" || !TYPES.has(r.type)) {
1444
+ continue;
1445
+ }
1446
+ out.push({
1447
+ name: r.name,
1448
+ type: r.type,
1449
+ label: typeof r.label === "string" ? r.label : r.name,
1450
+ default: r.default,
1451
+ options: Array.isArray(r.options) ? r.options : void 0,
1452
+ placeholder: typeof r.placeholder === "string" ? r.placeholder : void 0,
1453
+ bindable: r.bindable === true
1454
+ });
1455
+ }
1456
+ return out;
1457
+ }
1458
+
1459
+ // src/admin/logic/queryLoop.ts
1460
+ function findEnclosingLoop(root, id) {
1461
+ let found;
1462
+ const visit = (node, loop) => {
1463
+ if (node.id === id) {
1464
+ found = loop;
1465
+ return true;
1466
+ }
1467
+ const nextLoop = node.type === QUERY_LOOP_TYPE ? node : loop;
1468
+ if (node.slots) {
1469
+ for (const children of Object.values(node.slots)) {
1470
+ for (const child of children) {
1471
+ if (visit(child, nextLoop)) return true;
1472
+ }
1473
+ }
1474
+ }
1475
+ return false;
1476
+ };
1477
+ visit(root, void 0);
1478
+ return found;
1479
+ }
1480
+ function buildSort(field, dir) {
1481
+ if (!field) return "";
1482
+ return dir === "desc" ? `-${field}` : field;
1483
+ }
1484
+ function parseSort(sort) {
1485
+ if (!sort) return { field: "", dir: "asc" };
1486
+ return sort.startsWith("-") ? { field: sort.slice(1), dir: "desc" } : { field: sort, dir: "asc" };
1487
+ }
1488
+
1489
+ // src/admin/panels/inspectorTabs.ts
1490
+ function firstPopulatedTab(def) {
1491
+ if ((def?.contentFields?.length ?? 0) > 0) return "content";
1492
+ if ((def?.styleControls?.length ?? 0) > 0) return "style";
1493
+ return "advanced";
1494
+ }
1495
+
1496
+ // src/admin/panels/QueryLoopSettings.tsx
1497
+ import {
1498
+ Input as Input5,
1499
+ Select as Select2,
1500
+ SelectContent as SelectContent2,
1501
+ SelectItem as SelectItem2,
1502
+ SelectTrigger as SelectTrigger2,
1503
+ SelectValue as SelectValue2
1504
+ } from "@nextlyhq/ui";
1505
+ import { useEffect as useEffect4, useState as useState7 } from "react";
1506
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1507
+ var NONE = "__none__";
1508
+ function str4(v) {
1509
+ return typeof v === "string" ? v : "";
1510
+ }
1511
+ function QueryLoopSettings({ node }) {
1512
+ const { dispatch } = useEditor();
1513
+ const props = node.props;
1514
+ const collection = str4(props.collection);
1515
+ const [collections, setCollections] = useState7([]);
1516
+ const [fields, setFields] = useState7([]);
1517
+ const [error, setError] = useState7(null);
1518
+ const set = (patch) => dispatch({ type: "UPDATE_PROPS", id: node.id, props: patch });
1519
+ useEffect4(() => {
1520
+ let alive = true;
1521
+ listCollections().then((c) => alive && setCollections(c)).catch((e) => alive && setError(e.message));
1522
+ return () => {
1523
+ alive = false;
1524
+ };
1525
+ }, []);
1526
+ useEffect4(() => {
1527
+ let alive = true;
1528
+ if (!collection) {
1529
+ setFields([]);
1530
+ return;
1531
+ }
1532
+ getCollectionFields(collection).then((f) => alive && setFields(f)).catch(() => alive && setFields([]));
1533
+ return () => {
1534
+ alive = false;
1535
+ };
1536
+ }, [collection]);
1537
+ const sort = parseSort(str4(props.sort));
1538
+ const limit = typeof props.limit === "number" ? props.limit : 10;
1539
+ const columns = typeof props.columns === "number" ? props.columns : 1;
1540
+ const where = props.where ?? {};
1541
+ const filterField = Object.keys(where)[0] ?? "";
1542
+ const filterValue = filterField ? str4(where[filterField]?.equals) : "";
1543
+ const setSort = (field, dir) => set({ sort: buildSort(field === NONE ? "" : field, dir) });
1544
+ const setFilter = (field, value) => {
1545
+ if (!field || field === NONE || value === "")
1546
+ return set({ where: void 0 });
1547
+ set({ where: { [field]: { equals: value } } });
1548
+ };
1549
+ return /* @__PURE__ */ jsxs9("div", { style: { display: "grid", gap: 12 }, children: [
1550
+ /* @__PURE__ */ jsx12("div", { className: "nx-pb-section-label", children: "Data source" }),
1551
+ /* @__PURE__ */ jsx12("label", { className: "nx-pb-control-label", children: "Collection" }),
1552
+ /* @__PURE__ */ jsxs9(
1553
+ Select2,
1554
+ {
1555
+ value: collection || NONE,
1556
+ onValueChange: (v) => set({ collection: v === NONE ? "" : v }),
1557
+ children: [
1558
+ /* @__PURE__ */ jsx12(SelectTrigger2, { "aria-label": "Collection", children: /* @__PURE__ */ jsx12(SelectValue2, { placeholder: "Choose a collection\u2026" }) }),
1559
+ /* @__PURE__ */ jsxs9(SelectContent2, { children: [
1560
+ /* @__PURE__ */ jsx12(SelectItem2, { value: NONE, children: "Choose a collection\u2026" }),
1561
+ collections.map((c) => /* @__PURE__ */ jsx12(SelectItem2, { value: c.slug, children: c.label }, c.slug))
1562
+ ] })
1563
+ ]
1564
+ }
1565
+ ),
1566
+ error ? /* @__PURE__ */ jsxs9("p", { className: "nx-pb-empty", style: { color: "hsl(var(--destructive))" }, children: [
1567
+ "Couldn\u2019t load collections: ",
1568
+ error
1569
+ ] }) : null,
1570
+ /* @__PURE__ */ jsx12("div", { className: "nx-pb-section-label", children: "Order & layout" }),
1571
+ /* @__PURE__ */ jsx12("label", { className: "nx-pb-control-label", children: "Sort by" }),
1572
+ /* @__PURE__ */ jsxs9("div", { style: { display: "flex", gap: 6 }, children: [
1573
+ /* @__PURE__ */ jsxs9(
1574
+ Select2,
1575
+ {
1576
+ value: sort.field || NONE,
1577
+ onValueChange: (v) => setSort(v, sort.dir),
1578
+ children: [
1579
+ /* @__PURE__ */ jsx12(SelectTrigger2, { "aria-label": "Sort field", style: { flex: 1 }, children: /* @__PURE__ */ jsx12(SelectValue2, { placeholder: "Default" }) }),
1580
+ /* @__PURE__ */ jsxs9(SelectContent2, { children: [
1581
+ /* @__PURE__ */ jsx12(SelectItem2, { value: NONE, children: "Default order" }),
1582
+ fields.map((f) => /* @__PURE__ */ jsx12(SelectItem2, { value: f.name, children: f.label }, f.name))
1583
+ ] })
1584
+ ]
1585
+ }
1586
+ ),
1587
+ /* @__PURE__ */ jsxs9(
1588
+ Select2,
1589
+ {
1590
+ value: sort.dir,
1591
+ onValueChange: (v) => setSort(sort.field, v),
1592
+ children: [
1593
+ /* @__PURE__ */ jsx12(SelectTrigger2, { "aria-label": "Sort direction", style: { width: 110 }, children: /* @__PURE__ */ jsx12(SelectValue2, {}) }),
1594
+ /* @__PURE__ */ jsxs9(SelectContent2, { children: [
1595
+ /* @__PURE__ */ jsx12(SelectItem2, { value: "asc", children: "Asc" }),
1596
+ /* @__PURE__ */ jsx12(SelectItem2, { value: "desc", children: "Desc" })
1597
+ ] })
1598
+ ]
1599
+ }
1600
+ )
1601
+ ] }),
1602
+ /* @__PURE__ */ jsxs9("div", { style: { display: "flex", gap: 6 }, children: [
1603
+ /* @__PURE__ */ jsxs9("label", { className: "nx-pb-control-label", style: { flex: 1 }, children: [
1604
+ "Items",
1605
+ /* @__PURE__ */ jsx12(
1606
+ Input5,
1607
+ {
1608
+ type: "number",
1609
+ min: 1,
1610
+ value: limit,
1611
+ "aria-label": "Limit",
1612
+ onChange: (e) => set({
1613
+ limit: e.target.value === "" ? void 0 : Number(e.target.value)
1614
+ })
1615
+ }
1616
+ )
1617
+ ] }),
1618
+ /* @__PURE__ */ jsxs9("label", { className: "nx-pb-control-label", style: { flex: 1 }, children: [
1619
+ "Columns",
1620
+ /* @__PURE__ */ jsx12(
1621
+ Input5,
1622
+ {
1623
+ type: "number",
1624
+ min: 1,
1625
+ max: 12,
1626
+ value: columns,
1627
+ "aria-label": "Columns",
1628
+ onChange: (e) => set({
1629
+ columns: e.target.value === "" ? 1 : Number(e.target.value)
1630
+ })
1631
+ }
1632
+ )
1633
+ ] })
1634
+ ] }),
1635
+ /* @__PURE__ */ jsx12("div", { className: "nx-pb-section-label", children: "Filter" }),
1636
+ /* @__PURE__ */ jsxs9("div", { style: { display: "flex", gap: 6 }, children: [
1637
+ /* @__PURE__ */ jsxs9(
1638
+ Select2,
1639
+ {
1640
+ value: filterField || NONE,
1641
+ onValueChange: (v) => setFilter(v, v === NONE ? "" : filterValue),
1642
+ children: [
1643
+ /* @__PURE__ */ jsx12(SelectTrigger2, { "aria-label": "Filter field", style: { flex: 1 }, children: /* @__PURE__ */ jsx12(SelectValue2, { placeholder: "No filter" }) }),
1644
+ /* @__PURE__ */ jsxs9(SelectContent2, { children: [
1645
+ /* @__PURE__ */ jsx12(SelectItem2, { value: NONE, children: "No filter" }),
1646
+ fields.map((f) => /* @__PURE__ */ jsx12(SelectItem2, { value: f.name, children: f.label }, f.name))
1647
+ ] })
1648
+ ]
1649
+ }
1650
+ ),
1651
+ /* @__PURE__ */ jsx12(
1652
+ Input5,
1653
+ {
1654
+ value: filterValue,
1655
+ placeholder: "equals\u2026",
1656
+ "aria-label": "Filter value",
1657
+ disabled: !filterField,
1658
+ style: { flex: 1 },
1659
+ onChange: (e) => setFilter(filterField, e.target.value)
1660
+ }
1661
+ )
1662
+ ] })
1663
+ ] });
1664
+ }
1665
+
1666
+ // src/admin/panels/Inspector.tsx
1667
+ import { Fragment, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1668
+ var NONE2 = "__none__";
1669
+ registerDefaultControls();
1670
+ var BASE2 = "base";
1671
+ function SectionLabel({ children }) {
1672
+ return /* @__PURE__ */ jsx13("div", { className: "nx-pb-section-label", children });
1673
+ }
1674
+ function Empty({ children }) {
1675
+ return /* @__PURE__ */ jsx13("p", { className: "nx-pb-empty", children });
1676
+ }
1677
+ function Segmented({
1678
+ value,
1679
+ options,
1680
+ onChange,
1681
+ ariaLabel
1682
+ }) {
1683
+ return /* @__PURE__ */ jsx13("div", { className: "nx-pb-seg", role: "group", "aria-label": ariaLabel, children: options.map((o) => /* @__PURE__ */ jsx13(
1684
+ "button",
1685
+ {
1686
+ type: "button",
1687
+ className: "nx-pb-seg-btn",
1688
+ "aria-pressed": value === o.value,
1689
+ onClick: () => onChange(o.value),
1690
+ children: o.label
1691
+ },
1692
+ o.value
1693
+ )) });
1694
+ }
1695
+ function previewText(v) {
1696
+ if (v == null) return "\u2014";
1697
+ let s;
1698
+ if (typeof v === "string") s = v;
1699
+ else if (typeof v === "number" || typeof v === "boolean") s = String(v);
1700
+ else s = JSON.stringify(v) ?? "\u2014";
1701
+ return s.length > 40 ? `${s.slice(0, 40)}\u2026` : s;
1702
+ }
1703
+ function BindingPathEditor({
1704
+ node,
1705
+ field,
1706
+ binding
1707
+ }) {
1708
+ const { state, dispatch } = useEditor();
1709
+ const loop = findEnclosingLoop(state.document.root, node.id);
1710
+ const collection = loop && typeof loop.props.collection === "string" ? loop.props.collection : "";
1711
+ const [fields, setFields] = useState8([]);
1712
+ const [sample, setSample] = useState8(null);
1713
+ const setPath = (path) => dispatch({
1714
+ type: "SET_BINDING",
1715
+ id: node.id,
1716
+ prop: field.name,
1717
+ binding: { source: "field", path }
1718
+ });
1719
+ useEffect5(() => {
1720
+ let alive = true;
1721
+ if (!collection) {
1722
+ setFields([]);
1723
+ setSample(null);
1724
+ return;
1725
+ }
1726
+ getCollectionFields(collection).then((f) => alive && setFields(f)).catch(() => alive && setFields([]));
1727
+ getSampleEntries(collection, { limit: 1 }).then((rows) => alive && setSample(rows[0] ?? null)).catch(() => alive && setSample(null));
1728
+ return () => {
1729
+ alive = false;
1730
+ };
1731
+ }, [collection]);
1732
+ if (!collection || fields.length === 0) {
1733
+ return /* @__PURE__ */ jsxs10(Fragment, { children: [
1734
+ /* @__PURE__ */ jsx13(
1735
+ Input6,
1736
+ {
1737
+ value: binding.path,
1738
+ placeholder: "field path, e.g. title or author.name",
1739
+ "aria-label": `${field.label} binding path`,
1740
+ onChange: (e) => setPath(e.target.value)
1741
+ }
1742
+ ),
1743
+ /* @__PURE__ */ jsx13("p", { className: "nx-pb-empty", style: { marginTop: 2 }, children: "Resolves from the current Query Loop item." })
1744
+ ] });
1745
+ }
1746
+ return /* @__PURE__ */ jsxs10(Fragment, { children: [
1747
+ /* @__PURE__ */ jsxs10(
1748
+ Select3,
1749
+ {
1750
+ value: binding.path || NONE2,
1751
+ onValueChange: (v) => setPath(v === NONE2 ? "" : v),
1752
+ children: [
1753
+ /* @__PURE__ */ jsx13(SelectTrigger3, { "aria-label": `${field.label} field`, children: /* @__PURE__ */ jsx13(SelectValue3, { placeholder: "Choose a field\u2026" }) }),
1754
+ /* @__PURE__ */ jsxs10(SelectContent3, { children: [
1755
+ /* @__PURE__ */ jsx13(SelectItem3, { value: NONE2, children: "Choose a field\u2026" }),
1756
+ fields.map((f) => /* @__PURE__ */ jsx13(SelectItem3, { value: f.name, children: f.label }, f.name))
1757
+ ] })
1758
+ ]
1759
+ }
1760
+ ),
1761
+ /* @__PURE__ */ jsx13("p", { className: "nx-pb-empty", style: { marginTop: 2 }, children: binding.path ? /* @__PURE__ */ jsxs10(Fragment, { children: [
1762
+ "Preview:",
1763
+ " ",
1764
+ /* @__PURE__ */ jsx13("strong", { children: previewText(getPath(sample ?? {}, binding.path)) })
1765
+ ] }) : "Pick a field from the loop collection." })
1766
+ ] });
1767
+ }
1768
+ function BindableField({
1769
+ node,
1770
+ field
1771
+ }) {
1772
+ const { dispatch } = useEditor();
1773
+ const bound = node.bindings?.[field.name];
1774
+ if (!field.bindable) {
1775
+ return renderControl(field.type, {
1776
+ label: field.label,
1777
+ field,
1778
+ value: node.props[field.name],
1779
+ onChange: (value) => dispatch({
1780
+ type: "UPDATE_PROPS",
1781
+ id: node.id,
1782
+ props: { [field.name]: value }
1783
+ })
1784
+ });
1785
+ }
1786
+ return /* @__PURE__ */ jsxs10("div", { style: { marginBottom: 10 }, children: [
1787
+ /* @__PURE__ */ jsxs10(
1788
+ "div",
1789
+ {
1790
+ style: {
1791
+ display: "flex",
1792
+ alignItems: "center",
1793
+ justifyContent: "space-between",
1794
+ marginBottom: 4
1795
+ },
1796
+ children: [
1797
+ /* @__PURE__ */ jsx13(Label2, { className: "nx-pb-control-label", children: field.label }),
1798
+ /* @__PURE__ */ jsxs10(
1799
+ "label",
1800
+ {
1801
+ className: "nx-pb-control-label",
1802
+ style: { display: "flex", alignItems: "center", gap: 6 },
1803
+ children: [
1804
+ "Bind",
1805
+ /* @__PURE__ */ jsx13(
1806
+ Switch2,
1807
+ {
1808
+ checked: !!bound,
1809
+ onCheckedChange: (on) => dispatch({
1810
+ type: "SET_BINDING",
1811
+ id: node.id,
1812
+ prop: field.name,
1813
+ binding: on ? { source: "field", path: "" } : null
1814
+ })
1815
+ }
1816
+ )
1817
+ ]
1818
+ }
1819
+ )
1820
+ ]
1821
+ }
1822
+ ),
1823
+ bound ? /* @__PURE__ */ jsx13(BindingPathEditor, { node, field, binding: bound }) : renderControl(field.type, {
1824
+ field,
1825
+ value: node.props[field.name],
1826
+ onChange: (value) => dispatch({
1827
+ type: "UPDATE_PROPS",
1828
+ id: node.id,
1829
+ props: { [field.name]: value }
1830
+ })
1831
+ })
1832
+ ] });
1833
+ }
1834
+ function ContentTab({ node }) {
1835
+ if (node.type === QUERY_LOOP_TYPE) {
1836
+ return /* @__PURE__ */ jsx13(QueryLoopSettings, { node });
1837
+ }
1838
+ const def = defaultBlockRegistry.get(node.type);
1839
+ const fields = narrowContentFields(def?.contentFields);
1840
+ if (fields.length === 0) {
1841
+ return /* @__PURE__ */ jsx13(Empty, { children: "This block has no content options." });
1842
+ }
1843
+ return /* @__PURE__ */ jsxs10("div", { children: [
1844
+ /* @__PURE__ */ jsx13(SectionLabel, { children: "Content" }),
1845
+ fields.map((field) => /* @__PURE__ */ jsx13(BindableField, { node, field }, field.name))
1846
+ ] });
1847
+ }
1848
+ function StyleTab({
1849
+ node,
1850
+ styleState,
1851
+ setStyleState
1852
+ }) {
1853
+ const { state, dispatch } = useEditor();
1854
+ const def = defaultBlockRegistry.get(node.type);
1855
+ const controls = def?.styleControls ?? [];
1856
+ const bp = state.activeBreakpoint;
1857
+ const tree = styleState === "hover" ? node.styleHover : node.style;
1858
+ if (controls.length === 0) {
1859
+ return /* @__PURE__ */ jsx13(Empty, { children: "This block has no style options." });
1860
+ }
1861
+ return /* @__PURE__ */ jsxs10("div", { children: [
1862
+ /* @__PURE__ */ jsxs10(
1863
+ "div",
1864
+ {
1865
+ style: {
1866
+ display: "flex",
1867
+ alignItems: "center",
1868
+ justifyContent: "space-between",
1869
+ marginBottom: 12
1870
+ },
1871
+ children: [
1872
+ /* @__PURE__ */ jsx13(
1873
+ Segmented,
1874
+ {
1875
+ value: styleState,
1876
+ onChange: setStyleState,
1877
+ ariaLabel: "Style state",
1878
+ options: [
1879
+ { value: "normal", label: "Normal" },
1880
+ { value: "hover", label: "Hover" }
1881
+ ]
1882
+ }
1883
+ ),
1884
+ /* @__PURE__ */ jsx13(
1885
+ "span",
1886
+ {
1887
+ className: "nx-pb-device-badge",
1888
+ "data-active": bp !== BASE2 || void 0,
1889
+ title: "Editing styles for this device (change with the toolbar)",
1890
+ children: bp === BASE2 ? "Desktop" : bp
1891
+ }
1892
+ )
1893
+ ]
1894
+ }
1895
+ ),
1896
+ styleState === "hover" ? /* @__PURE__ */ jsxs10("p", { className: "nx-pb-empty", style: { margin: "0 0 10px" }, children: [
1897
+ "Applied on ",
1898
+ /* @__PURE__ */ jsx13("strong", { children: ":hover" }),
1899
+ "; empty values fall back to Normal."
1900
+ ] }) : null,
1901
+ /* @__PURE__ */ jsx13(SectionLabel, { children: "Style" }),
1902
+ controls.map((ref) => /* @__PURE__ */ jsx13("div", { children: renderControl(ref.control, {
1903
+ label: ref.label,
1904
+ value: readStyleValue(tree, ref.styleKey, bp),
1905
+ onChange: (value) => dispatch({
1906
+ type: "UPDATE_STYLE",
1907
+ id: node.id,
1908
+ breakpoint: bp,
1909
+ styleState,
1910
+ style: { [ref.styleKey]: value }
1911
+ })
1912
+ }) }, `${ref.control}:${ref.styleKey}`))
1913
+ ] });
1914
+ }
1915
+ function AdvancedTab({ node }) {
1916
+ const { state, dispatch } = useEditor();
1917
+ const loc = locateNode(state.document.root, node.id);
1918
+ const move = (delta) => {
1919
+ if (!loc) return;
1920
+ const next = loc.index + delta;
1921
+ if (next < 0 || next >= loc.count) return;
1922
+ dispatch({
1923
+ type: "MOVE",
1924
+ id: node.id,
1925
+ parentId: loc.parentId,
1926
+ slot: loc.slot,
1927
+ index: next
1928
+ });
1929
+ };
1930
+ return /* @__PURE__ */ jsxs10("div", { style: { display: "grid", gap: 14 }, children: [
1931
+ loc ? /* @__PURE__ */ jsxs10("div", { children: [
1932
+ /* @__PURE__ */ jsx13(SectionLabel, { children: "Arrange" }),
1933
+ /* @__PURE__ */ jsxs10("div", { style: { display: "flex", gap: 6 }, children: [
1934
+ /* @__PURE__ */ jsxs10(
1935
+ "button",
1936
+ {
1937
+ type: "button",
1938
+ className: "nx-pb-icon-btn",
1939
+ disabled: loc.index <= 0,
1940
+ "aria-label": "Move block up",
1941
+ onClick: () => move(-1),
1942
+ children: [
1943
+ /* @__PURE__ */ jsx13(ArrowUp, { size: 15, "aria-hidden": true }),
1944
+ " Up"
1945
+ ]
1946
+ }
1947
+ ),
1948
+ /* @__PURE__ */ jsxs10(
1949
+ "button",
1950
+ {
1951
+ type: "button",
1952
+ className: "nx-pb-icon-btn",
1953
+ disabled: loc.index >= loc.count - 1,
1954
+ "aria-label": "Move block down",
1955
+ onClick: () => move(1),
1956
+ children: [
1957
+ /* @__PURE__ */ jsx13(ArrowDown, { size: 15, "aria-hidden": true }),
1958
+ " Down"
1959
+ ]
1960
+ }
1961
+ ),
1962
+ /* @__PURE__ */ jsxs10(
1963
+ "button",
1964
+ {
1965
+ type: "button",
1966
+ className: "nx-pb-icon-btn",
1967
+ "aria-label": "Duplicate block",
1968
+ onClick: () => dispatch({ type: "DUPLICATE", id: node.id }),
1969
+ children: [
1970
+ /* @__PURE__ */ jsx13(Copy, { size: 15, "aria-hidden": true }),
1971
+ " Duplicate"
1972
+ ]
1973
+ }
1974
+ )
1975
+ ] })
1976
+ ] }) : null,
1977
+ /* @__PURE__ */ jsxs10("div", { children: [
1978
+ /* @__PURE__ */ jsx13(SectionLabel, { children: "Custom CSS class" }),
1979
+ renderControl("text", {
1980
+ value: node.customClass ?? "",
1981
+ onChange: (value) => dispatch({
1982
+ type: "SET_CUSTOM_CLASS",
1983
+ id: node.id,
1984
+ customClass: typeof value === "string" ? value : ""
1985
+ })
1986
+ })
1987
+ ] }),
1988
+ /* @__PURE__ */ jsxs10("div", { className: "nx-pb-empty", children: [
1989
+ "Type ",
1990
+ /* @__PURE__ */ jsx13("code", { children: node.type })
1991
+ ] }),
1992
+ /* @__PURE__ */ jsxs10(
1993
+ "button",
1994
+ {
1995
+ type: "button",
1996
+ className: "nx-pb-icon-btn",
1997
+ "aria-label": "Delete block",
1998
+ style: {
1999
+ color: "hsl(var(--destructive))",
2000
+ borderColor: "hsl(var(--destructive) / 0.4)"
2001
+ },
2002
+ onClick: () => dispatch({ type: "REMOVE", id: node.id }),
2003
+ children: [
2004
+ /* @__PURE__ */ jsx13(Trash2, { size: 15, "aria-hidden": true }),
2005
+ " Delete block"
2006
+ ]
2007
+ }
2008
+ )
2009
+ ] });
2010
+ }
2011
+ function Inspector() {
2012
+ const { state } = useEditor();
2013
+ const [tab, setTab] = useState8("content");
2014
+ const [styleState, setStyleState] = useState8("normal");
2015
+ const node = state.selectedId ? findNode(state.document.root, state.selectedId) : void 0;
2016
+ const def = node ? defaultBlockRegistry.get(node.type) : void 0;
2017
+ useEffect5(() => {
2018
+ const initial = node?.type === QUERY_LOOP_TYPE ? "content" : firstPopulatedTab(def);
2019
+ setTab(initial);
2020
+ setStyleState("normal");
2021
+ }, [state.selectedId]);
2022
+ if (!node) {
2023
+ return /* @__PURE__ */ jsxs10(Fragment, { children: [
2024
+ /* @__PURE__ */ jsx13("div", { className: "nx-pb-pane-header", children: "Settings" }),
2025
+ /* @__PURE__ */ jsx13("div", { className: "nx-pb-inspector-empty", children: "Select a block on the canvas to edit its content and style." })
2026
+ ] });
2027
+ }
2028
+ const Icon = blockIcon(def?.icon);
2029
+ return /* @__PURE__ */ jsxs10("div", { className: "nx-pb-inspector", children: [
2030
+ /* @__PURE__ */ jsxs10("div", { className: "nx-pb-inspector-header", children: [
2031
+ /* @__PURE__ */ jsx13("span", { className: "nx-pb-block-icon", children: /* @__PURE__ */ jsx13(Icon, { size: 16, "aria-hidden": true }) }),
2032
+ /* @__PURE__ */ jsxs10("div", { children: [
2033
+ /* @__PURE__ */ jsx13("div", { className: "nx-pb-inspector-title", children: def?.label ?? node.type }),
2034
+ /* @__PURE__ */ jsx13("div", { className: "nx-pb-inspector-sub", children: def?.category ?? "block" })
2035
+ ] })
2036
+ ] }),
2037
+ /* @__PURE__ */ jsxs10(
2038
+ Tabs,
2039
+ {
2040
+ value: tab,
2041
+ onValueChange: (v) => setTab(v),
2042
+ style: {
2043
+ display: "flex",
2044
+ flexDirection: "column",
2045
+ flex: 1,
2046
+ minHeight: 0
2047
+ },
2048
+ children: [
2049
+ /* @__PURE__ */ jsxs10(TabsList, { style: { margin: "10px 12px 0" }, children: [
2050
+ /* @__PURE__ */ jsx13(TabsTrigger, { value: "content", children: "Content" }),
2051
+ /* @__PURE__ */ jsx13(TabsTrigger, { value: "style", children: "Style" }),
2052
+ /* @__PURE__ */ jsx13(TabsTrigger, { value: "advanced", children: "Advanced" })
2053
+ ] }),
2054
+ /* @__PURE__ */ jsxs10("div", { style: { padding: 12, overflow: "auto", flex: 1 }, children: [
2055
+ /* @__PURE__ */ jsx13(TabsContent, { value: "content", children: /* @__PURE__ */ jsx13(ContentTab, { node }) }),
2056
+ /* @__PURE__ */ jsx13(TabsContent, { value: "style", children: /* @__PURE__ */ jsx13(
2057
+ StyleTab,
2058
+ {
2059
+ node,
2060
+ styleState,
2061
+ setStyleState
2062
+ }
2063
+ ) }),
2064
+ /* @__PURE__ */ jsx13(TabsContent, { value: "advanced", children: /* @__PURE__ */ jsx13(AdvancedTab, { node }) })
2065
+ ] })
2066
+ ]
2067
+ }
2068
+ )
2069
+ ] });
2070
+ }
2071
+
2072
+ // src/admin/EditorSurface.tsx
2073
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
2074
+ var BREAKPOINTS = [
2075
+ { id: "base", label: "Desktop", Icon: Monitor },
2076
+ { id: "tablet", label: "Tablet", Icon: Tablet },
2077
+ { id: "mobile", label: "Mobile", Icon: Smartphone }
2078
+ ];
2079
+ function EditorSurface() {
2080
+ const { state, dispatch } = useEditor();
2081
+ const root = state.document.root;
2082
+ const onDragEnd = (event) => {
2083
+ if (event.canceled) return;
2084
+ const { source, target } = event.operation;
2085
+ if (!source || !target) return;
2086
+ const action = planDrop(
2087
+ source.data ?? {},
2088
+ target.data ?? {},
2089
+ root,
2090
+ defaultBlockRegistry
2091
+ );
2092
+ if (action) dispatch(action);
2093
+ };
2094
+ return /* @__PURE__ */ jsxs11(DragDropProvider, { onDragEnd, children: [
2095
+ /* @__PURE__ */ jsxs11("div", { className: "nx-pb-editor", children: [
2096
+ /* @__PURE__ */ jsx14("div", { className: "nx-pb-toolbar", children: /* @__PURE__ */ jsx14("div", { className: "nx-pb-seg", role: "group", "aria-label": "Preview device", children: BREAKPOINTS.map(({ id, label, Icon }) => /* @__PURE__ */ jsxs11(
2097
+ "button",
2098
+ {
2099
+ type: "button",
2100
+ className: "nx-pb-seg-btn",
2101
+ "aria-pressed": state.activeBreakpoint === id,
2102
+ "aria-label": label,
2103
+ onClick: () => dispatch({ type: "SET_BREAKPOINT", breakpoint: id }),
2104
+ children: [
2105
+ /* @__PURE__ */ jsx14(Icon, { size: 15, "aria-hidden": true }),
2106
+ label
2107
+ ]
2108
+ },
2109
+ id
2110
+ )) }) }),
2111
+ /* @__PURE__ */ jsxs11("div", { className: "nx-pb-body", children: [
2112
+ /* @__PURE__ */ jsx14("aside", { className: "nx-pb-pane nx-pb-pane--left", children: /* @__PURE__ */ jsx14(BlockLibrary, {}) }),
2113
+ /* @__PURE__ */ jsx14("main", { className: "nx-pb-pane--center", children: /* @__PURE__ */ jsx14(Canvas, {}) }),
2114
+ /* @__PURE__ */ jsx14("aside", { className: "nx-pb-pane nx-pb-pane--right", children: /* @__PURE__ */ jsx14(Inspector, {}) })
2115
+ ] })
2116
+ ] }),
2117
+ /* @__PURE__ */ jsx14(DragOverlay, { children: (source) => /* @__PURE__ */ jsxs11(
2118
+ "div",
2119
+ {
2120
+ style: {
2121
+ display: "inline-flex",
2122
+ alignItems: "center",
2123
+ gap: 6,
2124
+ padding: "6px 12px",
2125
+ borderRadius: "var(--radius)",
2126
+ background: "hsl(var(--primary))",
2127
+ color: "hsl(var(--primary-foreground))",
2128
+ fontSize: 13,
2129
+ fontWeight: 600,
2130
+ boxShadow: "0 8px 24px rgb(0 0 0 / 0.25)",
2131
+ pointerEvents: "none",
2132
+ whiteSpace: "nowrap"
2133
+ },
2134
+ children: [
2135
+ /* @__PURE__ */ jsx14("span", { "aria-hidden": true, children: "\u283F" }),
2136
+ dragLabel(source?.data ?? {}, root, defaultBlockRegistry)
2137
+ ]
2138
+ }
2139
+ ) })
2140
+ ] });
2141
+ }
2142
+
2143
+ // src/admin/SaveShell.tsx
2144
+ import { Button as Button2, Input as Input7, Label as Label3 } from "@nextlyhq/ui";
2145
+ import { useState as useState9 } from "react";
2146
+
2147
+ // src/admin/api/adminFetch.ts
2148
+ var ENTRIES = "/admin/api/collections/pages/entries";
2149
+ async function handle(res) {
2150
+ const body = await res.json().catch(() => ({}));
2151
+ if (!res.ok) {
2152
+ const message = typeof body.error === "string" && body.error || typeof body.message === "string" && body.message || `Request failed (${res.status})`;
2153
+ throw new Error(message);
2154
+ }
2155
+ return body.item ?? body;
2156
+ }
2157
+ async function savePage(input) {
2158
+ const { id, ...payload } = input;
2159
+ const res = await fetch(id ? `${ENTRIES}/${id}` : ENTRIES, {
2160
+ method: id ? "PATCH" : "POST",
2161
+ headers: { "Content-Type": "application/json" },
2162
+ credentials: "same-origin",
2163
+ body: JSON.stringify(payload)
2164
+ });
2165
+ return handle(res);
2166
+ }
2167
+ async function deletePage(id) {
2168
+ const res = await fetch(`${ENTRIES}/${id}`, {
2169
+ method: "DELETE",
2170
+ credentials: "same-origin"
2171
+ });
2172
+ await handle(res);
2173
+ }
2174
+
2175
+ // src/admin/SaveShell.tsx
2176
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
2177
+ function SaveShell({ props }) {
2178
+ const { state, dispatch } = useEditor();
2179
+ const data = props.initialData ?? {};
2180
+ const str5 = (v) => typeof v === "string" ? v : "";
2181
+ const [title, setTitle] = useState9(str5(data.title));
2182
+ const [slug, setSlug] = useState9(str5(data.slug));
2183
+ const customCss = str5(data.customCss);
2184
+ const [busy, setBusy] = useState9(false);
2185
+ const [error, setError] = useState9(null);
2186
+ async function save(status) {
2187
+ if (!title.trim() || !slug.trim()) {
2188
+ setError("Title and slug are required.");
2189
+ return;
2190
+ }
2191
+ setBusy(true);
2192
+ setError(null);
2193
+ try {
2194
+ const entry = await savePage({
2195
+ id: props.entryId,
2196
+ title,
2197
+ slug,
2198
+ content: state.document,
2199
+ customCss,
2200
+ status
2201
+ });
2202
+ dispatch({ type: "MARK_SAVED" });
2203
+ props.onSuccess?.(entry);
2204
+ } catch (e) {
2205
+ setError(e.message);
2206
+ } finally {
2207
+ setBusy(false);
2208
+ }
2209
+ }
2210
+ async function remove() {
2211
+ if (!props.entryId) {
2212
+ props.onCancel?.();
2213
+ return;
2214
+ }
2215
+ setBusy(true);
2216
+ try {
2217
+ await deletePage(props.entryId);
2218
+ props.onDelete?.();
2219
+ } catch (e) {
2220
+ setError(e.message);
2221
+ } finally {
2222
+ setBusy(false);
2223
+ }
2224
+ }
2225
+ return /* @__PURE__ */ jsxs12(
2226
+ "div",
2227
+ {
2228
+ style: {
2229
+ display: "flex",
2230
+ gap: 12,
2231
+ alignItems: "flex-end",
2232
+ flexWrap: "wrap"
2233
+ },
2234
+ children: [
2235
+ /* @__PURE__ */ jsxs12("div", { children: [
2236
+ /* @__PURE__ */ jsx15(Label3, { htmlFor: "nx-pb-title", children: "Title" }),
2237
+ /* @__PURE__ */ jsx15(
2238
+ Input7,
2239
+ {
2240
+ id: "nx-pb-title",
2241
+ value: title,
2242
+ onChange: (e) => setTitle(e.target.value)
2243
+ }
2244
+ )
2245
+ ] }),
2246
+ /* @__PURE__ */ jsxs12("div", { children: [
2247
+ /* @__PURE__ */ jsx15(Label3, { htmlFor: "nx-pb-slug", children: "Slug" }),
2248
+ /* @__PURE__ */ jsx15(
2249
+ Input7,
2250
+ {
2251
+ id: "nx-pb-slug",
2252
+ value: slug,
2253
+ onChange: (e) => setSlug(e.target.value)
2254
+ }
2255
+ )
2256
+ ] }),
2257
+ /* @__PURE__ */ jsxs12("div", { style: { display: "flex", gap: 8, marginLeft: "auto" }, children: [
2258
+ /* @__PURE__ */ jsx15(
2259
+ Button2,
2260
+ {
2261
+ variant: "outline",
2262
+ disabled: busy,
2263
+ onClick: () => void save("draft"),
2264
+ children: "Save Draft"
2265
+ }
2266
+ ),
2267
+ /* @__PURE__ */ jsx15(Button2, { disabled: busy, onClick: () => void save("published"), children: "Publish" }),
2268
+ /* @__PURE__ */ jsx15(Button2, { variant: "ghost", disabled: busy, onClick: () => void remove(), children: props.entryId ? "Delete" : "Cancel" })
2269
+ ] }),
2270
+ error ? /* @__PURE__ */ jsx15(
2271
+ "p",
2272
+ {
2273
+ style: { color: "hsl(var(--destructive))", width: "100%", margin: 0 },
2274
+ children: error
2275
+ }
2276
+ ) : null
2277
+ ]
2278
+ }
2279
+ );
2280
+ }
2281
+
2282
+ // src/admin/PageBuilderEditView.tsx
2283
+ import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
2284
+ function emptyDoc() {
2285
+ return {
2286
+ version: 1,
2287
+ root: makeNode("core/container", {}, void 0, { default: [] })
2288
+ };
2289
+ }
2290
+ function PageBuilderEditView(props) {
2291
+ const data = props.initialData ?? {};
2292
+ const doc = data.content ?? emptyDoc();
2293
+ return /* @__PURE__ */ jsx16(
2294
+ EditorProvider,
2295
+ {
2296
+ document: doc,
2297
+ draftKey: draftKeyFor(props.collectionSlug, props.entryId),
2298
+ children: /* @__PURE__ */ jsxs13("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
2299
+ /* @__PURE__ */ jsx16(SaveShell, { props }),
2300
+ /* @__PURE__ */ jsx16(EditorSurface, {})
2301
+ ] })
2302
+ }
2303
+ );
2304
+ }
2305
+
2306
+ // src/admin/PageBuilderField.tsx
2307
+ import { useController } from "react-hook-form";
2308
+ import { jsx as jsx17 } from "react/jsx-runtime";
2309
+ function emptyDoc2() {
2310
+ return {
2311
+ version: 1,
2312
+ root: makeNode("core/container", {}, void 0, { default: [] })
2313
+ };
2314
+ }
2315
+ function isDocument(v) {
2316
+ return typeof v === "object" && v !== null && "root" in v && typeof v.root === "object";
2317
+ }
2318
+ function PageBuilderField({ name, control }) {
2319
+ const { field } = useController({ control, name });
2320
+ const doc = isDocument(field.value) ? field.value : emptyDoc2();
2321
+ return /* @__PURE__ */ jsx17(
2322
+ EditorProvider,
2323
+ {
2324
+ document: doc,
2325
+ draftKey: draftKeyFor("field", name),
2326
+ onDocumentChange: (next) => field.onChange(next),
2327
+ children: /* @__PURE__ */ jsx17(EditorSurface, {})
2328
+ }
2329
+ );
2330
+ }
2331
+
2332
+ // src/admin/PageBuilderModeToggle.tsx
2333
+ import { Button as Button3 } from "@nextlyhq/ui";
2334
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
2335
+ function PageBuilderModeToggle({
2336
+ controllerField,
2337
+ value,
2338
+ onChange
2339
+ }) {
2340
+ if (!controllerField) return null;
2341
+ const isBuilder = value === "builder";
2342
+ return /* @__PURE__ */ jsxs14(
2343
+ "div",
2344
+ {
2345
+ role: "group",
2346
+ "aria-label": "Editor mode",
2347
+ className: "inline-flex items-center gap-0.5 rounded-md border border-border p-0.5",
2348
+ children: [
2349
+ /* @__PURE__ */ jsx18(
2350
+ Button3,
2351
+ {
2352
+ type: "button",
2353
+ size: "sm",
2354
+ variant: isBuilder ? "ghost" : "default",
2355
+ "aria-pressed": !isBuilder,
2356
+ onClick: () => onChange?.("default"),
2357
+ children: "Normal"
2358
+ }
2359
+ ),
2360
+ /* @__PURE__ */ jsx18(
2361
+ Button3,
2362
+ {
2363
+ type: "button",
2364
+ size: "sm",
2365
+ variant: isBuilder ? "default" : "ghost",
2366
+ "aria-pressed": isBuilder,
2367
+ onClick: () => onChange?.("builder"),
2368
+ children: "Page Builder"
2369
+ }
2370
+ )
2371
+ ]
2372
+ }
2373
+ );
2374
+ }
2375
+
2376
+ // src/admin/PageBuilderToggle.tsx
2377
+ import { Label as Label4, Switch as Switch3 } from "@nextlyhq/ui";
2378
+ import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
2379
+ function isCanvasField(f) {
2380
+ if (f.type === PAGE_BUILDER_TYPE) return true;
2381
+ const component = f.admin?.component;
2382
+ return typeof component === "string" && component.includes("plugin-page-builder");
2383
+ }
2384
+ function hasPageBuilderFields(fields) {
2385
+ return fields.some((f) => f.name === EDITOR_MODE_FIELD) && fields.some(isCanvasField);
2386
+ }
2387
+ function addPageBuilderFields(fields) {
2388
+ if (hasPageBuilderFields(fields)) return fields;
2389
+ const editormode = {
2390
+ id: "pb-editormode",
2391
+ name: EDITOR_MODE_FIELD,
2392
+ label: "Editor",
2393
+ type: "select",
2394
+ validation: {},
2395
+ defaultValue: "default",
2396
+ options: [
2397
+ { value: "default", label: "Default" },
2398
+ { value: "builder", label: "Page Builder" }
2399
+ ],
2400
+ // Hidden: stores the per-entry mode but never shows as a field. The choice is
2401
+ // a toolbar toggle; it's also filtered out of the schema-builder field list.
2402
+ admin: { hidden: true }
2403
+ };
2404
+ const content = {
2405
+ id: "pb-content",
2406
+ name: PAGE_BUILDER_CONTENT_FIELD,
2407
+ label: "Page Builder",
2408
+ type: PAGE_BUILDER_TYPE,
2409
+ validation: {},
2410
+ admin: { condition: { field: EDITOR_MODE_FIELD, equals: "builder" } }
2411
+ };
2412
+ return [...fields, editormode, content];
2413
+ }
2414
+ function removePageBuilderFields(fields) {
2415
+ return fields.filter((f) => f.name !== EDITOR_MODE_FIELD && !isCanvasField(f));
2416
+ }
2417
+ function PageBuilderToggle({
2418
+ fields,
2419
+ setFields,
2420
+ disabled
2421
+ }) {
2422
+ const on = hasPageBuilderFields(fields);
2423
+ return /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-3 py-2", children: [
2424
+ /* @__PURE__ */ jsx19(
2425
+ Switch3,
2426
+ {
2427
+ checked: on,
2428
+ disabled,
2429
+ "aria-label": "Use Page Builder",
2430
+ onCheckedChange: (next) => setFields(
2431
+ next ? addPageBuilderFields(fields) : removePageBuilderFields(fields)
2432
+ )
2433
+ }
2434
+ ),
2435
+ /* @__PURE__ */ jsx19(Label4, { className: "cursor-pointer", children: "Use Page Builder" }),
2436
+ /* @__PURE__ */ jsx19("span", { className: "text-xs text-muted-foreground", children: "Let entries choose a visual Page Builder canvas instead of the fields." })
2437
+ ] });
2438
+ }
2439
+
2440
+ // src/admin/index.ts
2441
+ registerDefaultControls();
2442
+ var EDIT_VIEW_PATH = "@nextlyhq/plugin-page-builder/admin#PageBuilderEditView";
2443
+ var FIELD_PATH = "@nextlyhq/plugin-page-builder/admin#PageBuilderField";
2444
+ var TOGGLE_PATH = "@nextlyhq/plugin-page-builder/admin#PageBuilderToggle";
2445
+ var MODE_TOGGLE_PATH = "@nextlyhq/plugin-page-builder/admin#PageBuilderModeToggle";
2446
+ var COMPONENTS = {
2447
+ [EDIT_VIEW_PATH]: PageBuilderEditView,
2448
+ [FIELD_PATH]: PageBuilderField,
2449
+ [TOGGLE_PATH]: PageBuilderToggle,
2450
+ [MODE_TOGGLE_PATH]: PageBuilderModeToggle
2451
+ };
2452
+ registerComponents(COMPONENTS);
2453
+ registerKnownPlugin("@nextlyhq/plugin-page-builder", () => {
2454
+ registerComponents(COMPONENTS);
2455
+ return Promise.resolve();
2456
+ });
2457
+ export {
2458
+ PageBuilderEditView,
2459
+ PageBuilderField,
2460
+ PageBuilderModeToggle,
2461
+ PageBuilderToggle
2462
+ };
2463
+ //# sourceMappingURL=index.js.map