@jbpark/live-editor 4.1.1 → 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 (50) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/{ast-jab6hQrT.js → ast-CMD20ykx.js} +94 -25
  4. package/dist/ast-CMD20ykx.js.map +1 -0
  5. package/dist/{compile-CBOAtQMz.js → compile-0Dvp8zp5.js} +37 -5
  6. package/dist/compile-0Dvp8zp5.js.map +1 -0
  7. package/dist/{constants-BD3FZKWE.js → constants-CKvdqZtO.js} +11 -4
  8. package/dist/constants-CKvdqZtO.js.map +1 -0
  9. package/dist/{context-DQ-UtoyS.js → context-93X3gyfJ.js} +2 -2
  10. package/dist/{context-DQ-UtoyS.js.map → context-93X3gyfJ.js.map} +1 -1
  11. package/dist/{core-DaXVtvH5.js → core-B3ruccKb.js} +2 -2
  12. package/dist/{core-DaXVtvH5.js.map → core-B3ruccKb.js.map} +1 -1
  13. package/dist/{detect-typescript-b0A751v2.js → detect-typescript-5MV4mCM7.js} +2 -2
  14. package/dist/{detect-typescript-b0A751v2.js.map → detect-typescript-5MV4mCM7.js.map} +1 -1
  15. package/dist/dnd/index.d.ts +2 -2
  16. package/dist/dnd/index.js +2 -2
  17. package/dist/{dnd-CMrXfD8O.js → dnd-DBaH2L8N.js} +545 -138
  18. package/dist/dnd-DBaH2L8N.js.map +1 -0
  19. package/dist/{document-C5Dypv4_.js → document-C3eCRQlj.js} +71 -38
  20. package/dist/document-C3eCRQlj.js.map +1 -0
  21. package/dist/{document-J9Lo9p81.d.ts → document-Ct4jW78J.d.ts} +42 -22
  22. package/dist/editor/index.js +2 -2
  23. package/dist/{editor-C3tQaMlU.js → editor-DvMruljw.js} +2 -2
  24. package/dist/{editor-C3tQaMlU.js.map → editor-DvMruljw.js.map} +1 -1
  25. package/dist/{index-C4t0OawE.d.ts → index-DVgwXnPa.d.ts} +109 -34
  26. package/dist/{index-B_p74BRM.d.ts → index-w3kLcCXl.d.ts} +29 -5
  27. package/dist/index.d.ts +4 -4
  28. package/dist/index.js +4 -4
  29. package/dist/index.js.map +1 -1
  30. package/dist/preview/index.js +2 -2
  31. package/dist/{preview-BwEfltgi.js → preview-B9Jhv1Up.js} +2 -2
  32. package/dist/{preview-BwEfltgi.js.map → preview-B9Jhv1Up.js.map} +1 -1
  33. package/dist/provider/index.js +1 -1
  34. package/dist/sections-A4TAXUP8.js +24 -0
  35. package/dist/sections-A4TAXUP8.js.map +1 -0
  36. package/dist/{use-dynamic-tailwind-DuGLeDR3.js → use-dynamic-tailwind-BDfytfYC.js} +16 -7
  37. package/dist/use-dynamic-tailwind-BDfytfYC.js.map +1 -0
  38. package/dist/utils/ast/index.d.ts +3 -3
  39. package/dist/utils/ast/index.js +3 -3
  40. package/dist/utils/index.d.ts +7 -6
  41. package/dist/utils/index.js +5 -5
  42. package/package.json +1 -1
  43. package/dist/ast-jab6hQrT.js.map +0 -1
  44. package/dist/compile-CBOAtQMz.js.map +0 -1
  45. package/dist/constants-BD3FZKWE.js.map +0 -1
  46. package/dist/dnd-CMrXfD8O.js.map +0 -1
  47. package/dist/document-C5Dypv4_.js.map +0 -1
  48. package/dist/sections-B7dWoNVJ.js +0 -19
  49. package/dist/sections-B7dWoNVJ.js.map +0 -1
  50. package/dist/use-dynamic-tailwind-DuGLeDR3.js.map +0 -1
@@ -1,18 +1,18 @@
1
- import { d as nanoid, h as __toESM, n as createSectionPreviewCache, p as require_lib, r as fillSectionIds } from "./document-C5Dypv4_.js";
2
- import { a as DRAGGABLE_ITEMS, t as BINDING_PROP } from "./constants-BD3FZKWE.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 { C as parseBinding, E as canLosslesslyEvaluateSource, M as parseValue, O as extractNodeValue, S as getStructuredValue, _ as updateArrayItemValue, b as findEditableChildren, d as duplicateArrayItems, f as moveArrayItem, g as updateArrayItemProperty, h as removeArrayItems, i as replaceIds, j as parseArrayExpression, k as extractObjectProperties, l as extract, o as update, p as moveArrayItems, r as fillIds, s as getChildrenSignatures, t as validateBindingValue, u as appendArrayItem, v as canStructurallyEditArray, x as getCurrentValue, y as moveSelectedIndices } from "./ast-jab6hQrT.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
5
  import { t as cn } from "./cn-CAnad2vL.js";
