@hirokisakabe/pom-editor 0.2.4 → 0.2.6

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
@@ -1,14 +1,33 @@
1
- # @hirokisakabe/pom-editor
1
+ <h1 align="center">pom-editor</h1>
2
+ <p align="center">
3
+ Visual AST editor for <a href="https://www.npmjs.com/package/@hirokisakabe/pom">pom</a> — drag-and-drop reordering of slide node trees.
4
+ </p>
2
5
 
3
- Visual AST editor for [pom](https://github.com/hirokisakabe/pom) — drag-and-drop reordering of slide node trees.
6
+ <p align="center">
7
+ <a href="https://www.npmjs.com/package/@hirokisakabe/pom-editor"><img src="https://img.shields.io/npm/v/@hirokisakabe/pom-editor.svg" alt="npm version"></a>
8
+ <a href="https://github.com/hirokisakabe/pom/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@hirokisakabe/pom-editor.svg" alt="License"></a>
9
+ </p>
4
10
 
5
- ## Install
11
+ ---
12
+
13
+ ## Features
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.
17
+ - **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
+
20
+ ## Installation
21
+
22
+ > Requires React 18+
6
23
 
7
24
  ```bash
8
- npm install @hirokisakabe/pom-editor @hirokisakabe/pom react
25
+ npm install @hirokisakabe/pom-editor react
9
26
  ```
10
27
 
11
- ## Usage
28
+ `@hirokisakabe/pom` is pulled in automatically as a regular dependency — no separate install needed.
29
+
30
+ ## Quick Start
12
31
 
13
32
  ```tsx
14
33
  import { PomAstEditor } from "@hirokisakabe/pom-editor";
@@ -50,11 +69,6 @@ function App() {
50
69
 
51
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.
52
71
 
53
- ## Requirements
54
-
55
- - React 18 or later
56
- - `@hirokisakabe/pom` (peer dependency resolved automatically as a workspace dependency; install separately in non-monorepo setups)
57
-
58
72
  ## License
59
73
 
60
74
  MIT
@@ -1 +1 @@
1
- {"version":3,"file":"AstTree.d.ts","sourceRoot":"","sources":["../src/AstTree.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAe1B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AA0J3D,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,qBAoEtD"}
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"}
package/dist/AstTree.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState } from "react";
2
3
  import { DndContext, PointerSensor, useSensor, useSensors, closestCenter, } from "@dnd-kit/core";
3
4
  import { SortableContext, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
4
5
  import { CSS } from "@dnd-kit/utilities";
@@ -44,7 +45,10 @@ function nodeLabel(node) {
44
45
  }
45
46
  return base;
46
47
  }
48
+ const InvalidOverContext = createContext(null);
47
49
  function SortableItem({ astNode, depth, onChange, ast }) {
50
+ const invalidOverId = useContext(InvalidOverContext);
51
+ const isInvalidOver = invalidOverId === astNode.id;
48
52
  const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({
49
53
  id: astNode.id,
50
54
  data: { parentId: astNode.parentId },
@@ -54,6 +58,11 @@ function SortableItem({ astNode, depth, onChange, ast }) {
54
58
  transition,
55
59
  opacity: isDragging ? 0.4 : 1,
56
60
  };
61
+ const handleCursor = isInvalidOver
62
+ ? "not-allowed"
63
+ : isDragging
64
+ ? "grabbing"
65
+ : "grab";
57
66
  return (_jsxs("div", { ref: setNodeRef, style: style, children: [_jsxs("div", { style: {
58
67
  display: "flex",
59
68
  alignItems: "center",
@@ -63,9 +72,12 @@ function SortableItem({ astNode, depth, onChange, ast }) {
63
72
  paddingBottom: "3px",
64
73
  borderRadius: "4px",
65
74
  userSelect: "none",
75
+ backgroundColor: isInvalidOver ? "#fee2e2" : undefined,
76
+ outline: isInvalidOver ? "1px solid #dc2626" : undefined,
77
+ cursor: isInvalidOver ? "not-allowed" : undefined,
66
78
  }, children: [_jsx("span", { ...listeners, ...attributes, style: {
67
- cursor: isDragging ? "grabbing" : "grab",
68
- color: "#9ca3af",
79
+ cursor: handleCursor,
80
+ color: isInvalidOver ? "#dc2626" : "#9ca3af",
69
81
  fontSize: "12px",
70
82
  lineHeight: 1,
71
83
  flexShrink: 0,
@@ -82,32 +94,57 @@ function SortableChildrenList({ children, depth, onChange, ast, }) {
82
94
  const ids = children.map((c) => c.id);
83
95
  return (_jsx(SortableContext, { items: ids, strategy: verticalListSortingStrategy, children: children.map((child) => (_jsx(SortableItem, { astNode: child, depth: depth, onChange: onChange, ast: ast }, child.id))) }));
84
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;
103
+ }
85
104
  export function AstTree({ ast, onChange }) {
86
105
  const sensors = useSensors(useSensor(PointerSensor, {
87
106
  activationConstraint: { distance: 4 },
88
107
  }));
108
+ const [invalidOverId, setInvalidOverId] = useState(null);
109
+ function onDragOver({ active, over }) {
110
+ if (!over || active.id === over.id) {
111
+ setInvalidOverId(null);
112
+ return;
113
+ }
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);
121
+ }
122
+ }
89
123
  function onDragEnd({ active, over }) {
124
+ setInvalidOverId(null);
90
125
  if (!over || active.id === over.id)
91
126
  return;
92
- const activeParentId = active.data.current
93
- .parentId;
94
- const overParentId = over.data.current.parentId;
95
- if (activeParentId !== overParentId)
127
+ const activeParentId = getParentId(active.data.current);
128
+ const overParentId = getParentId(over.data.current);
129
+ if (activeParentId === null || activeParentId !== overParentId)
96
130
  return;
97
131
  const newAst = applyReorder(ast, active.id, over.id, activeParentId);
98
132
  onChange(rebuildNodes(newAst));
99
133
  }
134
+ function onDragCancel() {
135
+ setInvalidOverId(null);
136
+ }
100
137
  const rootIds = ast.map((n) => n.id);
101
- return (_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: onDragEnd, children: _jsx(SortableContext, { items: rootIds, strategy: verticalListSortingStrategy, children: ast.map((astNode, i) => (_jsxs("div", { children: [i > 0 && (_jsx("div", { style: {
102
- height: "1px",
103
- backgroundColor: "#e5e7eb",
104
- margin: "8px 0",
105
- } })), _jsxs("div", { style: {
106
- fontSize: "11px",
107
- color: "#6b7280",
108
- padding: "2px 0 4px 0",
109
- fontWeight: 600,
110
- letterSpacing: "0.05em",
111
- textTransform: "uppercase",
112
- }, children: ["Slide ", i + 1] }), _jsx(SortableItem, { astNode: astNode, depth: 0, onChange: onChange, ast: ast })] }, astNode.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))) }) }) }) }));
113
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirokisakabe/pom-editor",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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",
@@ -37,20 +37,30 @@
37
37
  "@dnd-kit/core": "^6.3.1",
38
38
  "@dnd-kit/sortable": "^10.0.0",
39
39
  "@dnd-kit/utilities": "^3.2.2",
40
- "@hirokisakabe/pom": "^8.5.0"
40
+ "@hirokisakabe/pom": "^8.6.0"
41
41
  },
42
42
  "devDependencies": {
43
- "@types/react": "^19"
43
+ "@testing-library/jest-dom": "^6.9.1",
44
+ "@testing-library/react": "^16.3.2",
45
+ "@types/react": "^19",
46
+ "@types/react-dom": "^19",
47
+ "@vitejs/plugin-react": "^6.0.2",
48
+ "jsdom": "^28.0.1",
49
+ "react": "^19",
50
+ "react-dom": "^19",
51
+ "vitest": "^4.1.8"
44
52
  },
45
53
  "publishConfig": {
46
54
  "access": "public"
47
55
  },
48
56
  "scripts": {
49
- "build": "tsc",
57
+ "build": "tsc -p tsconfig.build.json",
50
58
  "fmt": "prettier --write .",
51
59
  "fmt:check": "prettier --check .",
52
60
  "knip": "knip",
53
61
  "lint": "eslint",
62
+ "test": "vitest",
63
+ "test:run": "vitest run",
54
64
  "typecheck": "tsc --noEmit -p tsconfig.json"
55
65
  }
56
66
  }