@jbpark/live-editor 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/ast-CMD20ykx.js +1613 -0
  4. package/dist/ast-CMD20ykx.js.map +1 -0
  5. package/dist/{boundary-C90q3YsT.js → boundary-D2IFja-3.js} +2 -2
  6. package/dist/boundary-D2IFja-3.js.map +1 -0
  7. package/dist/cache-B0-XcIxJ.js +51 -0
  8. package/dist/cache-B0-XcIxJ.js.map +1 -0
  9. package/dist/{utils-BQ7WL7Jh.js → cn-CAnad2vL.js} +3 -128
  10. package/dist/cn-CAnad2vL.js.map +1 -0
  11. package/dist/compile-0Dvp8zp5.js +135 -0
  12. package/dist/compile-0Dvp8zp5.js.map +1 -0
  13. package/dist/constants-CKvdqZtO.js +1457 -0
  14. package/dist/constants-CKvdqZtO.js.map +1 -0
  15. package/dist/{context-Y-M9fViD.js → context-93X3gyfJ.js} +3 -3
  16. package/dist/context-93X3gyfJ.js.map +1 -0
  17. package/dist/{core-Co8Nlq16.js → core-B3ruccKb.js} +3 -2
  18. package/dist/core-B3ruccKb.js.map +1 -0
  19. package/dist/detect-typescript-5MV4mCM7.js +10 -0
  20. package/dist/detect-typescript-5MV4mCM7.js.map +1 -0
  21. package/dist/dnd/index.d.ts +2 -2
  22. package/dist/dnd/index.js +2 -2
  23. package/dist/{dnd-D_rG92-R.js → dnd-DBaH2L8N.js} +548 -137
  24. package/dist/dnd-DBaH2L8N.js.map +1 -0
  25. package/dist/{extract-abHYmNsu.js → document-C3eCRQlj.js} +6791 -8938
  26. package/dist/document-C3eCRQlj.js.map +1 -0
  27. package/dist/{document-J9Lo9p81.d.ts → document-Ct4jW78J.d.ts} +42 -22
  28. package/dist/editor/index.js +2 -2
  29. package/dist/{editor-aNRx7hRY.js → editor-DvMruljw.js} +2 -2
  30. package/dist/{editor-aNRx7hRY.js.map → editor-DvMruljw.js.map} +1 -1
  31. package/dist/editor-caches-ClMvdcQF.js +23 -0
  32. package/dist/editor-caches-ClMvdcQF.js.map +1 -0
  33. package/dist/error/index.js +2 -2
  34. package/dist/{error-o0SBwtTv.js → error-KW8XCcjH.js} +2 -2
  35. package/dist/{error-o0SBwtTv.js.map → error-KW8XCcjH.js.map} +1 -1
  36. package/dist/{index-Doh4VTXr.d.ts → index-DVgwXnPa.d.ts} +109 -34
  37. package/dist/{index-B_p74BRM.d.ts → index-w3kLcCXl.d.ts} +29 -5
  38. package/dist/index.d.ts +4 -4
  39. package/dist/index.js +5 -5
  40. package/dist/index.js.map +1 -1
  41. package/dist/preview/index.js +2 -2
  42. package/dist/{preview-CS6Rsppd.js → preview-B9Jhv1Up.js} +4 -4
  43. package/dist/preview-B9Jhv1Up.js.map +1 -0
  44. package/dist/provider/index.js +1 -1
  45. package/dist/sections-A4TAXUP8.js +24 -0
  46. package/dist/sections-A4TAXUP8.js.map +1 -0
  47. package/dist/{use-dynamic-tailwind-SOgrb2qc.js → use-dynamic-tailwind-BDfytfYC.js} +33 -9
  48. package/dist/use-dynamic-tailwind-BDfytfYC.js.map +1 -0
  49. package/dist/utils/ast/index.d.ts +3 -3
  50. package/dist/utils/ast/index.js +3 -3
  51. package/dist/utils/index.d.ts +20 -9
  52. package/dist/utils/index.js +7 -3
  53. package/package.json +1 -1
  54. package/dist/ast-CZwpVrJv.js +0 -851
  55. package/dist/ast-CZwpVrJv.js.map +0 -1
  56. package/dist/boundary-C90q3YsT.js.map +0 -1
  57. package/dist/context-Y-M9fViD.js.map +0 -1
  58. package/dist/core-Co8Nlq16.js.map +0 -1
  59. package/dist/dnd-D_rG92-R.js.map +0 -1
  60. package/dist/extract-abHYmNsu.js.map +0 -1
  61. package/dist/preview-CS6Rsppd.js.map +0 -1
  62. package/dist/use-dynamic-tailwind-SOgrb2qc.js.map +0 -1
  63. package/dist/utils-BQ7WL7Jh.js.map +0 -1
@@ -1,15 +1,18 @@
1
- import { D as fillSectionIds, E as createSectionPreviewCache, I as nanoid, K as __toESM, L as require_lib, U as DRAGGABLE_ITEMS, _ as parseArrayExpression, a as findEditableChildren, c as parseBinding, d as canLosslesslyEvaluateSource, m as extractObjectProperties, n as extract, o as getCurrentValue, p as extractNodeValue, s as getStructuredValue, v as parseValue, z as BINDING_PROP } from "./extract-abHYmNsu.js";
2
- import { c as generateSection, d as preloadScripts, i as cn, p as replaceSections, s as extractSections } from "./utils-BQ7WL7Jh.js";
1
+ import { _ as __toESM, c as inspectDocument, h as require_lib, n as createSectionPreviewCache, p as nanoid, r as fillSectionIds } from "./document-C3eCRQlj.js";
2
+ import { c as RESERVED_BINDING_PROPERTIES, o as DRAGGABLE_ITEMS, t as BINDING_PROP } from "./constants-CKvdqZtO.js";
3
3
  import { i as usePreview } from "./states-Ci1AvoQ9.js";