6
- import { o as preloadScripts } from "./compile-CBOAtQMz.js";
7
- import { a as Frame, i as useStableModules, n as useCompiledModule, t as useDynamicTailwind } from "./use-dynamic-tailwind-DuGLeDR3.js";
8
- import { t as Core } from "./core-DaXVtvH5.js";
9
- import { i as replaceSections, n as generateSection, t as extractSections } from "./sections-B7dWoNVJ.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
10
  import { n as Error$1, t as ErrorBoundary } from "./boundary-D2IFja-3.js";
11
11
  import { Children, createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
12
12
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
- 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";
14
14
  import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
15
- import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
15
+ import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
16
16
  import { Button, Card, Checkbox, ColorPicker, DatePicker, Drawer, Input, RichTextEditor, Select, Space, Splitter, Toast, Typography } from "@jbpark/ui-kit";
17
17
  import { useDebounce, useMultiSelect, useResponsiveSize } from "@jbpark/use-hooks";
18
18
  import { ArrowDown, ArrowUp, ChevronDown, ChevronUp, Copy, LayoutGrid, Plus, Trash, X } from "lucide-react";
@@ -70,9 +70,16 @@ const DefaultDraggableItem = ({ item, onAdd, tapToAdd = false }) => /* @__PURE__
70
70
  ref,
71
71
  style: { opacity: isDragging ? .5 : 1 },
72
72
  ...dragProps,
73
- 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"),
74
74
  onClick: tapToAdd && onAdd ? () => onAdd(item) : void 0,
75
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
+ },
76
83
  children: item.name
77
84
  })
78
85
  });
@@ -91,9 +98,11 @@ const useDndLayout = () => useDndRegions("useDndLayout()");
91
98
  //#region src/components/dnd/panel-binding.ts
92
99
  const readAttribute = (node, name) => node.dataAttributes.find((attribute) => attribute.name === name)?.value;
93
100
  const canEditBindingValue = (node, binding) => {
101
+ if (RESERVED_BINDING_PROPERTIES.includes(binding.property)) return false;
94
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;
95
103
  const attribute = node.attributes.find((candidate) => candidate.name === binding.property);
96
- return attribute?.isStringLiteral === true || canLosslesslyEvaluateSource(attribute?.value ?? "");
104
+ if (!attribute) return true;
105
+ return attribute.isStringLiteral === true || canLosslesslyEvaluateSource(attribute?.value ?? "");
97
106
  };
98
107
  const toBindingFields = (binding) => ({
99
108
  label: binding.label,
@@ -124,12 +133,19 @@ const resolveRenderEntry = (render, key) => {
124
133
  render: entry
125
134
  };
126
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);
127
142
  const toPanelBindingData = (node, id, binding) => ({
128
143
  ...toBindingFields(binding),
129
144
  id,
130
145
  value: getStructuredValue(node, binding.property, binding.type),
131
146
  rawValue: getCurrentValue(node, binding.property),
132
- ...!canEditBindingValue(node, binding) && { canEditValue: false }
147
+ ...!canEditBindingValue(node, binding) && { canEditValue: false },
148
+ ...isAttributeAbsent(node, binding) && { present: false }
133
149
  });
134
150
  const resolvePanelBindings = (node) => {
135
151
  const id = readAttribute(node, "data-id");
@@ -266,7 +282,7 @@ const useStructuralSelection = (count, revision) => {
266
282
  };
267
283
  };
268
284
  //#endregion
269
- //#region src/components/dnd/panel/use-children-editor.ts
285
+ //#region src/components/dnd/panel/use-dnd-children.ts
270
286
  const producedBy = (expected, action) => {
271
287
  if (action.type === "move-selected") {
272
288
  const moved = JSON.stringify(moveSelectedIndices(expected, new Set(action.indices), action.direction).items);
@@ -278,7 +294,7 @@ const producedBy = (expected, action) => {
278
294
  return next.length === expected.length + added && expected.every((signature, index) => next[index] === signature);
279
295
  };
280
296
  };
281
- const useChildrenEditor = (items, { onChange } = {}) => {
297
+ const useDndChildren = (items, { onChange } = {}) => {
282
298
  const expected = useMemo(() => getChildrenSignatures(items), [items]);
283
299
  const revision = JSON.stringify(expected);
284
300
  const { selection, record } = useStructuralSelection(items.length, revision);
@@ -326,7 +342,7 @@ const useChildrenEditor = (items, { onChange } = {}) => {
326
342
  //#region src/components/dnd/panel/children.tsx
327
343
  const Children$1 = ({ value, onChange, onNodeChange }) => {
328
344
  const items = useMemo(() => Array.isArray(value) ? value : [], [value]);
329
- const { selection, actions } = useChildrenEditor(items, { onChange });
345
+ const { selection, actions } = useDndChildren(items, { onChange });
330
346
  const editableChildrenMap = useMemo(() => {
331
347
  const map = /* @__PURE__ */ new Map();
332
348
  items.forEach((item, index) => {
@@ -450,7 +466,40 @@ const Children$1 = ({ value, onChange, onNodeChange }) => {
450
466
  });
451
467
  };
452
468
  //#endregion
453
- //#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
+ };
454
503
  const bindingsInJSX = (source) => {
455
504
  const nodes = extract(source);
456
505
  const container = nodes.find((n) => n.bindings?.some((b) => b.property === "children"));
@@ -548,7 +597,7 @@ const moveId = (ids, from, to) => {
548
597
  const removeIds = (ids, indices) => {
549
598
  return ids.filter((_, index) => !indices.has(index));
550
599
  };
551
- const useItemsEditor = (value, { render, onChange, onNodeChange } = {}) => {
600
+ const useDndItems = (value, { render, onChange, onNodeChange } = {}) => {
552
601
  const { objectItems, primitiveItems, parseError } = useMemo(() => parseSource(value), [value]);
553
602
  const canEditStructure = useMemo(() => canStructurallyEditArray(value), [value]);
554
603
  const { reportError } = useDndEditOptions();
@@ -575,7 +624,33 @@ const useItemsEditor = (value, { render, onChange, onNodeChange } = {}) => {
575
624
  value
576
625
  ]);
577
626
  const { selection, record } = useStructuralSelection(isPrimitive ? primitiveItems.length : objectItems.length, value);
578
- 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) => {
579
654
  if (next === null) {
580
655
  reportError({
581
656
  type: "items",
@@ -586,17 +661,30 @@ const useItemsEditor = (value, { render, onChange, onNodeChange } = {}) => {
586
661
  }
587
662
  const parsed = parseSource(next);
588
663
  const nextItems = kind === "primitive" ? parsed.primitiveItems : parsed.objectItems;
589
- if (nextIds) setIdentityState(createIdentityState(next, kind, nextItems, nextIds));
590
- 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));
591
672
  record(nextSelection, (revision) => revision === next, { now: true });
592
673
  onChange?.(next);
593
674
  return true;
594
675
  };
595
- const elementIndicesOf = (indices) => {
596
- return new Set((isPrimitive ? primitiveItems : objectItems).filter((item) => indices.has(item.index)).map((item) => item.elementIndex));
597
- };
676
+ const elementIndicesOf = (snapshot, indices) => new Set(snapshot.items.filter((item) => indices.has(item.index)).map((item) => item.elementIndex));
598
677
  const updateProperty = (elementIndex, propertyKey, next) => {
599
- 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));
600
688
  };
601
689
  return {
602
690
  kind,
@@ -613,7 +701,7 @@ const useItemsEditor = (value, { render, onChange, onNodeChange } = {}) => {
613
701
  value: item.value ?? "",
614
702
  rawValue: item.type === "unknown" ? item.source : String(item.value ?? ""),
615
703
  canEditValue: canLosslesslyEvaluateSource(item.source),
616
- onChange: (next) => commit(updateArrayItemValue(value, item.elementIndex, next), void 0, new Set(selection.selected))
704
+ onChange: (next) => updateValue(item.elementIndex, next)
617
705
  }
618
706
  })) : objectItems.map((item) => {
619
707
  const id = identity.ids[item.index] ?? item.id;
@@ -669,41 +757,47 @@ const useItemsEditor = (value, { render, onChange, onNodeChange } = {}) => {
669
757
  selection,
670
758
  actions: {
671
759
  add: () => {
672
- const next = appendArrayItem(value, kind);
673
- 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));
674
763
  },
675
764
  move: (elementIndex, toIndex) => {
676
- const items = isPrimitive ? primitiveItems : objectItems;
677
- const from = items.find((item) => item.elementIndex === elementIndex);
678
- 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);
679
768
  if (!from || !target) return;
680
- 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());
681
770
  },
