astro-tractstack 2.2.6 → 2.2.7

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.7",
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 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 {
@@ -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',