southpaw 0.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/README.md +51 -0
- package/api/client.ts +83 -0
- package/api/helpers/fs.ts +98 -0
- package/api/helpers/stream.ts +6 -0
- package/api/index.ts +50 -0
- package/api/server.ts +566 -0
- package/constants/fonts.ts +1583 -0
- package/constants/icons.ts +2124 -0
- package/constants/langs.ts +2592 -0
- package/constants/ssr-loading.tsx +17 -0
- package/contexts/dev.tsx +124 -0
- package/hooks/useDebounceCb.ts +21 -0
- package/modules/app.tsx +102 -0
- package/modules/behaviors.ts +161 -0
- package/modules/component.tsx +483 -0
- package/modules/config/index.ts +8 -0
- package/modules/database.ts +25 -0
- package/modules/document.tsx +23 -0
- package/modules/error.tsx +16 -0
- package/modules/page/autozoom.tsx +92 -0
- package/modules/page/browser.tsx +166 -0
- package/modules/page/index.tsx +327 -0
- package/modules/page/insert-modal.tsx +225 -0
- package/modules/page/inspect/index.tsx +237 -0
- package/modules/page/inspect/wrapper.tsx +88 -0
- package/modules/page/knobs/action.tsx +58 -0
- package/modules/page/knobs/color.tsx +78 -0
- package/modules/page/knobs/helpers.ts +71 -0
- package/modules/page/knobs/icon.tsx +85 -0
- package/modules/page/knobs/linker.tsx +188 -0
- package/modules/page/knobs/list-json.tsx +149 -0
- package/modules/page/knobs/list-text.tsx +129 -0
- package/modules/page/knobs/number.tsx +84 -0
- package/modules/page/knobs/segmented.tsx +81 -0
- package/modules/page/knobs/select.tsx +89 -0
- package/modules/page/knobs/size.tsx +106 -0
- package/modules/page/knobs/switch.tsx +75 -0
- package/modules/page/knobs/text.tsx +93 -0
- package/modules/page/left-tabs.tsx +36 -0
- package/modules/page/right-tabs.tsx +31 -0
- package/modules/page/seo.tsx +69 -0
- package/modules/page/tree.tsx +860 -0
- package/modules/state.ts +787 -0
- package/modules/store.ts +37 -0
- package/modules/theme.ts +105 -0
- package/modules/translations.ts +15 -0
- package/package.json +68 -0
- package/scripts/bin.js +607 -0
- package/scripts/test.js +145 -0
- package/themes/base.tsx +8962 -0
- package/themes/index.ts +4 -0
- package/themes/m2.ts +39 -0
- package/themes/newspaper.ts +39 -0
- package/types/api.ts +57 -0
- package/types/fiber_node.ts +69 -0
- package/types/react_element.ts +14 -0
- package/ui/accordion.tsx +70 -0
- package/ui/actionicon.tsx +41 -0
- package/ui/affix.tsx +14 -0
- package/ui/alert.tsx +37 -0
- package/ui/anchor.tsx +52 -0
- package/ui/arrow.tsx +116 -0
- package/ui/aspectratio.tsx +21 -0
- package/ui/autocomplete.tsx +34 -0
- package/ui/avatar.tsx +36 -0
- package/ui/backgroundimage.tsx +16 -0
- package/ui/badge.tsx +28 -0
- package/ui/blockquote.tsx +15 -0
- package/ui/blocks/faq.tsx +145 -0
- package/ui/blocks/features.tsx +167 -0
- package/ui/blocks/footer.tsx +182 -0
- package/ui/blocks/herobanner.tsx +391 -0
- package/ui/blocks/index.ts +44 -0
- package/ui/blocks/leftnav.tsx +141 -0
- package/ui/blocks/pricing.tsx +210 -0
- package/ui/blocks/testimonials.tsx +149 -0
- package/ui/blocks/uidocs.tsx +1429 -0
- package/ui/box.tsx +27 -0
- package/ui/breadcrumbs.tsx +33 -0
- package/ui/burger.tsx +23 -0
- package/ui/button.tsx +85 -0
- package/ui/card.tsx +28 -0
- package/ui/center.tsx +26 -0
- package/ui/checkbox.tsx +35 -0
- package/ui/chip.tsx +32 -0
- package/ui/closebutton.tsx +15 -0
- package/ui/code.tsx +104 -0
- package/ui/collapse.tsx +16 -0
- package/ui/colorinput.tsx +32 -0
- package/ui/colorpicker.tsx +26 -0
- package/ui/colorswatch.tsx +17 -0
- package/ui/combobox.tsx +80 -0
- package/ui/container.tsx +27 -0
- package/ui/dialog.tsx +15 -0
- package/ui/divider.tsx +58 -0
- package/ui/drawer.tsx +28 -0
- package/ui/fieldset.tsx +15 -0
- package/ui/filebutton.tsx +23 -0
- package/ui/fileinput.tsx +36 -0
- package/ui/flex.tsx +139 -0
- package/ui/focustrap.tsx +15 -0
- package/ui/form.tsx +26 -0
- package/ui/grid.tsx +69 -0
- package/ui/highlight.tsx +15 -0
- package/ui/hoverable.tsx +66 -0
- package/ui/hovercard.tsx +45 -0
- package/ui/icon.tsx +76 -0
- package/ui/image.tsx +32 -0
- package/ui/index.ts +1355 -0
- package/ui/indicator.tsx +42 -0
- package/ui/infobox.tsx +200 -0
- package/ui/input.tsx +18 -0
- package/ui/inviewport.tsx +23 -0
- package/ui/jsoninput.tsx +35 -0
- package/ui/kbd.tsx +20 -0
- package/ui/list.tsx +33 -0
- package/ui/loader.tsx +26 -0
- package/ui/loadingoverlay.tsx +22 -0
- package/ui/mark.tsx +15 -0
- package/ui/menu.tsx +89 -0
- package/ui/modal.tsx +27 -0
- package/ui/multiselect.tsx +42 -0
- package/ui/nativeselect.tsx +17 -0
- package/ui/navlink.tsx +40 -0
- package/ui/notification.tsx +25 -0
- package/ui/numberformatter.tsx +15 -0
- package/ui/numberinput.tsx +37 -0
- package/ui/overlay.tsx +15 -0
- package/ui/pagination.tsx +21 -0
- package/ui/paper.tsx +33 -0
- package/ui/passwordinput.tsx +36 -0
- package/ui/pill.tsx +17 -0
- package/ui/pillsinput.tsx +37 -0
- package/ui/pininput.tsx +30 -0
- package/ui/popover.tsx +43 -0
- package/ui/portal.tsx +15 -0
- package/ui/progress.tsx +49 -0
- package/ui/props.ts +91 -0
- package/ui/propsDef.ts +38 -0
- package/ui/radio.tsx +30 -0
- package/ui/rating.tsx +25 -0
- package/ui/ringprogress.tsx +17 -0
- package/ui/scrollarea.tsx +27 -0
- package/ui/segmentedcontrol.tsx +38 -0
- package/ui/select.tsx +43 -0
- package/ui/simplegrid.tsx +23 -0
- package/ui/skeleton.tsx +15 -0
- package/ui/slider.tsx +31 -0
- package/ui/space.tsx +19 -0
- package/ui/spoiler.tsx +23 -0
- package/ui/stepper.tsx +63 -0
- package/ui/switch.tsx +29 -0
- package/ui/table.tsx +93 -0
- package/ui/tabs.tsx +80 -0
- package/ui/tagsinput.tsx +19 -0
- package/ui/terminal.tsx +44 -0
- package/ui/text.tsx +168 -0
- package/ui/textarea.tsx +36 -0
- package/ui/textinput.tsx +34 -0
- package/ui/timeline.tsx +30 -0
- package/ui/title.tsx +42 -0
- package/ui/tooltip.tsx +32 -0
- package/ui/transition.tsx +21 -0
- package/ui/tree.tsx +42 -0
- package/ui/typographystylesprovider.tsx +20 -0
- package/ui/unstyledbutton.tsx +18 -0
- package/ui/video.tsx +16 -0
- package/ui/visuallyhidden.tsx +15 -0
- package/utils/index.ts +76 -0
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionIcon,
|
|
3
|
+
Center,
|
|
4
|
+
Flex,
|
|
5
|
+
Group,
|
|
6
|
+
Menu,
|
|
7
|
+
RenderTreeNodePayload,
|
|
8
|
+
Tabs,
|
|
9
|
+
Text,
|
|
10
|
+
Tree,
|
|
11
|
+
TreeProps,
|
|
12
|
+
useTree,
|
|
13
|
+
} from "@mantine/core";
|
|
14
|
+
import { modals } from "@mantine/modals";
|
|
15
|
+
import {
|
|
16
|
+
FC,
|
|
17
|
+
FunctionComponent,
|
|
18
|
+
useCallback,
|
|
19
|
+
useEffect,
|
|
20
|
+
useRef,
|
|
21
|
+
useState,
|
|
22
|
+
} from "react";
|
|
23
|
+
import { ReactElementType } from "../../types/react_element";
|
|
24
|
+
import {
|
|
25
|
+
IconChevronDown,
|
|
26
|
+
IconChevronRight,
|
|
27
|
+
IconChevronUp,
|
|
28
|
+
IconCircle,
|
|
29
|
+
IconComponents,
|
|
30
|
+
IconCopy,
|
|
31
|
+
IconDotsVertical,
|
|
32
|
+
IconGripVertical,
|
|
33
|
+
IconList,
|
|
34
|
+
IconPlus,
|
|
35
|
+
IconTrash,
|
|
36
|
+
} from "@tabler/icons-react";
|
|
37
|
+
import {
|
|
38
|
+
DndContext,
|
|
39
|
+
DragEndEvent,
|
|
40
|
+
DragMoveEvent,
|
|
41
|
+
MeasuringStrategy,
|
|
42
|
+
PointerSensor,
|
|
43
|
+
useSensor,
|
|
44
|
+
useSensors,
|
|
45
|
+
} from "@dnd-kit/core";
|
|
46
|
+
import { SortableContext, useSortable } from "@dnd-kit/sortable";
|
|
47
|
+
import { CSS as dndCSS } from "@dnd-kit/utilities";
|
|
48
|
+
import { ModalInsert } from "./insert-modal";
|
|
49
|
+
import { FETCH } from "southpaw/api/client";
|
|
50
|
+
import { TNode, useContextDev } from "southpaw/contexts/dev";
|
|
51
|
+
import { FiberNodeType } from "../../types/fiber_node";
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Types
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
type DropPosition = "before" | "after" | "within";
|
|
58
|
+
|
|
59
|
+
type DragOverData = {
|
|
60
|
+
overId: string | null;
|
|
61
|
+
position: DropPosition | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type TTreePayload = Omit<RenderTreeNodePayload, "node"> & { node: TNode };
|
|
65
|
+
|
|
66
|
+
type ReactJsxEl = ReactElementType | string | number | ReactJsxEl[];
|
|
67
|
+
|
|
68
|
+
type Props = { jsx: ReactElementType };
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Pure helpers — defined outside the component so closures are never stale
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
function flattenIds(items: TreeProps["data"]): string[] {
|
|
75
|
+
return items.reduce<string[]>((acc, item) => {
|
|
76
|
+
acc.push(item.value);
|
|
77
|
+
if (item.children?.length) acc.push(...flattenIds(item.children));
|
|
78
|
+
return acc;
|
|
79
|
+
}, []);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findNodeByValue(
|
|
83
|
+
items: TreeProps["data"],
|
|
84
|
+
value: string,
|
|
85
|
+
): TreeProps["data"][number] | null {
|
|
86
|
+
for (const item of items) {
|
|
87
|
+
if (item.value === value) return item;
|
|
88
|
+
if (item.children?.length) {
|
|
89
|
+
const found = findNodeByValue(item.children, value);
|
|
90
|
+
if (found) return found;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function moveNodeOptimistically(
|
|
97
|
+
currentData: TreeProps["data"],
|
|
98
|
+
activeId: string,
|
|
99
|
+
overId: string,
|
|
100
|
+
position: DropPosition,
|
|
101
|
+
): TreeProps["data"] {
|
|
102
|
+
let activeNode: TreeProps["data"][number] | null = null;
|
|
103
|
+
|
|
104
|
+
const removeNode = (items: TreeProps["data"]): TreeProps["data"] =>
|
|
105
|
+
items
|
|
106
|
+
.filter((item) => {
|
|
107
|
+
if (item.value === activeId) {
|
|
108
|
+
activeNode = item;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
})
|
|
113
|
+
.map((item) => ({ ...item, children: removeNode(item.children ?? []) }));
|
|
114
|
+
|
|
115
|
+
const withoutActive = removeNode(currentData);
|
|
116
|
+
if (!activeNode) return currentData;
|
|
117
|
+
|
|
118
|
+
const insertNode = (items: TreeProps["data"]): TreeProps["data"] => {
|
|
119
|
+
const idx = items.findIndex((i) => i.value === overId);
|
|
120
|
+
if (idx !== -1) {
|
|
121
|
+
const copy = [...items];
|
|
122
|
+
if (position === "before") {
|
|
123
|
+
copy.splice(idx, 0, activeNode!);
|
|
124
|
+
return copy;
|
|
125
|
+
}
|
|
126
|
+
if (position === "after") {
|
|
127
|
+
copy.splice(idx + 1, 0, activeNode!);
|
|
128
|
+
return copy;
|
|
129
|
+
}
|
|
130
|
+
// "within" — append as last child of the target
|
|
131
|
+
return copy.map((item) =>
|
|
132
|
+
item.value === overId
|
|
133
|
+
? { ...item, children: [...(item.children ?? []), activeNode!] }
|
|
134
|
+
: item,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return items.map((item) => ({
|
|
138
|
+
...item,
|
|
139
|
+
children: insertNode(item.children ?? []),
|
|
140
|
+
}));
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return insertNode(withoutActive);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function safeParse(str: string): unknown {
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(str);
|
|
149
|
+
} catch {
|
|
150
|
+
return str;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns the [data-value] of the element under the pointer.
|
|
156
|
+
*
|
|
157
|
+
* The actively-dragged SortableLeaf has `pointer-events: none`, so
|
|
158
|
+
* elementFromPoint skips the ghost and returns the actual drop target
|
|
159
|
+
* underneath — even when dragging a non-child over a parent that already
|
|
160
|
+
* has visible children.
|
|
161
|
+
*/
|
|
162
|
+
function getNodeValueAtPoint(x: number, y: number): string | null {
|
|
163
|
+
const el = document.elementFromPoint(x, y);
|
|
164
|
+
if (!el) return null;
|
|
165
|
+
return el.closest<HTMLElement>("[data-value]")?.dataset.value ?? null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Given a target node value and the current pointer Y, decide whether the
|
|
170
|
+
* pointer is in the top-25% (before), bottom-25% (after), or middle-50%
|
|
171
|
+
* (within) zone of that node's row.
|
|
172
|
+
*/
|
|
173
|
+
function computeDropPosition(
|
|
174
|
+
overId: string,
|
|
175
|
+
pointerY: number,
|
|
176
|
+
overNode: TreeProps["data"][number] | null,
|
|
177
|
+
): DropPosition {
|
|
178
|
+
const el = document.querySelector<HTMLElement>(
|
|
179
|
+
`[data-value="${CSS.escape(overId)}"]`,
|
|
180
|
+
);
|
|
181
|
+
if (!el) return "after";
|
|
182
|
+
|
|
183
|
+
const rect = el.getBoundingClientRect();
|
|
184
|
+
const relY = pointerY - rect.top;
|
|
185
|
+
|
|
186
|
+
const canNest =
|
|
187
|
+
overNode != null &&
|
|
188
|
+
Object.keys(overNode.nodeProps ?? {}).includes("children");
|
|
189
|
+
|
|
190
|
+
if (canNest) {
|
|
191
|
+
const threshold = rect.height / 4;
|
|
192
|
+
if (relY < threshold) return "before";
|
|
193
|
+
if (relY > rect.height - threshold) return "after";
|
|
194
|
+
return "within";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return relY < rect.height / 2 ? "before" : "after";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// LeafIcon
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
const LeafIcon: FC<{ label: string; isFolder: boolean; expanded: boolean }> = ({
|
|
205
|
+
label,
|
|
206
|
+
isFolder,
|
|
207
|
+
expanded,
|
|
208
|
+
}) => {
|
|
209
|
+
if (isFolder) {
|
|
210
|
+
return expanded ? (
|
|
211
|
+
<IconChevronDown strokeWidth="1px" size={16} />
|
|
212
|
+
) : (
|
|
213
|
+
<IconChevronRight strokeWidth="1px" size={16} />
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return label.startsWith("Blocks.") ? (
|
|
217
|
+
<IconComponents size={16} />
|
|
218
|
+
) : (
|
|
219
|
+
<IconCircle size={16} />
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// LeafMenu
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
const LeafMenu: FC<{
|
|
228
|
+
node: TNode;
|
|
229
|
+
level: number;
|
|
230
|
+
dispatch: ReturnType<typeof useContextDev>["dispatch"];
|
|
231
|
+
cannotInsertWithin: boolean;
|
|
232
|
+
isRoot: boolean;
|
|
233
|
+
}> = ({ node, level, dispatch, cannotInsertWithin, isRoot }) => (
|
|
234
|
+
<Menu shadow="md" width={200} withinPortal={false}>
|
|
235
|
+
<Menu.Target>
|
|
236
|
+
<ActionIcon
|
|
237
|
+
flex="none"
|
|
238
|
+
onClick={(e) => e.stopPropagation()}
|
|
239
|
+
mr="sm"
|
|
240
|
+
p={2}
|
|
241
|
+
size="compact-xs"
|
|
242
|
+
variant="subtle"
|
|
243
|
+
color="dark"
|
|
244
|
+
>
|
|
245
|
+
<IconDotsVertical size={20} />
|
|
246
|
+
</ActionIcon>
|
|
247
|
+
</Menu.Target>
|
|
248
|
+
|
|
249
|
+
<Menu.Dropdown>
|
|
250
|
+
<Menu.Item
|
|
251
|
+
disabled={level === 1}
|
|
252
|
+
onClick={(e) => {
|
|
253
|
+
e.stopPropagation();
|
|
254
|
+
dispatch({
|
|
255
|
+
type: "TREE_SET_MENU_NODE",
|
|
256
|
+
args: { node, insertMode: "before" },
|
|
257
|
+
});
|
|
258
|
+
dispatch({ type: "MODALS_OPEN", args: { name: "insert" } });
|
|
259
|
+
}}
|
|
260
|
+
leftSection={
|
|
261
|
+
<IconList size={20} style={{ transform: "scale(-1,1)" }} />
|
|
262
|
+
}
|
|
263
|
+
>
|
|
264
|
+
Insert before
|
|
265
|
+
</Menu.Item>
|
|
266
|
+
|
|
267
|
+
<Menu.Item
|
|
268
|
+
disabled={cannotInsertWithin}
|
|
269
|
+
onClick={(e) => {
|
|
270
|
+
e.stopPropagation();
|
|
271
|
+
dispatch({
|
|
272
|
+
type: "TREE_SET_MENU_NODE",
|
|
273
|
+
args: { node, insertMode: "within" },
|
|
274
|
+
});
|
|
275
|
+
dispatch({ type: "MODALS_OPEN", args: { name: "insert" } });
|
|
276
|
+
}}
|
|
277
|
+
leftSection={<IconPlus size={20} />}
|
|
278
|
+
>
|
|
279
|
+
Insert within
|
|
280
|
+
</Menu.Item>
|
|
281
|
+
|
|
282
|
+
<Menu.Item
|
|
283
|
+
disabled={level === 1}
|
|
284
|
+
onClick={(e) => {
|
|
285
|
+
e.stopPropagation();
|
|
286
|
+
dispatch({
|
|
287
|
+
type: "TREE_SET_MENU_NODE",
|
|
288
|
+
args: { node, insertMode: "after" },
|
|
289
|
+
});
|
|
290
|
+
dispatch({ type: "MODALS_OPEN", args: { name: "insert" } });
|
|
291
|
+
}}
|
|
292
|
+
leftSection={<IconList size={20} />}
|
|
293
|
+
>
|
|
294
|
+
Insert after
|
|
295
|
+
</Menu.Item>
|
|
296
|
+
|
|
297
|
+
<Menu.Divider />
|
|
298
|
+
|
|
299
|
+
<Menu.Item
|
|
300
|
+
disabled={isRoot}
|
|
301
|
+
onClick={async (e) => {
|
|
302
|
+
e.stopPropagation();
|
|
303
|
+
await FETCH.page.duplicateNode({
|
|
304
|
+
source: JSON.parse(node.value) as FiberNodeType["_debugSource"],
|
|
305
|
+
});
|
|
306
|
+
}}
|
|
307
|
+
leftSection={<IconCopy size={20} />}
|
|
308
|
+
>
|
|
309
|
+
Duplicate
|
|
310
|
+
</Menu.Item>
|
|
311
|
+
|
|
312
|
+
<Menu.Divider />
|
|
313
|
+
|
|
314
|
+
<Menu.Item
|
|
315
|
+
disabled={isRoot}
|
|
316
|
+
onClick={async (e) => {
|
|
317
|
+
e.stopPropagation();
|
|
318
|
+
await FETCH.page.moveUpNode({
|
|
319
|
+
source: JSON.parse(node.value) as FiberNodeType["_debugSource"],
|
|
320
|
+
});
|
|
321
|
+
}}
|
|
322
|
+
leftSection={<IconChevronUp size={20} />}
|
|
323
|
+
>
|
|
324
|
+
Move up
|
|
325
|
+
</Menu.Item>
|
|
326
|
+
|
|
327
|
+
<Menu.Item
|
|
328
|
+
disabled={isRoot}
|
|
329
|
+
onClick={async (e) => {
|
|
330
|
+
e.stopPropagation();
|
|
331
|
+
await FETCH.page.moveDownNode({
|
|
332
|
+
source: JSON.parse(node.value) as FiberNodeType["_debugSource"],
|
|
333
|
+
});
|
|
334
|
+
}}
|
|
335
|
+
leftSection={<IconChevronDown size={20} />}
|
|
336
|
+
>
|
|
337
|
+
Move down
|
|
338
|
+
</Menu.Item>
|
|
339
|
+
|
|
340
|
+
<Menu.Divider />
|
|
341
|
+
|
|
342
|
+
<Menu.Item
|
|
343
|
+
disabled={level === 1}
|
|
344
|
+
color="red"
|
|
345
|
+
onClick={(e) => {
|
|
346
|
+
e.stopPropagation();
|
|
347
|
+
modals.openConfirmModal({
|
|
348
|
+
title: "Delete node",
|
|
349
|
+
children: (
|
|
350
|
+
<Text size="sm">
|
|
351
|
+
Are you sure you want to delete this {node.label}? This action
|
|
352
|
+
cannot be undone.
|
|
353
|
+
</Text>
|
|
354
|
+
),
|
|
355
|
+
labels: { confirm: "Delete", cancel: "Cancel" },
|
|
356
|
+
confirmProps: { color: "red" },
|
|
357
|
+
onConfirm: async () => {
|
|
358
|
+
await FETCH.page.deleteNode({
|
|
359
|
+
source: JSON.parse(node.value) as FiberNodeType["_debugSource"],
|
|
360
|
+
});
|
|
361
|
+
dispatch({
|
|
362
|
+
type: "TREE_SELECT_NODE",
|
|
363
|
+
args: { selectedNode: null },
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}}
|
|
368
|
+
leftSection={<IconTrash size={20} />}
|
|
369
|
+
>
|
|
370
|
+
Delete
|
|
371
|
+
</Menu.Item>
|
|
372
|
+
</Menu.Dropdown>
|
|
373
|
+
</Menu>
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// SortableLeaf
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
const SortableLeaf: FC<TTreePayload & { dragOverData: DragOverData }> = ({
|
|
381
|
+
node,
|
|
382
|
+
expanded,
|
|
383
|
+
hasChildren,
|
|
384
|
+
elementProps,
|
|
385
|
+
selected,
|
|
386
|
+
level,
|
|
387
|
+
dragOverData,
|
|
388
|
+
}) => {
|
|
389
|
+
const { dispatch } = useContextDev();
|
|
390
|
+
const isRoot = level === 1;
|
|
391
|
+
const label = node.label!.toString();
|
|
392
|
+
const cannotInsertWithin = !Object.keys(node.nodeProps ?? {}).includes(
|
|
393
|
+
"children",
|
|
394
|
+
);
|
|
395
|
+
const isOverWithin =
|
|
396
|
+
dragOverData.overId === node.value && dragOverData.position === "within";
|
|
397
|
+
const isOverBefore =
|
|
398
|
+
dragOverData.overId === node.value && dragOverData.position === "before";
|
|
399
|
+
const isOverAfter =
|
|
400
|
+
dragOverData.overId === node.value && dragOverData.position === "after";
|
|
401
|
+
|
|
402
|
+
const { attributes, listeners, setNodeRef, transform, isDragging } =
|
|
403
|
+
useSortable({
|
|
404
|
+
id: node.value,
|
|
405
|
+
disabled: isRoot,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
if (selected) {
|
|
410
|
+
dispatch({ type: "TREE_SELECT_NODE", args: { selectedNode: node } });
|
|
411
|
+
}
|
|
412
|
+
}, [selected, dispatch, node]);
|
|
413
|
+
|
|
414
|
+
const { style: elementStyle, ...restElementProps } = elementProps ?? {};
|
|
415
|
+
|
|
416
|
+
return (
|
|
417
|
+
<div
|
|
418
|
+
ref={setNodeRef}
|
|
419
|
+
style={{
|
|
420
|
+
transform: dndCSS.Translate.toString(transform),
|
|
421
|
+
// No CSS transition — eliminates jitter on fast drags.
|
|
422
|
+
opacity: isDragging ? 0.3 : 1,
|
|
423
|
+
// KEY FIX: pointer-events:none while dragging makes document.elementFromPoint
|
|
424
|
+
// skip this element entirely. Without this, when dragging node X over a
|
|
425
|
+
// parent P that already has children, elementFromPoint would return X's
|
|
426
|
+
// ghost (still at its original DOM position) instead of P, so "within"
|
|
427
|
+
// would never fire for P.
|
|
428
|
+
pointerEvents: isDragging ? "none" : undefined,
|
|
429
|
+
cursor: "default",
|
|
430
|
+
position: "relative",
|
|
431
|
+
backgroundColor:
|
|
432
|
+
isOverWithin || selected
|
|
433
|
+
? "var(--mantine-primary-color-light)"
|
|
434
|
+
: "transparent",
|
|
435
|
+
borderRadius: "var(--mantine-radius-sm)",
|
|
436
|
+
outline:
|
|
437
|
+
isOverWithin || selected
|
|
438
|
+
? `1px ${isOverWithin ? "dashed" : "solid"} var(--mantine-primary-color-filled)`
|
|
439
|
+
: "none",
|
|
440
|
+
outlineOffset: -1,
|
|
441
|
+
...elementStyle,
|
|
442
|
+
}}
|
|
443
|
+
{...restElementProps}
|
|
444
|
+
>
|
|
445
|
+
{!isRoot && (isOverBefore || isOverAfter) && (
|
|
446
|
+
<div
|
|
447
|
+
style={{
|
|
448
|
+
position: "absolute",
|
|
449
|
+
top: isOverBefore ? -2 : undefined,
|
|
450
|
+
bottom: isOverAfter ? -2 : undefined,
|
|
451
|
+
left: (level - 1) * 20,
|
|
452
|
+
right: 0,
|
|
453
|
+
height: 4,
|
|
454
|
+
backgroundColor: "var(--mantine-primary-color-filled)",
|
|
455
|
+
borderRadius: 2,
|
|
456
|
+
zIndex: 10,
|
|
457
|
+
pointerEvents: "none",
|
|
458
|
+
display: "flex",
|
|
459
|
+
alignItems: "center",
|
|
460
|
+
}}
|
|
461
|
+
>
|
|
462
|
+
<div
|
|
463
|
+
style={{
|
|
464
|
+
width: 8,
|
|
465
|
+
height: 8,
|
|
466
|
+
backgroundColor: "var(--mantine-primary-color-filled)",
|
|
467
|
+
borderRadius: "50%",
|
|
468
|
+
marginLeft: -4,
|
|
469
|
+
boxShadow: "0 0 4px rgba(0,0,0,0.2)",
|
|
470
|
+
}}
|
|
471
|
+
/>
|
|
472
|
+
</div>
|
|
473
|
+
)}
|
|
474
|
+
<Flex align="center" gap={5} pl={isRoot ? 10 : undefined}>
|
|
475
|
+
<Group flex={1} wrap="nowrap" style={{ overflow: "hidden" }} gap={0}>
|
|
476
|
+
{!isRoot && (
|
|
477
|
+
<Center
|
|
478
|
+
p={4}
|
|
479
|
+
style={{ cursor: "grab" }}
|
|
480
|
+
{...attributes}
|
|
481
|
+
{...listeners}
|
|
482
|
+
onClick={(e) => e.stopPropagation()}
|
|
483
|
+
>
|
|
484
|
+
<IconGripVertical size={14} color="gray" />
|
|
485
|
+
</Center>
|
|
486
|
+
)}
|
|
487
|
+
<Center flex="none" ml={isRoot ? "sm" : 0}>
|
|
488
|
+
<LeafIcon
|
|
489
|
+
label={label}
|
|
490
|
+
isFolder={hasChildren}
|
|
491
|
+
expanded={expanded}
|
|
492
|
+
/>
|
|
493
|
+
</Center>
|
|
494
|
+
<Text p="xs" truncate title={label}>
|
|
495
|
+
{label}
|
|
496
|
+
</Text>
|
|
497
|
+
</Group>
|
|
498
|
+
|
|
499
|
+
{label !== "Fragment" && (
|
|
500
|
+
<LeafMenu
|
|
501
|
+
node={node}
|
|
502
|
+
level={level}
|
|
503
|
+
dispatch={dispatch}
|
|
504
|
+
cannotInsertWithin={cannotInsertWithin}
|
|
505
|
+
isRoot={isRoot}
|
|
506
|
+
/>
|
|
507
|
+
)}
|
|
508
|
+
</Flex>
|
|
509
|
+
</div>
|
|
510
|
+
);
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// -------------------------------------------------------------------------
|
|
514
|
+
// generateData — static recursive helper
|
|
515
|
+
// -------------------------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
const generateData = async (
|
|
518
|
+
reactJsxEl: ReactJsxEl,
|
|
519
|
+
index: number,
|
|
520
|
+
): Promise<TreeProps["data"][number] | null> => {
|
|
521
|
+
if (
|
|
522
|
+
Array.isArray(reactJsxEl) ||
|
|
523
|
+
typeof reactJsxEl === "string" ||
|
|
524
|
+
typeof reactJsxEl === "number" ||
|
|
525
|
+
typeof reactJsxEl === "boolean" ||
|
|
526
|
+
typeof reactJsxEl === "function"
|
|
527
|
+
) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const isBasicComponent =
|
|
532
|
+
reactJsxEl.$$typeof === Symbol.for("react.element") &&
|
|
533
|
+
typeof reactJsxEl.type === "function";
|
|
534
|
+
|
|
535
|
+
if (isBasicComponent) {
|
|
536
|
+
const fn = reactJsxEl.type as FC;
|
|
537
|
+
return {
|
|
538
|
+
label: fn.displayName || fn.name,
|
|
539
|
+
value: JSON.stringify(reactJsxEl._source),
|
|
540
|
+
children: [],
|
|
541
|
+
nodeProps: reactJsxEl.props,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const isFragment = reactJsxEl.type.toString() === "Symbol(react.fragment)";
|
|
546
|
+
if (isFragment) {
|
|
547
|
+
const nodeProps = reactJsxEl.props;
|
|
548
|
+
const raw = nodeProps.children
|
|
549
|
+
? Array.isArray(nodeProps.children)
|
|
550
|
+
? nodeProps.children
|
|
551
|
+
: [nodeProps.children]
|
|
552
|
+
: [];
|
|
553
|
+
const children = (await Promise.all(raw.map(generateData))).filter(Boolean);
|
|
554
|
+
return {
|
|
555
|
+
label: "Fragment",
|
|
556
|
+
value: `fragment-${index}`,
|
|
557
|
+
children,
|
|
558
|
+
nodeProps,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const isHtmlElement = typeof reactJsxEl.type === "string";
|
|
563
|
+
if (isHtmlElement) {
|
|
564
|
+
const nodeProps = reactJsxEl.props;
|
|
565
|
+
const raw = nodeProps.children
|
|
566
|
+
? Array.isArray(nodeProps.children)
|
|
567
|
+
? nodeProps.children
|
|
568
|
+
: [nodeProps.children]
|
|
569
|
+
: [];
|
|
570
|
+
const children = (await Promise.all(raw.map(generateData))).filter(Boolean);
|
|
571
|
+
return {
|
|
572
|
+
label: reactJsxEl.type as string,
|
|
573
|
+
value: JSON.stringify(reactJsxEl._source),
|
|
574
|
+
children,
|
|
575
|
+
nodeProps,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const displayName = (reactJsxEl.type as FunctionComponent).displayName;
|
|
580
|
+
if (!displayName) return null;
|
|
581
|
+
|
|
582
|
+
const isUI = displayName.startsWith("UI.");
|
|
583
|
+
const isBlock = displayName.startsWith("Blocks.");
|
|
584
|
+
const isComponent = displayName.startsWith("Components.");
|
|
585
|
+
if (!isUI && !isBlock && !isComponent) return null;
|
|
586
|
+
|
|
587
|
+
const [, name] = displayName.split(".");
|
|
588
|
+
let comp: { params: { props: Record<string, unknown> } };
|
|
589
|
+
|
|
590
|
+
if (isUI) ({ default: comp } = await import(`southpaw/ui/${name}`));
|
|
591
|
+
else if (isBlock)
|
|
592
|
+
({ default: comp } = await import(`southpaw/ui/blocks/${name}`));
|
|
593
|
+
else ({ default: comp } = await import(`@/components/${name}`));
|
|
594
|
+
|
|
595
|
+
const nodeProps = { ...comp.params.props, ...reactJsxEl.props };
|
|
596
|
+
const raw = nodeProps.children
|
|
597
|
+
? Array.isArray(nodeProps.children)
|
|
598
|
+
? nodeProps.children
|
|
599
|
+
: [nodeProps.children]
|
|
600
|
+
: [];
|
|
601
|
+
const children = (await Promise.all(raw.map(generateData))).filter(Boolean);
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
label: displayName,
|
|
605
|
+
value: JSON.stringify(reactJsxEl._source),
|
|
606
|
+
nodeProps,
|
|
607
|
+
children,
|
|
608
|
+
};
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
// TabPageTree
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
export const TabPageTree: FC<Props> = ({ jsx }) => {
|
|
616
|
+
const { dispatch } = useContextDev();
|
|
617
|
+
const [data, setData] = useState<TreeProps["data"]>([]);
|
|
618
|
+
const [dragOverData, setDragOverData] = useState<DragOverData>({
|
|
619
|
+
overId: null,
|
|
620
|
+
position: null,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Ref mirrors so event handlers always read fresh values without being in
|
|
624
|
+
// useCallback dependency arrays (which would cause unnecessary re-subscriptions).
|
|
625
|
+
const dragOverDataRef = useRef<DragOverData>({
|
|
626
|
+
overId: null,
|
|
627
|
+
position: null,
|
|
628
|
+
});
|
|
629
|
+
const dataRef = useRef<TreeProps["data"]>([]);
|
|
630
|
+
useEffect(() => {
|
|
631
|
+
dataRef.current = data;
|
|
632
|
+
}, [data]);
|
|
633
|
+
|
|
634
|
+
const tree = useTree();
|
|
635
|
+
|
|
636
|
+
// rAF handle for onDragMove throttling.
|
|
637
|
+
const rafRef = useRef<number | null>(null);
|
|
638
|
+
|
|
639
|
+
// Auto-expand: when hovering "within" a node for 600 ms, expand it.
|
|
640
|
+
const autoExpandTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
641
|
+
|
|
642
|
+
const clearAutoExpand = useCallback(() => {
|
|
643
|
+
if (autoExpandTimer.current !== null) {
|
|
644
|
+
clearTimeout(autoExpandTimer.current);
|
|
645
|
+
autoExpandTimer.current = null;
|
|
646
|
+
}
|
|
647
|
+
}, []);
|
|
648
|
+
|
|
649
|
+
useEffect(
|
|
650
|
+
() => () => {
|
|
651
|
+
clearAutoExpand();
|
|
652
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
653
|
+
},
|
|
654
|
+
[clearAutoExpand],
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
// -------------------------------------------------------------------------
|
|
658
|
+
// setDragOver — single write path that keeps state + ref in sync and manages
|
|
659
|
+
// the auto-expand timer. Skips update if nothing changed.
|
|
660
|
+
// Safe to call from rAF callbacks (no setState in side-effects).
|
|
661
|
+
// -------------------------------------------------------------------------
|
|
662
|
+
|
|
663
|
+
const setDragOver = useCallback(
|
|
664
|
+
(next: DragOverData) => {
|
|
665
|
+
const prev = dragOverDataRef.current;
|
|
666
|
+
if (prev.overId === next.overId && prev.position === next.position)
|
|
667
|
+
return;
|
|
668
|
+
|
|
669
|
+
clearAutoExpand();
|
|
670
|
+
|
|
671
|
+
if (next.position === "within" && next.overId !== null) {
|
|
672
|
+
const id = next.overId;
|
|
673
|
+
autoExpandTimer.current = setTimeout(() => tree.expand(id), 600);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
dragOverDataRef.current = next;
|
|
677
|
+
setDragOverData(next);
|
|
678
|
+
},
|
|
679
|
+
[clearAutoExpand, tree],
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
const refresh = useCallback(async () => {
|
|
683
|
+
const result = (
|
|
684
|
+
await Promise.all([jsx].map((el, i) => generateData(el, i)))
|
|
685
|
+
).filter(Boolean) as TreeProps["data"];
|
|
686
|
+
setData(result);
|
|
687
|
+
}, [jsx]);
|
|
688
|
+
|
|
689
|
+
useEffect(() => {
|
|
690
|
+
refresh();
|
|
691
|
+
}, [refresh]);
|
|
692
|
+
|
|
693
|
+
// -------------------------------------------------------------------------
|
|
694
|
+
// Sensors
|
|
695
|
+
// -------------------------------------------------------------------------
|
|
696
|
+
|
|
697
|
+
const sensors = useSensors(
|
|
698
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
// -------------------------------------------------------------------------
|
|
702
|
+
// Drag handlers
|
|
703
|
+
// -------------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
const handleDragStart = useCallback(() => {
|
|
706
|
+
setDragOver({ overId: null, position: null });
|
|
707
|
+
}, [setDragOver]);
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* WHY onDragMove instead of onDragOver:
|
|
711
|
+
*
|
|
712
|
+
* onDragOver fires only when dnd-kit's collision target changes — i.e. when
|
|
713
|
+
* the pointer crosses from one droppable rect into another. Moving within a
|
|
714
|
+
* single node's row (e.g. from the top-25% "before" zone down into the
|
|
715
|
+
* middle-50% "within" zone) never changes over.id, so onDragOver never fires
|
|
716
|
+
* again and the position indicator freezes. onDragMove fires on every pointer
|
|
717
|
+
* move, so our zone calculation stays live.
|
|
718
|
+
*
|
|
719
|
+
* We throttle to one DOM read per animation frame (~60 fps).
|
|
720
|
+
*/
|
|
721
|
+
const handleDragMove = useCallback(
|
|
722
|
+
({ active, activatorEvent, delta }: DragMoveEvent) => {
|
|
723
|
+
// Snapshot coordinates now — the event object may be reused/pooled.
|
|
724
|
+
const activeId = active.id as string;
|
|
725
|
+
const clientX = (activatorEvent as MouseEvent).clientX + delta.x;
|
|
726
|
+
const clientY = (activatorEvent as MouseEvent).clientY + delta.y;
|
|
727
|
+
|
|
728
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
729
|
+
|
|
730
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
731
|
+
rafRef.current = null;
|
|
732
|
+
|
|
733
|
+
// elementFromPoint skips the dragged element (pointer-events:none)
|
|
734
|
+
// and returns the actual node row underneath.
|
|
735
|
+
const overId = getNodeValueAtPoint(clientX, clientY);
|
|
736
|
+
|
|
737
|
+
if (!overId || overId === activeId) {
|
|
738
|
+
setDragOver({ overId: null, position: null });
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const overNode = findNodeByValue(dataRef.current, overId);
|
|
743
|
+
let position = computeDropPosition(overId, clientY, overNode);
|
|
744
|
+
|
|
745
|
+
// If dragging over the root node, force 'within' position
|
|
746
|
+
const isRootNode = dataRef.current?.[0]?.value === overId;
|
|
747
|
+
if (isRootNode && (position === "before" || position === "after")) {
|
|
748
|
+
position = "within";
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
setDragOver({ overId, position });
|
|
752
|
+
});
|
|
753
|
+
},
|
|
754
|
+
[setDragOver],
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Reads dragOverDataRef (always current) rather than event.over.
|
|
759
|
+
* event.over comes from dnd-kit's collision detection, which we've replaced
|
|
760
|
+
* with our elementFromPoint approach via onDragMove — so event.over may
|
|
761
|
+
* be stale or simply wrong for our use case.
|
|
762
|
+
*/
|
|
763
|
+
const handleDragEnd = useCallback(
|
|
764
|
+
async ({ active }: DragEndEvent) => {
|
|
765
|
+
if (rafRef.current !== null) {
|
|
766
|
+
cancelAnimationFrame(rafRef.current);
|
|
767
|
+
rafRef.current = null;
|
|
768
|
+
}
|
|
769
|
+
clearAutoExpand();
|
|
770
|
+
|
|
771
|
+
const { overId, position } = dragOverDataRef.current;
|
|
772
|
+
|
|
773
|
+
// Reset visual state before any async work.
|
|
774
|
+
dragOverDataRef.current = { overId: null, position: null };
|
|
775
|
+
setDragOverData({ overId: null, position: null });
|
|
776
|
+
|
|
777
|
+
const activeValue = active.id as string;
|
|
778
|
+
|
|
779
|
+
if (
|
|
780
|
+
!overId ||
|
|
781
|
+
!position ||
|
|
782
|
+
overId === activeValue ||
|
|
783
|
+
!findNodeByValue(dataRef.current, activeValue) ||
|
|
784
|
+
!findNodeByValue(dataRef.current, overId)
|
|
785
|
+
) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Expand immediately so the newly-nested child is visible right away.
|
|
790
|
+
if (position === "within") tree.expand(overId);
|
|
791
|
+
|
|
792
|
+
// Optimistic local update before API round-trip.
|
|
793
|
+
setData((prev) =>
|
|
794
|
+
moveNodeOptimistically(prev, activeValue, overId, position),
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
await FETCH.page.moveNode({
|
|
798
|
+
source: safeParse(activeValue) as FiberNodeType["_debugSource"],
|
|
799
|
+
target: safeParse(overId) as FiberNodeType["_debugSource"],
|
|
800
|
+
position,
|
|
801
|
+
});
|
|
802
|
+
},
|
|
803
|
+
[clearAutoExpand, tree],
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
const handleDragCancel = useCallback(() => {
|
|
807
|
+
if (rafRef.current !== null) {
|
|
808
|
+
cancelAnimationFrame(rafRef.current);
|
|
809
|
+
rafRef.current = null;
|
|
810
|
+
}
|
|
811
|
+
clearAutoExpand();
|
|
812
|
+
dragOverDataRef.current = { overId: null, position: null };
|
|
813
|
+
setDragOverData({ overId: null, position: null });
|
|
814
|
+
}, [clearAutoExpand]);
|
|
815
|
+
|
|
816
|
+
// -------------------------------------------------------------------------
|
|
817
|
+
// Render
|
|
818
|
+
// -------------------------------------------------------------------------
|
|
819
|
+
|
|
820
|
+
const allIds = flattenIds(data);
|
|
821
|
+
|
|
822
|
+
return (
|
|
823
|
+
<Tabs.Panel
|
|
824
|
+
value="tree"
|
|
825
|
+
w={300}
|
|
826
|
+
style={{ overflow: "auto" }}
|
|
827
|
+
onClick={() => {
|
|
828
|
+
tree.clearSelected();
|
|
829
|
+
dispatch({ type: "TREE_SELECT_NODE", args: { selectedNode: null } });
|
|
830
|
+
}}
|
|
831
|
+
>
|
|
832
|
+
<ModalInsert />
|
|
833
|
+
<DndContext
|
|
834
|
+
sensors={sensors}
|
|
835
|
+
collisionDetection={() => []}
|
|
836
|
+
onDragStart={handleDragStart}
|
|
837
|
+
onDragMove={handleDragMove}
|
|
838
|
+
onDragEnd={handleDragEnd}
|
|
839
|
+
onDragCancel={handleDragCancel}
|
|
840
|
+
measuring={{ droppable: { strategy: MeasuringStrategy.Always } }}
|
|
841
|
+
>
|
|
842
|
+
<SortableContext items={allIds} strategy={() => null}>
|
|
843
|
+
<Tree
|
|
844
|
+
tree={tree}
|
|
845
|
+
p="md"
|
|
846
|
+
selectOnClick
|
|
847
|
+
data={data}
|
|
848
|
+
onClick={(e) => e.stopPropagation()}
|
|
849
|
+
renderNode={(payload) => (
|
|
850
|
+
<SortableLeaf
|
|
851
|
+
{...(payload as TTreePayload)}
|
|
852
|
+
dragOverData={dragOverData}
|
|
853
|
+
/>
|
|
854
|
+
)}
|
|
855
|
+
/>
|
|
856
|
+
</SortableContext>
|
|
857
|
+
</DndContext>
|
|
858
|
+
</Tabs.Panel>
|
|
859
|
+
);
|
|
860
|
+
};
|