682
771
  remove: (elementIndex) => {
683
- const removed = (isPrimitive ? primitiveItems : objectItems).find((item) => item.elementIndex === elementIndex);
684
- 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());
685
776
  },
686
777
  duplicateSelected: () => {
687
- const selected = elementIndicesOf(selection.selected);
688
- const next = duplicateArrayItems(value, selected);
689
- const copied = rawItems.filter((item) => selected.has(item.elementIndex)).sort((a, b) => a.elementIndex - b.elementIndex).map(() => nanoid(6));
690
- 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({
691
783
  type: "duplicate",
692
- indices: [...selection.selected]
693
- }, selection.selected, rawItems.length));
784
+ indices: [...snapshot.selected]
785
+ }, snapshot.selected, snapshot.items.length));
694
786
  },
695
787
  moveSelected: (direction) => {
696
- const nextIdentity = moveSelectedIndices(identity.ids, selection.selected, direction);
697
- 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);
698
791
  if (!result) {
699
- commit(null, void 0, selection.selected);
792
+ commit(snapshot, null, void 0, snapshot.selected);
700
793
  return;
701
794
  }
702
795
  const visible = parseArrayExpression(result.code)?.elements.flatMap((node, index) => import_lib.isExpression(node) && import_lib.isObjectExpression(node) === !isPrimitive ? [index] : []) ?? [];