4
- import { _ as moveSelectedIndices, c as appendArrayItem, d as moveArrayItems, g as canStructurallyEditArray, h as updateArrayItemValue, i as replaceIds, l as duplicateArrayItems, m as updateArrayItemProperty, o as update, p as removeArrayItems, r as fillIds, s as getChildrenSignatures, t as validateBindingValue, u as moveArrayItem } from "./ast-CZwpVrJv.js";
5
- import { i as Frame, n as useCompiledModule, t as useDynamicTailwind } from "./use-dynamic-tailwind-SOgrb2qc.js";
6
- import { t as Core } from "./core-Co8Nlq16.js";
7
- import { n as Error$1, t as ErrorBoundary } from "./boundary-C90q3YsT.js";
4
+ import { A as extractObjectProperties, C as getStructuredValue, D as canLosslesslyEvaluateSource, M as parseArrayExpression, N as parseValue, S as getCurrentValue, _ as updateArrayItemProperty, b as moveSelectedIndices, c as getChildrenSignatures, d as appendArrayItem, f as duplicateArrayItems, g as removeArrayItems, i as replaceIds, k as extractNodeValue, m as moveArrayItems, p as moveArrayItem, r as fillIds, s as updateAll, t as validateBindingValue, u as extract, v as updateArrayItemValue, w as parseBinding, x as findEditableChildren, y as canStructurallyEditArray } from "./ast-CMD20ykx.js";
5
+ import { t as cn } from "./cn-CAnad2vL.js";
6
+ import { o as preloadScripts } from "./compile-0Dvp8zp5.js";
7
+ import { a as Frame, i as useStableModules, n as useCompiledModule, t as useDynamicTailwind } from "./use-dynamic-tailwind-BDfytfYC.js";
8
+ import { t as Core } from "./core-B3ruccKb.js";
9
+ import { a as replaceSections, n as extractSections, r as generateSection } from "./sections-A4TAXUP8.js";
10
+ import { n as Error$1, t as ErrorBoundary } from "./boundary-D2IFja-3.js";
8
11
  import { Children, createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
9
12
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
- import { DndContext, DragOverlay, PointerSensor, closestCenter, useDndContext, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
13
+ import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter, useDndContext, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
11
14
  import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
12
- import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
15
+ import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
13
16
  import { Button, Card, Checkbox, ColorPicker, DatePicker, Drawer, Input, RichTextEditor, Select, Space, Splitter, Toast, Typography } from "@jbpark/ui-kit";
14
17
  import { useDebounce, useMultiSelect, useResponsiveSize } from "@jbpark/use-hooks";
15
18
  import { ArrowDown, ArrowUp, ChevronDown, ChevronUp, Copy, LayoutGrid, Plus, Trash, X } from "lucide-react";
@@ -67,9 +70,16 @@ const DefaultDraggableItem = ({ item, onAdd, tapToAdd = false }) => /* @__PURE__
67
70
  ref,
68
71
  style: { opacity: isDragging ? .5 : 1 },
69
72
  ...dragProps,
70
- className: cn("cursor-grab", "outline-none", "hover:border-blue-300 hover:shadow-md", isDragging && "opacity-50"),
73
+ className: cn("cursor-grab", "outline-none focus-visible:ring-2 focus-visible:ring-blue-500", "hover:border-blue-300 hover:shadow-md", isDragging && "opacity-50"),
71
74
  onClick: tapToAdd && onAdd ? () => onAdd(item) : void 0,
72
75
  onDoubleClick: onAdd ? () => onAdd(item) : void 0,
76
+ onKeyDown: (event) => {
77
+ dragProps.onKeyDown?.(event);
78
+ if (onAdd && event.key === "Enter" && event.target === event.currentTarget && !event.defaultPrevented) {
79
+ event.preventDefault();
80
+ onAdd(item);
81
+ }
82
+ },
73
83
  children: item.name
74
84
  })
75
85
  });
@@ -88,9 +98,11 @@ const useDndLayout = () => useDndRegions("useDndLayout()");
88
98
  //#region src/components/dnd/panel-binding.ts
89
99
  const readAttribute = (node, name) => node.dataAttributes.find((attribute) => attribute.name === name)?.value;
90
100
  const canEditBindingValue = (node, binding) => {
101
+ if (RESERVED_BINDING_PROPERTIES.includes(binding.property)) return false;
91
102
  if (binding.property === "children" || binding.property === "innerText" || binding.property === "innerHTML" || binding.property === "items" || binding.property === "data" || binding.type === "array" || binding.type === "jsx" || binding.type === "richtext") return true;
92
103
  const attribute = node.attributes.find((candidate) => candidate.name === binding.property);
93
- return attribute?.isStringLiteral === true || canLosslesslyEvaluateSource(attribute?.value ?? "");
104
+ if (!attribute) return true;
105
+ return attribute.isStringLiteral === true || canLosslesslyEvaluateSource(attribute?.value ?? "");
94
106
  };
95
107
  const toBindingFields = (binding) => ({
96
108
  label: binding.label,
@@ -121,12 +133,19 @@ const resolveRenderEntry = (render, key) => {
121
133
  render: entry
122
134
  };
123
135
  };
136
+ const CONTENT_PROPERTIES = /* @__PURE__ */ new Set([
137
+ "innerText",
138
+ "innerHTML",
139
+ "children"
140
+ ]);
141
+ const isAttributeAbsent = (node, binding) => !CONTENT_PROPERTIES.has(binding.property) && !node.attributes.some((candidate) => candidate.name === binding.property);
124
142
  const toPanelBindingData = (node, id, binding) => ({
125
143
  ...toBindingFields(binding),
126
144
  id,
127
145
  value: getStructuredValue(node, binding.property, binding.type),
128
146
  rawValue: getCurrentValue(node, binding.property),
129
- ...!canEditBindingValue(node, binding) && { canEditValue: false }
147
+ ...!canEditBindingValue(node, binding) && { canEditValue: false },
148
+ ...isAttributeAbsent(node, binding) && { present: false }
130
149
  });
131
150
  const resolvePanelBindings = (node) => {
132
151
  const id = readAttribute(node, "data-id");
@@ -263,7 +282,7 @@ const useStructuralSelection = (count, revision) => {
263
282
  };
264
283
  };
265
284
  //#endregion
266
- //#region src/components/dnd/panel/use-children-editor.ts
285
+ //#region src/components/dnd/panel/use-dnd-children.ts
267
286
  const producedBy = (expected, action) => {
268
287
  if (action.type === "move-selected") {
269
288
  const moved = JSON.stringify(moveSelectedIndices(expected, new Set(action.indices), action.direction).items);
@@ -275,7 +294,7 @@ const producedBy = (expected, action) => {
275
294
  return next.length === expected.length + added && expected.every((signature, index) => next[index] === signature);
276
295
  };
277
296
  };
278
- const useChildrenEditor = (items, { onChange } = {}) => {
297
+ const useDndChildren = (items, { onChange } = {}) => {
279
298
  const expected = useMemo(() => getChildrenSignatures(items), [items]);
280
299
  const revision = JSON.stringify(expected);
281
300
  const { selection, record } = useStructuralSelection(items.length, revision);
@@ -323,7 +342,7 @@ const useChildrenEditor = (items, { onChange } = {}) => {
323
342
  //#region src/components/dnd/panel/children.tsx
324
343
  const Children$1 = ({ value, onChange, onNodeChange }) => {
325
344
  const items = useMemo(() => Array.isArray(value) ? value : [], [value]);
326
- const { selection, actions } = useChildrenEditor(items, { onChange });
345
+ const { selection, actions } = useDndChildren(items, { onChange });
327
346
  const editableChildrenMap = useMemo(() => {
328
347
  const map = /* @__PURE__ */ new Map();
329
348
  items.forEach((item, index) => {
@@ -447,7 +466,40 @@ const Children$1 = ({ value, onChange, onNodeChange }) => {
447
466
  });
448
467
  };
449
468
  //#endregion
450
- //#region src/components/dnd/panel/use-items-editor.ts
469
+ //#region src/components/dnd/panel/field-kind.ts
470
+ const isColorProperty = (property) => property.toLowerCase().includes("color");
471
+ const getFieldKind = (binding) => {
472
+ const { property, type, value } = binding;
473
+ if (binding.canEditValue === false) return "readonly";
474
+ if (property === "items" || property === "data" || type === "array") return "items";
475
+ if (type === "richtext") return "richtext";
476
+ if (property === BINDING_PROP.INNER_HTML) return "html";
477
+ if (type === "jsx") return "jsx";
478
+ if (property === "children" && Array.isArray(value)) return "children";
479
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) return "object";
480
+ if (type === "boolean" || typeof value === "boolean") return "boolean";
481
+ if (binding.options && Array.isArray(binding.options)) return "select";
482
+ if (type === "color" || isColorProperty(property)) return "color";
483
+ if (type === "date") return "date";
484
+ if (type === "url") return "url";
485
+ if (type === "number" || typeof value === "number") return "number";
486
+ return "text";
487
+ };
488
+ const isStructuralFieldKind = (kind) => kind === "items" || kind === "children";
489
+ //#endregion
490
+ //#region src/components/dnd/panel/use-dnd-items.ts
491
+ const createPendingCommit = () => {
492
+ let current = null;
493
+ return {
494
+ read: () => current,
495
+ write: (next) => {
496
+ current = next;
497
+ },
498
+ clear: () => {
499
+ current = null;
500
+ }
501
+ };
502
+ };
451
503
  const bindingsInJSX = (source) => {
452
504
  const nodes = extract(source);
453
505
  const container = nodes.find((n) => n.bindings?.some((b) => b.property === "children"));
@@ -545,7 +597,7 @@ const moveId = (ids, from, to) => {
545
597
  const removeIds = (ids, indices) => {
546
598
  return ids.filter((_, index) => !indices.has(index));
547
599
  };
548
- const useItemsEditor = (value, { render, onChange, onNodeChange } = {}) => {
600
+ const useDndItems = (value, { render, onChange, onNodeChange } = {}) => {
549
601
  const { objectItems, primitiveItems, parseError } = useMemo(() => parseSource(value), [value]);
550
602
  const canEditStructure = useMemo(() => canStructurallyEditArray(value), [value]);
551
603
  const { reportError } = useDndEditOptions();
@@ -572,7 +624,33 @@ const useItemsEditor = (value, { render, onChange, onNodeChange } = {}) => {
572
624
  value
573
625
  ]);
574
626
  const { selection, record } = useStructuralSelection(isPrimitive ? primitiveItems.length : objectItems.length, value);
575
- const commit = (next, nextIds, nextSelection) => {
627
+ const [pending] = useState(createPendingCommit);
628
+ useEffect(() => {
629
+ pending.clear();
630
+ });
631
+ const latest = () => {
632
+ const last = pending.read();
633
+ if (last?.from !== value) return {
634
+ value,
635
+ items: rawItems,
636
+ ids: identity.ids,
637
+ selected: selection.selected
638
+ };
639
+ const parsed = parseSource(last.value);
640
+ return {
641
+ value: last.value,
642
+ items: isPrimitive ? parsed.primitiveItems : parsed.objectItems,
643
+ ids: last.ids,
644
+ selected: last.selected
645
+ };
646
+ };
647
+ const locate = (snapshot, elementIndex) => {
648
+ const shown = rawItems.find((item) => item.elementIndex === elementIndex);
649
+ if (!shown) return;
650
+ const index = snapshot.ids.indexOf(identity.ids[shown.index]);
651
+ return index >= 0 ? snapshot.items[index] : null;
652
+ };
653
+ const commit = (snapshot, next, nextIds, nextSelection) => {
576
654
  if (next === null) {
577
655
  reportError({
578
656
  type: "items",
@@ -583,17 +661,30 @@ const useItemsEditor = (value, { render, onChange, onNodeChange } = {}) => {
583
661
  }
584
662
  const parsed = parseSource(next);
585
663
  const nextItems = kind === "primitive" ? parsed.primitiveItems : parsed.objectItems;
586
- if (nextIds) setIdentityState(createIdentityState(next, kind, nextItems, nextIds));
587
- else setIdentityState(createIdentityState(next, kind, nextItems, identity.ids));
664
+ const ids = nextIds ?? snapshot.ids;
665
+ pending.write({
666
+ from: value,
667
+ value: next,
668
+ ids,
669
+ selected: nextSelection
670
+ });
671
+ setIdentityState(createIdentityState(next, kind, nextItems, ids));
588
672
  record(nextSelection, (revision) => revision === next, { now: true });
589
673
  onChange?.(next);
590
674
  return true;
591
675
  };
592
- const elementIndicesOf = (indices) => {
593
- return new Set((isPrimitive ? primitiveItems : objectItems).filter((item) => indices.has(item.index)).map((item) => item.elementIndex));
594
- };
676
+ const elementIndicesOf = (snapshot, indices) => new Set(snapshot.items.filter((item) => indices.has(item.index)).map((item) => item.elementIndex));
595
677
  const updateProperty = (elementIndex, propertyKey, next) => {
596
- commit(updateArrayItemProperty(value, elementIndex, propertyKey, next, render), void 0, new Set(selection.selected));
678
+ const snapshot = latest();
679
+ const item = locate(snapshot, elementIndex);
680
+ if (item === null) return;
681
+ commit(snapshot, updateArrayItemProperty(snapshot.value, item?.elementIndex ?? elementIndex, propertyKey, next, render), void 0, new Set(snapshot.selected));
682
+ };
683
+ const updateValue = (elementIndex, next) => {
684
+ const snapshot = latest();
685
+ const item = locate(snapshot, elementIndex);
686
+ if (item === null) return;
687
+ commit(snapshot, updateArrayItemValue(snapshot.value, item?.elementIndex ?? elementIndex, next), void 0, new Set(snapshot.selected));
597
688
  };
598
689
  return {
599
690
  kind,
@@ -610,7 +701,7 @@ const useItemsEditor = (value, { render, onChange, onNodeChange } = {}) => {
610
701
  value: item.value ?? "",
611
702
  rawValue: item.type === "unknown" ? item.source : String(item.value ?? ""),
612
703
  canEditValue: canLosslesslyEvaluateSource(item.source),
613
- onChange: (next) => commit(updateArrayItemValue(value, item.elementIndex, next), void 0, new Set(selection.selected))
704
+ onChange: (next) => updateValue(item.elementIndex, next)
614
705
  }
615
706
  })) : objectItems.map((item) => {
616
707
  const id = identity.ids[item.index] ?? item.id;
@@ -666,41 +757,47 @@ const useItemsEditor = (value, { render, onChange, onNodeChange } = {}) => {
666
757
  selection,
667
758
  actions: {
668
759
  add: () => {
669
- const next = appendArrayItem(value, kind);
670
- commit(next, next ? [...identity.ids, nanoid(6)] : void 0, selectionAfter({ type: "append" }, selection.selected, rawItems.length));
760
+ const snapshot = latest();
761
+ const next = appendArrayItem(snapshot.value, kind);
762
+ commit(snapshot, next, next ? [...snapshot.ids, nanoid(6)] : void 0, selectionAfter({ type: "append" }, snapshot.selected, snapshot.items.length));
671
763
  },
672
764
  move: (elementIndex, toIndex) => {
673
- const items = isPrimitive ? primitiveItems : objectItems;
674
- const from = items.find((item) => item.elementIndex === elementIndex);
675
- const target = items.find((item) => item.index === toIndex);
765
+ const snapshot = latest();
766
+ const from = locate(snapshot, elementIndex);
767
+ const target = snapshot.items.find((item) => item.index === toIndex);
676
768
  if (!from || !target) return;
677
- commit(moveArrayItem(value, elementIndex, target.elementIndex), moveId(identity.ids, from.index, target.index), /* @__PURE__ */ new Set());
769
+ commit(snapshot, moveArrayItem(snapshot.value, from.elementIndex, target.elementIndex), moveId(snapshot.ids, from.index, target.index), /* @__PURE__ */ new Set());
678
770
  },
679
771
  remove: (elementIndex) => {
680
- const removed = (isPrimitive ? primitiveItems : objectItems).find((item) => item.elementIndex === elementIndex);
681
- commit(removeArrayItems(value, /* @__PURE__ */ new Set([elementIndex]), kind), removed ? removeIds(identity.ids, /* @__PURE__ */ new Set([removed.index])) : void 0, /* @__PURE__ */ new Set());
772
+ const snapshot = latest();
773
+ const removed = locate(snapshot, elementIndex);
774
+ if (removed === null) return;
775
+ commit(snapshot, removeArrayItems(snapshot.value, /* @__PURE__ */ new Set([removed?.elementIndex ?? elementIndex]), kind), removed ? removeIds(snapshot.ids, /* @__PURE__ */ new Set([removed.index])) : void 0, /* @__PURE__ */ new Set());
682
776
  },
683
777
  duplicateSelected: () => {
684
- const selected = elementIndicesOf(selection.selected);
685
- const next = duplicateArrayItems(value, selected);
686
- const copied = rawItems.filter((item) => selected.has(item.elementIndex)).sort((a, b) => a.elementIndex - b.elementIndex).map(() => nanoid(6));
687
- commit(next, next ? [...identity.ids, ...copied] : void 0, selectionAfter({
778
+ const snapshot = latest();
779
+ const selected = elementIndicesOf(snapshot, snapshot.selected);
780
+ const next = duplicateArrayItems(snapshot.value, selected);
781
+ const copied = snapshot.items.filter((item) => selected.has(item.elementIndex)).sort((a, b) => a.elementIndex - b.elementIndex).map(() => nanoid(6));
782
+ commit(snapshot, next, next ? [...snapshot.ids, ...copied] : void 0, selectionAfter({
688
783
  type: "duplicate",
689
- indices: [...selection.selected]
690
- }, selection.selected, rawItems.length));
784
+ indices: [...snapshot.selected]
785
+ }, snapshot.selected, snapshot.items.length));
691
786
  },
692
787
  moveSelected: (direction) => {
693
- const nextIdentity = moveSelectedIndices(identity.ids, selection.selected, direction);
694
- const result = moveArrayItems(value, elementIndicesOf(selection.selected), direction);
788
+ const snapshot = latest();
789
+ const nextIdentity = moveSelectedIndices(snapshot.ids, snapshot.selected, direction);
790
+ const result = moveArrayItems(snapshot.value, elementIndicesOf(snapshot, snapshot.selected), direction);
695
791
  if (!result) {
696
- commit(null, void 0, selection.selected);
792
+ commit(snapshot, null, void 0, snapshot.selected);
697
793
  return;
698
794
  }
699
795
  const visible = parseArrayExpression(result.code)?.elements.flatMap((node, index) => import_lib.isExpression(node) && import_lib.isObjectExpression(node) === !isPrimitive ? [index] : []) ?? [];
700
- commit(result.code, nextIdentity.items, new Set(visible.flatMap((position, index) => result.indices.has(position) ? [index] : [])));
796
+ commit(snapshot, result.code, nextIdentity.items, new Set(visible.flatMap((position, index) => result.indices.has(position) ? [index] : [])));
701
797
  },
702
798
  removeSelected: () => {
703
- commit(removeArrayItems(value, elementIndicesOf(selection.selected), kind), removeIds(identity.ids, selection.selected), /* @__PURE__ */ new Set());
799
+ const snapshot = latest();
800
+ commit(snapshot, removeArrayItems(snapshot.value, elementIndicesOf(snapshot, snapshot.selected), kind), removeIds(snapshot.ids, snapshot.selected), /* @__PURE__ */ new Set());
704
801
  }
705
802
  },
706
803
  canEditStructure,
@@ -766,7 +863,7 @@ const NestedGroup = ({ group, onNodeChange }) => {
766
863
  });
767
864
  };
768
865
  const Items = ({ value, render, onChange, onChildChange }) => {
769
- const { kind, items, selection, actions, canEditStructure, parseError } = useItemsEditor(value, {
866
+ const { kind, items, selection, actions, canEditStructure, parseError } = useDndItems(value, {
770
867
  render,
771
868
  onChange,
772
869
  onNodeChange: onChildChange
@@ -932,9 +1029,6 @@ const Items = ({ value, render, onChange, onChildChange }) => {
932
1029
  };
933
1030
  //#endregion
934
1031
  //#region src/components/dnd/panel/field.tsx
935
- const isColorProperty = (propertyName) => {
936
- return propertyName.toLowerCase().includes("color");
937
- };
938
1032
  const normalizeToHex = (value) => {
939
1033
  const trimmed = value.trim();
940
1034
  if (/^#([0-9A-Fa-f]{3}){1,2}$/.test(trimmed)) return trimmed;
@@ -1021,6 +1115,10 @@ const ColorPickerField = ({ value, onChange }) => {
1021
1115
  }
1022
1116
  });
1023
1117
  };
1118
+ const FieldError = ({ message }) => message ? /* @__PURE__ */ jsx("p", {
1119
+ className: "mt-1 mb-0 text-xs text-red-500",
1120
+ children: message
1121
+ }) : null;
1024
1122
  const BuiltinField = ({ binding, onNodeChange }) => {
1025
1123
  const { id, value, rawValue, onChange } = binding;
1026
1124
  const [validationError, setValidationError] = useState(null);
@@ -1030,7 +1128,8 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1030
1128
  setPrevRawValue(rawValue);
1031
1129
  setText(rawValue);
1032
1130
  }
1033
- if (binding.canEditValue === false) return /* @__PURE__ */ jsxs("div", {
1131
+ const kind = getFieldKind(binding);
1132
+ if (kind === "readonly") return /* @__PURE__ */ jsxs("div", {
1034
1133
  className: "space-y-1 rounded border border-dashed border-amber-200 p-3\n text-xs text-amber-700",
1035
1134
  children: [
1036
1135
  /* @__PURE__ */ jsx("div", { children: "This expression is preserved but is not editable here." }),
@@ -1044,31 +1143,28 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1044
1143
  const commitIfChanged = (next) => {
1045
1144
  if (next !== (typeof next === "string" ? rawValue : value)) onChange(next);
1046
1145
  };
1047
- if (binding.property === "items" || binding.property === "data" || binding.type === "array") return /* @__PURE__ */ jsx(Items, {
1146
+ if (kind === "items") return /* @__PURE__ */ jsx(Items, {
1048
1147
  value: rawValue,
1049
1148
  render: binding.render,
1050
1149
  onChange,
1051
1150
  onChildChange: onNodeChange
1052
1151
  });
1053
- if (binding.type === "richtext") return /* @__PURE__ */ jsx(RichTextEditor, {
1152
+ if (kind === "richtext") return /* @__PURE__ */ jsx(RichTextEditor, {
1054
1153
  value: rawValue,
1055
1154
  toolbar: RICHTEXT_TOOLBAR,
1056
1155
  onChange: commitIfChanged
1057
1156
  });
1058
- if (binding.property === BINDING_PROP.INNER_HTML || binding.type === "jsx") {
1059
- const isHTML = binding.property === BINDING_PROP.INNER_HTML;
1060
- return /* @__PURE__ */ jsx(JSXEditorField, {
1061
- value: rawValue,
1062
- isHTML,
1063
- onSave: commitIfChanged
1064
- });
1065
- }
1066
- if (binding.property === "children" && Array.isArray(value)) return /* @__PURE__ */ jsx(Children$1, {
1157
+ if (kind === "html" || kind === "jsx") return /* @__PURE__ */ jsx(JSXEditorField, {
1158
+ value: rawValue,
1159
+ isHTML: kind === "html",
1160
+ onSave: commitIfChanged
1161
+ });
1162
+ if (kind === "children") return /* @__PURE__ */ jsx(Children$1, {
1067
1163
  value,
1068
1164
  onChange,
1069
1165
  onNodeChange
1070
1166
  });
1071
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1167
+ if (kind === "object") {
1072
1168
  const objectValue = value;
1073
1169
  return /* @__PURE__ */ jsx("div", {
1074
1170
  className: "space-y-2 rounded border border-gray-200 bg-gray-50 p-2",
@@ -1098,23 +1194,23 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1098
1194
  })
1099
1195
  });
1100
1196
  }
1101
- if (binding.type === "boolean" || typeof value === "boolean") return /* @__PURE__ */ jsx(Checkbox, {
1197
+ if (kind === "boolean") return /* @__PURE__ */ jsx(Checkbox, {
1102
1198
  checked: value === true || value === "true",
1103
1199
  onChange: (checked) => {
1104
1200
  onChange(checked);
1105
1201
  }
1106
1202
  });
1107
1203
  const stringValue = rawValue;
1108
- if (binding.options && Array.isArray(binding.options)) return /* @__PURE__ */ jsx(Select, {
1204
+ if (kind === "select") return /* @__PURE__ */ jsx(Select, {
1109
1205
  value: stringValue,
1110
- options: binding.options,
1206
+ options: binding.options ?? [],
1111
1207
  onChange: commitIfChanged
1112
1208
  });
1113
- if (binding.type === "color" || isColorProperty(binding.property)) return /* @__PURE__ */ jsx(ColorPickerField, {
1209
+ if (kind === "color") return /* @__PURE__ */ jsx(ColorPickerField, {
1114
1210
  value: normalizeToHex(stringValue),
1115
1211
  onChange: commitIfChanged
1116
1212
  });
1117
- if (binding.type === "date") return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(DatePicker, {
1213
+ if (kind === "date") return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(DatePicker, {
1118
1214
  value: parseDateValue(stringValue),
1119
1215
  onChange: (date) => {
1120
1216
  const next = date ? formatDateValue(date) : "";
@@ -1126,11 +1222,8 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1126
1222
  setValidationError(null);
1127
1223
  commitIfChanged(next);
1128
1224
  }
1129
- }), validationError && /* @__PURE__ */ jsx("p", {
1130
- className: "mt-1 text-xs text-red-500",
1131
- children: validationError
1132
- })] });
1133
- if (binding.type === "url") return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Input, {
1225
+ }), /* @__PURE__ */ jsx(FieldError, { message: validationError })] });
1226
+ if (kind === "url") return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Input, {
1134
1227
  type: "url",
1135
1228
  value: text,
1136
1229
  onChange: (e) => setText(e.target.value),
@@ -1145,11 +1238,8 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1145
1238
  setValidationError(null);
1146
1239
  commitIfChanged(next);
1147
1240
  }
1148
- }), validationError && /* @__PURE__ */ jsx("p", {
1149
- className: "mt-1 text-xs text-red-500",
1150
- children: validationError
1151
- })] });
1152
- if (binding.type === "number" || typeof value === "number") return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Input, {
1241
+ }), /* @__PURE__ */ jsx(FieldError, { message: validationError })] });
1242
+ if (kind === "number") return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Input, {
1153
1243
  type: "number",
1154
1244
  value: text,
1155
1245
  onChange: (e) => setText(e.target.value),
@@ -1165,10 +1255,7 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1165
1255
  setValidationError(null);
1166
1256
  commitIfChanged(next);
1167
1257
  }
1168
- }), validationError && /* @__PURE__ */ jsx("p", {
1169
- className: "mt-1 text-xs text-red-500",
1170
- children: validationError
1171
- })] });
1258
+ }), /* @__PURE__ */ jsx(FieldError, { message: validationError })] });
1172
1259
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Input.TextArea, {
1173
1260
  value: text,
1174
1261
  onChange: (e) => setText(e.target.value),
@@ -1184,10 +1271,7 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1184
1271
  setValidationError(null);
1185
1272
  commitIfChanged(next);
1186
1273
  }
1187
- }), validationError && /* @__PURE__ */ jsx("p", {
1188
- className: "mt-1 text-xs text-red-500",
1189
- children: validationError
1190
- })] });
1274
+ }), /* @__PURE__ */ jsx(FieldError, { message: validationError })] });
1191
1275
  };
