@hirokisakabe/pom-editor 0.2.7 → 0.3.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.
package/README.md CHANGED
@@ -12,10 +12,11 @@
12
12
 
13
13
  ## Features
14
14
 
15
- - **Drag-and-Drop Reordering** — Sort sibling nodes within `VStack` / `HStack` / `Layer` containers, plus top-level slides, by dragging them in the AST tree.
16
- - **XML In / XML Out** — Accepts a pom XML string via `xml` and returns the updated XML via `onChange` after each reorder, so it drops into any editor / preview layout.
15
+ - **Drag-and-Drop Structure Editing** — Reorder siblings, move nodes between `VStack` / `HStack` / `Layer` containers, pull container children up to the root, or nest a container inside another — all by dragging in the AST tree.
16
+ - **Distinct "between" vs "inside" Drop Targets** — Dropping on the gap before/after a row inserts as a sibling; dropping on a container body itself nests the node inside.
17
+ - **XML In / XML Out** — Accepts a pom XML string via `xml` and returns the updated XML via `onChange` after each edit, so it drops into any editor / preview layout.
17
18
  - **AST-Aware Tree View** — Renders the parsed pom AST as a labeled tree so the structure of slides and nested containers is visible at a glance.
18
- - **Powered by `@dnd-kit`** — Built on `@dnd-kit/core` + `@dnd-kit/sortable` for accessible, keyboard-friendly drag interactions.
19
+ - **Powered by `@dnd-kit/core`** — Built on `@dnd-kit/core`'s `PointerSensor` for pointer-driven drag interactions.
19
20
 
20
21
  ## Installation
21
22
 