703
- 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] : [])));
704
797
  },
705
798
  removeSelected: () => {
706
- 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());
707
801
  }
708
802
  },
709
803
  canEditStructure,
@@ -769,7 +863,7 @@ const NestedGroup = ({ group, onNodeChange }) => {
769
863
  });
770
864
  };
771
865
  const Items = ({ value, render, onChange, onChildChange }) => {
772
- const { kind, items, selection, actions, canEditStructure, parseError } = useItemsEditor(value, {
866
+ const { kind, items, selection, actions, canEditStructure, parseError } = useDndItems(value, {
773
867
  render,
774
868
  onChange,
775
869
  onNodeChange: onChildChange
@@ -935,9 +1029,6 @@ const Items = ({ value, render, onChange, onChildChange }) => {
935
1029
  };
936
1030
  //#endregion
937
1031
  //#region src/components/dnd/panel/field.tsx
938
- const isColorProperty = (propertyName) => {
939
- return propertyName.toLowerCase().includes("color");
940
- };
941
1032
  const normalizeToHex = (value) => {
942
1033
  const trimmed = value.trim();
943
1034
  if (/^#([0-9A-Fa-f]{3}){1,2}$/.test(trimmed)) return trimmed;
@@ -1024,6 +1115,10 @@ const ColorPickerField = ({ value, onChange }) => {
1024
1115
  }
1025
1116
  });
1026
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;
1027
1122
  const BuiltinField = ({ binding, onNodeChange }) => {
1028
1123
  const { id, value, rawValue, onChange } = binding;
1029
1124
  const [validationError, setValidationError] = useState(null);
@@ -1033,7 +1128,8 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1033
1128
  setPrevRawValue(rawValue);
1034
1129
  setText(rawValue);
1035
1130
  }
1036
- if (binding.canEditValue === false) return /* @__PURE__ */ jsxs("div", {
1131
+ const kind = getFieldKind(binding);
1132
+ if (kind === "readonly") return /* @__PURE__ */ jsxs("div", {
1037
1133
  className: "space-y-1 rounded border border-dashed border-amber-200 p-3\n text-xs text-amber-700",
1038
1134
  children: [
1039
1135
  /* @__PURE__ */ jsx("div", { children: "This expression is preserved but is not editable here." }),
@@ -1047,31 +1143,28 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1047
1143
  const commitIfChanged = (next) => {
1048
1144
  if (next !== (typeof next === "string" ? rawValue : value)) onChange(next);
1049
1145
  };
1050
- if (binding.property === "items" || binding.property === "data" || binding.type === "array") return /* @__PURE__ */ jsx(Items, {
1146
+ if (kind === "items") return /* @__PURE__ */ jsx(Items, {
1051
1147
  value: rawValue,
1052
1148
  render: binding.render,
1053
1149
  onChange,
1054
1150
  onChildChange: onNodeChange
1055
1151
  });
1056
- if (binding.type === "richtext") return /* @__PURE__ */ jsx(RichTextEditor, {
1152
+ if (kind === "richtext") return /* @__PURE__ */ jsx(RichTextEditor, {
1057
1153
  value: rawValue,
1058
1154
  toolbar: RICHTEXT_TOOLBAR,
1059
1155
  onChange: commitIfChanged
1060
1156
  });
1061
- if (binding.property === BINDING_PROP.INNER_HTML || binding.type === "jsx") {
1062
- const isHTML = binding.property === BINDING_PROP.INNER_HTML;
1063
- return /* @__PURE__ */ jsx(JSXEditorField, {
1064
- value: rawValue,
1065
- isHTML,
1066
- onSave: commitIfChanged
1067
- });
1068
- }
1069
- 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, {
1070
1163
  value,
1071
1164
  onChange,
1072
1165
  onNodeChange
1073
1166
  });
1074
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1167
+ if (kind === "object") {
1075
1168
  const objectValue = value;
1076
1169
  return /* @__PURE__ */ jsx("div", {
1077
1170
  className: "space-y-2 rounded border border-gray-200 bg-gray-50 p-2",
@@ -1101,23 +1194,23 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1101
1194
  })
1102
1195
  });
1103
1196
  }
1104
- if (binding.type === "boolean" || typeof value === "boolean") return /* @__PURE__ */ jsx(Checkbox, {
1197
+ if (kind === "boolean") return /* @__PURE__ */ jsx(Checkbox, {
1105
1198
  checked: value === true || value === "true",
1106
1199
  onChange: (checked) => {
1107
1200
  onChange(checked);
1108
1201
  }
1109
1202
  });
1110
1203
  const stringValue = rawValue;
1111
- if (binding.options && Array.isArray(binding.options)) return /* @__PURE__ */ jsx(Select, {
1204
+ if (kind === "select") return /* @__PURE__ */ jsx(Select, {
1112
1205
  value: stringValue,
1113
- options: binding.options,
1206
+ options: binding.options ?? [],
1114
1207
  onChange: commitIfChanged
1115
1208
  });
1116
- if (binding.type === "color" || isColorProperty(binding.property)) return /* @__PURE__ */ jsx(ColorPickerField, {
1209
+ if (kind === "color") return /* @__PURE__ */ jsx(ColorPickerField, {
1117
1210
  value: normalizeToHex(stringValue),
1118
1211
  onChange: commitIfChanged
1119
1212
  });
1120
- if (binding.type === "date") return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(DatePicker, {
1213
+ if (kind === "date") return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(DatePicker, {
1121
1214
  value: parseDateValue(stringValue),
1122
1215
  onChange: (date) => {
1123
1216
  const next = date ? formatDateValue(date) : "";
@@ -1129,11 +1222,8 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1129
1222
  setValidationError(null);
1130
1223
  commitIfChanged(next);
1131
1224
  }
1132
- }), validationError && /* @__PURE__ */ jsx("p", {
1133
- className: "mt-1 text-xs text-red-500",
1134
- children: validationError
1135
- })] });
1136
- 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, {
1137
1227
  type: "url",
1138
1228
  value: text,
1139
1229
  onChange: (e) => setText(e.target.value),
@@ -1148,11 +1238,8 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1148
1238
  setValidationError(null);
1149
1239
  commitIfChanged(next);
1150
1240
  }
1151
- }), validationError && /* @__PURE__ */ jsx("p", {
1152
- className: "mt-1 text-xs text-red-500",
1153
- children: validationError
1154
- })] });
1155
- 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, {
1156
1243
  type: "number",
1157
1244
  value: text,
1158
1245
  onChange: (e) => setText(e.target.value),
@@ -1168,10 +1255,7 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1168
1255
  setValidationError(null);
1169
1256
  commitIfChanged(next);
1170
1257
  }
1171
- }), validationError && /* @__PURE__ */ jsx("p", {
1172
- className: "mt-1 text-xs text-red-500",
1173
- children: validationError
1174
- })] });
1258
+ }), /* @__PURE__ */ jsx(FieldError, { message: validationError })] });
1175
1259
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Input.TextArea, {
1176
1260
  value: text,
1177
1261
  onChange: (e) => setText(e.target.value),
@@ -1187,10 +1271,7 @@ const BuiltinField = ({ binding, onNodeChange }) => {
1187
1271
  setValidationError(null);
1188
1272
  commitIfChanged(next);
1189
1273
  }
1190
- }), validationError && /* @__PURE__ */ jsx("p", {
1191
- className: "mt-1 text-xs text-red-500",
1192
- children: validationError
1193
- })] });
1274
+ }), /* @__PURE__ */ jsx(FieldError, { message: validationError })] });
1194
1275
  };