1192
1276
  const Field = (props) => {
1193
1277
  const { renderField } = useDndEditOptions();
@@ -1232,7 +1316,7 @@ const groupBindingsById = (bindings) => {
1232
1316
  }
1233
1317
  return [...groups.values()];
1234
1318
  };
1235
- const Panel$1 = ({ item, onDelete, onMoveUp, onMoveDown, canMoveUp = false, canMoveDown = false, bindings, onNodeChange }) => {
1319
+ const Panel$1 = ({ item, onDelete, onMoveUp, onMoveDown, canMoveUp = false, canMoveDown = false, bindings, onNodeChange, readOnly = false }) => {
1236
1320
  const groups = groupBindingsById(bindings);
1237
1321
  if (!item) return /* @__PURE__ */ jsx(Typography.Paragraph, {
1238
1322
  className: cn("p-4 text-sm text-gray-500"),
@@ -1270,6 +1354,11 @@ const Panel$1 = ({ item, onDelete, onMoveUp, onMoveDown, canMoveUp = false, canM
1270
1354
  ]
1271
1355
  })]
1272
1356
  }),
1357
+ readOnly && /* @__PURE__ */ jsx(Typography.Text, {
1358
+ role: "status",
1359
+ className: "block text-xs text-amber-700",
1360
+ children: "The document has a syntax error. These fields are from the last version that parsed, and can't be edited until it parses again."
1361
+ }),
1273
1362
  !groups.length && /* @__PURE__ */ jsx(Typography.Text, {
1274
1363
  className: "text-xs text-gray-400",
1275
1364
  children: "No editable elements."
@@ -1296,7 +1385,7 @@ const PaletteItems = ({ subject }) => {
1296
1385
  });
1297
1386
  };
