@pyreon/dnd 0.11.0

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.
@@ -0,0 +1,236 @@
1
+ //#region src/types.d.ts
2
+ /** Data attached to a draggable item. */
3
+ type DragData = Record<string, unknown>;
4
+ /** Position of a drop relative to the target element. */
5
+ type DropEdge = "top" | "bottom" | "left" | "right";
6
+ /** Drop location information. */
7
+ interface DropLocation {
8
+ /** The edge closest to the drop point. */
9
+ edge: DropEdge | null;
10
+ /** Custom data attached to the drop target. */
11
+ data: DragData;
12
+ }
13
+ interface UseDraggableOptions<T extends DragData = DragData> {
14
+ /** Ref callback or element getter for the draggable element. */
15
+ element: () => HTMLElement | null;
16
+ /** Data to transfer on drag. Can be a function for dynamic data. */
17
+ data: T | (() => T);
18
+ /** Optional drag handle element (subset of the draggable). */
19
+ handle?: () => HTMLElement | null;
20
+ /** Whether dragging is disabled. Reactive. */
21
+ disabled?: boolean | (() => boolean);
22
+ /** Called when drag starts. */
23
+ onDragStart?: () => void;
24
+ /** Called when drag ends (drop or cancel). */
25
+ onDragEnd?: () => void;
26
+ }
27
+ interface UseDraggableResult {
28
+ /** Whether this element is currently being dragged. */
29
+ isDragging: () => boolean;
30
+ }
31
+ interface UseDroppableOptions<T extends DragData = DragData> {
32
+ /** Ref callback or element getter for the drop target. */
33
+ element: () => HTMLElement | null;
34
+ /** Data to attach to the drop target. */
35
+ data?: T | (() => T);
36
+ /** Filter what can be dropped. Return false to reject. */
37
+ canDrop?: (sourceData: DragData) => boolean;
38
+ /** Called when a draggable enters this target. */
39
+ onDragEnter?: (sourceData: DragData) => void;
40
+ /** Called when a draggable leaves this target. */
41
+ onDragLeave?: () => void;
42
+ /** Called when an item is dropped on this target. */
43
+ onDrop?: (sourceData: DragData) => void;
44
+ }
45
+ interface UseDroppableResult {
46
+ /** Whether something is currently being dragged over this target. */
47
+ isOver: () => boolean;
48
+ }
49
+ interface UseSortableOptions<T> {
50
+ /** Reactive list of items to sort. */
51
+ items: () => T[];
52
+ /** Key extractor — matches Pyreon's <For by={...}> pattern. */
53
+ by: (item: T) => string | number;
54
+ /** Called with the reordered items after a drop. */
55
+ onReorder: (items: T[]) => void;
56
+ /** Sort axis. Default: "vertical". */
57
+ axis?: "vertical" | "horizontal";
58
+ }
59
+ interface UseSortableResult {
60
+ /** Attach to the scroll container. */
61
+ containerRef: (el: HTMLElement) => void;
62
+ /** Attach to each sortable item. Call with the item's key. */
63
+ itemRef: (key: string | number) => (el: HTMLElement) => void;
64
+ /** The key of the currently dragging item. */
65
+ activeId: () => string | number | null;
66
+ /** The key of the item being hovered over. */
67
+ overId: () => string | number | null;
68
+ /** The closest edge of the hovered item ("top"/"bottom" or "left"/"right"). */
69
+ overEdge: () => DropEdge | null;
70
+ }
71
+ //#endregion
72
+ //#region src/use-drag-monitor.d.ts
73
+ interface UseDragMonitorOptions {
74
+ /** Called on any drag start in the page. */
75
+ onDragStart?: (data: DragData) => void;
76
+ /** Called on any drop in the page. */
77
+ onDrop?: (sourceData: DragData, targetData: DragData) => void;
78
+ /** Filter which drags to monitor. */
79
+ canMonitor?: (data: DragData) => boolean;
80
+ }
81
+ interface UseDragMonitorResult {
82
+ /** Whether any element is currently being dragged. */
83
+ isDragging: () => boolean;
84
+ /** Data of the currently dragging element (null if not dragging). */
85
+ dragData: () => DragData | null;
86
+ }
87
+ /**
88
+ * Monitor all drag operations on the page.
89
+ * Useful for global drag indicators, analytics, or coordination between
90
+ * multiple drag-and-drop areas.
91
+ *
92
+ * @example
93
+ * ```tsx
94
+ * const { isDragging, dragData } = useDragMonitor({
95
+ * canMonitor: (data) => data.type === "card",
96
+ * onDrop: (source, target) => logDrop(source, target),
97
+ * })
98
+ *
99
+ * <Show when={isDragging()}>
100
+ * <div class="global-drag-overlay">
101
+ * Dragging: {() => dragData()?.name}
102
+ * </div>
103
+ * </Show>
104
+ * ```
105
+ */
106
+ declare function useDragMonitor(options?: UseDragMonitorOptions): UseDragMonitorResult;
107
+ //#endregion
108
+ //#region src/use-draggable.d.ts
109
+ /**
110
+ * Make an element draggable with signal-driven state.
111
+ *
112
+ * @example
113
+ * ```tsx
114
+ * let cardEl: HTMLElement | null = null
115
+ *
116
+ * const { isDragging } = useDraggable({
117
+ * element: () => cardEl,
118
+ * data: { id: card.id, type: "card" },
119
+ * })
120
+ *
121
+ * <div ref={(el) => cardEl = el} class={isDragging() ? "opacity-50" : ""}>
122
+ * {card.title}
123
+ * </div>
124
+ * ```
125
+ */
126
+ declare function useDraggable<T extends DragData = DragData>(options: UseDraggableOptions<T>): UseDraggableResult;
127
+ //#endregion
128
+ //#region src/use-droppable.d.ts
129
+ /**
130
+ * Make an element a drop target with signal-driven state.
131
+ *
132
+ * @example
133
+ * ```tsx
134
+ * let zoneEl: HTMLElement | null = null
135
+ *
136
+ * const { isOver } = useDroppable({
137
+ * element: () => zoneEl,
138
+ * onDrop: (data) => handleDrop(data),
139
+ * canDrop: (data) => data.type === "card",
140
+ * })
141
+ *
142
+ * <div ref={(el) => zoneEl = el} class={isOver() ? "bg-blue-50" : ""}>
143
+ * Drop here
144
+ * </div>
145
+ * ```
146
+ */
147
+ declare function useDroppable<T extends DragData = DragData>(options: UseDroppableOptions<T>): UseDroppableResult;
148
+ //#endregion
149
+ //#region src/use-file-drop.d.ts
150
+ interface UseFileDropOptions {
151
+ /** Element getter for the drop zone. */
152
+ element: () => HTMLElement | null;
153
+ /** Called when files are dropped. */
154
+ onDrop: (files: File[]) => void;
155
+ /** Filter accepted file types (e.g. ["image/*", ".pdf"]). */
156
+ accept?: string[];
157
+ /** Maximum number of files. */
158
+ maxFiles?: number;
159
+ /** Whether drop is disabled. */
160
+ disabled?: boolean | (() => boolean);
161
+ }
162
+ interface UseFileDropResult {
163
+ /** Whether files are being dragged over the drop zone. */
164
+ isOver: () => boolean;
165
+ /** Whether files are being dragged anywhere on the page. */
166
+ isDraggingFiles: () => boolean;
167
+ }
168
+ /**
169
+ * File drop zone with signal-driven state.
170
+ * Uses the native file drag events via pragmatic-drag-and-drop.
171
+ *
172
+ * @example
173
+ * ```tsx
174
+ * let dropZone: HTMLElement | null = null
175
+ *
176
+ * const { isOver, isDraggingFiles } = useFileDrop({
177
+ * element: () => dropZone,
178
+ * accept: ["image/*", ".pdf"],
179
+ * maxFiles: 5,
180
+ * onDrop: (files) => upload(files),
181
+ * })
182
+ *
183
+ * <div
184
+ * ref={(el) => dropZone = el}
185
+ * class={isOver() ? "drop-active" : isDraggingFiles() ? "drop-ready" : ""}
186
+ * >
187
+ * Drop files here
188
+ * </div>
189
+ * ```
190
+ */
191
+ declare function useFileDrop(options: UseFileDropOptions): UseFileDropResult;
192
+ //#endregion
193
+ //#region src/use-sortable.d.ts
194
+ /**
195
+ * Sortable list with signal-driven state, auto-scroll, and edge detection.
196
+ *
197
+ * Features:
198
+ * - Keyed drag items matching `<For by={...}>` pattern
199
+ * - Auto-scroll when dragging near container edges
200
+ * - Closest-edge detection (drop above/below or left/right)
201
+ * - Axis constraint (vertical/horizontal)
202
+ * - Keyboard reordering (Alt+Arrow keys)
203
+ *
204
+ * @example
205
+ * ```tsx
206
+ * const items = signal([
207
+ * { id: "1", name: "Alice" },
208
+ * { id: "2", name: "Bob" },
209
+ * { id: "3", name: "Charlie" },
210
+ * ])
211
+ *
212
+ * const { containerRef, itemRef, activeId, overId, overEdge } = useSortable({
213
+ * items,
214
+ * by: (item) => item.id,
215
+ * onReorder: (newItems) => items.set(newItems),
216
+ * })
217
+ *
218
+ * <ul ref={containerRef}>
219
+ * <For each={items()} by={item => item.id}>
220
+ * {(item) => (
221
+ * <li
222
+ * ref={itemRef(item.id)}
223
+ * class={activeId() === item.id ? "dragging" : ""}
224
+ * style={overId() === item.id ? `border-${overEdge()}: 2px solid blue` : ""}
225
+ * >
226
+ * {item.name}
227
+ * </li>
228
+ * )}
229
+ * </For>
230
+ * </ul>
231
+ * ```
232
+ */
233
+ declare function useSortable<T>(options: UseSortableOptions<T>): UseSortableResult;
234
+ //#endregion
235
+ export { type DragData, type DropEdge, type DropLocation, type UseDragMonitorOptions, type UseDragMonitorResult, type UseDraggableOptions, type UseDraggableResult, type UseDroppableOptions, type UseDroppableResult, type UseFileDropOptions, type UseFileDropResult, type UseSortableOptions, type UseSortableResult, useDragMonitor, useDraggable, useDroppable, useFileDrop, useSortable };
236
+ //# sourceMappingURL=index2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/use-drag-monitor.ts","../../../src/use-draggable.ts","../../../src/use-droppable.ts","../../../src/use-file-drop.ts","../../../src/use-sortable.ts"],"mappings":";;KACY,QAAA,GAAW,MAAA;;KAGX,QAAA;;UAGK,YAAA;EAHL;EAKV,IAAA,EAAM,QAAA;;EAEN,IAAA,EAAM,QAAA;AAAA;AAAA,UAKS,mBAAA,WAA8B,QAAA,GAAW,QAAA;EAT7B;EAW3B,OAAA,QAAe,WAAA;EAPD;EASd,IAAA,EAAM,CAAA,UAAW,CAAA;EAXX;EAaN,MAAA,SAAe,WAAA;EAXT;EAaN,QAAA;EAbc;EAed,WAAA;EAVkC;EAYlC,SAAA;AAAA;AAAA,UAGe,kBAAA;EAbA;EAef,UAAA;AAAA;AAAA,UAKe,mBAAA,WAA8B,QAAA,GAAW,QAAA;EAhB9B;EAkB1B,OAAA,QAAe,WAAA;EAxBoB;EA0BnC,IAAA,GAAO,CAAA,UAAW,CAAA;EA1BsC;EA4BxD,OAAA,IAAW,UAAA,EAAY,QAAA;EA1BR;EA4Bf,WAAA,IAAe,UAAA,EAAY,QAAA;EA1BrB;EA4BN,WAAA;EA1BA;EA4BA,MAAA,IAAU,UAAA,EAAY,QAAA;AAAA;AAAA,UAGP,kBAAA;EAzBf;EA2BA,MAAA;AAAA;AAAA,UAKe,kBAAA;EA7BkB;EA+BjC,KAAA,QAAa,CAAA;EA7Bb;EA+BA,EAAA,GAAK,IAAA,EAAM,CAAA;EA1BI;EA4Bf,SAAA,GAAY,KAAA,EAAO,CAAA;EA5Be;EA8BlC,IAAA;AAAA;AAAA,UAGe,iBAAA;EA7BR;EA+BP,YAAA,GAAe,EAAA,EAAI,WAAA;EA7BI;EA+BvB,OAAA,GAAU,GAAA,uBAA0B,EAAA,EAAI,WAAA;EAzBlB;EA2BtB,QAAA;EA3B8B;EA6B9B,MAAA;EAzC6C;EA2C7C,QAAA,QAAgB,QAAA;AAAA;;;UC7ED,qBAAA;EDHG;ECKlB,WAAA,IAAe,IAAA,EAAM,QAAA;EDLA;ECOrB,MAAA,IAAU,UAAA,EAAY,QAAA,EAAU,UAAA,EAAY,QAAA;EDJlC;ECMV,UAAA,IAAc,IAAA,EAAM,QAAA;AAAA;AAAA,UAGL,oBAAA;EDTG;ECWlB,UAAA;EDR2B;ECU3B,QAAA,QAAgB,QAAA;AAAA;;;;;;;ADDlB;;;;;;;;;;;;;iBCuBgB,cAAA,CAAe,OAAA,GAAU,qBAAA,GAAwB,oBAAA;;;ADtCjE;;;;;AAGA;;;;;AAGA;;;;;;;AANA,iBEoBgB,YAAA,WAAuB,QAAA,GAAW,QAAA,CAAA,CAChD,OAAA,EAAS,mBAAA,CAAoB,CAAA,IAC5B,kBAAA;;;AFtBH;;;;;AAGA;;;;;AAGA;;;;;;;;AANA,iBGqBgB,YAAA,WAAuB,QAAA,GAAW,QAAA,CAAA,CAChD,OAAA,EAAS,mBAAA,CAAoB,CAAA,IAC5B,kBAAA;;;UCjBc,kBAAA;EJNL;EIQV,OAAA,QAAe,WAAA;;EAEf,MAAA,GAAS,KAAA,EAAO,IAAA;EJVW;EIY3B,MAAA;EJTkB;EIWlB,QAAA;EJXkB;EIalB,QAAA;AAAA;AAAA,UAGe,iBAAA;;EAEf,MAAA;EJbA;EIeA,eAAA;AAAA;;;;AJRF;;;;;;;;;;;;;;;;;;;;iBIkCgB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,iBAAA;;;AJjD1D;;;;;AAGA;;;;;AAGA;;;;;;;;;;AASA;;;;;;;;;;;;;;;;;;;AAfA,iBKsDgB,WAAA,GAAA,CAAe,OAAA,EAAS,kBAAA,CAAmB,CAAA,IAAK,iBAAA"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@pyreon/dnd",
3
+ "version": "0.11.0",
4
+ "description": "Signal-driven drag and drop for Pyreon — wraps @atlaskit/pragmatic-drag-and-drop",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/pyreon/pyreon.git",
9
+ "directory": "packages/fundamentals/dnd"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/pyreon/pyreon/issues"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "files": [
18
+ "lib",
19
+ "src",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "exports": {
26
+ ".": {
27
+ "bun": "./src/index.ts",
28
+ "import": "./lib/index.js",
29
+ "types": "./lib/types/index.d.ts"
30
+ }
31
+ },
32
+ "scripts": {
33
+ "build": "vl_rolldown_build",
34
+ "dev": "vl_rolldown_build-watch",
35
+ "test": "vitest run",
36
+ "typecheck": "tsc --noEmit",
37
+ "lint": "biome check ."
38
+ },
39
+ "dependencies": {
40
+ "@atlaskit/pragmatic-drag-and-drop": "^1.7.0",
41
+ "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "^2.0.0",
42
+ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.0"
43
+ },
44
+ "peerDependencies": {
45
+ "@pyreon/core": "^0.11.0",
46
+ "@pyreon/reactivity": "^0.11.0"
47
+ },
48
+ "devDependencies": {
49
+ "@pyreon/typescript": "^0.11.0"
50
+ }
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type {
2
+ DragData,
3
+ DropEdge,
4
+ DropLocation,
5
+ UseDraggableOptions,
6
+ UseDraggableResult,
7
+ UseDroppableOptions,
8
+ UseDroppableResult,
9
+ UseSortableOptions,
10
+ UseSortableResult,
11
+ } from "./types"
12
+
13
+ export type { UseDragMonitorOptions, UseDragMonitorResult } from "./use-drag-monitor"
14
+ export { useDragMonitor } from "./use-drag-monitor"
15
+
16
+ export { useDraggable } from "./use-draggable"
17
+ export { useDroppable } from "./use-droppable"
18
+ export type { UseFileDropOptions, UseFileDropResult } from "./use-file-drop"
19
+ export { useFileDrop } from "./use-file-drop"
20
+ export { useSortable } from "./use-sortable"
@@ -0,0 +1,281 @@
1
+ import { signal } from "@pyreon/reactivity"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ // ─── useDraggable ───────────────────────────────────────────────────────────
5
+
6
+ describe("useDraggable", () => {
7
+ it("exports useDraggable", async () => {
8
+ const { useDraggable } = await import("../index")
9
+ expect(typeof useDraggable).toBe("function")
10
+ })
11
+
12
+ it("returns isDragging signal initialized to false", async () => {
13
+ const { useDraggable } = await import("../use-draggable")
14
+ const el = document.createElement("div")
15
+ const { isDragging } = useDraggable({ element: () => el, data: { id: "1" } })
16
+ expect(isDragging()).toBe(false)
17
+ })
18
+
19
+ it("accepts function data for dynamic values", async () => {
20
+ const { useDraggable } = await import("../use-draggable")
21
+ const el = document.createElement("div")
22
+ const counter = signal(0)
23
+ const { isDragging } = useDraggable({
24
+ element: () => el,
25
+ data: () => ({ count: counter() }),
26
+ })
27
+ expect(isDragging()).toBe(false)
28
+ })
29
+
30
+ it("accepts disabled as boolean", async () => {
31
+ const { useDraggable } = await import("../use-draggable")
32
+ const el = document.createElement("div")
33
+ const { isDragging } = useDraggable({
34
+ element: () => el,
35
+ data: { id: "1" },
36
+ disabled: true,
37
+ })
38
+ expect(isDragging()).toBe(false)
39
+ })
40
+
41
+ it("accepts disabled as reactive getter", async () => {
42
+ const { useDraggable } = await import("../use-draggable")
43
+ const el = document.createElement("div")
44
+ const disabled = signal(false)
45
+ const { isDragging } = useDraggable({
46
+ element: () => el,
47
+ data: { id: "1" },
48
+ disabled,
49
+ })
50
+ expect(isDragging()).toBe(false)
51
+ })
52
+
53
+ it("handles null element gracefully", async () => {
54
+ const { useDraggable } = await import("../use-draggable")
55
+ const { isDragging } = useDraggable({
56
+ element: () => null,
57
+ data: { id: "1" },
58
+ })
59
+ expect(isDragging()).toBe(false)
60
+ })
61
+ })
62
+
63
+ // ─── useDroppable ───────────────────────────────────────────────────────────
64
+
65
+ describe("useDroppable", () => {
66
+ it("exports useDroppable", async () => {
67
+ const { useDroppable } = await import("../index")
68
+ expect(typeof useDroppable).toBe("function")
69
+ })
70
+
71
+ it("returns isOver signal initialized to false", async () => {
72
+ const { useDroppable } = await import("../use-droppable")
73
+ const el = document.createElement("div")
74
+ const { isOver } = useDroppable({ element: () => el, onDrop: () => {} })
75
+ expect(isOver()).toBe(false)
76
+ })
77
+
78
+ it("accepts canDrop filter", async () => {
79
+ const { useDroppable } = await import("../use-droppable")
80
+ const el = document.createElement("div")
81
+ const { isOver } = useDroppable({
82
+ element: () => el,
83
+ canDrop: (data) => data.type === "card",
84
+ onDrop: () => {},
85
+ })
86
+ expect(isOver()).toBe(false)
87
+ })
88
+
89
+ it("handles null element gracefully", async () => {
90
+ const { useDroppable } = await import("../use-droppable")
91
+ const { isOver } = useDroppable({ element: () => null, onDrop: () => {} })
92
+ expect(isOver()).toBe(false)
93
+ })
94
+ })
95
+
96
+ // ─── useSortable ────────────────────────────────────────────────────────────
97
+
98
+ describe("useSortable", () => {
99
+ it("exports useSortable", async () => {
100
+ const { useSortable } = await import("../index")
101
+ expect(typeof useSortable).toBe("function")
102
+ })
103
+
104
+ it("returns full sortable API with overEdge", async () => {
105
+ const { useSortable } = await import("../use-sortable")
106
+ const items = signal([
107
+ { id: "1", name: "A" },
108
+ { id: "2", name: "B" },
109
+ { id: "3", name: "C" },
110
+ ])
111
+
112
+ const result = useSortable({
113
+ items,
114
+ by: (item) => item.id,
115
+ onReorder: (newItems) => items.set(newItems),
116
+ })
117
+
118
+ expect(typeof result.containerRef).toBe("function")
119
+ expect(typeof result.itemRef).toBe("function")
120
+ expect(result.activeId()).toBeNull()
121
+ expect(result.overId()).toBeNull()
122
+ expect(result.overEdge()).toBeNull()
123
+ })
124
+
125
+ it("itemRef returns a ref callback per key", async () => {
126
+ const { useSortable } = await import("../use-sortable")
127
+ const items = signal([{ id: "1" }, { id: "2" }])
128
+
129
+ const { itemRef } = useSortable({
130
+ items,
131
+ by: (item) => item.id,
132
+ onReorder: () => {},
133
+ })
134
+
135
+ const ref1 = itemRef("1")
136
+ const ref2 = itemRef("2")
137
+ expect(typeof ref1).toBe("function")
138
+ expect(typeof ref2).toBe("function")
139
+ expect(ref1).not.toBe(ref2)
140
+ })
141
+
142
+ it("itemRef sets accessibility attributes on elements", async () => {
143
+ const { useSortable } = await import("../use-sortable")
144
+ const items = signal([{ id: "1" }])
145
+
146
+ const { itemRef } = useSortable({
147
+ items,
148
+ by: (item) => item.id,
149
+ onReorder: () => {},
150
+ })
151
+
152
+ const el = document.createElement("div")
153
+ itemRef("1")(el)
154
+
155
+ expect(el.getAttribute("role")).toBe("listitem")
156
+ expect(el.getAttribute("aria-roledescription")).toBe("sortable item")
157
+ expect(el.getAttribute("tabindex")).toBe("0")
158
+ expect(el.dataset.pyreonSortKey).toBe("1")
159
+ })
160
+
161
+ it("does not override existing tabindex", async () => {
162
+ const { useSortable } = await import("../use-sortable")
163
+ const items = signal([{ id: "1" }])
164
+
165
+ const { itemRef } = useSortable({
166
+ items,
167
+ by: (item) => item.id,
168
+ onReorder: () => {},
169
+ })
170
+
171
+ const el = document.createElement("div")
172
+ el.setAttribute("tabindex", "-1")
173
+ itemRef("1")(el)
174
+
175
+ expect(el.getAttribute("tabindex")).toBe("-1")
176
+ })
177
+
178
+ it("supports horizontal axis", async () => {
179
+ const { useSortable } = await import("../use-sortable")
180
+ const items = signal([{ id: "1" }, { id: "2" }])
181
+
182
+ const result = useSortable({
183
+ items,
184
+ by: (item) => item.id,
185
+ onReorder: () => {},
186
+ axis: "horizontal",
187
+ })
188
+
189
+ expect(result.activeId()).toBeNull()
190
+ })
191
+ })
192
+
193
+ // ─── useFileDrop ────────────────────────────────────────────────────────────
194
+
195
+ describe("useFileDrop", () => {
196
+ it("exports useFileDrop", async () => {
197
+ const { useFileDrop } = await import("../index")
198
+ expect(typeof useFileDrop).toBe("function")
199
+ })
200
+
201
+ it("returns isOver and isDraggingFiles signals", async () => {
202
+ const { useFileDrop } = await import("../use-file-drop")
203
+ const el = document.createElement("div")
204
+ const { isOver, isDraggingFiles } = useFileDrop({
205
+ element: () => el,
206
+ onDrop: () => {},
207
+ })
208
+ expect(isOver()).toBe(false)
209
+ expect(isDraggingFiles()).toBe(false)
210
+ })
211
+
212
+ it("accepts accept filter and maxFiles", async () => {
213
+ const { useFileDrop } = await import("../use-file-drop")
214
+ const el = document.createElement("div")
215
+ const { isOver } = useFileDrop({
216
+ element: () => el,
217
+ accept: ["image/*", ".pdf"],
218
+ maxFiles: 5,
219
+ onDrop: () => {},
220
+ })
221
+ expect(isOver()).toBe(false)
222
+ })
223
+
224
+ it("accepts disabled option", async () => {
225
+ const { useFileDrop } = await import("../use-file-drop")
226
+ const el = document.createElement("div")
227
+ const disabled = signal(true)
228
+ const { isOver } = useFileDrop({
229
+ element: () => el,
230
+ disabled,
231
+ onDrop: () => {},
232
+ })
233
+ expect(isOver()).toBe(false)
234
+ })
235
+ })
236
+
237
+ // ─── useDragMonitor ─────────────────────────────────────────────────────────
238
+
239
+ describe("useDragMonitor", () => {
240
+ it("exports useDragMonitor", async () => {
241
+ const { useDragMonitor } = await import("../index")
242
+ expect(typeof useDragMonitor).toBe("function")
243
+ })
244
+
245
+ it("returns isDragging and dragData signals", async () => {
246
+ const { useDragMonitor } = await import("../use-drag-monitor")
247
+ const { isDragging, dragData } = useDragMonitor()
248
+ expect(isDragging()).toBe(false)
249
+ expect(dragData()).toBeNull()
250
+ })
251
+
252
+ it("accepts canMonitor filter", async () => {
253
+ const { useDragMonitor } = await import("../use-drag-monitor")
254
+ const { isDragging } = useDragMonitor({
255
+ canMonitor: (data) => data.type === "card",
256
+ })
257
+ expect(isDragging()).toBe(false)
258
+ })
259
+
260
+ it("accepts onDragStart and onDrop callbacks", async () => {
261
+ const { useDragMonitor } = await import("../use-drag-monitor")
262
+ const { isDragging } = useDragMonitor({
263
+ onDragStart: () => {},
264
+ onDrop: () => {},
265
+ })
266
+ expect(isDragging()).toBe(false)
267
+ })
268
+ })
269
+
270
+ // ─── Module exports ─────────────────────────────────────────────────────────
271
+
272
+ describe("module exports", () => {
273
+ it("exports all 5 hooks", async () => {
274
+ const mod = await import("../index")
275
+ expect(mod.useDraggable).toBeDefined()
276
+ expect(mod.useDroppable).toBeDefined()
277
+ expect(mod.useSortable).toBeDefined()
278
+ expect(mod.useFileDrop).toBeDefined()
279
+ expect(mod.useDragMonitor).toBeDefined()
280
+ })
281
+ })
package/src/types.ts ADDED
@@ -0,0 +1,83 @@
1
+ /** Data attached to a draggable item. */
2
+ export type DragData = Record<string, unknown>
3
+
4
+ /** Position of a drop relative to the target element. */
5
+ export type DropEdge = "top" | "bottom" | "left" | "right"
6
+
7
+ /** Drop location information. */
8
+ export interface DropLocation {
9
+ /** The edge closest to the drop point. */
10
+ edge: DropEdge | null
11
+ /** Custom data attached to the drop target. */
12
+ data: DragData
13
+ }
14
+
15
+ // ─── useDraggable ───────────────────────────────────────────────────────────
16
+
17
+ export interface UseDraggableOptions<T extends DragData = DragData> {
18
+ /** Ref callback or element getter for the draggable element. */
19
+ element: () => HTMLElement | null
20
+ /** Data to transfer on drag. Can be a function for dynamic data. */
21
+ data: T | (() => T)
22
+ /** Optional drag handle element (subset of the draggable). */
23
+ handle?: () => HTMLElement | null
24
+ /** Whether dragging is disabled. Reactive. */
25
+ disabled?: boolean | (() => boolean)
26
+ /** Called when drag starts. */
27
+ onDragStart?: () => void
28
+ /** Called when drag ends (drop or cancel). */
29
+ onDragEnd?: () => void
30
+ }
31
+
32
+ export interface UseDraggableResult {
33
+ /** Whether this element is currently being dragged. */
34
+ isDragging: () => boolean
35
+ }
36
+
37
+ // ─── useDroppable ───────────────────────────────────────────────────────────
38
+
39
+ export interface UseDroppableOptions<T extends DragData = DragData> {
40
+ /** Ref callback or element getter for the drop target. */
41
+ element: () => HTMLElement | null
42
+ /** Data to attach to the drop target. */
43
+ data?: T | (() => T)
44
+ /** Filter what can be dropped. Return false to reject. */
45
+ canDrop?: (sourceData: DragData) => boolean
46
+ /** Called when a draggable enters this target. */
47
+ onDragEnter?: (sourceData: DragData) => void
48
+ /** Called when a draggable leaves this target. */
49
+ onDragLeave?: () => void
50
+ /** Called when an item is dropped on this target. */
51
+ onDrop?: (sourceData: DragData) => void
52
+ }
53
+
54
+ export interface UseDroppableResult {
55
+ /** Whether something is currently being dragged over this target. */
56
+ isOver: () => boolean
57
+ }
58
+
59
+ // ─── useSortable ────────────────────────────────────────────────────────────
60
+
61
+ export interface UseSortableOptions<T> {
62
+ /** Reactive list of items to sort. */
63
+ items: () => T[]
64
+ /** Key extractor — matches Pyreon's <For by={...}> pattern. */
65
+ by: (item: T) => string | number
66
+ /** Called with the reordered items after a drop. */
67
+ onReorder: (items: T[]) => void
68
+ /** Sort axis. Default: "vertical". */
69
+ axis?: "vertical" | "horizontal"
70
+ }
71
+
72
+ export interface UseSortableResult {
73
+ /** Attach to the scroll container. */
74
+ containerRef: (el: HTMLElement) => void
75
+ /** Attach to each sortable item. Call with the item's key. */
76
+ itemRef: (key: string | number) => (el: HTMLElement) => void
77
+ /** The key of the currently dragging item. */
78
+ activeId: () => string | number | null
79
+ /** The key of the item being hovered over. */
80
+ overId: () => string | number | null
81
+ /** The closest edge of the hovered item ("top"/"bottom" or "left"/"right"). */
82
+ overEdge: () => DropEdge | null
83
+ }