1195
1276
  const Field = (props) => {
1196
1277
  const { renderField } = useDndEditOptions();
@@ -1235,7 +1316,7 @@ const groupBindingsById = (bindings) => {
1235
1316
  }
1236
1317
  return [...groups.values()];
1237
1318
  };
1238
- 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 }) => {
1239
1320
  const groups = groupBindingsById(bindings);
1240
1321
  if (!item) return /* @__PURE__ */ jsx(Typography.Paragraph, {
1241
1322
  className: cn("p-4 text-sm text-gray-500"),
@@ -1273,6 +1354,11 @@ const Panel$1 = ({ item, onDelete, onMoveUp, onMoveDown, canMoveUp = false, canM
1273
1354
  ]
1274
1355
  })]
1275
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
+ }),
1276
1362
  !groups.length && /* @__PURE__ */ jsx(Typography.Text, {
1277
1363
  className: "text-xs text-gray-400",
1278
1364
  children: "No editable elements."
@@ -1299,7 +1385,7 @@ const PaletteItems = ({ subject }) => {
1299
1385
  });
1300
1386
  };
1301
1387
  const PanelFields = ({ subject }) => {
1302
- 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);
1303
1389
  return /* @__PURE__ */ jsx(Panel$1, {
1304
1390
  item,
1305
1391
  onDelete,
@@ -1308,7 +1394,8 @@ const PanelFields = ({ subject }) => {
1308
1394
  canMoveUp,
1309
1395
  canMoveDown,
1310
1396
  bindings,
1311
- onNodeChange
1397
+ onNodeChange,
1398
+ readOnly
1312
1399
  });
1313
1400
  };
