astro-tractstack 2.2.6 → 2.2.8

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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { fileURLToPath as d } from "node:url";
2
2
  import { dirname as i, resolve as l } from "node:path";
3
- import { existsSync as o, mkdirSync as x, copyFileSync as k, writeFileSync as u } from "node:fs";
3
+ import { existsSync as n, mkdirSync as x, copyFileSync as k, writeFileSync as u } from "node:fs";
4
4
  import { resolve as a } from "path";
5
5
  function b(t) {
6
6
  const e = i(d(t));
@@ -94,6 +94,10 @@ async function w(t, e, c) {
94
94
  src: t("../templates/src/components/compositor/Node.tsx"),
95
95
  dest: "src/components/compositor/Node.tsx"
96
96
  },
97
+ {
98
+ src: t("../templates/src/components/compositor/ToolDragLayer.tsx"),
99
+ dest: "src/components/compositor/ToolDragLayer.tsx"
100
+ },
97
101
  {
98
102
  src: t(
99
103
  "../templates/src/components/compositor/tools/NodeOverlay.tsx"
@@ -574,6 +578,10 @@ async function w(t, e, c) {
574
578
  src: t("../templates/src/stores/backend.ts"),
575
579
  dest: "src/stores/backend.ts"
576
580
  },
581
+ {
582
+ src: t("../templates/src/stores/toolDrag.ts"),
583
+ dest: "src/stores/toolDrag.ts"
584
+ },
577
585
  {
578
586
  src: t("../templates/src/stores/resources.ts"),
579
587
  dest: "src/stores/resources.ts"
@@ -617,6 +625,10 @@ async function w(t, e, c) {
617
625
  src: t("../templates/src/utils/compositor/savePipeline.ts"),
618
626
  dest: "src/utils/compositor/savePipeline.ts"
619
627
  },
628
+ {
629
+ src: t("../templates/src/utils/compositor/toolDragManager.ts"),
630
+ dest: "src/utils/compositor/toolDragManager.ts"
631
+ },
620
632
  {
621
633
  src: t("../templates/src/utils/compositor/aiPaneParser.ts"),
622
634
  dest: "src/utils/compositor/aiPaneParser.ts"
@@ -2195,10 +2207,10 @@ async function w(t, e, c) {
2195
2207
  for (const s of r)
2196
2208
  try {
2197
2209
  const p = i(s.dest);
2198
- o(p) || x(p, { recursive: !0 });
2199
- const n = !s.protected && (s.dest === "tailwind.config.cjs" || s.dest.startsWith("src/components/codehooks/") || s.dest.startsWith("src/components/widgets/") || s.dest.startsWith("src/") || s.dest.startsWith("public/client/") || s.dest === ".gitignore");
2200
- if (!o(s.dest) || n)
2201
- if (o(s.src))
2210
+ n(p) || x(p, { recursive: !0 });
2211
+ const o = !s.protected && (s.dest === "tailwind.config.cjs" || s.dest.startsWith("src/components/codehooks/") || s.dest.startsWith("src/components/widgets/") || s.dest.startsWith("src/") || s.dest.startsWith("public/client/") || s.dest === ".gitignore");
2212
+ if (!n(s.dest) || o)
2213
+ if (n(s.src))
2202
2214
  k(s.src, s.dest), e.info(`Updated ${s.dest}`);
2203
2215
  else {
2204
2216
  const m = y(s.dest);
@@ -2206,8 +2218,8 @@ async function w(t, e, c) {
2206
2218
  }
2207
2219
  else s.protected ? e.info(`Protected: ${s.dest} (skipped overwrite)`) : e.info(`Skipped existing ${s.dest}`);
2208
2220
  } catch (p) {
2209
- const n = p instanceof Error ? p.message : String(p);
2210
- e.error(`Failed to create ${s.dest}: ${n}`);
2221
+ const o = p instanceof Error ? p.message : String(p);
2222
+ e.error(`Failed to create ${s.dest}: ${o}`);
2211
2223
  }
2212
2224
  }
2213
2225
  function y(t) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.2.6",
3
+ "version": "2.2.8",
4
4
  "description": "Astro integration for TractStack - the free web press by At Risk Media",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,38 @@
1
+ import { useStore } from '@nanostores/react';
2
+ import { toolDragStore } from '@/stores/toolDrag';
3
+ import { toolAddModeTitles } from '@/constants';
4
+ import type { ToolAddMode } from '@/types/compositorTypes';
5
+ import ArrowsUpDownIcon from '@heroicons/react/24/outline/ArrowsUpDownIcon';
6
+
7
+ const ToolDragLayer = () => {
8
+ const { isDragging, pointer, payload, dragType } = useStore(toolDragStore);
9
+
10
+ if (!isDragging || !payload) return null;
11
+
12
+ const style = {
13
+ transform: `translate(${pointer.x}px, ${pointer.y}px)`,
14
+ position: 'fixed' as const,
15
+ left: 0,
16
+ top: 0,
17
+ zIndex: 9999,
18
+ pointerEvents: 'none' as const,
19
+ marginTop: '-20px',
20
+ marginLeft: '-20px',
21
+ };
22
+
23
+ return (
24
+ <div style={style} className="pointer-events-none flex items-center gap-2">
25
+ {dragType === 'insert' ? (
26
+ <div className="rounded-lg border-2 border-myblue bg-white px-4 py-2 font-bold text-myblue opacity-90 shadow-xl">
27
+ {toolAddModeTitles[payload as ToolAddMode] || payload}
28
+ </div>
29
+ ) : (
30
+ <div className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-myblue bg-white text-myblue opacity-90 shadow-xl">
31
+ <ArrowsUpDownIcon className="h-6 w-6" />
32
+ </div>
33
+ )}
34
+ </div>
35
+ );
36
+ };
37
+
38
+ export default ToolDragLayer;
@@ -11,6 +11,8 @@ import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
11
11
  import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
12
12
  import { getCtx } from '@/stores/nodes';
13
13
  import { settingsPanelStore } from '@/stores/storykeep';
14
+ import { toolDragStore, startToolDrag } from '@/stores/toolDrag';
15
+ import { initToolDragListeners } from '@/utils/compositor/toolDragManager';
14
16
  import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
15
17
  import { getTemplateNode } from '@/utils/compositor/nodesHelper';
16
18
  import { classNames } from '@/utils/helpers';
@@ -44,9 +46,9 @@ export const NodeOverlay = ({
44
46
  const toolMode = useStore(ctx.toolModeValStore).value;
45
47
  const toolAddMode = useStore(ctx.toolAddModeStore).value;
46
48
  const settingsPanel = useStore(settingsPanelStore);
49
+ const dragStore = useStore(toolDragStore);
47
50
  const [hoverZone, setHoverZone] = useState<'before' | 'after' | null>(null);
48
51
 
49
- // put a contentEditable={false} component inside a tree that inherits contentEditable={true}.
50
52
  const chromeRef = useRef<HTMLDivElement>(null);
51
53
 
52
54
  useEffect(() => {
@@ -92,11 +94,30 @@ export const NodeOverlay = ({
92
94
  }
93
95
  };
94
96
 
97
+ const handleReorderStart = (e: MouseEvent) => {
98
+ e.preventDefault();
99
+ e.stopPropagation();
100
+ startToolDrag('reorder', nodeId, e.clientX, e.clientY);
101
+ initToolDragListeners();
102
+ };
103
+
95
104
  const canInsert =
96
105
  toolMode === 'insert'
97
106
  ? ctx.allowInsert(nodeId, toolAddMode || 'p')
98
107
  : { allowInsertBefore: false, allowInsertAfter: false };
99
108
 
109
+ const isReorderMode = toolMode === 'reorder';
110
+ const isDragging = dragStore.isDragging;
111
+
112
+ const showZones =
113
+ (toolMode === 'insert' && toolAddMode !== `span`) ||
114
+ (isReorderMode && isDragging);
115
+
116
+ const isDragTarget = dragStore.activeDropZone?.nodeId === nodeId;
117
+ const activeLocation = isDragTarget
118
+ ? dragStore.activeDropZone?.location
119
+ : null;
120
+
100
121
  const iconSrc = getIconForTag(node.tagName);
101
122
 
102
123
  return (
@@ -104,14 +125,19 @@ export const NodeOverlay = ({
104
125
  className={classNames(
105
126
  'compositor-wrapper group relative transition-all duration-200',
106
127
  zIndexClass,
107
- toolMode === 'text' ? outlineClass : ''
128
+ toolMode === 'text' ? outlineClass : '',
129
+ isReorderMode && !isDragging
130
+ ? 'cursor-grab hover:outline-dotted hover:outline-2 hover:outline-offset-2 hover:outline-cyan-500'
131
+ : ''
108
132
  )}
109
133
  style={isInline ? { display: 'inline-block' } : {}}
110
134
  data-node-overlay={nodeId}
135
+ onMouseDown={
136
+ isReorderMode && !isDragging ? handleReorderStart : undefined
137
+ }
111
138
  >
112
139
  {children}
113
140
 
114
- {/* Text Mode: Tool Cart */}
115
141
  {toolMode === 'text' && (
116
142
  <div
117
143
  ref={chromeRef}
@@ -156,18 +182,19 @@ export const NodeOverlay = ({
156
182
  </div>
157
183
  )}
158
184
 
159
- {/* Insert Mode: Split Drop Zones */}
160
- {toolMode === 'insert' && toolAddMode !== `span` && (
185
+ {showZones && (
161
186
  <div
162
187
  className="compositor-chrome absolute inset-0 z-50 flex flex-col"
163
188
  data-attr="exclude"
164
189
  >
165
- {/* Top / Before Zone */}
166
190
  <div
167
191
  className={classNames(
168
192
  'flex-1 transition-colors duration-200',
169
- canInsert.allowInsertBefore
170
- ? 'cursor-pointer hover:bg-blue-500/10'
193
+ activeLocation === 'before'
194
+ ? 'bg-blue-500 bg-opacity-10'
195
+ : 'hover:bg-blue-500 hover:bg-opacity-10',
196
+ canInsert.allowInsertBefore || isReorderMode
197
+ ? 'cursor-pointer'
171
198
  : 'cursor-not-allowed opacity-0'
172
199
  )}
173
200
  onMouseEnter={() => setHoverZone('before')}
@@ -176,7 +203,7 @@ export const NodeOverlay = ({
176
203
  canInsert.allowInsertBefore && handleInsert('before', e)
177
204
  }
178
205
  >
179
- {canInsert.allowInsertBefore && (
206
+ {toolMode === 'insert' && canInsert.allowInsertBefore && (
180
207
  <div
181
208
  className={classNames(
182
209
  'absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 transform transition-opacity duration-200',
@@ -190,12 +217,14 @@ export const NodeOverlay = ({
190
217
  )}
191
218
  </div>
192
219
 
193
- {/* Bottom / After Zone */}
194
220
  <div
195
221
  className={classNames(
196
222
  'flex-1 transition-colors duration-200',
197
- canInsert.allowInsertAfter
198
- ? 'cursor-pointer hover:bg-blue-500/10'
223
+ activeLocation === 'after'
224
+ ? 'bg-blue-500 bg-opacity-10'
225
+ : 'hover:bg-blue-500 hover:bg-opacity-10',
226
+ canInsert.allowInsertAfter || isReorderMode
227
+ ? 'cursor-pointer'
199
228
  : 'cursor-not-allowed opacity-0'
200
229
  )}
201
230
  onMouseEnter={() => setHoverZone('after')}
@@ -204,7 +233,7 @@ export const NodeOverlay = ({
204
233
  canInsert.allowInsertAfter && handleInsert('after', e)
205
234
  }
206
235
  >
207
- {canInsert.allowInsertAfter && (
236
+ {toolMode === 'insert' && canInsert.allowInsertAfter && (
208
237
  <div
209
238
  className={classNames(
210
239
  'absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 transform transition-opacity duration-200',
@@ -3,6 +3,8 @@ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
3
3
  import { getCtx } from '@/stores/nodes';
4
4
  import { toggleSettingsPanel } from '@/stores/storykeep';
5
5
  import { toolAddModeTitles, toolAddModes } from '@/constants';
6
+ import { startToolDrag } from '@/stores/toolDrag';
7
+ import { initToolDragListeners } from '@/utils/compositor/toolDragManager';
6
8
 
7
9
  import type { ToolAddMode } from '@/types/compositorTypes';
8
10
 
@@ -18,11 +20,18 @@ const AddElementsPanel = ({
18
20
  ctx.notifyNode('root');
19
21
  };
20
22
 
23
+ const handleMouseDown = (e: React.MouseEvent, mode: ToolAddMode) => {
24
+ e.preventDefault();
25
+ startToolDrag('insert', mode, e.clientX, e.clientY);
26
+ initToolDragListeners();
27
+ };
28
+
21
29
  return (
22
30
  <>
23
31
  {toolAddModes.map((mode) => (
24
32
  <button
25
33
  key={mode}
34
+ onMouseDown={(e) => handleMouseDown(e, mode)}
26
35
  onClick={() => handleElementClick(mode)}
27
36
  className={`rounded px-3 py-1.5 text-sm font-bold transition-colors ${
28
37
  currentToolAddMode === mode
@@ -39,12 +48,9 @@ const AddElementsPanel = ({
39
48
 
40
49
  const StoryKeepToolBar = () => {
41
50
  const ctx = getCtx();
42
-
43
- // Connect to stores
44
51
  const { value: toolModeVal } = useStore(ctx.toolModeValStore);
45
52
  const { value: toolAddModeVal } = useStore(ctx.toolAddModeStore);
46
53
 
47
- // Only show when in insert mode
48
54
  if (toolModeVal !== 'insert') {
49
55
  return null;
50
56
  }
@@ -67,6 +73,9 @@ const StoryKeepToolBar = () => {
67
73
  <div className="flex flex-wrap gap-x-2 gap-y-1">
68
74
  <AddElementsPanel currentToolAddMode={toolAddModeVal} />
69
75
  </div>
76
+ <p className="px-2 pt-4 text-xs">
77
+ Drag and drop, or select element and click the + to insert into a pane.
78
+ </p>
70
79
  </div>
71
80
  );
72
81
  };
@@ -2,11 +2,8 @@ import { useRef, useEffect } from 'react';
2
2
  import { useStore } from '@nanostores/react';
3
3
  import PencilSquareIcon from '@heroicons/react/24/outline/PencilSquareIcon';
4
4
  import PaintBrushIcon from '@heroicons/react/24/outline/PaintBrushIcon';
5
- import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
6
- import SparklesIcon from '@heroicons/react/24/outline/SparklesIcon';
7
5
  import ArrowsUpDownIcon from '@heroicons/react/24/outline/ArrowsUpDownIcon';
8
6
  import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
9
- import BugAntIcon from '@heroicons/react/24/outline/BugAntIcon';
10
7
  import LinkIcon from '@heroicons/react/24/solid/LinkIcon';
11
8
  import ChatBubbleBottomCenterTextIcon from '@heroicons/react/24/outline/ChatBubbleBottomCenterTextIcon';
12
9
  import XMarkIcon from '@heroicons/react/24/solid/XMarkIcon';
@@ -29,6 +26,12 @@ const storykeepToolModes = [
29
26
  title: 'Add',
30
27
  description: 'Add new element, e.g. paragraph or image',
31
28
  },
29
+ {
30
+ key: 'reorder',
31
+ Icon: ArrowsUpDownIcon,
32
+ title: 'Reorder',
33
+ description: 'Drag and drop to reorder elements',
34
+ },
32
35
  ] as const;
33
36
 
34
37
  interface StoryKeepToolModeProps {
@@ -13,6 +13,9 @@ import ArchiveBoxArrowDownIcon from '@heroicons/react/24/outline/ArchiveBoxArrow
13
13
  import ArrowPathRoundedSquareIcon from '@heroicons/react/24/outline/ArrowPathRoundedSquareIcon';
14
14
  import ArrowDownTrayIcon from '@heroicons/react/24/outline/ArrowDownTrayIcon';
15
15
  import SparklesIcon from '@heroicons/react/24/solid/SparklesIcon';
16
+ import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
17
+ import ArrowUpIcon from '@heroicons/react/24/outline/ArrowUpIcon';
18
+ import ArrowDownIcon from '@heroicons/react/24/outline/ArrowDownIcon';
16
19
  import {
17
20
  isContextPaneNode,
18
21
  hasBeliefPayload,
@@ -27,7 +30,11 @@ import { AiRestylePaneModal } from '@/components/edit/pane/AiRestylePaneModal';
27
30
  import PaneTitlePanel from './PanePanel_title';
28
31
  import PaneMagicPathPanel from './PanePanel_path';
29
32
  import PaneImpressionPanel from './PanePanel_impression';
30
- import { PaneConfigMode, type PaneNode } from '@/types/compositorTypes';
33
+ import {
34
+ PaneConfigMode,
35
+ type PaneNode,
36
+ type StoryFragmentNode,
37
+ } from '@/types/compositorTypes';
31
38
 
32
39
  interface ConfigPanePanelProps {
33
40
  nodeId: string;
@@ -61,6 +68,23 @@ const ConfigPanePanel = ({
61
68
  const buttonClass =
62
69
  'px-2 py-1 bg-white text-cyan-700 text-sm rounded hover:bg-cyan-700 hover:text-white focus:bg-cyan-700 focus:text-white shadow-sm transition-colors whitespace-nowrap mb-1';
63
70
 
71
+ // Determine Position for Reordering
72
+ const parentNode = paneNode.parentId
73
+ ? (allNodes.get(paneNode.parentId) as StoryFragmentNode)
74
+ : null;
75
+ let isFirst = false;
76
+ let isLast = false;
77
+
78
+ if (parentNode && parentNode.nodeType === 'StoryFragment') {
79
+ if (parentNode.paneIds && Array.isArray(parentNode.paneIds)) {
80
+ const idx = parentNode.paneIds.indexOf(nodeId);
81
+ if (idx !== -1) {
82
+ isFirst = idx === 0;
83
+ isLast = idx === parentNode.paneIds.length - 1;
84
+ }
85
+ }
86
+ }
87
+
64
88
  const [mode, setMode] = useState<PaneConfigMode>(
65
89
  isActiveMode && activePaneMode.mode
66
90
  ? (activePaneMode.mode as PaneConfigMode)
@@ -147,6 +171,30 @@ const ConfigPanePanel = ({
147
171
  }
148
172
  };
149
173
 
174
+ // Delete & Reorder Handlers
175
+ const handleDelete = (e: MouseEvent) => {
176
+ e.stopPropagation();
177
+ if (window.confirm('Are you sure you want to delete this pane?')) {
178
+ ctx.deleteNode(nodeId);
179
+ }
180
+ };
181
+
182
+ const handleMoveUp = (e: MouseEvent) => {
183
+ e.stopPropagation();
184
+ if (!isFirst) {
185
+ ctx.moveNode(nodeId, 'before');
186
+ if (paneNode.parentId) ctx.notifyNode(paneNode.parentId);
187
+ }
188
+ };
189
+
190
+ const handleMoveDown = (e: MouseEvent) => {
191
+ e.stopPropagation();
192
+ if (!isLast) {
193
+ ctx.moveNode(nodeId, 'after');
194
+ if (paneNode.parentId) ctx.notifyNode(paneNode.parentId);
195
+ }
196
+ };
197
+
150
198
  if (mode === PaneConfigMode.TITLE) {
151
199
  return <PaneTitlePanel nodeId={nodeId} setMode={setSaveMode} />;
152
200
  } else if (mode === PaneConfigMode.PATH) {
@@ -251,8 +299,46 @@ const ConfigPanePanel = ({
251
299
  )}
252
300
  </div>
253
301
 
254
- {/* Design Library Tools (Right Aligned) */}
255
- <div className="ml-auto flex items-center gap-2 border-l border-gray-300 px-2">
302
+ {/* Right Aligned Tools */}
303
+ <div className="ml-auto flex items-center gap-2 px-2">
304
+ {/* Delete & Reorder Tools */}
305
+ {!isTemplate && !isContextPane && !isHtmlAstPane && (
306
+ <div className="flex items-center gap-1 border-r border-gray-300 pr-2">
307
+ <button
308
+ onClick={handleMoveUp}
309
+ disabled={isFirst}
310
+ title={isFirst ? 'First pane' : 'Move pane up'}
311
+ className={`flex h-7 w-7 items-center justify-center rounded-full p-1 shadow-sm transition-colors ${
312
+ isFirst
313
+ ? 'cursor-not-allowed bg-gray-200 text-gray-400'
314
+ : 'bg-white text-gray-600 hover:bg-gray-100'
315
+ }`}
316
+ >
317
+ <ArrowUpIcon className="h-4 w-4" />
318
+ </button>
319
+ <button
320
+ onClick={handleMoveDown}
321
+ disabled={isLast}
322
+ title={isLast ? 'Last pane' : 'Move pane down'}
323
+ className={`flex h-7 w-7 items-center justify-center rounded-full p-1 shadow-sm transition-colors ${
324
+ isLast
325
+ ? 'cursor-not-allowed bg-gray-200 text-gray-400'
326
+ : 'bg-white text-gray-600 hover:bg-gray-100'
327
+ }`}
328
+ >
329
+ <ArrowDownIcon className="h-4 w-4" />
330
+ </button>
331
+ <button
332
+ onClick={handleDelete}
333
+ title="Delete Pane"
334
+ className="flex h-7 w-7 items-center justify-center rounded-full bg-white p-1 text-red-500 shadow-sm hover:bg-red-50 hover:text-red-700"
335
+ >
336
+ <TrashIcon className="h-4 w-4" />
337
+ </button>
338
+ </div>
339
+ )}
340
+
341
+ {/* Design Library Tools */}
256
342
  {!isHtmlAstPane && !isSandboxMode && (
257
343
  <button
258
344
  title="Save Pane to Design Library"
@@ -12,6 +12,7 @@ import StoryKeepToolBar from '@/components/edit/ToolBar';
12
12
  import StoryKeepToolMode from '@/components/edit/ToolMode';
13
13
  import SettingsPanel from '@/components/edit/SettingsPanel';
14
14
  import { Compositor } from '@/components/compositor/Compositor';
15
+ import ToolDragLayer from '@/components/compositor/ToolDragLayer';
15
16
  import { requireAdminOrEditor } from '@/utils/auth';
16
17
  import { preHealthCheck } from '@/utils/backend';
17
18
 
@@ -217,6 +218,7 @@ for (const [key, value] of Astro.url.searchParams) {
217
218
  />
218
219
  </div>
219
220
  </aside>
221
+ <ToolDragLayer client:only="react" />
220
222
  </Layout>
221
223
 
222
224
  <script>
@@ -0,0 +1,57 @@
1
+ import { map } from 'nanostores';
2
+
3
+ export type DragType = 'insert' | 'reorder';
4
+
5
+ export interface ToolDragState {
6
+ isDragging: boolean;
7
+ dragType: DragType | null;
8
+ payload: string | null;
9
+ pointer: { x: number; y: number };
10
+ activeDropZone: {
11
+ nodeId: string;
12
+ location: 'before' | 'after';
13
+ } | null;
14
+ }
15
+
16
+ export const toolDragStore = map<ToolDragState>({
17
+ isDragging: false,
18
+ dragType: null,
19
+ payload: null,
20
+ pointer: { x: 0, y: 0 },
21
+ activeDropZone: null,
22
+ });
23
+
24
+ export const startToolDrag = (
25
+ type: DragType,
26
+ payload: string,
27
+ startX: number,
28
+ startY: number
29
+ ) => {
30
+ toolDragStore.set({
31
+ isDragging: true,
32
+ dragType: type,
33
+ payload,
34
+ pointer: { x: startX, y: startY },
35
+ activeDropZone: null,
36
+ });
37
+ };
38
+
39
+ export const updateToolDragPosition = (x: number, y: number) => {
40
+ toolDragStore.setKey('pointer', { x, y });
41
+ };
42
+
43
+ export const updateActiveDropZone = (
44
+ zone: { nodeId: string; location: 'before' | 'after' } | null
45
+ ) => {
46
+ toolDragStore.setKey('activeDropZone', zone);
47
+ };
48
+
49
+ export const endToolDrag = () => {
50
+ toolDragStore.set({
51
+ isDragging: false,
52
+ dragType: null,
53
+ payload: null,
54
+ pointer: { x: 0, y: 0 },
55
+ activeDropZone: null,
56
+ });
57
+ };
@@ -4,7 +4,7 @@ export type LispToken = string | number | LispToken[];
4
4
 
5
5
  export type ViewportKey = 'mobile' | 'tablet' | 'desktop' | 'auto';
6
6
  export type ViewportAuto = 'mobile' | 'tablet' | 'desktop';
7
- export type ToolModeVal = 'text' | 'insert';
7
+ export type ToolModeVal = 'text' | 'insert' | 'reorder';
8
8
 
9
9
  export const toolAddModes = [
10
10
  'p',
@@ -0,0 +1,69 @@
1
+ import {
2
+ toolDragStore,
3
+ updateToolDragPosition,
4
+ updateActiveDropZone,
5
+ endToolDrag,
6
+ } from '@/stores/toolDrag';
7
+ import { getCtx } from '@/stores/nodes';
8
+ import { getTemplateNode } from '@/utils/compositor/nodesHelper';
9
+ import type { ToolAddMode } from '@/types/compositorTypes';
10
+
11
+ export const initToolDragListeners = () => {
12
+ const handleMouseMove = (e: MouseEvent) => {
13
+ const state = toolDragStore.get();
14
+ if (!state.isDragging) return;
15
+
16
+ e.preventDefault();
17
+ updateToolDragPosition(e.clientX, e.clientY);
18
+
19
+ const elements = document.elementsFromPoint(e.clientX, e.clientY);
20
+ const targetOverlay = elements.find((el) =>
21
+ el.hasAttribute('data-node-overlay')
22
+ );
23
+
24
+ if (!targetOverlay) {
25
+ updateActiveDropZone(null);
26
+ return;
27
+ }
28
+
29
+ const nodeId = targetOverlay.getAttribute('data-node-overlay');
30
+ if (!nodeId) return;
31
+
32
+ if (state.dragType === 'reorder' && state.payload === nodeId) {
33
+ updateActiveDropZone(null);
34
+ return;
35
+ }
36
+
37
+ const rect = targetOverlay.getBoundingClientRect();
38
+ const midPoint = rect.top + rect.height / 2;
39
+ const location = e.clientY < midPoint ? 'before' : 'after';
40
+
41
+ updateActiveDropZone({ nodeId, location });
42
+ };
43
+
44
+ const handleMouseUp = (_: MouseEvent) => {
45
+ const state = toolDragStore.get();
46
+ if (!state.isDragging) return;
47
+
48
+ if (state.activeDropZone) {
49
+ const ctx = getCtx();
50
+ const { nodeId, location } = state.activeDropZone;
51
+
52
+ if (state.dragType === 'reorder' && state.payload) {
53
+ ctx.moveNodeTo(state.payload, nodeId, location);
54
+ } else if (state.dragType === 'insert' && state.payload) {
55
+ const template = getTemplateNode(state.payload as ToolAddMode);
56
+ if (template) {
57
+ ctx.addTemplateNode(nodeId, template, nodeId, location);
58
+ }
59
+ }
60
+ }
61
+
62
+ endToolDrag();
63
+ window.removeEventListener('mousemove', handleMouseMove);
64
+ window.removeEventListener('mouseup', handleMouseUp);
65
+ };
66
+
67
+ window.addEventListener('mousemove', handleMouseMove);
68
+ window.addEventListener('mouseup', handleMouseUp);
69
+ };
@@ -95,6 +95,10 @@ export async function injectTemplateFiles(
95
95
  src: resolve('../templates/src/components/compositor/Node.tsx'),
96
96
  dest: 'src/components/compositor/Node.tsx',
97
97
  },
98
+ {
99
+ src: resolve('../templates/src/components/compositor/ToolDragLayer.tsx'),
100
+ dest: 'src/components/compositor/ToolDragLayer.tsx',
101
+ },
98
102
  {
99
103
  src: resolve(
100
104
  '../templates/src/components/compositor/tools/NodeOverlay.tsx'
@@ -576,6 +580,10 @@ export async function injectTemplateFiles(
576
580
  src: resolve('../templates/src/stores/backend.ts'),
577
581
  dest: 'src/stores/backend.ts',
578
582
  },
583
+ {
584
+ src: resolve('../templates/src/stores/toolDrag.ts'),
585
+ dest: 'src/stores/toolDrag.ts',
586
+ },
579
587
  {
580
588
  src: resolve('../templates/src/stores/resources.ts'),
581
589
  dest: 'src/stores/resources.ts',
@@ -621,6 +629,10 @@ export async function injectTemplateFiles(
621
629
  src: resolve('../templates/src/utils/compositor/savePipeline.ts'),
622
630
  dest: 'src/utils/compositor/savePipeline.ts',
623
631
  },
632
+ {
633
+ src: resolve('../templates/src/utils/compositor/toolDragManager.ts'),
634
+ dest: 'src/utils/compositor/toolDragManager.ts',
635
+ },
624
636
  {
625
637
  src: resolve('../templates/src/utils/compositor/aiPaneParser.ts'),
626
638
  dest: 'src/utils/compositor/aiPaneParser.ts',