@drawnagency/primitives 0.1.106 → 0.1.107

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.
@@ -410,7 +410,7 @@ import { z as z3 } from "zod";
410
410
  import { List, Columns2 } from "lucide-react";
411
411
 
412
412
  // src/components/sections/IconList/IconList.tsx
413
- import { useRef as useRef3, useEffect as useEffect4, useState as useState4, useCallback } from "react";
413
+ import { useRef as useRef3, useEffect as useEffect4, useState as useState4, useCallback, useId } from "react";
414
414
  import { Check as Check2, CopyPlus, Pencil, X as X2 } from "lucide-react";
415
415
 
416
416
  // src/components/primitives/IconPicker.tsx
@@ -670,12 +670,47 @@ function EditIconList({
670
670
  iconClassName,
671
671
  onItemsChange
672
672
  }) {
673
- const { wrappedItems, onReorder, onAdd, onRemove } = useEditableCollection({
673
+ const listId = useId();
674
+ const { wrappedItems, onReorder, onAdd, onInsertItem, onRemove } = useEditableCollection({
674
675
  items,
675
676
  onChange: onItemsChange,
676
677
  createItem: () => ({ label: "", text: "" })
677
678
  });
678
679
  const [dragState, setDragState] = useState4({ sourceId: null, targetId: null });
680
+ const cfgRef = useRef3({ listId, onReorder, onRemove, onInsertItem });
681
+ cfgRef.current = { listId, onReorder, onRemove, onInsertItem };
682
+ useEffect4(() => {
683
+ let cleanup;
684
+ let cancelled = false;
685
+ import("./adapter-HH47ZPGM.js").then(({ monitorForElements }) => {
686
+ if (cancelled) return;
687
+ cleanup = monitorForElements({
688
+ onDrop: ({ source, location }) => {
689
+ if (source.data.dragType !== "icon-list-row") return;
690
+ setDragState({ sourceId: null, targetId: null });
691
+ const { listId: my, onReorder: onReorder2, onRemove: onRemove2, onInsertItem: onInsertItem2 } = cfgRef.current;
692
+ const target = location.current.dropTargets[0];
693
+ if (!target) return;
694
+ const srcListId = source.data.listId;
695
+ const dstListId = target.data.listId;
696
+ const fromIndex = source.data.index;
697
+ const targetIndex = target.data.index;
698
+ if (srcListId === my && dstListId === my) {
699
+ const toIndex = target.data.trailing ? targetIndex - 1 : targetIndex;
700
+ if (fromIndex !== toIndex) onReorder2(fromIndex, toIndex);
701
+ } else if (srcListId === my) {
702
+ onRemove2(source.data.id);
703
+ } else if (dstListId === my) {
704
+ onInsertItem2(targetIndex, source.data.item);
705
+ }
706
+ }
707
+ });
708
+ });
709
+ return () => {
710
+ cancelled = true;
711
+ cleanup?.();
712
+ };
713
+ }, []);
679
714
  const updateItem = useCallback(
680
715
  (index, patch) => {
681
716
  const updated = wrappedItems.map(
@@ -703,6 +738,7 @@ function EditIconList({
703
738
  EditableRow,
704
739
  {
705
740
  id: wrapped.id,
741
+ listId,
706
742
  index,
707
743
  item: wrapped.data,
708
744
  defaultIcon,
@@ -715,7 +751,6 @@ function EditIconList({
715
751
  iconClassName,
716
752
  dragState,
717
753
  setDragState,
718
- onReorder,
719
754
  onRemove,
720
755
  onUpdateItem: (patch) => updateItem(index, patch),
721
756
  onDuplicate: () => duplicateItem(index)
@@ -725,10 +760,10 @@ function EditIconList({
725
760
  /* @__PURE__ */ jsx8(
726
761
  TrailingDropZone,
727
762
  {
763
+ listId,
728
764
  index: wrappedItems.length,
729
765
  dragState,
730
- setDragState,
731
- onReorder
766
+ setDragState
732
767
  }
733
768
  ),
734
769
  /* @__PURE__ */ jsx8(
@@ -746,6 +781,7 @@ function EditIconList({
746
781
  }
747
782
  function EditableRow({
748
783
  id,
784
+ listId,
749
785
  index,
750
786
  item,
751
787
  defaultIcon,
@@ -758,13 +794,14 @@ function EditableRow({
758
794
  iconClassName,
759
795
  dragState,
760
796
  setDragState,
761
- onReorder,
762
797
  onRemove,
763
798
  onUpdateItem,
764
799
  onDuplicate
765
800
  }) {
766
801
  const rowRef = useRef3(null);
767
802
  const handleRef = useRef3(null);
803
+ const itemRef = useRef3(item);
804
+ itemRef.current = item;
768
805
  const [iconHover, setIconHover] = useState4(false);
769
806
  const [pickerOpen, setPickerOpen] = useState4(false);
770
807
  const [dodontOpen, setDodontOpen] = useState4(false);
@@ -782,22 +819,21 @@ function EditableRow({
782
819
  if (cancelled) return;
783
820
  const cleanupDraggable = draggable({
784
821
  element: handle,
785
- getInitialData: () => ({ dragType: "icon-list-row", id, index }),
822
+ // `item` rides along so a destination list can insert it verbatim —
823
+ // persisted icon-list items have no ids, only positions. Read from a ref
824
+ // so typing in the row doesn't re-register the drag mid-gesture.
825
+ getInitialData: () => ({ dragType: "icon-list-row", listId, id, index, item: itemRef.current }),
786
826
  onDragStart: () => setDragState({ sourceId: id, targetId: null }),
787
827
  onDrop: () => setDragState({ sourceId: null, targetId: null })
788
828
  });
789
829
  const cleanupDropTarget = dropTargetForElements({
790
830
  element: row,
791
831
  canDrop: ({ source }) => source.data.dragType === "icon-list-row",
792
- getData: () => ({ id, index }),
832
+ getData: () => ({ listId, id, index }),
793
833
  onDragEnter: () => setDragState((prev) => ({ ...prev, targetId: id })),
794
834
  onDragLeave: () => setDragState(
795
835
  (prev) => prev.targetId === id ? { ...prev, targetId: null } : prev
796
- ),
797
- onDrop: ({ source }) => {
798
- const fromIndex = source.data.index;
799
- if (fromIndex !== index) onReorder(fromIndex, index);
800
- }
836
+ )
801
837
  });
802
838
  cleanup = () => {
803
839
  cleanupDraggable();
@@ -808,7 +844,7 @@ function EditableRow({
808
844
  cancelled = true;
809
845
  cleanup?.();
810
846
  };
811
- }, [id, index, onReorder, setDragState]);
847
+ }, [id, index, listId, setDragState]);
812
848
  const isDropTarget = dragState.targetId === id && dragState.sourceId !== id;
813
849
  const onEditableActivity = (e) => {
814
850
  const t = e.target;
@@ -964,10 +1000,10 @@ function EditableRow({
964
1000
  );
965
1001
  }
966
1002
  function TrailingDropZone({
1003
+ listId,
967
1004
  index,
968
1005
  dragState,
969
- setDragState,
970
- onReorder
1006
+ setDragState
971
1007
  }) {
972
1008
  const ref = useRef3(null);
973
1009
  const trailingId = "__trailing__";
@@ -981,23 +1017,19 @@ function TrailingDropZone({
981
1017
  cleanup = dropTargetForElements({
982
1018
  element: el,
983
1019
  canDrop: ({ source }) => source.data.dragType === "icon-list-row",
984
- getData: () => ({ id: trailingId, index }),
1020
+ getData: () => ({ listId, id: trailingId, index, trailing: true }),
985
1021
  onDragEnter: () => setDragState((prev) => ({ ...prev, targetId: trailingId })),
986
1022
  onDragLeave: () => setDragState(
987
1023
  (prev) => prev.targetId === trailingId ? { ...prev, targetId: null } : prev
988
- ),
989
- onDrop: ({ source }) => {
990
- const fromIndex = source.data.index;
991
- if (fromIndex !== index - 1) onReorder(fromIndex, index - 1);
992
- }
1024
+ )
993
1025
  });
994
1026
  });
995
1027
  return () => {
996
1028
  cancelled = true;
997
1029
  cleanup?.();
998
1030
  };
999
- }, [index, onReorder, setDragState]);
1000
- const isDropTarget = dragState.targetId === trailingId && dragState.sourceId !== null;
1031
+ }, [index, listId, setDragState]);
1032
+ const isDropTarget = dragState.targetId === trailingId;
1001
1033
  return /* @__PURE__ */ jsx8(
1002
1034
  "div",
1003
1035
  {
@@ -1358,6 +1390,24 @@ function DocumentDropZone({ onDocumentSelected, className }) {
1358
1390
  );
1359
1391
  }
1360
1392
 
1393
+ // src/lib/insert-menu.ts
1394
+ var PRESET_PREFIX = "__preset:";
1395
+ var INSERT_MENU_ORDER = [
1396
+ "container",
1397
+ "link_heading",
1398
+ "sub_heading",
1399
+ "sub_sub_heading",
1400
+ "prose",
1401
+ "media",
1402
+ "video",
1403
+ "button",
1404
+ "colors",
1405
+ "document",
1406
+ "icon_list",
1407
+ `${PRESET_PREFIX}do_dont_list`,
1408
+ "dodont_media"
1409
+ ];
1410
+
1361
1411
  // src/lib/container-ops.ts
1362
1412
  function getContainerChildren(block) {
1363
1413
  const content = block.content;
@@ -9,7 +9,7 @@ import {
9
9
  stripHtmlToPlainText,
10
10
  trimTrailingSpacers,
11
11
  truncate
12
- } from "./chunk-QLBHERK6.js";
12
+ } from "./chunk-IPKZ5FU6.js";
13
13
  import {
14
14
  DeleteButton,
15
15
  DragHandle,
@@ -483,7 +483,7 @@ import { z as z5 } from "zod";
483
483
  import { LayoutTemplate } from "lucide-react";
484
484
 
485
485
  // src/components/sections/Container/Container.tsx
486
- import "react";
486
+ import { useRef as useRef3 } from "react";
487
487
 
488
488
  // src/components/editor/ChildBlockWrapper.tsx
489
489
  import { forwardRef } from "react";
@@ -859,13 +859,16 @@ function Container({ content, isEditMode, onChange, openModal }) {
859
859
  content: content.content
860
860
  };
861
861
  const emit = (nextSection) => onChange?.({ ...content, content: nextSection.content });
862
+ const pendingChildren = useRef3(children);
863
+ pendingChildren.current = children;
862
864
  const updateChild = (index, childContent) => {
863
- const next = children.map((c, i) => {
865
+ const next = pendingChildren.current.map((c, i) => {
864
866
  if (i !== index) return c;
865
867
  const merged = { ...childContent, id: c.id ?? newId() };
866
868
  if (c.layout !== void 0) merged.layout = c.layout;
867
869
  return merged;
868
870
  });
871
+ pendingChildren.current = next;
869
872
  onChange?.({ ...content, content: { ...content.content, children: next } });
870
873
  };
871
874
  const gridClass = cn(
@@ -1 +1 @@
1
- {"version":3,"file":"Container.d.ts","sourceRoot":"","sources":["../../../../src/components/sections/Container/Container.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAcnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AA4GzD,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE,CAAC;IACpD,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAwFD,wBAAgB,SAAS,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,cAAc,kDA8RrF"}
1
+ {"version":3,"file":"Container.d.ts","sourceRoot":"","sources":["../../../../src/components/sections/Container/Container.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAcnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AA4GzD,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAA;KAAE,CAAC;IACpD,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAwFD,wBAAgB,SAAS,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,cAAc,kDAwSrF"}
@@ -2,7 +2,7 @@ import {
2
2
  ColorItemSchema,
3
3
  ColorSpaceSchema,
4
4
  brandGuideSectionDefs
5
- } from "../../chunk-QLBHERK6.js";
5
+ } from "../../chunk-IPKZ5FU6.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
7
  import "../../chunk-5IZ2KTMU.js";
8
8
  import "../../chunk-D6TLUVOP.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  allSectionDefs
3
- } from "../../chunk-Z7VXESB6.js";
4
- import "../../chunk-QLBHERK6.js";
3
+ } from "../../chunk-JIQBLG6Y.js";
4
+ import "../../chunk-IPKZ5FU6.js";
5
5
  import "../../chunk-ZHHJVKU7.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
7
  import "../../chunk-LUAI5DWR.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  allSectionDefs
3
- } from "../../chunk-Z7VXESB6.js";
4
- import "../../chunk-QLBHERK6.js";
3
+ } from "../../chunk-JIQBLG6Y.js";
4
+ import "../../chunk-IPKZ5FU6.js";
5
5
  import "../../chunk-ZHHJVKU7.js";
6
6
  import "../../chunk-5THS4K5D.js";
7
7
  import "../../chunk-LUAI5DWR.js";
@@ -1 +1 @@
1
- {"version":3,"file":"MediaLibraryModal.d.ts","sourceRoot":"","sources":["../../../src/components/shell/MediaLibraryModal.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MediaLibraryModal.d.ts","sourceRoot":"","sources":["../../../src/components/shell/MediaLibraryModal.tsx"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SectionTypePicker.d.ts","sourceRoot":"","sources":["../../../src/components/shell/SectionTypePicker.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE1E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,UAAU,sBAAsB;IAC9B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,YAAY,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CAC9C;AAED,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,sBAAsB,2CAmCnG"}
1
+ {"version":3,"file":"SectionTypePicker.d.ts","sourceRoot":"","sources":["../../../src/components/shell/SectionTypePicker.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE1E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,UAAU,sBAAsB;IAC9B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,YAAY,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CAC9C;AAKD,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,sBAAsB,2CA0DnG"}
@@ -1 +1 @@
1
- {"version":3,"file":"container-ops.d.ts","sourceRoot":"","sources":["../../src/lib/container-ops.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAWnD,qEAAqE;AACrE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,CAG9D;AAWD,wBAAgB,YAAY,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAOlF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,EAAE,CAIzF;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAMxF;AAED,wBAAgB,aAAa,CAC3B,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE,MAAM,GACZ;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,CAMjD;AAED,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,MAAM,GAClB,OAAO,CAOT;AAED,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAaxF;AAOD,8CAA8C;AAC9C,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEhD;AAED,uCAAuC;AACvC,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,MAAM,GAAG,OAAO,CAEvD;AAED,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,8FAA8F;AAC9F,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAM/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE,OAAO,EACd,CAAC,EAAE,MAAM,EACT,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,MAAM,GAClB,OAAO,CAiBT"}
1
+ {"version":3,"file":"container-ops.d.ts","sourceRoot":"","sources":["../../src/lib/container-ops.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAYnD,qEAAqE;AACrE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,CAG9D;AAWD,wBAAgB,YAAY,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAOlF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,EAAE,CAMzF;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAMxF;AAED,wBAAgB,aAAa,CAC3B,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE,MAAM,GACZ;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,CAMjD;AAED,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,MAAM,GAClB,OAAO,CAOT;AAED,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAaxF;AAOD,8CAA8C;AAC9C,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEhD;AAED,uCAAuC;AACvC,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,MAAM,GAAG,OAAO,CAEvD;AAED,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,8FAA8F;AAC9F,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,CAM/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE,OAAO,EACd,CAAC,EAAE,MAAM,EACT,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,MAAM,GAClB,OAAO,CAiBT"}
@@ -9,9 +9,13 @@ export interface InsertMenuOption {
9
9
  label: string;
10
10
  icon?: ReactNode;
11
11
  }
12
- /** Every non-spacer section type, followed by presets contributed by registered
13
- * sections. Presets appear only when their owning section is registered, so a
14
- * core-only build (no brand-guide sections) shows no brand-guide presets. */
12
+ /** Sort insert-menu entries into `INSERT_MENU_ORDER`, with unlisted entries
13
+ * (custom sections) alphabetical by label at the end. Pure; returns a copy. */
14
+ export declare function sortInsertMenuOptions<T extends InsertMenuOption>(options: T[]): T[];
15
+ /** Every non-spacer section type, plus presets contributed by registered
16
+ * sections, in `INSERT_MENU_ORDER`. Presets appear only when their owning
17
+ * section is registered, so a core-only build (no brand-guide sections) shows
18
+ * no brand-guide presets. */
15
19
  export declare function buildInsertMenuOptions(sections: SectionDefinition[]): InsertMenuOption[];
16
20
  /** Resolve an Add-menu selection to ready-to-insert content. `newId` mints ids for
17
21
  * any nested children a preset creates. Returns null for an unknown type/preset. */
@@ -1 +1 @@
1
- {"version":3,"file":"insert-menu.d.ts","sourceRoot":"","sources":["../../src/lib/insert-menu.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,wEAAwE;AACxE,eAAO,MAAM,aAAa,cAAc,CAAC;AAEzC,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED;;8EAE8E;AAC9E,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,gBAAgB,EAAE,CAQxF;AAED;qFACqF;AACrF,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,KAAK,EAAE,MAAM,MAAM,GAClB,cAAc,GAAG,IAAI,CAQvB"}
1
+ {"version":3,"file":"insert-menu.d.ts","sourceRoot":"","sources":["../../src/lib/insert-menu.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,wEAAwE;AACxE,eAAO,MAAM,aAAa,cAAc,CAAC;AAEzC,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAsBD;gFACgF;AAChF,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,gBAAgB,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CASnF;AAED;;;8BAG8B;AAC9B,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,gBAAgB,EAAE,CAQxF;AAED;qFACqF;AACrF,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,KAAK,EAAE,MAAM,MAAM,GAClB,cAAc,GAAG,IAAI,CAQvB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawnagency/primitives",
3
- "version": "0.1.106",
3
+ "version": "0.1.107",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
@@ -1,4 +1,4 @@
1
- import { type ComponentType, type ReactNode } from "react";
1
+ import { useRef, type ComponentType, type ReactNode } from "react";
2
2
  import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
3
3
  import { getSection, type OpenModal } from "../../../lib/registry";
4
4
  import { cn } from "../../../lib/cn";
@@ -243,13 +243,23 @@ export function Container({ content, isEditMode, onChange, openModal }: Containe
243
243
  // the existing child — an id-less child fails save validation for the whole
244
244
  // container. Minting when the existing child has no id self-heals drafts saved
245
245
  // by the pre-fix path.
246
+ // Folded onto a ref rather than straight onto `children` so that TWO children
247
+ // updating in the same tick compose instead of the second clobbering the
248
+ // first. That happens for real: dragging an icon-list row between two lists in
249
+ // one container removes from one child and inserts into the other, and both
250
+ // emits carry the whole container's content. Re-seeded from props each render,
251
+ // so a committed update is always the base for the next tick.
252
+ const pendingChildren = useRef(children);
253
+ pendingChildren.current = children;
254
+
246
255
  const updateChild = (index: number, childContent: unknown) => {
247
- const next = children.map((c, i) => {
256
+ const next = pendingChildren.current.map((c, i) => {
248
257
  if (i !== index) return c;
249
258
  const merged = { ...(childContent as Section), id: c.id ?? newId() };
250
259
  if (c.layout !== undefined) merged.layout = c.layout;
251
260
  return merged;
252
261
  });
262
+ pendingChildren.current = next;
253
263
  onChange?.({ ...content, content: { ...content.content, children: next } });
254
264
  };
255
265
 
@@ -1,4 +1,4 @@
1
- import { useRef, useEffect, useState, useCallback } from "react";
1
+ import { useRef, useEffect, useState, useCallback, useId } from "react";
2
2
  import type { SectionContent } from "../../../schemas/sections";
3
3
  import { cn } from "../../../lib/cn";
4
4
  import { getIcon } from "../../../lib/icons";
@@ -200,7 +200,8 @@ function EditIconList({
200
200
  iconClassName: string;
201
201
  onItemsChange: (items: IconListItem[]) => void;
202
202
  }) {
203
- const { wrappedItems, onReorder, onAdd, onRemove } = useEditableCollection({
203
+ const listId = useId();
204
+ const { wrappedItems, onReorder, onAdd, onInsertItem, onRemove } = useEditableCollection({
204
205
  items,
205
206
  onChange: onItemsChange,
206
207
  createItem: () => ({ label: "", text: "" }),
@@ -211,6 +212,57 @@ function EditIconList({
211
212
  targetId: string | null;
212
213
  }>({ sourceId: null, targetId: null });
213
214
 
215
+ // The drop transaction lives here, not in the row/trailing drop targets,
216
+ // because this monitor is GLOBAL: every icon list on the page sees the same
217
+ // drop and acts on its own collection — the source removes, the destination
218
+ // inserts, and a same-list drop reorders. Rows only advertise where they are.
219
+ // Handlers are read through a ref so the monitor subscribes exactly once.
220
+ const cfgRef = useRef({ listId, onReorder, onRemove, onInsertItem });
221
+ cfgRef.current = { listId, onReorder, onRemove, onInsertItem };
222
+
223
+ useEffect(() => {
224
+ let cleanup: (() => void) | undefined;
225
+ let cancelled = false;
226
+
227
+ import("@atlaskit/pragmatic-drag-and-drop/element/adapter").then(({ monitorForElements }) => {
228
+ if (cancelled) return;
229
+ cleanup = monitorForElements({
230
+ onDrop: ({ source, location }) => {
231
+ if (source.data.dragType !== "icon-list-row") return;
232
+ // Every list clears its own indicator on any drop — this is what stops
233
+ // the destination's drop line sticking after a cross-list transfer.
234
+ setDragState({ sourceId: null, targetId: null });
235
+
236
+ const { listId: my, onReorder, onRemove, onInsertItem } = cfgRef.current;
237
+ const target = location.current.dropTargets[0];
238
+ if (!target) return;
239
+ const srcListId = source.data.listId as string;
240
+ const dstListId = target.data.listId as string;
241
+ const fromIndex = source.data.index as number;
242
+ const targetIndex = target.data.index as number;
243
+
244
+ if (srcListId === my && dstListId === my) {
245
+ // The trailing zone reports the post-list index; splicing the row out
246
+ // first shifts everything after it down one, hence the -1.
247
+ const toIndex = target.data.trailing ? targetIndex - 1 : targetIndex;
248
+ if (fromIndex !== toIndex) onReorder(fromIndex, toIndex);
249
+ } else if (srcListId === my) {
250
+ onRemove(source.data.id as string);
251
+ } else if (dstListId === my) {
252
+ // Nothing is spliced out of this list, so the target index is the
253
+ // insert position as-is (the trailing zone's index appends).
254
+ onInsertItem(targetIndex, source.data.item as IconListItem);
255
+ }
256
+ },
257
+ });
258
+ });
259
+
260
+ return () => {
261
+ cancelled = true;
262
+ cleanup?.();
263
+ };
264
+ }, []);
265
+
214
266
  const updateItem = useCallback(
215
267
  (index: number, patch: Partial<IconListItem>) => {
216
268
  const updated = wrappedItems.map((w, i) =>
@@ -244,6 +296,7 @@ function EditIconList({
244
296
  <EditableRow
245
297
  key={wrapped.id}
246
298
  id={wrapped.id}
299
+ listId={listId}
247
300
  index={index}
248
301
  item={wrapped.data}
249
302
  defaultIcon={defaultIcon}
@@ -256,7 +309,6 @@ function EditIconList({
256
309
  iconClassName={iconClassName}
257
310
  dragState={dragState}
258
311
  setDragState={setDragState}
259
- onReorder={onReorder}
260
312
  onRemove={onRemove}
261
313
  onUpdateItem={(patch) => updateItem(index, patch)}
262
314
  onDuplicate={() => duplicateItem(index)}
@@ -264,10 +316,10 @@ function EditIconList({
264
316
  ))}
265
317
 
266
318
  <TrailingDropZone
319
+ listId={listId}
267
320
  index={wrappedItems.length}
268
321
  dragState={dragState}
269
322
  setDragState={setDragState}
270
- onReorder={onReorder}
271
323
  />
272
324
 
273
325
  {/* Add button — centered below list, visible on list hover */}
@@ -285,6 +337,7 @@ function EditIconList({
285
337
 
286
338
  function EditableRow({
287
339
  id,
340
+ listId,
288
341
  index,
289
342
  item,
290
343
  defaultIcon,
@@ -297,12 +350,12 @@ function EditableRow({
297
350
  iconClassName,
298
351
  dragState,
299
352
  setDragState,
300
- onReorder,
301
353
  onRemove,
302
354
  onUpdateItem,
303
355
  onDuplicate,
304
356
  }: {
305
357
  id: string;
358
+ listId: string;
306
359
  index: number;
307
360
  item: IconListItem;
308
361
  defaultIcon: string | null | undefined;
@@ -315,13 +368,14 @@ function EditableRow({
315
368
  iconClassName: string;
316
369
  dragState: { sourceId: string | null; targetId: string | null };
317
370
  setDragState: React.Dispatch<React.SetStateAction<{ sourceId: string | null; targetId: string | null }>>;
318
- onReorder: (from: number, to: number) => void;
319
371
  onRemove: (id: string) => void;
320
372
  onUpdateItem: (patch: Partial<IconListItem>) => void;
321
373
  onDuplicate: () => void;
322
374
  }) {
323
375
  const rowRef = useRef<HTMLDivElement>(null);
324
376
  const handleRef = useRef<HTMLButtonElement>(null);
377
+ const itemRef = useRef(item);
378
+ itemRef.current = item;
325
379
  const [iconHover, setIconHover] = useState(false);
326
380
  const [pickerOpen, setPickerOpen] = useState(false);
327
381
  const [dodontOpen, setDodontOpen] = useState(false);
@@ -344,24 +398,25 @@ function EditableRow({
344
398
 
345
399
  const cleanupDraggable = draggable({
346
400
  element: handle,
347
- getInitialData: () => ({ dragType: "icon-list-row", id, index }),
401
+ // `item` rides along so a destination list can insert it verbatim —
402
+ // persisted icon-list items have no ids, only positions. Read from a ref
403
+ // so typing in the row doesn't re-register the drag mid-gesture.
404
+ getInitialData: () => ({ dragType: "icon-list-row", listId, id, index, item: itemRef.current }),
348
405
  onDragStart: () => setDragState({ sourceId: id, targetId: null }),
349
406
  onDrop: () => setDragState({ sourceId: null, targetId: null }),
350
407
  });
351
408
 
409
+ // Advertise only — EditIconList's monitor performs the move, so that one
410
+ // handler can see both sides of a cross-list transfer.
352
411
  const cleanupDropTarget = dropTargetForElements({
353
412
  element: row,
354
413
  canDrop: ({ source }) => source.data.dragType === "icon-list-row",
355
- getData: () => ({ id, index }),
414
+ getData: () => ({ listId, id, index }),
356
415
  onDragEnter: () => setDragState((prev) => ({ ...prev, targetId: id })),
357
416
  onDragLeave: () =>
358
417
  setDragState((prev) =>
359
418
  prev.targetId === id ? { ...prev, targetId: null } : prev,
360
419
  ),
361
- onDrop: ({ source }) => {
362
- const fromIndex = source.data.index as number;
363
- if (fromIndex !== index) onReorder(fromIndex, index);
364
- },
365
420
  });
366
421
 
367
422
  cleanup = () => {
@@ -374,7 +429,7 @@ function EditableRow({
374
429
  cancelled = true;
375
430
  cleanup?.();
376
431
  };
377
- }, [id, index, onReorder, setDragState]);
432
+ }, [id, index, listId, setDragState]);
378
433
 
379
434
  const isDropTarget = dragState.targetId === id && dragState.sourceId !== id;
380
435
 
@@ -525,15 +580,15 @@ function EditableRow({
525
580
  }
526
581
 
527
582
  function TrailingDropZone({
583
+ listId,
528
584
  index,
529
585
  dragState,
530
586
  setDragState,
531
- onReorder,
532
587
  }: {
588
+ listId: string;
533
589
  index: number;
534
590
  dragState: { sourceId: string | null; targetId: string | null };
535
591
  setDragState: React.Dispatch<React.SetStateAction<{ sourceId: string | null; targetId: string | null }>>;
536
- onReorder: (fromIndex: number, toIndex: number) => void;
537
592
  }) {
538
593
  const ref = useRef<HTMLDivElement>(null);
539
594
  const trailingId = "__trailing__";
@@ -551,16 +606,12 @@ function TrailingDropZone({
551
606
  cleanup = dropTargetForElements({
552
607
  element: el,
553
608
  canDrop: ({ source }) => source.data.dragType === "icon-list-row",
554
- getData: () => ({ id: trailingId, index }),
609
+ getData: () => ({ listId, id: trailingId, index, trailing: true }),
555
610
  onDragEnter: () => setDragState((prev) => ({ ...prev, targetId: trailingId })),
556
611
  onDragLeave: () =>
557
612
  setDragState((prev) =>
558
613
  prev.targetId === trailingId ? { ...prev, targetId: null } : prev,
559
614
  ),
560
- onDrop: ({ source }) => {
561
- const fromIndex = source.data.index as number;
562
- if (fromIndex !== index - 1) onReorder(fromIndex, index - 1);
563
- },
564
615
  });
565
616
  });
566
617
 
@@ -568,9 +619,11 @@ function TrailingDropZone({
568
619
  cancelled = true;
569
620
  cleanup?.();
570
621
  };
571
- }, [index, onReorder, setDragState]);
622
+ }, [index, listId, setDragState]);
572
623
 
573
- const isDropTarget = dragState.targetId === trailingId && dragState.sourceId !== null;
624
+ // targetId is only ever set by an icon-list drag entering this zone, so its
625
+ // presence is the whole condition — a cross-list drag leaves sourceId null here.
626
+ const isDropTarget = dragState.targetId === trailingId;
574
627
 
575
628
  return (
576
629
  <div
@@ -2451,7 +2451,10 @@ function EditorContent({
2451
2451
  }, [onBlockMove]);
2452
2452
 
2453
2453
  return (
2454
- <div>
2454
+ // `.section-wrapper:first-child` zeroes the 2.25rem that every other section
2455
+ // reserves above itself for its hover chrome, so the first section's controls
2456
+ // would sit under the fixed toolbar. Re-reserve it on the list instead.
2457
+ <div className="pt-9">
2455
2458
  {displaySections.map(({ section, meta }, index) => {
2456
2459
  const definition = getSection(section.type);
2457
2460
  if (!definition) {
@@ -94,7 +94,7 @@ function DocumentTile({ doc, manage, isSelected, refCount, showFolderChip, onCli
94
94
  isSelected: boolean;
95
95
  refCount: number;
96
96
  showFolderChip: boolean;
97
- onClick: () => void;
97
+ onClick: (e: React.MouseEvent) => void;
98
98
  }) {
99
99
  return (
100
100
  <button
@@ -262,7 +262,7 @@ function UploadZone({ onUpload, onFolderUpload }: {
262
262
 
263
263
  function MediaCard({ item, manage, isSelected, dragIds, onClick, children }: {
264
264
  item: MediaItem; manage: boolean; isSelected: boolean;
265
- dragIds: string[]; onClick: () => void; children: React.ReactNode;
265
+ dragIds: string[]; onClick: (e: React.MouseEvent) => void; children: React.ReactNode;
266
266
  }) {
267
267
  const drag = useMediaDraggable<HTMLDivElement>({
268
268
  enabled: manage,
@@ -446,7 +446,18 @@ export function MediaLibraryModal({
446
446
  })
447
447
  : [];
448
448
 
449
+ // Visual order of the item grid: documents render before images, and a CSS
450
+ // grid lays both out in source order, so an index range here is exactly the
451
+ // range a shift-click spans on screen — rows included.
452
+ const orderedIds = [...filteredDocuments.map((doc) => doc.id), ...filtered.map((item) => item.id)];
453
+
454
+ // Anchor for shift-click range selection. Stored as an id, not an index:
455
+ // `filtered`/`filteredDocuments` are recomputed every render, so an index
456
+ // would go stale on search, filter, or folder navigation.
457
+ const rangeAnchorId = useRef<string | null>(null);
458
+
449
459
  function toggleSelect(id: string) {
460
+ rangeAnchorId.current = id;
450
461
  setSelected((prev) => {
451
462
  const next = new Set(prev);
452
463
  if (next.has(id)) {
@@ -458,6 +469,32 @@ export function MediaLibraryModal({
458
469
  });
459
470
  }
460
471
 
472
+ function selectRangeTo(id: string) {
473
+ const anchor = rangeAnchorId.current;
474
+ const from = anchor === null ? -1 : orderedIds.indexOf(anchor);
475
+ const to = orderedIds.indexOf(id);
476
+ // No usable anchor (never clicked, or it was filtered/deleted away) — fall
477
+ // back to a plain toggle so the click is never a no-op.
478
+ if (from === -1 || to === -1) {
479
+ toggleSelect(id);
480
+ return;
481
+ }
482
+ const range = orderedIds.slice(Math.min(from, to), Math.max(from, to) + 1);
483
+ // The anchor stays put so the range can be re-extended by shift-clicking
484
+ // again, and the range adds rather than toggles (matching file explorers).
485
+ setSelected((prev) => new Set([...prev, ...range]));
486
+ }
487
+
488
+ function handleItemClick(id: string, e: React.MouseEvent) {
489
+ if (e.shiftKey) {
490
+ // Shift-click otherwise drags a native text selection across the grid.
491
+ e.preventDefault();
492
+ selectRangeTo(id);
493
+ } else {
494
+ toggleSelect(id);
495
+ }
496
+ }
497
+
461
498
  const [confirmDelete, setConfirmDelete] = useState(false);
462
499
 
463
500
  // One selection, two kinds. Everything downstream — delete dispatch, usage
@@ -837,7 +874,7 @@ export function MediaLibraryModal({
837
874
  isSelected={selected.has(doc.id)}
838
875
  refCount={documentReferenceCountMap[doc.id] ?? 0}
839
876
  showFolderChip={filtering && documentsFolderEnabled}
840
- onClick={() => (manage ? toggleSelect(doc.id) : onSelect(doc.id))}
877
+ onClick={(e) => (manage ? handleItemClick(doc.id, e) : onSelect(doc.id))}
841
878
  />
842
879
  ))}
843
880
  {filtered.map((item) => {
@@ -853,11 +890,11 @@ export function MediaLibraryModal({
853
890
  manage={manage}
854
891
  isSelected={isSelected}
855
892
  dragIds={dragIds}
856
- onClick={() => {
893
+ onClick={(e) => {
857
894
  if (mode === "select") {
858
895
  onSelect(item.id);
859
896
  } else {
860
- toggleSelect(item.id);
897
+ handleItemClick(item.id, e);
861
898
  }
862
899
  }}
863
900
  >
@@ -13,10 +13,19 @@ interface SectionTypePickerProps {
13
13
  containerRef?: RefObject<HTMLElement | null>;
14
14
  }
15
15
 
16
+ const BUTTON_CLASS =
17
+ "cursor-pointer flex items-center gap-3 rounded-lg border border-base-200 bg-base px-4 py-3 text-left text-sm text-base-contrast-light transition-colors hover:border-base-300 hover:bg-base-accent hover:text-base-contrast";
18
+
16
19
  export function SectionTypePicker({ types, onSelect, onClose, containerRef }: SectionTypePickerProps) {
17
20
  const panelRef = useRef<HTMLDivElement>(null);
18
21
  const dismissRef = containerRef ?? panelRef;
19
22
 
23
+ // Container is a layout choice rather than a content block, so it gets its own
24
+ // full-width row above a divider. It's absent from the container child picker
25
+ // (one level of nesting), in which case there's no lead row and no divider.
26
+ const lead = types.find((t) => t.type === "container");
27
+ const rest = types.filter((t) => t !== lead);
28
+
20
29
  useEffect(() => {
21
30
  function handleMouseDown(e: MouseEvent) {
22
31
  if (dismissRef.current && !dismissRef.current.contains(e.target as Node)) {
@@ -32,10 +41,27 @@ export function SectionTypePicker({ types, onSelect, onClose, containerRef }: Se
32
41
  ref={panelRef}
33
42
  className="z-50 mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3"
34
43
  >
35
- {types.map(({ type, label, icon }) => (
44
+ {lead && (
45
+ <>
46
+ <button
47
+ key={lead.type}
48
+ className={`${BUTTON_CLASS} col-span-full`}
49
+ onClick={() => onSelect(lead.type)}
50
+ >
51
+ {lead.icon && (
52
+ <span className="flex h-5 w-5 shrink-0 items-center justify-center text-base-contrast-light/60">
53
+ {lead.icon}
54
+ </span>
55
+ )}
56
+ <span>{lead.label}</span>
57
+ </button>
58
+ <hr className="col-span-full border-base-200" />
59
+ </>
60
+ )}
61
+ {rest.map(({ type, label, icon }) => (
36
62
  <button
37
63
  key={type}
38
- className="cursor-pointer flex items-center gap-3 rounded-lg border border-base-200 bg-base px-4 py-3 text-left text-sm text-base-contrast-light transition-colors hover:border-base-300 hover:bg-base-accent hover:text-base-contrast"
64
+ className={BUTTON_CLASS}
39
65
  onClick={() => onSelect(type)}
40
66
  >
41
67
  {icon && (
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { Section } from "../schemas/sections";
3
3
  import { getAllSections } from "./registry";
4
+ import { sortInsertMenuOptions } from "./insert-menu";
4
5
 
5
6
  interface ContainerContent {
6
7
  columns: number;
@@ -40,9 +41,11 @@ export function reorderChild(container: Section, from: number, to: number): Sect
40
41
  * single, testable source of truth (the container toolbar's "+" picker uses it).
41
42
  */
42
43
  export function childInsertableTypes(): { type: string; label: string; icon: ReactNode }[] {
43
- return getAllSections()
44
- .filter((d) => d.type !== "container" && d.type !== "spacer")
45
- .map((d) => ({ type: d.type, label: d.label, icon: d.icon }));
44
+ return sortInsertMenuOptions(
45
+ getAllSections()
46
+ .filter((d) => d.type !== "container" && d.type !== "spacer")
47
+ .map((d) => ({ type: d.type, label: d.label, icon: d.icon })),
48
+ );
46
49
  }
47
50
 
48
51
  export function insertChildAt(container: Section, child: Section, index: number): Section {
@@ -12,9 +12,43 @@ export interface InsertMenuOption {
12
12
  icon?: ReactNode;
13
13
  }
14
14
 
15
- /** Every non-spacer section type, followed by presets contributed by registered
16
- * sections. Presets appear only when their owning section is registered, so a
17
- * core-only build (no brand-guide sections) shows no brand-guide presets. */
15
+ /** Authored display order for the Add-section menu, most-reached-for first.
16
+ * `container` leads because the picker gives it its own full-width row above a
17
+ * divider. Anything absent — client-repo custom sections and their presets —
18
+ * sorts alphabetically after these. */
19
+ const INSERT_MENU_ORDER = [
20
+ "container",
21
+ "link_heading",
22
+ "sub_heading",
23
+ "sub_sub_heading",
24
+ "prose",
25
+ "media",
26
+ "video",
27
+ "button",
28
+ "colors",
29
+ "document",
30
+ "icon_list",
31
+ `${PRESET_PREFIX}do_dont_list`,
32
+ "dodont_media",
33
+ ];
34
+
35
+ /** Sort insert-menu entries into `INSERT_MENU_ORDER`, with unlisted entries
36
+ * (custom sections) alphabetical by label at the end. Pure; returns a copy. */
37
+ export function sortInsertMenuOptions<T extends InsertMenuOption>(options: T[]): T[] {
38
+ return [...options].sort((a, b) => {
39
+ const ai = INSERT_MENU_ORDER.indexOf(a.type);
40
+ const bi = INSERT_MENU_ORDER.indexOf(b.type);
41
+ if (ai !== -1 && bi !== -1) return ai - bi;
42
+ if (ai !== -1) return -1;
43
+ if (bi !== -1) return 1;
44
+ return a.label.localeCompare(b.label);
45
+ });
46
+ }
47
+
48
+ /** Every non-spacer section type, plus presets contributed by registered
49
+ * sections, in `INSERT_MENU_ORDER`. Presets appear only when their owning
50
+ * section is registered, so a core-only build (no brand-guide sections) shows
51
+ * no brand-guide presets. */
18
52
  export function buildInsertMenuOptions(sections: SectionDefinition[]): InsertMenuOption[] {
19
53
  const typeOptions = sections
20
54
  .filter((def) => def.type !== "spacer")
@@ -22,7 +56,7 @@ export function buildInsertMenuOptions(sections: SectionDefinition[]): InsertMen
22
56
  const presetOptions = sections
23
57
  .flatMap((def) => def.presets ?? [])
24
58
  .map((p) => ({ type: `${PRESET_PREFIX}${p.id}`, label: p.label, icon: p.icon }));
25
- return [...typeOptions, ...presetOptions];
59
+ return sortInsertMenuOptions([...typeOptions, ...presetOptions]);
26
60
  }
27
61
 
28
62
  /** Resolve an Add-menu selection to ready-to-insert content. `newId` mints ids for