1314
1401
  const Palette = ({ className, ...restProps }) => /* @__PURE__ */ jsx("div", {
@@ -1495,8 +1582,14 @@ const arePropsEqual = (prev, next) => {
1495
1582
  var renderer_default = memo(Renderer, arePropsEqual);
1496
1583
  //#endregion
1497
1584
  //#region src/components/dnd/sortable.tsx
1498
- const Sortable = ({ id, children, selected, onClick, onDelete: _onDelete, onCopy: _onCopy }) => {
1499
- 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 });
1500
1593
  const style = {
1501
1594
  transform: CSS.Transform.toString(transform),
1502
1595
  transition,
@@ -1504,6 +1597,30 @@ const Sortable = ({ id, children, selected, onClick, onDelete: _onDelete, onCopy
1504
1597
  cursor: "grab"
1505
1598
  };
1506
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
+ };
1507
1624
  const onDelete = (e) => {
1508
1625
  e.stopPropagation();
1509
1626
  _onDelete?.(id);
@@ -1513,10 +1630,11 @@ const Sortable = ({ id, children, selected, onClick, onDelete: _onDelete, onCopy
1513
1630
  _onCopy?.(id);
1514
1631
  };
1515
1632
  return /* @__PURE__ */ jsxs("div", {
1516
- ref: setNodeRef,
1633
+ ref: setRefs,
1517
1634
  style,
1518
1635
  ...attributes,
1519
1636
  ...listeners,
1637
+ onKeyDown,
1520
1638
  onClick,
1521
1639
  className: cn("relative", selected && "z-10 outline-2 outline-offset-2 outline-blue-500", isNewItemOver && "border-t-4 border-t-green-500"),
1522
1640
  children: [
@@ -1547,7 +1665,7 @@ const Overlay = ({ sections, isForced, renderProps }) => {
1547
1665
  }
1548
1666
  const section = sections.find((s) => s.id === active.id);
1549
1667
  if (section) {
1550
- const preview = generateSection(section.code, renderProps.fullCode);
1668
+ const preview = generateSection(section.code, renderProps.fullCode, { containerId: renderProps.containerId });
1551
1669
  return /* @__PURE__ */ jsx(Sortable, {
1552
1670
  id: section.id,
1553
1671
  name: section.name,
@@ -1567,29 +1685,100 @@ const Overlay = ({ sections, isForced, renderProps }) => {
1567
1685
  };
1568
1686
  //#endregion
1569
1687
  //#region src/components/dnd/use-section-document.ts
1570
- 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 } = {}) => {
1571
1701
  const { setCode } = usePreview();
1572
1702
  const [selectedId, setSelectedId] = useState(null);
1573
- const document = useMemo(() => fillSectionIds(value), [value]);
1574
- 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]);
1575
1732
  const [previewCache] = useState(() => createSectionPreviewCache());
1576
- const previews = useMemo(() => previewCache.compute(document, sections), [
1733
+ const previews = useMemo(() => previewCache.compute(view.document, sections, { containerId }), [
1734
+ containerId,
1577
1735
  previewCache,
1578
1736
  sections,
1579
- document
1737
+ view.document
1580
1738
  ]);
1581
1739
  const selectedIndex = sections.findIndex((s) => s.id === selectedId);
1582
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]);
1583
1750
  const commit = useCallback((nextSections) => {
1584
- 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
+ };
1585
1756
  onChange?.(nextCode);
1586
1757
  setCode(nextCode);
1587
- return extractSections(nextCode);
1758
+ return readSections(nextCode);
1588
1759
  }, [
1760
+ containerId,
1589
1761
  document,
1762
+ latestDocument,
1590
1763
  onChange,
1764
+ readSections,
1591
1765
  setCode
1592
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
+ ]);
1593
1782
  return {
1594
1783
  sections,
1595
1784
  previews,
@@ -1599,54 +1788,91 @@ const useSectionDocument = (value, onChange) => {
1599
1788
  select: useCallback((id) => {
1600
1789
  setSelectedId((prev) => prev === id ? null : id);
1601
1790
  }, []),
1791
+ selectOnly: useCallback((id) => setSelectedId(id), []),
1602
1792
  clearSelection: useCallback(() => setSelectedId(null), []),
1603
1793
  add: useCallback((item, atIndex) => {
1794
+ if (refuse()) return;
1795
+ const current = latestSections();
1604
1796
  const next = { code: item.code };
1605
- commit(atIndex === void 0 || atIndex < 0 ? [...sections, next] : [
1606
- ...sections.slice(0, atIndex),
1797
+ commit(atIndex === void 0 || atIndex < 0 ? [...current, next] : [
1798
+ ...current.slice(0, atIndex),
1607
1799
  next,
1608
- ...sections.slice(atIndex)
1800
+ ...current.slice(atIndex)
1609
1801
  ]);
1610
- }, [commit, sections]),
1802
+ }, [
1803
+ commit,
1804
+ refuse,
1805
+ latestSections
1806
+ ]),
1611
1807
  remove: useCallback((id) => {
1612
- 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);
1613
1811
  if (index < 0) return;
1614
1812
  if (id === selectedId) setSelectedId(null);
1615
- commit(sections.filter((_, i) => i !== index));
1813
+ commit(current.filter((_, i) => i !== index));
1616
1814
  }, [
1617
1815
  commit,
1618
- sections,
1816
+ refuse,
1817
+ latestSections,
1619
1818
  selectedId
1620
1819
  ]),
1621
1820
  copy: useCallback((id) => {
1622
- const index = sections.findIndex((s) => s.id === id);
1623
- 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];
1624
1825
  if (!source) return;
1625
1826
  const committed = commit([
1626
- ...sections.slice(0, index + 1),
1827
+ ...current.slice(0, index + 1),
1627
1828
  { code: replaceIds(source.code) },
1628
- ...sections.slice(index + 1)
1829
+ ...current.slice(index + 1)
1629
1830
  ]);
1630
1831
  setSelectedId(committed[index + 1]?.id ?? null);
1631
- }, [commit, sections]),
1832
+ }, [
1833
+ commit,
1834
+ refuse,
1835
+ latestSections
1836
+ ]),
1632
1837
  move: useCallback((id, direction) => {
1633
- 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);
1634
1841
  const targetIndex = direction === "up" ? index - 1 : index + 1;
1635
- if (index < 0 || targetIndex < 0 || targetIndex >= sections.length) return;
1636
- commit(arrayMove(sections, index, targetIndex));
1637
- }, [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
+ ]),
1638
1849
  reorder: useCallback((activeId, overId) => {
1639
- const prevIndex = sections.findIndex((s) => s.id === activeId);
1640
- 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);
1641
1854
  if (prevIndex < 0 || nextIndex < 0 || prevIndex === nextIndex) return;
1642
- commit(arrayMove(sections, prevIndex, nextIndex));
1643
- }, [commit, sections]),
1855
+ commit(arrayMove(current, prevIndex, nextIndex));
1856
+ }, [
1857
+ commit,
1858
+ refuse,
1859
+ latestSections
1860
+ ]),
1644
1861
  patch: useCallback((next) => {
1645
- 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 ? {
1646
1865
  ...s,
1647
1866
  ...next
1648
1867
  } : s));
1649
- }, [commit, sections])
1868
+ }, [
1869
+ commit,
1870
+ refuse,
1871
+ latestSections
1872
+ ]),
1873
+ getCommittedSection,
1874
+ problem,
1875
+ stale
1650
1876
  };