@@ -67,7 +68,12 @@ function App() {
67
68
  | `xml` | `string` | pom XML string (one or more `<Slide>` elements) |
68
69
  | `onChange` | `(xml: string) => void` | Called with updated XML after each drag-and-drop reorder |
69
70
 
70
- Renders a tree of nodes from the parsed XML. Nodes within the same parent container (`VStack`, `HStack`, `Layer`) can be reordered by dragging. Top-level slides can also be reordered.
71
+ Renders a tree of nodes from the parsed XML. Each row supports two drop targets:
72
+
73
+ - A thin gap between rows — drop here to insert as a **sibling** at that position (works across parents, so a node can be moved to any container or pulled up to the root).
74
+ - The container row body itself — drop here to nest the dragged node as the **last child of that container** (`VStack` / `HStack` / `Layer` only). Drops on non-container bodies are ignored.
75
+
76
+ Top-level slides can be reordered via the root-level gaps. Cycle-forming drops (e.g. moving a container into its own descendant) are silently rejected.
71
77
 
72
78
  ## License
73
79
 
@@ -1 +1 @@
1
- {"version":3,"file":"AstTree.d.ts","sourceRoot":"","sources":["../src/AstTree.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8C,MAAM,OAAO,CAAC;AAgBnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAuK3D,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,OAAO,EAAE,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACtC;AAUD,wBAAgB,OAAO,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,YAAY,qBA+FtD"}
1
+ {"version":3,"file":"AstTree.d.ts","sourceRoot":"","sources":["../src/AstTree.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8C,MAAM,OAAO,CAAC;AAYnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAmN3D,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,OAAO,EAAE,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACtC;AAED,wBAAgB,OAAO,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,YAAY,qBAwFtD"}
package/dist/AstTree.js CHANGED
@@ -1,9 +1,7 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useContext, useState } from "react";
3
- import { DndContext, PointerSensor, useSensor, useSensors, closestCenter, } from "@dnd-kit/core";
4
- import { SortableContext, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
5
- import { CSS } from "@dnd-kit/utilities";
6
- import { applyReorder, rebuildNodes } from "./ast.js";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React, { createContext, useContext, useState } from "react";
3
+ import { DndContext, PointerSensor, useDraggable, useDroppable, useSensor, useSensors, pointerWithin, } from "@dnd-kit/core";
4
+ import { applyMoveInside, applyMoveToGap, isContainerType, rebuildNodes, } from "./ast.js";
7
5
  const NODE_LABELS = {
8
6
  text: "Text",
9
7
  image: "Image",
@@ -45,25 +43,64 @@ function nodeLabel(node) {
45
43
  }
46
44
  return base;
47
45
  }
48
- const InvalidOverContext = createContext(null);
49
- function SortableItem({ astNode, depth, onChange, ast }) {
50
- const invalidOverId = useContext(InvalidOverContext);
51
- const isInvalidOver = invalidOverId === astNode.id;
52
- const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({
53
- id: astNode.id,
54
- data: { parentId: astNode.parentId },
46
+ const OverIdContext = createContext(null);
47
+ const ActiveIdContext = createContext(null);
48
+ const GAP_PREFIX = "gap:";
49
+ const INSIDE_PREFIX = "inside:";
50
+ function gapId(parentId, index) {
51
+ return `${GAP_PREFIX}${parentId}:${index}`;
52
+ }
53
+ function insideId(nodeId) {
54
+ return `${INSIDE_PREFIX}${nodeId}`;
55
+ }
56
+ function parseGapId(id) {
57
+ if (!id.startsWith(GAP_PREFIX))
58
+ return null;
59
+ const rest = id.slice(GAP_PREFIX.length);
60
+ const sep = rest.lastIndexOf(":");
61
+ if (sep === -1)
62
+ return null;
63
+ const parentId = rest.slice(0, sep);
64
+ const index = Number.parseInt(rest.slice(sep + 1), 10);
65
+ if (Number.isNaN(index))
66
+ return null;
67
+ return { parentId, index };
68
+ }
69
+ function parseInsideId(id) {
70
+ if (!id.startsWith(INSIDE_PREFIX))
71
+ return null;
72
+ return id.slice(INSIDE_PREFIX.length);
73
+ }
74
+ function GapStrip({ parentId, index, depth }) {
75
+ const id = gapId(parentId, index);
76
+ const { setNodeRef } = useDroppable({ id });
77
+ const overId = useContext(OverIdContext);
78
+ const activeId = useContext(ActiveIdContext);
79
+ const isOver = overId === id;
80
+ const isDragging = activeId !== null;
81
+ return (_jsx("div", { ref: setNodeRef, "data-testid": id, style: {
82
+ height: isOver ? "10px" : isDragging ? "8px" : "2px",
83
+ marginLeft: `${depth * 16}px`,
84
+ backgroundColor: isOver ? "#3b82f6" : "transparent",
85
+ borderRadius: "2px",
86
+ } }));
87
+ }
88
+ function Row({ astNode, depth }) {
89
+ const isContainer = isContainerType(astNode.node.type);
90
+ const overId = useContext(OverIdContext);
91
+ const activeId = useContext(ActiveIdContext);
92
+ const drag = useDraggable({ id: astNode.id });
93
+ const isDragging = activeId === astNode.id;
94
+ const inside = useDroppable({
95
+ id: insideId(astNode.id),
96
+ disabled: !isContainer || isDragging,
55
97
  });
56
- const style = {
57
- transform: CSS.Transform.toString(transform),
58
- transition,
59
- opacity: isDragging ? 0.4 : 1,
98
+ const setBodyRef = (el) => {
99
+ inside.setNodeRef(el);
100
+ drag.setNodeRef(el);
60
101
  };
61
- const handleCursor = isInvalidOver
62
- ? "not-allowed"
63
- : isDragging
64
- ? "grabbing"
65
- : "grab";
66
- return (_jsxs("div", { ref: setNodeRef, style: style, children: [_jsxs("div", { style: {
102
+ const isInsideOver = isContainer && overId === insideId(astNode.id);
103
+ return (_jsxs("div", { children: [_jsxs("div", { ref: setBodyRef, style: {
67
104
  display: "flex",
68
105
  alignItems: "center",
69
106
  gap: "6px",
@@ -72,12 +109,13 @@ function SortableItem({ astNode, depth, onChange, ast }) {
72
109
  paddingBottom: "3px",
73
110
  borderRadius: "4px",
74
111
  userSelect: "none",
75
- backgroundColor: isInvalidOver ? "#fee2e2" : undefined,
76
- outline: isInvalidOver ? "1px solid #dc2626" : undefined,
77
- cursor: isInvalidOver ? "not-allowed" : undefined,
78
- }, children: [_jsx("span", { ...listeners, ...attributes, style: {
79
- cursor: handleCursor,
80
- color: isInvalidOver ? "#dc2626" : "#9ca3af",
112
+ opacity: isDragging ? 0.4 : 1,
113
+ backgroundColor: isInsideOver ? "#dbeafe" : undefined,
114
+ outline: isInsideOver ? "1px solid #2563eb" : undefined,
115
+ transition: "background-color 50ms",
116
+ }, children: [_jsx("span", { ...drag.listeners, ...drag.attributes, style: {
117
+ cursor: isDragging ? "grabbing" : "grab",
118
+ color: "#9ca3af",
81
119
  fontSize: "12px",
82
120
  lineHeight: 1,
83
121
  flexShrink: 0,
@@ -88,63 +126,59 @@ function SortableItem({ astNode, depth, onChange, ast }) {
88
126
  whiteSpace: "nowrap",
89
127
  overflow: "hidden",
90
128
  textOverflow: "ellipsis",
91
- }, children: nodeLabel(astNode.node) })] }), astNode.children && astNode.children.length > 0 && (_jsx(SortableChildrenList, { children: astNode.children, depth: depth + 1, onChange: onChange, ast: ast }))] }));
129
+ }, children: nodeLabel(astNode.node) })] }), astNode.children && (_jsx(ChildList, { parentId: astNode.id, nodes: astNode.children, depth: depth + 1 }))] }));
92
130
  }
93
- function SortableChildrenList({ children, depth, onChange, ast, }) {
94
- const ids = children.map((c) => c.id);
95
- return (_jsx(SortableContext, { items: ids, strategy: verticalListSortingStrategy, children: children.map((child) => (_jsx(SortableItem, { astNode: child, depth: depth, onChange: onChange, ast: ast }, child.id))) }));
96
- }
97
- function getParentId(data) {
98
- if (typeof data === "object" && data !== null && "parentId" in data) {
99
- const value = data.parentId;
100
- return typeof value === "string" ? value : null;
101
- }
102
- return null;
131
+ function ChildList({ parentId, nodes, depth }) {
132
+ return (_jsxs(_Fragment, { children: [_jsx(GapStrip, { parentId: parentId, index: 0, depth: depth }), nodes.map((child, i) => (_jsxs(React.Fragment, { children: [_jsx(Row, { astNode: child, depth: depth }), _jsx(GapStrip, { parentId: parentId, index: i + 1, depth: depth })] }, child.id)))] }));
103
133
  }
104
134
  export function AstTree({ ast, onChange }) {
105
135
  const sensors = useSensors(useSensor(PointerSensor, {
106
136
  activationConstraint: { distance: 4 },
107
137
  }));
108
- const [invalidOverId, setInvalidOverId] = useState(null);
109
- function onDragOver({ active, over }) {
110
- if (!over || active.id === over.id) {
111
- setInvalidOverId(null);
138
+ const [overId, setOverId] = useState(null);
139
+ const [activeId, setActiveId] = useState(null);
140
+ function onDragStart(event) {
141
+ setActiveId(String(event.active.id));
142
+ }
143
+ function onDragOver({ over }) {
144
+ setOverId(over ? String(over.id) : null);
145
+ }
146
+ function applyDrop(activeId, overId) {
147
+ const gap = parseGapId(overId);
148
+ if (gap) {
149
+ const newAst = applyMoveToGap(ast, activeId, gap.parentId, gap.index);
150
+ if (newAst !== ast)
151
+ onChange(rebuildNodes(newAst));
112
152
  return;
113
153
  }
114
- const activeParentId = getParentId(active.data.current);
115
- const overParentId = getParentId(over.data.current);
116
- if (activeParentId !== overParentId) {
117
- setInvalidOverId(over.id);
118
- }
119
- else {
120
- setInvalidOverId(null);
154
+ const insideTarget = parseInsideId(overId);
155
+ if (insideTarget !== null) {
156
+ const newAst = applyMoveInside(ast, activeId, insideTarget);
157
+ if (newAst !== ast)
158
+ onChange(rebuildNodes(newAst));
121
159
  }
122
160
  }
123
161
  function onDragEnd({ active, over }) {
124
- setInvalidOverId(null);
125
- if (!over || active.id === over.id)
126
- return;
127
- const activeParentId = getParentId(active.data.current);
128
- const overParentId = getParentId(over.data.current);
129
- if (activeParentId === null || activeParentId !== overParentId)
162
+ setOverId(null);
163
+ setActiveId(null);
164
+ if (!over)
130
165
  return;
131
- const newAst = applyReorder(ast, active.id, over.id, activeParentId);
132
- onChange(rebuildNodes(newAst));
166
+ applyDrop(String(active.id), String(over.id));
133
167
  }
134
168
  function onDragCancel() {
135
- setInvalidOverId(null);
169
+ setOverId(null);
170
+ setActiveId(null);
136
171
  }
137
- const rootIds = ast.map((n) => n.id);
138
- return (_jsx("div", { style: { cursor: invalidOverId !== null ? "not-allowed" : undefined }, children: _jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragOver: onDragOver, onDragEnd: onDragEnd, onDragCancel: onDragCancel, children: _jsx(InvalidOverContext.Provider, { value: invalidOverId, children: _jsx(SortableContext, { items: rootIds, strategy: verticalListSortingStrategy, children: ast.map((astNode, i) => (_jsxs("div", { children: [i > 0 && (_jsx("div", { style: {
139
- height: "1px",
140
- backgroundColor: "#e5e7eb",
141
- margin: "8px 0",
142
- } })), _jsxs("div", { style: {
143
- fontSize: "11px",
144
- color: "#6b7280",
145
- padding: "2px 0 4px 0",
146
- fontWeight: 600,
147
- letterSpacing: "0.05em",
148
- textTransform: "uppercase",
149
- }, children: ["Slide ", i + 1] }), _jsx(SortableItem, { astNode: astNode, depth: 0, onChange: onChange, ast: ast })] }, astNode.id))) }) }) }) }));
172
+ return (_jsx("div", { children: _jsx(DndContext, { sensors: sensors, collisionDetection: pointerWithin, onDragStart: onDragStart, onDragOver: onDragOver, onDragEnd: onDragEnd, onDragCancel: onDragCancel, children: _jsx(ActiveIdContext.Provider, { value: activeId, children: _jsxs(OverIdContext.Provider, { value: overId, children: [_jsx(GapStrip, { parentId: "root", index: 0, depth: 0 }), ast.map((astNode, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("div", { style: {
173
+ height: "1px",
174
+ backgroundColor: "#e5e7eb",
175
+ margin: "8px 0",
176
+ } })), _jsxs("div", { style: {
177
+ fontSize: "11px",
178
+ color: "#6b7280",
179
+ padding: "2px 0 4px 0",
180
+ fontWeight: 600,
181
+ letterSpacing: "0.05em",
182
+ textTransform: "uppercase",
183
+ }, children: ["Slide ", i + 1] }), _jsx(Row, { astNode: astNode, depth: 0 }), _jsx(GapStrip, { parentId: "root", index: i + 1, depth: 0 })] }, astNode.id)))] }) }) }) }));
150
184
  }
package/dist/ast.d.ts CHANGED
@@ -5,9 +5,11 @@ export interface AstNode {
5
5
  parentId: string;
6
6
  children?: AstNode[];
7
7
  }
8
+ export declare function isContainerType(type: string): boolean;
8
9
  export declare function buildAst(nodes: POMNode[], parentId: string, counter: {
9
10
  value: number;
10
11
  }): AstNode[];
11
12
  export declare function rebuildNodes(astNodes: AstNode[]): POMNode[];
12
- export declare function applyReorder(ast: AstNode[], activeId: string, overId: string, activeParentId: string): AstNode[];
13
+ export declare function applyMoveToGap(ast: AstNode[], activeId: string, newParentId: string, newIndex: number): AstNode[];
14
+ export declare function applyMoveInside(ast: AstNode[], activeId: string, containerId: string): AstNode[];
13
15
  //# sourceMappingURL=ast.d.ts.map
package/dist/ast.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAG3D,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAID,wBAAgB,QAAQ,CACtB,KAAK,EAAE,OAAO,EAAE,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GACzB,OAAO,EAAE,CAeX;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAO3D;AAyCD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,OAAO,EAAE,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,OAAO,EAAE,CAgBX"}
1
+ {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAE3D,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAID,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,QAAQ,CACtB,KAAK,EAAE,OAAO,EAAE,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GACzB,OAAO,EAAE,CAeX;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAO3D;AAmFD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,EAAE,EACd,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,EAAE,CA+BX;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,EAAE,EACd,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,EAAE,CAqBX"}
package/dist/ast.js CHANGED
@@ -1,5 +1,7 @@
1
- import { arrayMove } from "@dnd-kit/sortable";
2
1
  const CONTAINER_TYPES = new Set(["vstack", "hstack", "layer"]);
2
+ export function isContainerType(type) {
3
+ return CONTAINER_TYPES.has(type);
4
+ }
3
5
  export function buildAst(nodes, parentId, counter) {
4
6
  return nodes.map((node) => {
5
7
  const id = String(counter.value++);
@@ -35,37 +37,110 @@ function findNode(astNodes, id) {
35
37
  }
36
38
  return null;
37
39
  }
38
- function reorderInParent(astNodes, parentId, oldIndex, newIndex) {
39
- return astNodes.map((astNode) => {
40
- if (astNode.id === parentId && astNode.children) {
40
+ function isDescendantOrSelf(node, id) {
41
+ if (node.id === id)
42
+ return true;
43
+ if (!node.children)
44
+ return false;
45
+ return node.children.some((c) => isDescendantOrSelf(c, id));
46
+ }
47
+ function removeById(astNodes, id) {
48
+ for (let i = 0; i < astNodes.length; i++) {
49
+ const current = astNodes[i];
50
+ if (current.id === id) {
51
+ return {
52
+ newAst: [...astNodes.slice(0, i), ...astNodes.slice(i + 1)],
53
+ removed: current,
54
+ };
55
+ }
56
+ if (current.children) {
57
+ const result = removeById(current.children, id);
58
+ if (result) {
59
+ return {
60
+ newAst: [
61
+ ...astNodes.slice(0, i),
62
+ { ...current, children: result.newAst },
63
+ ...astNodes.slice(i + 1),
64
+ ],
65
+ removed: result.removed,
66
+ };
67
+ }
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ function insertAt(astNodes, parentId, index, insertion) {
73
+ if (parentId === "root") {
74
+ const node = { ...insertion, parentId: "root" };
75
+ return [...astNodes.slice(0, index), node, ...astNodes.slice(index)];
76
+ }
77
+ return astNodes.map((n) => {
78
+ if (n.id === parentId) {
79
+ const children = n.children ?? [];
80
+ const node = { ...insertion, parentId };
41
81
  return {
42
- ...astNode,
43
- children: arrayMove(astNode.children, oldIndex, newIndex),
82
+ ...n,
83
+ children: [...children.slice(0, index), node, ...children.slice(index)],
44
84
  };
45
85
  }
46
- if (astNode.children) {
86
+ if (n.children) {
47
87
  return {
48
- ...astNode,
49
- children: reorderInParent(astNode.children, parentId, oldIndex, newIndex),
88
+ ...n,
89
+ children: insertAt(n.children, parentId, index, insertion),
50
90
  };
51
91
  }
52
- return astNode;
92
+ return n;
53
93
  });
54
94
  }
55
- export function applyReorder(ast, activeId, overId, activeParentId) {
56
- if (activeParentId === "root") {
57
- const oldIndex = ast.findIndex((c) => c.id === activeId);
58
- const newIndex = ast.findIndex((c) => c.id === overId);
59
- if (oldIndex === -1 || newIndex === -1)
95
+ function siblingsOf(ast, parentId) {
96
+ if (parentId === "root")
97
+ return ast;
98
+ const parent = findNode(ast, parentId);
99
+ return parent?.children ?? null;
100
+ }
101
+ export function applyMoveToGap(ast, activeId, newParentId, newIndex) {
102
+ const active = findNode(ast, activeId);
103
+ if (!active)
104
+ return ast;
105
+ if (newParentId !== "root") {
106
+ const newParent = findNode(ast, newParentId);
107
+ if (!newParent)
108
+ return ast;
109
+ if (!isContainerType(newParent.node.type))
60
110
  return ast;
61
- return arrayMove(ast, oldIndex, newIndex);
111
+ if (isDescendantOrSelf(active, newParentId))
112
+ return ast;
113
+ }
114
+ let adjustedIndex = newIndex;
115
+ if (active.parentId === newParentId) {
116
+ const siblings = siblingsOf(ast, newParentId);
117
+ if (siblings) {
118
+ const activeIndex = siblings.findIndex((c) => c.id === activeId);
119
+ if (activeIndex !== -1 && activeIndex < newIndex) {
120
+ adjustedIndex = newIndex - 1;
121
+ }
122
+ }
62
123
  }
63
- const parent = findNode(ast, activeParentId);
64
- if (!parent?.children)
124
+ const removeResult = removeById(ast, activeId);
125
+ if (!removeResult)
126
+ return ast;
127
+ return insertAt(removeResult.newAst, newParentId, adjustedIndex, removeResult.removed);
128
+ }
129
+ export function applyMoveInside(ast, activeId, containerId) {
130
+ const active = findNode(ast, activeId);
131
+ if (!active)
132
+ return ast;
133
+ if (isDescendantOrSelf(active, containerId))
134
+ return ast;
135
+ const container = findNode(ast, containerId);
136
+ if (!container)
137
+ return ast;
138
+ if (!isContainerType(container.node.type))
65
139
  return ast;
66
- const oldIndex = parent.children.findIndex((c) => c.id === activeId);
67
- const newIndex = parent.children.findIndex((c) => c.id === overId);
68
- if (oldIndex === -1 || newIndex === -1)
140
+ const removeResult = removeById(ast, activeId);
141
+ if (!removeResult)
69
142
  return ast;
70
- return reorderInParent(ast, activeParentId, oldIndex, newIndex);
143
+ const containerAfterRemove = findNode(removeResult.newAst, containerId);
144
+ const insertIndex = containerAfterRemove?.children?.length ?? 0;
145
+ return insertAt(removeResult.newAst, containerId, insertIndex, removeResult.removed);
71
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirokisakabe/pom-editor",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "Visual AST editor component for pom — drag-and-drop slide structure editing.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,9 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@dnd-kit/core": "^6.3.1",
38
- "@dnd-kit/sortable": "^10.0.0",
39
- "@dnd-kit/utilities": "^3.2.2",
40
- "@hirokisakabe/pom": "^8.7.0"
38
+ "@hirokisakabe/pom": "^8.8.0"
41
39
  },
42
40
  "devDependencies": {
43
41
  "@testing-library/jest-dom": "^6.9.1",
@@ -45,10 +43,10 @@
45
43
  "@types/react": "^19",
46
44
  "@types/react-dom": "^19",
47
45
  "@vitejs/plugin-react": "^6.0.2",
48
- "jsdom": "^28.0.1",
46
+ "jsdom": "^29.1.1",
49
47
  "react": "^19",
50
48
  "react-dom": "^19",
51
- "vitest": "^4.1.8"
49
+ "vitest": "^4.1.9"
52
50
  },
53
51
  "publishConfig": {
54
52
  "access": "public"