1298
1387
  const PanelFields = ({ subject }) => {
1299
- const { panel: { item, onDelete, onMoveUp, onMoveDown, canMoveUp, canMoveDown, bindings, onNodeChange } } = useDndRegions(subject);
1388
+ const { panel: { item, onDelete, onMoveUp, onMoveDown, canMoveUp, canMoveDown, bindings, onNodeChange, readOnly } } = useDndRegions(subject);
1300
1389
  return /* @__PURE__ */ jsx(Panel$1, {
1301
1390
  item,
1302
1391
  onDelete,
@@ -1305,7 +1394,8 @@ const PanelFields = ({ subject }) => {
1305
1394
  canMoveUp,
1306
1395
  canMoveDown,
1307
1396
  bindings,
1308
- onNodeChange
1397
+ onNodeChange,
1398
+ readOnly
1309
1399
  });
1310
1400
  };
1311
1401
  const Palette = ({ className, ...restProps }) => /* @__PURE__ */ jsx("div", {
@@ -1492,8 +1582,14 @@ const arePropsEqual = (prev, next) => {
1492
1582
  var renderer_default = memo(Renderer, arePropsEqual);
1493
1583
  //#endregion
1494
1584
  //#region src/components/dnd/sortable.tsx
1495
- const Sortable = ({ id, children, selected, onClick, onDelete: _onDelete, onCopy: _onCopy }) => {
1496
- const { attributes, listeners, setNodeRef, transform, transition, isDragging, isOver, active } = useSortable({ id });
1585
+ const NAVIGATION_KEYS = {
1586
+ ArrowUp: "previous",
1587
+ ArrowDown: "next",
1588
+ Home: "first",
1589
+ End: "last"
1590
+ };
1591
+ const Sortable = ({ id, children, selected, onClick, onDelete: _onDelete, onCopy: _onCopy, nodeRef, onNavigate, onDeleteKey }) => {
1592
+ const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging, isOver, active } = useSortable({ id });
1497
1593
  const style = {
1498
1594
  transform: CSS.Transform.toString(transform),
1499
1595
  transition,
@@ -1501,6 +1597,30 @@ const Sortable = ({ id, children, selected, onClick, onDelete: _onDelete, onCopy
1501
1597
  cursor: "grab"
1502
1598
  };
1503
1599
  const isNewItemOver = isOver && active?.data.current?.type === "new-item";
1600
+ const onKeyDown = (e) => {
1601
+ listeners?.onKeyDown?.(e);
1602
+ if (e.target !== e.currentTarget || active || e.defaultPrevented) return;
1603
+ if (e.key === "Enter") {
1604
+ e.preventDefault();
1605
+ onClick?.();
1606
+ return;
1607
+ }
1608
+ const to = NAVIGATION_KEYS[e.key];
1609
+ if (to && onNavigate) {
1610
+ e.preventDefault();
1611
+ onNavigate(to);
1612
+ return;
1613
+ }
1614
+ if ((e.key === "Delete" || e.key === "Backspace") && onDeleteKey) {
1615
+ e.preventDefault();
1616
+ onDeleteKey();
1617
+ }
1618
+ };
1619
+ const setRefs = (node) => {
1620
+ setNodeRef(node);
1621
+ setActivatorNodeRef(node);
1622
+ nodeRef?.(node);
1623
+ };
1504
1624
  const onDelete = (e) => {
1505
1625
  e.stopPropagation();
1506
1626
  _onDelete?.(id);
@@ -1510,10 +1630,11 @@ const Sortable = ({ id, children, selected, onClick, onDelete: _onDelete, onCopy
1510
1630
  _onCopy?.(id);
1511
1631
  };
1512
1632
  return /* @__PURE__ */ jsxs("div", {
1513
- ref: setNodeRef,
1633
+ ref: setRefs,
1514
1634
  style,
1515
1635
  ...attributes,
1516
1636
  ...listeners,
1637
+ onKeyDown,
1517
1638
  onClick,
1518
1639
  className: cn("relative", selected && "z-10 outline-2 outline-offset-2 outline-blue-500", isNewItemOver && "border-t-4 border-t-green-500"),
1519
1640
  children: [
@@ -1544,7 +1665,7 @@ const Overlay = ({ sections, isForced, renderProps }) => {
1544
1665
  }
1545
1666
  const section = sections.find((s) => s.id === active.id);
1546
1667
  if (section) {
1547
- const preview = generateSection(section.code, renderProps.fullCode);
1668
+ const preview = generateSection(section.code, renderProps.fullCode, { containerId: renderProps.containerId });
1548
1669
  return /* @__PURE__ */ jsx(Sortable, {
1549
1670
  id: section.id,
1550
1671
  name: section.name,
@@ -1564,29 +1685,100 @@ const Overlay = ({ sections, isForced, renderProps }) => {
1564
1685
  };
1565
1686
  //#endregion
1566
1687
  //#region src/components/dnd/use-section-document.ts
1567
- const useSectionDocument = (value, onChange) => {
1688
+ const createLastParsed = () => {
1689
+ let last = null;
1690
+ return {
1691
+ read: (containerId) => last && last.containerId === containerId ? last.document : void 0,
1692
+ write: (containerId, document) => {
1693
+ last = {
1694
+ containerId,
1695
+ document
1696
+ };
1697
+ }
1698
+ };
1699
+ };
1700
+ const useSectionDocument = (value, onChange, { containerId, sectionNameFallback, onBlockedEdit } = {}) => {
1568
1701
  const { setCode } = usePreview();
1569
1702
  const [selectedId, setSelectedId] = useState(null);
1570
- const document = useMemo(() => fillSectionIds(value), [value]);
1571
- const sections = useMemo(() => extractSections(document), [document]);
1703
+ const readSections = useCallback((code) => extractSections(code, {
1704
+ containerId,
1705
+ sectionNameFallback
1706
+ }), [containerId, sectionNameFallback]);
1707
+ const document = useMemo(() => fillSectionIds(value, void 0, { containerId }), [containerId, value]);
1708
+ const [lastParsed] = useState(createLastParsed);
1709
+ const view = useMemo(() => {
1710
+ const inspection = inspectDocument(document, { containerId });
1711
+ if (inspection.ok) {
1712
+ lastParsed.write(containerId, document);
1713
+ return {
1714
+ document,
1715
+ problem: null,
1716
+ stale: false
1717
+ };
1718
+ }
1719
+ const { ok: _ok, ...problem } = inspection;
1720
+ const last = problem.reason === "parse-error" ? lastParsed.read(containerId) : void 0;
1721
+ return {
1722
+ document: last ?? document,
1723
+ problem,
1724
+ stale: last !== void 0
1725
+ };
1726
+ }, [
1727
+ containerId,
1728
+ document,
1729
+ lastParsed
1730
+ ]);
1731
+ const sections = useMemo(() => readSections(view.document), [readSections, view.document]);
1572
1732
  const [previewCache] = useState(() => createSectionPreviewCache());
1573
- const previews = useMemo(() => previewCache.compute(document, sections), [
1733
+ const previews = useMemo(() => previewCache.compute(view.document, sections, { containerId }), [
1734
+ containerId,
1574
1735
  previewCache,
1575
1736
  sections,
1576
- document
1737
+ view.document
1577
1738
  ]);
1578
1739
  const selectedIndex = sections.findIndex((s) => s.id === selectedId);
1579
1740
  const selectedItem = selectedIndex >= 0 ? sections[selectedIndex] : void 0;
1741
+ const pendingRef = useRef(null);
1742
+ useEffect(() => {
1743
+ pendingRef.current = null;
1744
+ });
1745
+ const latestDocument = useCallback(() => {
1746
+ const pending = pendingRef.current;
1747
+ return pending?.from === document ? pending.code : document;
1748
+ }, [document]);
1749
+ const latestSections = useCallback(() => readSections(latestDocument()), [latestDocument, readSections]);
1580
1750
  const commit = useCallback((nextSections) => {
1581
- const nextCode = fillSectionIds(replaceSections(document, nextSections.map((s) => s.code)));
1751
+ const nextCode = fillSectionIds(replaceSections(latestDocument(), nextSections.map((s) => s.code), { containerId }), void 0, { containerId });
1752
+ pendingRef.current = {
1753
+ from: document,
1754
+ code: nextCode
1755
+ };
1582
1756
  onChange?.(nextCode);
1583
1757
  setCode(nextCode);
1584
- return extractSections(nextCode);
1758
+ return readSections(nextCode);
1585
1759
  }, [
1760
+ containerId,
1586
1761
  document,
1762
+ latestDocument,
1587
1763
  onChange,
1764
+ readSections,
1588
1765
  setCode
1589
1766
  ]);
1767
+ const getCommittedSection = useCallback((id) => {
1768
+ const pending = pendingRef.current;
1769
+ if (pending?.from !== document) return;
1770
+ return readSections(pending.code).find((s) => s.id === id);
1771
+ }, [document, readSections]);
1772
+ const { problem, stale } = view;
1773
+ const refuse = useCallback(() => {
1774
+ if (!stale || !problem) return false;
1775
+ onBlockedEdit?.(problem);
1776
+ return true;
1777
+ }, [
1778
+ onBlockedEdit,
1779
+ problem,
1780
+ stale
1781
+ ]);
1590
1782
  return {
1591
1783
  sections,
1592
1784
  previews,
@@ -1596,54 +1788,91 @@ const useSectionDocument = (value, onChange) => {
1596
1788
  select: useCallback((id) => {
1597
1789
  setSelectedId((prev) => prev === id ? null : id);
1598
1790
  }, []),
1791
+ selectOnly: useCallback((id) => setSelectedId(id), []),
1599
1792
  clearSelection: useCallback(() => setSelectedId(null), []),
1600
1793
  add: useCallback((item, atIndex) => {
1794
+ if (refuse()) return;
1795
+ const current = latestSections();
1601
1796
  const next = { code: item.code };
1602
- commit(atIndex === void 0 || atIndex < 0 ? [...sections, next] : [
1603
- ...sections.slice(0, atIndex),
1797
+ commit(atIndex === void 0 || atIndex < 0 ? [...current, next] : [
1798
+ ...current.slice(0, atIndex),
1604
1799
  next,
1605
- ...sections.slice(atIndex)
1800
+ ...current.slice(atIndex)
1606
1801
  ]);
1607
- }, [commit, sections]),
1802
+ }, [
1803
+ commit,
1804
+ refuse,
1805
+ latestSections
1806
+ ]),
1608
1807
  remove: useCallback((id) => {
1609
- const index = sections.findIndex((s) => s.id === id);
1808
+ if (refuse()) return;
1809
+ const current = latestSections();
1810
+ const index = current.findIndex((s) => s.id === id);
1610
1811
  if (index < 0) return;
1611
1812
  if (id === selectedId) setSelectedId(null);
1612
- commit(sections.filter((_, i) => i !== index));
1813
+ commit(current.filter((_, i) => i !== index));
1613
1814
  }, [
1614
1815
  commit,
1615
- sections,
1816
+ refuse,
1817
+ latestSections,
1616
1818
  selectedId
1617
1819
  ]),
1618
1820
  copy: useCallback((id) => {
1619
- const index = sections.findIndex((s) => s.id === id);
1620
- const source = sections[index];
1821
+ if (refuse()) return;
1822
+ const current = latestSections();
1823
+ const index = current.findIndex((s) => s.id === id);
1824
+ const source = current[index];
1621
1825
  if (!source) return;
1622
1826
  const committed = commit([
1623
- ...sections.slice(0, index + 1),
1827
+ ...current.slice(0, index + 1),
1624
1828
  { code: replaceIds(source.code) },
1625
- ...sections.slice(index + 1)
1829
+ ...current.slice(index + 1)
1626
1830
  ]);
1627
1831
  setSelectedId(committed[index + 1]?.id ?? null);
1628
- }, [commit, sections]),
1832
+ }, [
1833
+ commit,
1834
+ refuse,
1835
+ latestSections
1836
+ ]),
1629
1837
  move: useCallback((id, direction) => {
1630
- const index = sections.findIndex((s) => s.id === id);
1838
+ if (refuse()) return;
1839
+ const current = latestSections();
1840
+ const index = current.findIndex((s) => s.id === id);
1631
1841
  const targetIndex = direction === "up" ? index - 1 : index + 1;
1632
- if (index < 0 || targetIndex < 0 || targetIndex >= sections.length) return;
1633
- commit(arrayMove(sections, index, targetIndex));
1634
- }, [commit, sections]),
1842
+ if (index < 0 || targetIndex < 0 || targetIndex >= current.length) return;
1843
+ commit(arrayMove(current, index, targetIndex));
1844
+ }, [
1845
+ commit,
1846
+ refuse,
1847
+ latestSections
1848
+ ]),
1635
1849
  reorder: useCallback((activeId, overId) => {
1636
- const prevIndex = sections.findIndex((s) => s.id === activeId);
1637
- const nextIndex = sections.findIndex((s) => s.id === overId);
1850
+ if (refuse()) return;
1851
+ const current = latestSections();
1852
+ const prevIndex = current.findIndex((s) => s.id === activeId);
1853
+ const nextIndex = current.findIndex((s) => s.id === overId);
1638
1854
  if (prevIndex < 0 || nextIndex < 0 || prevIndex === nextIndex) return;
1639
- commit(arrayMove(sections, prevIndex, nextIndex));
1640
- }, [commit, sections]),
1855
+ commit(arrayMove(current, prevIndex, nextIndex));
1856
+ }, [
1857
+ commit,
1858
+ refuse,
1859
+ latestSections
1860
+ ]),
1641
1861
  patch: useCallback((next) => {
1642
- commit(sections.map((s) => s.id === next.id ? {
1862
+ if (refuse()) return;
1863
+ const current = latestSections();
1864
+ commit(current.map((s) => s.id === next.id ? {
1643
1865
  ...s,
1644
1866
  ...next
1645
1867
  } : s));
1646
- }, [commit, sections])
1868
+ }, [
1869
+ commit,
1870
+ refuse,
1871
+ latestSections
1872
+ ]),
1873
+ getCommittedSection,
1874
+ problem,
1875
+ stale
1647
1876
  };
1648
1877
  };
1649
1878
  //#endregion
@@ -1662,6 +1891,14 @@ const describeUpdateFailure = (failure, label) => {
1662
1891
  title: `Failed to update "${label}"`,
1663
1892
  description: `${failure.count} bindings share ${failure.property ? `property "${failure.property}"` : `label "${label}"`} on this element — remove the duplicate in its data-binding.`
1664
1893
  };
1894
+ case "reserved-property": return {
1895
+ title: `Cannot edit "${label}" in the panel`,
1896
+ description: `"${failure.property}" is managed by the editor, so a binding can't change it.`
1897
+ };
1898
+ case "required-property": return {
1899
+ title: `Cannot remove "${label}"`,
1900
+ description: `"${failure.property}" is marked required in its data-binding, so it can't be removed.`
1901
+ };
1665
1902
  case "no-binding": return {
1666
1903
  title: `Failed to update "${label}"`,
1667
1904
  description: "This element has no data-binding declaration."
@@ -1681,13 +1918,49 @@ const describeUpdateFailure = (failure, label) => {
1681
1918
  default: return { title: `Failed to update "${label}"` };
1682
1919
  }
1683
1920
  };
1921
+ const KEYBOARD_CODES = {
1922
+ start: ["Space"],
1923
+ cancel: ["Escape"],
1924
+ end: ["Space", "Enter"]
1925
+ };
1926
+ const SCREEN_READER_INSTRUCTIONS = { draggable: "On a palette item, press Enter to add it to the canvas. On a canvas section, press Enter to select it, the up and down arrow keys to go to the previous or next section, and Delete to remove it. Press Space to pick a section up, the arrow keys to move it, and Space or Enter to drop it. Press Escape to cancel." };
1927
+ const createNodeRegistry = () => {
1928
+ const nodes = /* @__PURE__ */ new Map();
1929
+ const callbacks = /* @__PURE__ */ new Map();
1930
+ return {
1931
+ get: (id) => nodes.get(id),
1932
+ register: (id) => {
1933
+ let callback = callbacks.get(id);
1934
+ if (!callback) {
1935
+ callback = (node) => {
1936
+ if (node) nodes.set(id, node);
1937
+ else {
1938
+ nodes.delete(id);
1939
+ callbacks.delete(id);
1940
+ }
1941
+ };
1942
+ callbacks.set(id, callback);
1943
+ }
1944
+ return callback;
1945
+ }
1946
+ };
1947
+ };
1948
+ const blockedEditError = (error) => ({
1949
+ type: "parse",
1950
+ target: "document",
1951
+ reason: "parse-error",
1952
+ error,
1953
+ title: "The document has a syntax error",
1954
+ description: "The canvas shows the last version that parsed. Fix the error in the code, then edit here again."
1955
+ });
1684
1956
  const conditionalModifiers = (args) => {
1685
1957
  const { active } = args;
1686
1958
  if (active?.data.current?.type === "new-item") return args.transform;
1687
1959
  return restrictToVerticalAxis(args);
1688
1960
  };
1689
1961
  const NO_MODULES = {};
1690
- const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange, className, items = [], frame, dynamicTailwind = false, provider, renderField, onEditError, renderSectionFallback, shouldForceSectionFallback, children, ...restProps }) => {
1962
+ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange: _onChange, className, items = [], frame, dynamicTailwind = false, provider, renderField, onEditError, renderSectionFallback, shouldForceSectionFallback, onBeforeDelete, sectionNameFallback, containerId, children, ...restProps }) => {
1963
+ const modules = useStableModules(_modules);
1691
1964
  const [mobilePaletteOpen, setMobilePaletteOpen] = useState(false);
1692
1965
  const { breakpoint } = useResponsiveSize();
1693
1966
  const isMobile = breakpoint.current === "xs" || breakpoint.current === "sm";
@@ -1700,9 +1973,35 @@ const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange
1700
1973
  return false;
1701
1974
  }
1702
1975
  };
1703
- const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 10 } }));
1976
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 10 } }), useSensor(KeyboardSensor, {
1977
+ coordinateGetter: sortableKeyboardCoordinates,
1978
+ keyboardCodes: KEYBOARD_CODES
1979
+ }));
1980
+ const nameOf = (id, data) => {
1981
+ return (data?.current)?.item?.name ?? sections.find((section) => section.id === id)?.name ?? "section";
1982
+ };
1983
+ const positionOf = (id) => sections.findIndex((section) => section.id === id) + 1;
1984
+ const announcements = {
1985
+ onDragStart: ({ active }) => `Picked up ${nameOf(active.id, active.data)}.`,
1986
+ onDragOver: ({ active, over }) => over && positionOf(over.id) > 0 ? `${nameOf(active.id, active.data)} moved to position ${positionOf(over.id)} of ${sections.length}.` : `${nameOf(active.id, active.data)} is no longer over a position.`,
1987
+ onDragEnd: ({ active, over }) => over && positionOf(over.id) > 0 ? `${nameOf(active.id, active.data)} dropped at position ${positionOf(over.id)} of ${sections.length}.` : `${nameOf(active.id, active.data)} dropped.`,
1988
+ onDragCancel: ({ active }) => `Moving ${nameOf(active.id, active.data)} was cancelled.`
1989
+ };
1704
1990
  const value = _value === void 0 ? code : _value;
1705
- const { sections, previews, selectedId, selectedItem, selectedIndex, select, clearSelection, add: addItem, remove: onDelete, copy: onCopy, move: moveSection, reorder, patch } = useSectionDocument(value, _onChange);
1991
+ const reportError = onEditError ?? toastEditError;
1992
+ const reportErrorRef = useRef(reportError);
1993
+ useEffect(() => {
1994
+ reportErrorRef.current = reportError;
1995
+ });
1996
+ const onBlockedEdit = useCallback((problem) => {
1997
+ if (problem.reason === "parse-error") reportErrorRef.current(blockedEditError(problem.error));
1998
+ }, []);
1999
+ const { sections, previews, selectedId, selectedItem, selectedIndex, select, selectOnly, clearSelection, add: addItem, remove, copy: onCopy, move: moveSection, reorder, patch, getCommittedSection, problem, stale } = useSectionDocument(value, _onChange, {
2000
+ containerId,
2001
+ sectionNameFallback,
2002
+ onBlockedEdit
2003
+ });
2004
+ const missingContainer = problem?.reason === "container-not-found" ? problem.containerId : null;
1706
2005
  const onDragStart = (_) => {};
1707
2006
  const onDragOver = (_e) => {};
1708
2007
  const onDragEnd = (event) => {
@@ -1717,6 +2016,65 @@ const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange
1717
2016
  if (active.id !== over.id && sections.some((s) => s.id === active.id)) reorder(String(active.id), String(over.id));
1718
2017
  };
1719
2018
  const onSelect = (id) => select(id);
2019
+ const removeRef = useRef(remove);
2020
+ useEffect(() => {
2021
+ removeRef.current = remove;
2022
+ });
2023
+ const requestDelete = (id, onDeleted) => {
2024
+ const section = sections.find((candidate) => candidate.id === id);
2025
+ if (!onBeforeDelete || !section) {
2026
+ remove(id);
2027
+ onDeleted?.();
2028
+ return;
2029
+ }
2030
+ const decide = (allowed) => {
2031
+ if (allowed) {
2032
+ removeRef.current(id);
2033
+ onDeleted?.();
2034
+ }
2035
+ };
2036
+ try {
2037
+ const answer = onBeforeDelete(section);
2038
+ if (typeof answer === "boolean") {
2039
+ decide(answer);
2040
+ return;
2041
+ }
2042
+ answer.then(decide, (error) => {
2043
+ console.error("onBeforeDelete rejected; the section was kept", error);
2044
+ });
2045
+ } catch (error) {
2046
+ console.error("onBeforeDelete threw; the section was kept", error);
2047
+ }
2048
+ };
2049
+ const onDelete = (id) => requestDelete(id);
2050
+ const [sectionNodes] = useState(createNodeRegistry);
2051
+ const focusSection = (id) => sectionNodes.get(id)?.focus();
2052
+ const onNavigate = (fromId, to) => {
2053
+ const from = sections.findIndex((section) => section.id === fromId);
2054
+ const index = to === "first" ? 0 : to === "last" ? sections.length - 1 : from + (to === "next" ? 1 : -1);
2055
+ const target = sections[index];
2056
+ if (from < 0 || !target || target.id === fromId) return;
2057
+ selectOnly(target.id);
2058
+ focusSection(target.id);
2059
+ };
2060
+ const onDeleteKey = (id) => {
2061
+ const index = sections.findIndex((section) => section.id === id);
2062
+ const neighbor = sections[index + 1] ?? sections[index - 1];
2063
+ requestDelete(id, () => {
2064
+ if (neighbor) focusSection(neighbor.id);
2065
+ });
2066
+ };
2067
+ useEffect(() => {
2068
+ if (selectedId === null) return;
2069
+ const frame = requestAnimationFrame(() => {
2070
+ sectionNodes.get(selectedId)?.scrollIntoView?.({ block: "nearest" });
2071
+ });
2072
+ return () => cancelAnimationFrame(frame);
2073
+ }, [
2074
+ sectionNodes,
2075
+ selectedId,
2076
+ selectedIndex
2077
+ ]);
1720
2078
  const onChange = (next) => {
1721
2079
  if (!next.id) return;
1722
2080
  patch(next);
@@ -1731,7 +2089,7 @@ const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange
1731
2089
  try {
1732
2090
  const updated = fillIds(selectedCode);
1733
2091
  return {
1734
- fields: extract(updated).filter((node) => node.tagName !== "section"),
2092
+ fields: extract(updated),
1735
2093
  updatedCode: updated !== selectedCode ? updated : selectedCode,
1736
2094
  parseError: null
1737
2095
  };
@@ -1744,11 +2102,16 @@ const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange
1744
2102
  };
1745
2103
  }
1746
2104
  }, [selectedCode]);