1651
1877
  };
1652
1878
  //#endregion
@@ -1665,6 +1891,14 @@ const describeUpdateFailure = (failure, label) => {
1665
1891
  title: `Failed to update "${label}"`,
1666
1892
  description: `${failure.count} bindings share ${failure.property ? `property "${failure.property}"` : `label "${label}"`} on this element — remove the duplicate in its data-binding.`
1667
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
+ };
1668
1902
  case "no-binding": return {
1669
1903
  title: `Failed to update "${label}"`,
1670
1904
  description: "This element has no data-binding declaration."
@@ -1684,13 +1918,48 @@ const describeUpdateFailure = (failure, label) => {
1684
1918
  default: return { title: `Failed to update "${label}"` };
1685
1919
  }
1686
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
+ });
1687
1956
  const conditionalModifiers = (args) => {
1688
1957
  const { active } = args;
1689
1958
  if (active?.data.current?.type === "new-item") return args.transform;
1690
1959
  return restrictToVerticalAxis(args);
1691
1960
  };
1692
1961
  const NO_MODULES = {};
1693
- const Dnd$1 = ({ value: _value, props, modules: _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 }) => {
1694
1963
  const modules = useStableModules(_modules);
1695
1964
  const [mobilePaletteOpen, setMobilePaletteOpen] = useState(false);
1696
1965
  const { breakpoint } = useResponsiveSize();
@@ -1704,9 +1973,35 @@ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange:
1704
1973
  return false;
1705
1974
  }
1706
1975
  };
1707
- 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
+ };
1708
1990
  const value = _value === void 0 ? code : _value;
1709
- 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;
1710
2005
  const onDragStart = (_) => {};
1711
2006
  const onDragOver = (_e) => {};
1712
2007
  const onDragEnd = (event) => {
@@ -1721,6 +2016,65 @@ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange:
1721
2016
  if (active.id !== over.id && sections.some((s) => s.id === active.id)) reorder(String(active.id), String(over.id));
1722
2017
  };
1723
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
+ ]);
1724
2078
  const onChange = (next) => {
1725
2079
  if (!next.id) return;
1726
2080
  patch(next);
@@ -1735,7 +2089,7 @@ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange:
1735
2089
  try {
1736
2090
  const updated = fillIds(selectedCode);
1737
2091
  return {
1738
- fields: extract(updated).filter((node) => node.tagName !== "section"),
2092
+ fields: extract(updated),
1739
2093
  updatedCode: updated !== selectedCode ? updated : selectedCode,
1740
2094
  parseError: null
1741
2095
  };
@@ -1748,11 +2102,16 @@ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange:
1748
2102
  };
1749
2103
  }
1750
2104
  }, [selectedCode]);
