@iloveagents/foundry-web-ui 0.33.0 → 0.33.2
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/components/sidebar.d.ts +33 -1
- package/dist/components/sidebar.js +94 -44
- package/dist/lib/nav-dnd.d.ts +109 -1
- package/dist/lib/nav-dnd.js +102 -2
- package/package.json +3 -3
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import type { NavGroup } from "../lib/nav-config.js";
|
|
2
|
+
import type { NavGroup, NavItemDnd } from "../lib/nav-config.js";
|
|
3
3
|
declare function scrollNavRowIntoView(nav: HTMLElement, row: HTMLElement): void;
|
|
4
4
|
export declare const _scrollNavRowIntoViewForTest: typeof scrollNavRowIntoView;
|
|
5
|
+
/** Drop zone rendered inside expanded containers so users can move items back to the container root.
|
|
6
|
+
*
|
|
7
|
+
* The strip stays IN FLOW and reserves its full height for the whole drag:
|
|
8
|
+
* 4px at rest, 28px from the moment a drag begins until it ends, with the
|
|
9
|
+
* band filling exactly what was reserved. Growing on hover instead — the
|
|
10
|
+
* obvious design — fails in both of its available shapes:
|
|
11
|
+
*
|
|
12
|
+
* - in flow, every row beneath jumps 24px at the exact moment the pointer
|
|
13
|
+
* crosses the strip, which is the moment the user is aiming at one of
|
|
14
|
+
* those rows. Dragging a file from a nested folder down to a sibling
|
|
15
|
+
* crosses one strip per open container, so the destination walks away
|
|
16
|
+
* from the cursor mid-drag (measured: 28px per crossing) and the drop
|
|
17
|
+
* lands on whatever slid underneath. From the user's seat the gesture
|
|
18
|
+
* simply does nothing, with no request made and nothing to read.
|
|
19
|
+
* - out of flow, nothing moves, but the expanded band overlaps ~12px of
|
|
20
|
+
* the rows on either side and, being positioned, takes their drops — a
|
|
21
|
+
* moving target traded for a wrong one.
|
|
22
|
+
*
|
|
23
|
+
* Reserving for the whole drag settles the layout once, before the user has
|
|
24
|
+
* aimed at anything. The opening is deliberately NOT animated: a 150ms
|
|
25
|
+
* height transition is a moving target for 150ms, which is inside the time
|
|
26
|
+
* a short drag to a nearby row takes. Only the collapse animates, and by
|
|
27
|
+
* then nothing is being aimed at.
|
|
28
|
+
*
|
|
29
|
+
* Exported for its test only — it is not in the package barrel, and the
|
|
30
|
+
* invariants (in flow, reserved by the drag rather than the pointer, band
|
|
31
|
+
* == strip) are ones jsdom can check from the class names and nothing can
|
|
32
|
+
* check from the outside.
|
|
33
|
+
*/
|
|
34
|
+
export declare function ContainerDropZone({ dnd }: {
|
|
35
|
+
dnd: NavItemDnd;
|
|
36
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
5
37
|
interface SidebarViewProps {
|
|
6
38
|
/** Display-only navigation override; the navigation registry remains intact. */
|
|
7
39
|
groups?: NavGroup[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
3
3
|
import { NavLink, useLocation, useNavigate } from "react-router";
|
|
4
4
|
import { Ellipsis, Layers, PanelLeft, PanelLeftOpen, SquarePen, ChevronDown, ChevronRight, GripVertical, Plus, Upload, ChevronLeft, } from "lucide-react";
|
|
@@ -8,7 +8,7 @@ import { useToolPanelStore } from "../lib/tool-panel-store.js";
|
|
|
8
8
|
import { useNewConversation } from "../lib/use-new-conversation.js";
|
|
9
9
|
import { useNavStore } from "../lib/nav-store.js";
|
|
10
10
|
import { useAppStore } from "../lib/app-store.js";
|
|
11
|
-
import {
|
|
11
|
+
import { beginNavDrag, subscribeNavDrag, getNavDragVersion, navDragAccepts, hasAnyDropTarget, hasExternalFilesData, } from "../lib/nav-dnd.js";
|
|
12
12
|
import { TooltipIconButton } from "./tooltip-icon-button.js";
|
|
13
13
|
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
14
14
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu.js";
|
|
@@ -262,6 +262,12 @@ function useNavItemDnd(dnd) {
|
|
|
262
262
|
return;
|
|
263
263
|
e.dataTransfer.setData(dnd.type, data);
|
|
264
264
|
e.dataTransfer.effectAllowed = "move";
|
|
265
|
+
// Container drop zones reserve their space for the whole drag rather
|
|
266
|
+
// than growing under the pointer — see `beginNavDrag`. The MIME type
|
|
267
|
+
// goes with it so only zones that would take THIS drag open, and the
|
|
268
|
+
// source element so its `dragend` is still heard if a nav refresh
|
|
269
|
+
// detaches this row mid-drag.
|
|
270
|
+
beginNavDrag(dnd.type, e.currentTarget);
|
|
265
271
|
}, [dnd]);
|
|
266
272
|
const onDragEnter = useCallback((e) => {
|
|
267
273
|
if (!hasMatchingDragData(e))
|
|
@@ -359,57 +365,101 @@ function useNavItemDnd(dnd) {
|
|
|
359
365
|
: {},
|
|
360
366
|
};
|
|
361
367
|
}
|
|
362
|
-
/** Drop zone rendered inside expanded containers so users can move items back to the container root.
|
|
363
|
-
|
|
368
|
+
/** Drop zone rendered inside expanded containers so users can move items back to the container root.
|
|
369
|
+
*
|
|
370
|
+
* The strip stays IN FLOW and reserves its full height for the whole drag:
|
|
371
|
+
* 4px at rest, 28px from the moment a drag begins until it ends, with the
|
|
372
|
+
* band filling exactly what was reserved. Growing on hover instead — the
|
|
373
|
+
* obvious design — fails in both of its available shapes:
|
|
374
|
+
*
|
|
375
|
+
* - in flow, every row beneath jumps 24px at the exact moment the pointer
|
|
376
|
+
* crosses the strip, which is the moment the user is aiming at one of
|
|
377
|
+
* those rows. Dragging a file from a nested folder down to a sibling
|
|
378
|
+
* crosses one strip per open container, so the destination walks away
|
|
379
|
+
* from the cursor mid-drag (measured: 28px per crossing) and the drop
|
|
380
|
+
* lands on whatever slid underneath. From the user's seat the gesture
|
|
381
|
+
* simply does nothing, with no request made and nothing to read.
|
|
382
|
+
* - out of flow, nothing moves, but the expanded band overlaps ~12px of
|
|
383
|
+
* the rows on either side and, being positioned, takes their drops — a
|
|
384
|
+
* moving target traded for a wrong one.
|
|
385
|
+
*
|
|
386
|
+
* Reserving for the whole drag settles the layout once, before the user has
|
|
387
|
+
* aimed at anything. The opening is deliberately NOT animated: a 150ms
|
|
388
|
+
* height transition is a moving target for 150ms, which is inside the time
|
|
389
|
+
* a short drag to a nearby row takes. Only the collapse animates, and by
|
|
390
|
+
* then nothing is being aimed at.
|
|
391
|
+
*
|
|
392
|
+
* Exported for its test only — it is not in the package barrel, and the
|
|
393
|
+
* invariants (in flow, reserved by the drag rather than the pointer, band
|
|
394
|
+
* == strip) are ones jsdom can check from the class names and nothing can
|
|
395
|
+
* check from the outside.
|
|
396
|
+
*/
|
|
397
|
+
export function ContainerDropZone({ dnd }) {
|
|
364
398
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
365
|
-
const [isFileDragOver, setIsFileDragOver] = useState(false);
|
|
366
399
|
const dragCounter = useRef(0);
|
|
367
400
|
const acceptsFiles = dnd.canDropFiles?.() ?? false;
|
|
368
401
|
const acceptsEntities = dnd.canDrop();
|
|
402
|
+
// Whether this zone is open is the DRAG's business, not the pointer's:
|
|
403
|
+
// the band is reserved before the pointer reaches it, so hover can no
|
|
404
|
+
// longer say anything about what is being dragged. And reserving space
|
|
405
|
+
// and painting a label is a promise that a drop will land here — so a
|
|
406
|
+
// zone opens only for a drag it would accept, which the drag's own MIME
|
|
407
|
+
// types decide.
|
|
408
|
+
//
|
|
409
|
+
// The subscribed snapshot is a VERSION, not the types themselves: a MIME
|
|
410
|
+
// token may legally contain whatever character one joins them with, and a
|
|
411
|
+
// split on it turns one type into two. The predicate reads the set.
|
|
412
|
+
useSyncExternalStore(subscribeNavDrag, getNavDragVersion, () => 0);
|
|
413
|
+
const dragActive = navDragAccepts({ type: dnd.type, acceptsEntities });
|
|
369
414
|
const hasExternalFiles = (e) => hasExternalFilesData(e.dataTransfer);
|
|
370
415
|
const hasMatchingDragData = (e) => Boolean((acceptsEntities && e.dataTransfer.types.includes(dnd.type)) ||
|
|
371
416
|
(acceptsFiles && hasExternalFiles(e)));
|
|
372
|
-
return (_jsx("div", {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
417
|
+
return (_jsx("div", { className: cn("relative mx-2",
|
|
418
|
+
// Reserved for the whole drag, so the rows below settle once —
|
|
419
|
+
// before the user has aimed — and never while they are aiming.
|
|
420
|
+
// The transition rides on the RESTING class, so the reservation
|
|
421
|
+
// opens instantly and only the collapse animates: an animated
|
|
422
|
+
// opening is a moving target for as long as it runs.
|
|
423
|
+
dragActive ? "mt-0 h-7" : "-mt-1 h-1 transition-[height,margin] duration-150"), children: _jsx("div", { onDragEnter: (e) => {
|
|
424
|
+
if (!hasMatchingDragData(e))
|
|
425
|
+
return;
|
|
426
|
+
e.preventDefault();
|
|
427
|
+
// Hover state only — the band's size is the drag's business, not
|
|
428
|
+
// the pointer's. Opening it from here is the original bug: the row
|
|
429
|
+
// the user is aiming at slides away as they reach it.
|
|
430
|
+
dragCounter.current++;
|
|
431
|
+
setIsDragOver(true);
|
|
432
|
+
}, onDragLeave: () => {
|
|
433
|
+
dragCounter.current--;
|
|
434
|
+
if (dragCounter.current <= 0) {
|
|
435
|
+
dragCounter.current = 0;
|
|
436
|
+
setIsDragOver(false);
|
|
437
|
+
}
|
|
438
|
+
}, onDragOver: (e) => {
|
|
439
|
+
if (!hasMatchingDragData(e))
|
|
440
|
+
return;
|
|
441
|
+
e.preventDefault();
|
|
442
|
+
e.dataTransfer.dropEffect = hasExternalFiles(e) ? "copy" : "move";
|
|
443
|
+
}, onDrop: async (e) => {
|
|
444
|
+
const files = Array.from(e.dataTransfer.files ?? []);
|
|
445
|
+
const draggedData = acceptsEntities ? e.dataTransfer.getData(dnd.type) : "";
|
|
446
|
+
const hasFiles = acceptsFiles && files.length > 0 && dnd.onFileDrop;
|
|
447
|
+
const hasEntityDrop = Boolean(acceptsEntities && draggedData);
|
|
448
|
+
if (!hasFiles && !hasEntityDrop)
|
|
449
|
+
return;
|
|
450
|
+
e.preventDefault();
|
|
451
|
+
e.stopPropagation();
|
|
382
452
|
dragCounter.current = 0;
|
|
383
|
-
setIsFileDragOver(false);
|
|
384
453
|
setIsDragOver(false);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const hasFiles = acceptsFiles && files.length > 0 && dnd.onFileDrop;
|
|
395
|
-
const hasEntityDrop = Boolean(acceptsEntities && draggedData);
|
|
396
|
-
if (!hasFiles && !hasEntityDrop)
|
|
397
|
-
return;
|
|
398
|
-
e.preventDefault();
|
|
399
|
-
e.stopPropagation();
|
|
400
|
-
dragCounter.current = 0;
|
|
401
|
-
setIsFileDragOver(false);
|
|
402
|
-
setIsDragOver(false);
|
|
403
|
-
if (hasFiles) {
|
|
404
|
-
await dnd.onFileDrop(files);
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
await dnd.onDrop(draggedData);
|
|
408
|
-
}, className: cn("relative mx-2 rounded-md overflow-hidden", isDragOver
|
|
409
|
-
? cn("h-7 flex items-center justify-center gap-1", isFileDragOver
|
|
410
|
-
? "bg-primary/10 border border-dashed border-primary"
|
|
411
|
-
: "bg-primary/10 border border-dashed border-primary/30")
|
|
412
|
-
: "-mt-1 h-1"), children: isDragOver && (_jsxs(_Fragment, { children: [isFileDragOver && _jsx(Upload, { className: "size-3 text-primary" }), _jsx("span", { className: "text-[10px] text-primary/70", children: getContainerDropZoneLabel(isFileDragOver) })] })) }));
|
|
454
|
+
if (hasFiles) {
|
|
455
|
+
await dnd.onFileDrop(files);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
await dnd.onDrop(draggedData);
|
|
459
|
+
}, className: cn(
|
|
460
|
+
// Fills the reserved strip exactly: what is painted is what
|
|
461
|
+
// accepts a drop, and nothing extends over a neighbouring row.
|
|
462
|
+
"h-full w-full rounded-md overflow-hidden", dragActive && "flex items-center justify-center", isDragOver && "bg-primary/10 border border-dashed border-primary/30"), children: dragActive && (_jsx("span", { className: cn("text-[10px] transition-colors", isDragOver ? "text-primary/70" : "text-sidebar-foreground/35"), children: "Move to root" })) }) }));
|
|
413
463
|
}
|
|
414
464
|
/**
|
|
415
465
|
* Leading disclosure cell on every nav row — the Confluence page-tree
|
package/dist/lib/nav-dnd.d.ts
CHANGED
|
@@ -5,4 +5,112 @@ export declare function hasExternalFilesData(dataTransfer: {
|
|
|
5
5
|
} | null;
|
|
6
6
|
}): boolean;
|
|
7
7
|
export declare function hasAnyDropTarget(isDropTarget: boolean, isFileDropTarget: boolean): boolean;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Is a drag that the nav can accept in flight, anywhere — and what is it?
|
|
10
|
+
*
|
|
11
|
+
* Container drop zones are a 4px strip at rest and a labelled band while a
|
|
12
|
+
* drag is in flight, and WHEN they grow decides whether the sidebar is
|
|
13
|
+
* usable. Growing on hover is the trap, in both available shapes:
|
|
14
|
+
*
|
|
15
|
+
* - in flow, every row below the strip jumps 24px at the moment the pointer
|
|
16
|
+
* crosses it — which is the moment the user is aiming at one of them, so
|
|
17
|
+
* the destination walks away from the cursor;
|
|
18
|
+
* - out of flow, nothing moves, but the expanded band then covers ~12px of
|
|
19
|
+
* the rows on either side and takes their drops.
|
|
20
|
+
*
|
|
21
|
+
* So the space is reserved for the whole drag instead. The layout settles
|
|
22
|
+
* once, before the user has aimed at anything, and from then on every zone's
|
|
23
|
+
* hit area is exactly the band that is painted — no moving target, and no
|
|
24
|
+
* neighbour quietly answering for another.
|
|
25
|
+
*
|
|
26
|
+
* "Before the user has aimed" is the whole value, and it is also the whole
|
|
27
|
+
* PRECONDITION. Only a drag that starts in this document has a moment before
|
|
28
|
+
* the aim: `dragstart` fires before any movement, so the layout can settle
|
|
29
|
+
* while the pointer is still on the row being picked up.
|
|
30
|
+
*
|
|
31
|
+
* A drag arriving from OUTSIDE — the desktop, or another window — has no such
|
|
32
|
+
* moment. The browser hit-tests whatever is under the pointer before any
|
|
33
|
+
* script runs, so the first `dragenter` this document sees is already aimed
|
|
34
|
+
* at something; opening the zones from it moves that something out from under
|
|
35
|
+
* a stationary pointer. That is the very failure this whole change exists to
|
|
36
|
+
* remove, arriving by a different road.
|
|
37
|
+
*
|
|
38
|
+
* So outside drags get no reservation: the strips stay 4px and nothing moves.
|
|
39
|
+
* They are not left without a target — container rows accept and highlight
|
|
40
|
+
* file drops themselves, which is the path a file drag was always going to
|
|
41
|
+
* take. The root-drop affordance is a convenience with a redundant route; the
|
|
42
|
+
* mis-aim was a correctness failure with none.
|
|
43
|
+
*
|
|
44
|
+
* WHAT is tracked is the gesture's set of MIME types, not just "a drag is
|
|
45
|
+
* happening". Reserving space and painting a label is a promise that a drop
|
|
46
|
+
* will land, so only a zone that would take this particular drag may make it.
|
|
47
|
+
* A set rather than one type because `dragstart` BUBBLES: a draggable row
|
|
48
|
+
* sits inside a draggable container, both handlers run, and tracking one
|
|
49
|
+
* would track whichever ran last — the ancestor's — leaving zones that match
|
|
50
|
+
* the child's payload shut.
|
|
51
|
+
*
|
|
52
|
+
* The set is read directly and never serialised. It was briefly published as
|
|
53
|
+
* a joined string, for `useSyncExternalStore`'s identity comparison — but a
|
|
54
|
+
* MIME token may legally contain the separator (`|` is not one of RFC 2045's
|
|
55
|
+
* tspecials), and then one type splits into two. The snapshot is a version
|
|
56
|
+
* counter instead, so there is nothing to encode.
|
|
57
|
+
*/
|
|
58
|
+
type NavDragListener = () => void;
|
|
59
|
+
/**
|
|
60
|
+
* Ends whatever drag is in flight.
|
|
61
|
+
*
|
|
62
|
+
* Bound to the window for `dragend` (a drop or a cancel) and `drop` (a
|
|
63
|
+
* source outside this document, which has no `dragend` here at all), and to
|
|
64
|
+
* the source element itself, whose own `dragend` is the only one left once a
|
|
65
|
+
* nav refresh has detached it.
|
|
66
|
+
*
|
|
67
|
+
* There is deliberately no `dragleave` ending. A relatedTarget-less
|
|
68
|
+
* `dragleave` is the only signal a drag leaving the window gives, and it is
|
|
69
|
+
* the same signal whether the drag is over or merely outside — so it ended
|
|
70
|
+
* live in-page drags, collapsing every zone for the rest of the gesture. It
|
|
71
|
+
* existed for drags with no `dragend` in this document, and none of those
|
|
72
|
+
* opens a reservation now.
|
|
73
|
+
*/
|
|
74
|
+
export declare function endNavDrag(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Called from `dragstart` on a nav row, with the MIME type that row wrote its
|
|
77
|
+
* payload under and the element it started from.
|
|
78
|
+
*
|
|
79
|
+
* The type ADDS rather than replaces: `dragstart` bubbles through every
|
|
80
|
+
* draggable ancestor, and each one's type belongs to the same gesture.
|
|
81
|
+
*
|
|
82
|
+
* The source is held so its `dragend` can be heard even after a nav refresh
|
|
83
|
+
* has removed the row mid-drag. A detached node still receives its own
|
|
84
|
+
* `dragend`, but the event reaches no window listener from there — so
|
|
85
|
+
* without this the reservation would stay open, every zone expanded, until
|
|
86
|
+
* some later drag happened to finish elsewhere.
|
|
87
|
+
*/
|
|
88
|
+
export declare function beginNavDrag(type: string, source?: Element | null): void;
|
|
89
|
+
export declare function subscribeNavDrag(listener: NavDragListener): () => void;
|
|
90
|
+
/**
|
|
91
|
+
* The snapshot `useSyncExternalStore` compares: a version, bumped on every
|
|
92
|
+
* change. A primitive, so identity comparison is meaningful, and carrying no
|
|
93
|
+
* encoding of its own — see the note about `|` above.
|
|
94
|
+
*/
|
|
95
|
+
export declare function getNavDragVersion(): number;
|
|
96
|
+
/**
|
|
97
|
+
* Would a zone with this `dnd.type` accept the live drag?
|
|
98
|
+
*
|
|
99
|
+
* Reserving space and painting a label is a promise that a drop will land,
|
|
100
|
+
* so only a zone that would actually take this drag may do either. "A drag
|
|
101
|
+
* is happening" was not enough: two modules with different `dnd.type`s
|
|
102
|
+
* opened each other's zones.
|
|
103
|
+
*
|
|
104
|
+
* Files need no case here. Only a nav row's `dragstart` opens a reservation
|
|
105
|
+
* and those carry an entity, so no zone ever opens for a file drag whatever
|
|
106
|
+
* it is configured to accept. It still HANDLES one — the resting strip takes
|
|
107
|
+
* a file drop like any other target — and the container's own row advertises
|
|
108
|
+
* that same drop at full size.
|
|
109
|
+
*/
|
|
110
|
+
export declare function navDragAccepts(zone: {
|
|
111
|
+
type: string;
|
|
112
|
+
acceptsEntities: boolean;
|
|
113
|
+
}): boolean;
|
|
114
|
+
/** Test seam — module-level state outlives a test file otherwise. */
|
|
115
|
+
export declare function __resetNavDragForTest(): void;
|
|
116
|
+
export {};
|
package/dist/lib/nav-dnd.js
CHANGED
|
@@ -4,6 +4,106 @@ export function hasExternalFilesData(dataTransfer) {
|
|
|
4
4
|
export function hasAnyDropTarget(isDropTarget, isFileDropTarget) {
|
|
5
5
|
return isDropTarget || isFileDropTarget;
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const navDragListeners = new Set();
|
|
8
|
+
/** The MIME types this gesture carries. Empty means no drag is in flight. */
|
|
9
|
+
const navDragTypes = new Set();
|
|
10
|
+
/** Bumped on every change — the `useSyncExternalStore` snapshot. */
|
|
11
|
+
let navDragVersion = 0;
|
|
12
|
+
/** The element the drag started from, when we were given one — so its
|
|
13
|
+
* `dragend` is heard even after a nav refresh detaches it mid-drag. */
|
|
14
|
+
let navDragSource = null;
|
|
15
|
+
let navDragWatchBound = false;
|
|
16
|
+
function publish() {
|
|
17
|
+
navDragVersion += 1;
|
|
18
|
+
for (const listener of navDragListeners)
|
|
19
|
+
listener();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Ends whatever drag is in flight.
|
|
23
|
+
*
|
|
24
|
+
* Bound to the window for `dragend` (a drop or a cancel) and `drop` (a
|
|
25
|
+
* source outside this document, which has no `dragend` here at all), and to
|
|
26
|
+
* the source element itself, whose own `dragend` is the only one left once a
|
|
27
|
+
* nav refresh has detached it.
|
|
28
|
+
*
|
|
29
|
+
* There is deliberately no `dragleave` ending. A relatedTarget-less
|
|
30
|
+
* `dragleave` is the only signal a drag leaving the window gives, and it is
|
|
31
|
+
* the same signal whether the drag is over or merely outside — so it ended
|
|
32
|
+
* live in-page drags, collapsing every zone for the rest of the gesture. It
|
|
33
|
+
* existed for drags with no `dragend` in this document, and none of those
|
|
34
|
+
* opens a reservation now.
|
|
35
|
+
*/
|
|
36
|
+
export function endNavDrag() {
|
|
37
|
+
navDragSource = null;
|
|
38
|
+
if (navDragTypes.size === 0)
|
|
39
|
+
return;
|
|
40
|
+
navDragTypes.clear();
|
|
41
|
+
publish();
|
|
42
|
+
}
|
|
43
|
+
function bindNavDragWatch() {
|
|
44
|
+
if (navDragWatchBound || typeof window === "undefined")
|
|
45
|
+
return;
|
|
46
|
+
navDragWatchBound = true;
|
|
47
|
+
window.addEventListener("dragend", endNavDrag, true);
|
|
48
|
+
window.addEventListener("drop", endNavDrag, true);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Called from `dragstart` on a nav row, with the MIME type that row wrote its
|
|
52
|
+
* payload under and the element it started from.
|
|
53
|
+
*
|
|
54
|
+
* The type ADDS rather than replaces: `dragstart` bubbles through every
|
|
55
|
+
* draggable ancestor, and each one's type belongs to the same gesture.
|
|
56
|
+
*
|
|
57
|
+
* The source is held so its `dragend` can be heard even after a nav refresh
|
|
58
|
+
* has removed the row mid-drag. A detached node still receives its own
|
|
59
|
+
* `dragend`, but the event reaches no window listener from there — so
|
|
60
|
+
* without this the reservation would stay open, every zone expanded, until
|
|
61
|
+
* some later drag happened to finish elsewhere.
|
|
62
|
+
*/
|
|
63
|
+
export function beginNavDrag(type, source) {
|
|
64
|
+
bindNavDragWatch();
|
|
65
|
+
if (source && source !== navDragSource) {
|
|
66
|
+
navDragSource = source;
|
|
67
|
+
source.addEventListener("dragend", endNavDrag, { once: true });
|
|
68
|
+
}
|
|
69
|
+
if (navDragTypes.has(type))
|
|
70
|
+
return;
|
|
71
|
+
navDragTypes.add(type);
|
|
72
|
+
publish();
|
|
73
|
+
}
|
|
74
|
+
export function subscribeNavDrag(listener) {
|
|
75
|
+
bindNavDragWatch();
|
|
76
|
+
navDragListeners.add(listener);
|
|
77
|
+
return () => navDragListeners.delete(listener);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The snapshot `useSyncExternalStore` compares: a version, bumped on every
|
|
81
|
+
* change. A primitive, so identity comparison is meaningful, and carrying no
|
|
82
|
+
* encoding of its own — see the note about `|` above.
|
|
83
|
+
*/
|
|
84
|
+
export function getNavDragVersion() {
|
|
85
|
+
return navDragVersion;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Would a zone with this `dnd.type` accept the live drag?
|
|
89
|
+
*
|
|
90
|
+
* Reserving space and painting a label is a promise that a drop will land,
|
|
91
|
+
* so only a zone that would actually take this drag may do either. "A drag
|
|
92
|
+
* is happening" was not enough: two modules with different `dnd.type`s
|
|
93
|
+
* opened each other's zones.
|
|
94
|
+
*
|
|
95
|
+
* Files need no case here. Only a nav row's `dragstart` opens a reservation
|
|
96
|
+
* and those carry an entity, so no zone ever opens for a file drag whatever
|
|
97
|
+
* it is configured to accept. It still HANDLES one — the resting strip takes
|
|
98
|
+
* a file drop like any other target — and the container's own row advertises
|
|
99
|
+
* that same drop at full size.
|
|
100
|
+
*/
|
|
101
|
+
export function navDragAccepts(zone) {
|
|
102
|
+
return zone.acceptsEntities && navDragTypes.has(zone.type);
|
|
103
|
+
}
|
|
104
|
+
/** Test seam — module-level state outlives a test file otherwise. */
|
|
105
|
+
export function __resetNavDragForTest() {
|
|
106
|
+
navDragTypes.clear();
|
|
107
|
+
navDragSource = null;
|
|
108
|
+
navDragVersion = 0;
|
|
9
109
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React agent UI core for Foundry UI — chat, composer, AG-UI adapter for assistant-ui, tool cards, panels, sidebar, theme runtime, and UI stores.",
|
|
6
6
|
"keywords": [
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
"recharts": "^3.10.1",
|
|
81
81
|
"remark-gfm": "^4.0.0",
|
|
82
82
|
"tailwind-merge": "^3.5.0",
|
|
83
|
-
"@iloveagents/foundry-agent": "^0.33.
|
|
84
|
-
"@iloveagents/foundry-web-primitives": "^0.33.
|
|
83
|
+
"@iloveagents/foundry-agent": "^0.33.2",
|
|
84
|
+
"@iloveagents/foundry-web-primitives": "^0.33.2"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@ag-ui/client": "^0.0.52",
|