1747
- const reportError = onEditError ?? toastEditError;
1748
- const reportErrorRef = useRef(reportError);
1749
2105
  useEffect(() => {
1750
- reportErrorRef.current = reportError;
1751
- });
2106
+ if (missingContainer !== null) reportErrorRef.current({
2107
+ type: "parse",
2108
+ target: "document",
2109
+ reason: "container-not-found",
2110
+ containerId: missingContainer,
2111
+ title: `No #${missingContainer} element in the document`,
2112
+ description: `Sections are the <section> elements inside the element with id="${missingContainer}". Start from createDocument(), or set containerId to match your document.`
2113
+ });
2114
+ }, [missingContainer]);
1752
2115
  useEffect(() => {
1753
2116
  if (parseError) reportErrorRef.current({
1754
2117
  type: "parse",
@@ -1758,9 +2121,21 @@ const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange
1758
2121
  description: "Check the console for details."
1759
2122
  });
1760
2123
  }, [parseError]);
1761
- const onFieldChange = ({ id, label, property, value: fieldValue }) => {
1762
- const result = update(updatedCode, id, label, fieldValue, property);
2124
+ const commitChanges = (changes) => {
2125
+ if (changes.length === 0) return;
2126
+ if (stale && problem?.reason === "parse-error") {
2127
+ reportError(blockedEditError(problem.error));
2128
+ return;
2129
+ }
2130
+ const committed = selectedItem && getCommittedSection(selectedItem.id)?.code;
2131
+ const result = updateAll(committed && committed !== selectedCode ? committed : updatedCode, changes.map(({ id, label, property, value: changeValue }) => ({
2132
+ dataId: id,
2133
+ label,
2134
+ property,
2135
+ value: changeValue
2136
+ })));
1763
2137
  if (!result.success) {
2138
+ const { id, label, property } = changes[result.index];
1764
2139
  reportError({
1765
2140
  type: "update",
1766
2141
  id,
@@ -1776,6 +2151,7 @@ const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange
1776
2151
  code: result.code
1777
2152
  });
1778
2153
  };
2154
+ const onFieldChange = (change) => commitChanges([change]);
1779
2155
  const bindingFields = useMemo(() => fields.flatMap((node) => resolvePanelBindings(node)?.bindings ?? []), [fields]);
1780
2156
  const bindings = withPanelCommit(bindingFields, onFieldChange);
1781
2157
  useEffect(() => {
@@ -1797,13 +2173,35 @@ const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange
1797
2173
  canMoveUp: selectedIndex > 0,
1798
2174
  canMoveDown: selectedIndex >= 0 && selectedIndex < sections.length - 1,
1799
2175
  bindings,
1800
- onNodeChange: onFieldChange
2176
+ onNodeChange: onFieldChange,
2177
+ onNodesChange: commitChanges,
2178
+ readOnly: stale
1801
2179
  };
1802
- const canvas = /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(Droppable, {
2180
+ const canvas = /* @__PURE__ */ jsxs(Fragment, { children: [stale && /* @__PURE__ */ jsx("div", {
2181
+ role: "status",
2182
+ className: cn("sticky top-0 z-70", "border-b border-amber-200 bg-amber-50 px-3 py-2", "text-sm text-amber-900"),
2183
+ children: "Showing the last version that parsed. Fix the syntax error in the code to edit here again."
2184
+ }), /* @__PURE__ */ jsx(Droppable, {
1803
2185
  className: cn(!sections.length && "h-full"),
1804
2186
  children: !sections.length ? /* @__PURE__ */ jsx("div", {
1805
2187
  className: cn("flex items-center justify-center", "h-full", "text-gray-500"),
1806
- children: /* @__PURE__ */ jsxs(Space, {
2188
+ children: problem?.reason === "parse-error" ? /* @__PURE__ */ jsxs(Space, {
2189
+ orientation: "vertical",
2190
+ align: "center",
2191
+ children: [/* @__PURE__ */ jsx(Typography.Paragraph, { children: "The document has a syntax error" }), /* @__PURE__ */ jsx(Typography.Text, { children: "Fix it in the code to see its sections" })]
2192
+ }) : missingContainer !== null ? /* @__PURE__ */ jsxs(Space, {
2193
+ orientation: "vertical",
2194
+ align: "center",
2195
+ children: [/* @__PURE__ */ jsxs(Typography.Paragraph, { children: [
2196
+ "No #",
2197
+ missingContainer,
2198
+ " element in the document"
2199
+ ] }), /* @__PURE__ */ jsxs(Typography.Text, { children: [
2200
+ "Sections go inside the element with id=\"",
2201
+ missingContainer,
2202
+ "\""
2203
+ ] })]
2204
+ }) : /* @__PURE__ */ jsxs(Space, {
1807
2205
  orientation: "vertical",
1808
2206
  align: "center",
1809
2207
  children: [/* @__PURE__ */ jsx(Typography.Paragraph, { children: "No sections available" }), /* @__PURE__ */ jsx(Typography.Text, { children: isMobile ? "Tap a component to add it" : "Drag a component from the left to add it" })]
@@ -1818,6 +2216,9 @@ const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange
1818
2216
  onClick: () => onSelect(section.id),
1819
2217
  onDelete,
1820
2218
  onCopy,
2219
+ nodeRef: sectionNodes.register(section.id),
2220
+ onNavigate: (to) => onNavigate(section.id, to),
2221
+ onDeleteKey: () => onDeleteKey(section.id),
1821
2222
  children: /* @__PURE__ */ jsx(renderer_default, {
1822
2223
  preview: previews[index],
1823
2224
  sectionId: section.id,
@@ -1832,12 +2233,16 @@ const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange
1832
2233
  })
1833
2234
  }, section.id))
1834
2235
  })
1835
- }) });
2236
+ })] });
1836
2237
  return /* @__PURE__ */ jsx(SectionFallbackContext.Provider, {
1837
2238
  value: renderSectionFallback,
1838
2239
  children: /* @__PURE__ */ jsxs(DndContext, {
1839
2240
  sensors,
1840
2241
  collisionDetection: closestCenter,
2242
+ accessibility: {
2243
+ announcements,
2244
+ screenReaderInstructions: SCREEN_READER_INSTRUCTIONS
2245
+ },
1841
2246
  modifiers: [conditionalModifiers],
1842
2247
  onDragStart,
1843
2248
  onDragOver,
@@ -1859,7 +2264,8 @@ const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange
1859
2264
  selectedId,
1860
2265
  clearSelection,
1861
2266
  paletteOpen: mobilePaletteOpen,
1862
- setPaletteOpen: setMobilePaletteOpen
2267
+ setPaletteOpen: setMobilePaletteOpen,
2268
+ documentError: problem?.reason ?? null
1863
2269
  },
1864
2270
  children: Children.toArray(children).length ? children : /* @__PURE__ */ jsx(Layout, {})
1865
2271
  })
@@ -1872,7 +2278,8 @@ const Dnd$1 = ({ value: _value, props, modules = NO_MODULES, onChange: _onChange
1872
2278
  modules,
1873
2279
  frame,
1874
2280
  dynamicTailwind,
1875
- ...props
2281
+ ...props,
2282
+ containerId
1876
2283
  }
1877
2284
  }) })]