1751
- const reportError = onEditError ?? toastEditError;
1752
- const reportErrorRef = useRef(reportError);
1753
2105
  useEffect(() => {
1754
- reportErrorRef.current = reportError;
1755
- });
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]);
1756
2115
  useEffect(() => {
1757
2116
  if (parseError) reportErrorRef.current({
1758
2117
  type: "parse",
@@ -1762,9 +2121,21 @@ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange:
1762
2121
  description: "Check the console for details."
1763
2122
  });
1764
2123
  }, [parseError]);
1765
- const onFieldChange = ({ id, label, property, value: fieldValue }) => {
1766
- 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
+ })));
1767
2137
  if (!result.success) {
2138
+ const { id, label, property } = changes[result.index];
1768
2139
  reportError({
1769
2140
  type: "update",
1770
2141
  id,
@@ -1780,6 +2151,7 @@ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange:
1780
2151
  code: result.code
1781
2152
  });
1782
2153
  };
2154
+ const onFieldChange = (change) => commitChanges([change]);
1783
2155
  const bindingFields = useMemo(() => fields.flatMap((node) => resolvePanelBindings(node)?.bindings ?? []), [fields]);
1784
2156
  const bindings = withPanelCommit(bindingFields, onFieldChange);
1785
2157
  useEffect(() => {
@@ -1801,13 +2173,35 @@ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange:
1801
2173
  canMoveUp: selectedIndex > 0,
1802
2174
  canMoveDown: selectedIndex >= 0 && selectedIndex < sections.length - 1,
1803
2175
  bindings,
1804
- onNodeChange: onFieldChange
2176
+ onNodeChange: onFieldChange,
2177
+ onNodesChange: commitChanges,
2178
+ readOnly: stale
1805
2179
  };
1806
- 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, {
1807
2185
  className: cn(!sections.length && "h-full"),
1808
2186
  children: !sections.length ? /* @__PURE__ */ jsx("div", {
1809
2187
  className: cn("flex items-center justify-center", "h-full", "text-gray-500"),
1810
- 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, {
1811
2205
  orientation: "vertical",
1812
2206
  align: "center",
1813
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" })]
@@ -1822,6 +2216,9 @@ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange:
1822
2216
  onClick: () => onSelect(section.id),
1823
2217
  onDelete,
1824
2218
  onCopy,
2219
+ nodeRef: sectionNodes.register(section.id),
2220
+ onNavigate: (to) => onNavigate(section.id, to),
2221
+ onDeleteKey: () => onDeleteKey(section.id),
1825
2222
  children: /* @__PURE__ */ jsx(renderer_default, {
1826
2223
  preview: previews[index],
1827
2224
  sectionId: section.id,
@@ -1836,12 +2233,16 @@ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange:
1836
2233
  })
1837
2234
  }, section.id))
1838
2235
  })
1839
- }) });
2236
+ })] });
1840
2237
  return /* @__PURE__ */ jsx(SectionFallbackContext.Provider, {
1841
2238
  value: renderSectionFallback,
1842
2239
  children: /* @__PURE__ */ jsxs(DndContext, {
1843
2240
  sensors,
1844
2241
  collisionDetection: closestCenter,
2242
+ accessibility: {
2243
+ announcements,
2244
+ screenReaderInstructions: SCREEN_READER_INSTRUCTIONS
2245
+ },
1845
2246
  modifiers: [conditionalModifiers],
1846
2247
  onDragStart,
1847
2248
  onDragOver,
@@ -1863,7 +2264,8 @@ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange:
1863
2264
  selectedId,
1864
2265
  clearSelection,
1865
2266
  paletteOpen: mobilePaletteOpen,
1866
- setPaletteOpen: setMobilePaletteOpen
2267
+ setPaletteOpen: setMobilePaletteOpen,
2268
+ documentError: problem?.reason ?? null
1867
2269
  },
1868
2270
  children: Children.toArray(children).length ? children : /* @__PURE__ */ jsx(Layout, {})
1869
2271
  })
@@ -1876,7 +2278,8 @@ const Dnd$1 = ({ value: _value, props, modules: _modules = NO_MODULES, onChange:
1876
2278
  modules,
1877
2279
  frame,
1878
2280
  dynamicTailwind,
1879
- ...props
2281
+ ...props,
2282
+ containerId
1880
2283
  }
1881
2284
  }) })]
1882
2285
  })
@@ -1891,7 +2294,11 @@ Dnd.Palette = Palette;
1891
2294
  Dnd.Canvas = Canvas;
1892
2295
  Dnd.Panel = Panel;
1893
2296
  Dnd.Layout = Layout;
2297
+ /** @deprecated Renamed to `useDndItems`. */
2298
+ const useItemsEditor = useDndItems;
2299
+ /** @deprecated Renamed to `useDndChildren`. */
2300
+ const useChildrenEditor = useDndChildren;
1894
2301
  //#endregion
1895
- 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 };
1896
2303
 
1897
- //# sourceMappingURL=dnd-CMrXfD8O.js.map
2304
+ //# sourceMappingURL=dnd-DBaH2L8N.js.map