1878
2285
  })
@@ -1887,7 +2294,11 @@ Dnd.Palette = Palette;
1887
2294
  Dnd.Canvas = Canvas;
1888
2295
  Dnd.Panel = Panel;
1889
2296
  Dnd.Layout = Layout;
2297
+ /** @deprecated Renamed to `useDndItems`. */
2298
+ const useItemsEditor = useDndItems;
2299
+ /** @deprecated Renamed to `useDndChildren`. */
2300
+ const useChildrenEditor = useDndChildren;
1890
2301
  //#endregion
1891
- export { Panel as a, useChildrenEditor as c, useDndPanel as d, DraggableItem as f, Palette as i, useDndLayout as l, Canvas as n, Field as o, Layout as r, useItemsEditor as s, Dnd as t, useDndPalette as u };
2302
+ export { useDndPanel as _, Layout as a, Field as c, isStructuralFieldKind as d, useDndChildren as f, useDndPalette as g, useDndLayout as h, Canvas as i, useDndItems as l, withPanelCommit as m, useChildrenEditor as n, Palette as o, resolvePanelBindings as p, useItemsEditor as r, Panel as s, Dnd as t, getFieldKind as u, DraggableItem as v };
1892
2303
 
1893
- //# sourceMappingURL=dnd-D_rG92-R.js.map
2304
+ //# sourceMappingURL=dnd-DBaH2L8N.js.map