@snapgridjs/react 0.1.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,999 @@
1
+ import { DragDropProvider, useDragDropMonitor, useDraggable, useDraggable as useDraggable$1, useDroppable, useDroppable as useDroppable$1 } from "@dnd-kit/react";
2
+ import { beginDrag, beginReceive, beginResize, bottom, calcGridItemPosition, calcXY, commitLayout, defaultGridConfig, dragResize, dragTo, findOrGenerateResponsiveLayout, getBreakpointFromWidth, getColsFromBreakpoint, getCompactor, horizontalCompactor, noCompactor, nudge, removeItemWithCompactor, toPositionParams, verticalCompactor, verticalCompactor as verticalCompactor$1 } from "@snapgridjs/core";
3
+ import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { Feedback, Feedback as Feedback$1, KeyboardSensor, KeyboardSensor as KeyboardSensor$1, PointerActivationConstraints, PointerSensor, PointerSensor as PointerSensor$1 } from "@dnd-kit/dom";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { createPortal } from "react-dom";
7
+ //#region src/context.ts
8
+ const GridContext = createContext(null);
9
+ /** Read the grid runtime; throws if used outside a `SnapGridProvider`. */
10
+ function useGridRuntime() {
11
+ const runtime = useContext(GridContext);
12
+ if (!runtime) throw new Error("snapgrid: hooks and components must be rendered inside a <SnapGridProvider> (or <GridLayout>).");
13
+ return runtime;
14
+ }
15
+ //#endregion
16
+ //#region src/dragFlow.ts
17
+ /**
18
+ * Pure decision helpers for the drag interaction, extracted from
19
+ * {@link SnapGridProvider} so the tricky bits — grab-offset cell mapping and the
20
+ * cross-grid drop lifecycle — are unit-testable without a DOM or dnd-kit.
21
+ */
22
+ /**
23
+ * Map a client-space pointer to a grid cell, accounting for where *within* the
24
+ * dragged tile the pointer grabbed it. Subtracting the grab offset means the
25
+ * tile's top-left (not the cursor) maps to the cell, so a received tile's
26
+ * placeholder aligns with the floating overlay instead of jumping its corner to
27
+ * the cursor. External drops pass `{ x: 0, y: 0 }` (no meaningful grab point).
28
+ */
29
+ function receiveCell(pointer, gridRect, grabOffset, w, h, pp) {
30
+ return calcXY(pp, pointer.y - grabOffset.y - gridRect.top, pointer.x - grabOffset.x - gridRect.left, w, h);
31
+ }
32
+ /** Pure classification of a drag end. See {@link DropAction}. */
33
+ function classifyDrop(s) {
34
+ if (s.canceled) {
35
+ if (s.kind === "resize") return "cancel-resize";
36
+ if (s.ownsItem) return "cancel-move";
37
+ return "noop";
38
+ }
39
+ if (s.kind === "resize") return "commit-resize";
40
+ if (s.ownsItem && s.hasData) {
41
+ if (s.dest === s.myId && s.kind === "move") return "commit-in-grid";
42
+ if (s.dest) return "remove-source";
43
+ return "revert";
44
+ }
45
+ if (s.dest === s.myId && s.kind === "move") return s.hasData ? "commit-dest" : "external-drop";
46
+ return "noop";
47
+ }
48
+ //#endregion
49
+ //#region src/grouping.ts
50
+ function createGridRegistry() {
51
+ const grids = /* @__PURE__ */ new Map();
52
+ let grabOffset = null;
53
+ return {
54
+ register(id, getRect) {
55
+ grids.set(id, getRect);
56
+ return () => {
57
+ if (grids.get(id) === getRect) grids.delete(id);
58
+ };
59
+ },
60
+ gridAt(point) {
61
+ for (const [id, getRect] of grids) {
62
+ const r = getRect();
63
+ if (r && point.x >= r.left && point.x <= r.right && point.y >= r.top && point.y <= r.bottom) return id;
64
+ }
65
+ return null;
66
+ },
67
+ setGrabOffset(offset) {
68
+ grabOffset = offset;
69
+ },
70
+ getGrabOffset() {
71
+ return grabOffset ?? {
72
+ x: 0,
73
+ y: 0
74
+ };
75
+ }
76
+ };
77
+ }
78
+ /** Non-null when grids are wrapped in a `<SnapGridGroup>` (shared cross-grid registry). */
79
+ const SnapGridGroupContext = createContext(null);
80
+ //#endregion
81
+ //#region src/hooks/dndShared.ts
82
+ /** Marker attribute placed on resize-handle elements. */
83
+ const RESIZE_HANDLE_ATTR = "data-snapgrid-resize-handle";
84
+ const NO_FEEDBACK = [Feedback$1.configure({ feedback: "none" })];
85
+ /**
86
+ * Whether a pointer-down on `target` should NOT start an item move. Pure and
87
+ * exported for testing. Honors three rules, in order:
88
+ * - never start a move from a resize handle;
89
+ * - never start from a region matching `dragConfig.cancel`;
90
+ * - if `dragConfig.handle` is set, only start from within it.
91
+ */
92
+ function shouldPreventItemDrag(target, cfg) {
93
+ if (!(target instanceof Element)) return false;
94
+ if (target.closest(`[data-snapgrid-resize-handle]`)) return true;
95
+ if (cfg?.cancel && target.closest(cfg.cancel)) return true;
96
+ if (cfg?.handle && !target.closest(cfg.handle)) return true;
97
+ return false;
98
+ }
99
+ /**
100
+ * Sensors for item (move) draggables, built from the drag config: a distance
101
+ * activation threshold (so clicks don't start drags) plus handle/cancel/resize
102
+ * gating, with the keyboard sensor kept for accessibility.
103
+ */
104
+ function buildItemSensors(threshold, getDragConfig) {
105
+ return [PointerSensor$1.configure({
106
+ activationConstraints: () => threshold > 0 ? [new PointerActivationConstraints.Distance({ value: threshold })] : void 0,
107
+ preventActivation: (event) => shouldPreventItemDrag(event.target, getDragConfig())
108
+ }), KeyboardSensor$1];
109
+ }
110
+ //#endregion
111
+ //#region src/SnapGridProvider.tsx
112
+ const DEFAULT_HANDLES = ["se"];
113
+ /** Read snapgrid's payload off a dnd-kit drag source. */
114
+ function dragData(event) {
115
+ return (event.operation.source?.data)?.snapGrid;
116
+ }
117
+ /** Size/id spec for an external (non-grid) draggable the grid may accept, or null. */
118
+ function externalDropSpec(source, dropConfig) {
119
+ if (!dropConfig?.enabled || !source) return null;
120
+ const data = source.data;
121
+ if (data?.snapGrid) return null;
122
+ if (dropConfig.accept && !dropConfig.accept(source)) return null;
123
+ const spec = data?.snapGridDrop;
124
+ return {
125
+ i: spec?.i,
126
+ w: spec?.w ?? dropConfig.defaultItem?.w ?? 1,
127
+ h: spec?.h ?? dropConfig.defaultItem?.h ?? 1
128
+ };
129
+ }
130
+ /**
131
+ * Headless provider for a grid. Standalone grids get their own isolated dnd-kit
132
+ * provider; grids inside a {@link SnapGridGroup} share that group's provider and
133
+ * registry so tiles can be dragged between them. Owns this grid's drag/resize
134
+ * session; the consumer owns all markup/styling.
135
+ */
136
+ function SnapGridProvider(props) {
137
+ const groupRegistry = useContext(SnapGridGroupContext);
138
+ const runtime = /* @__PURE__ */ jsx(SnapGridRuntime, {
139
+ groupRegistry,
140
+ ...props
141
+ });
142
+ return groupRegistry ? runtime : /* @__PURE__ */ jsx(DragDropProvider, { children: runtime });
143
+ }
144
+ function SnapGridRuntime(props) {
145
+ const autoId = useId();
146
+ const containerId = props.id ?? autoId;
147
+ const gridConfig = useMemo(() => ({
148
+ ...defaultGridConfig,
149
+ ...props.gridConfig
150
+ }), [props.gridConfig]);
151
+ const positionParams = useMemo(() => toPositionParams(gridConfig, props.width), [gridConfig, props.width]);
152
+ const compactor = props.compactor ?? verticalCompactor$1;
153
+ const [session, setSession] = useState(null);
154
+ const [overlay, setOverlay] = useState(null);
155
+ const propsRef = useRef(props);
156
+ propsRef.current = props;
157
+ const ppRef = useRef(positionParams);
158
+ ppRef.current = positionParams;
159
+ const gridRef = useRef(gridConfig);
160
+ gridRef.current = gridConfig;
161
+ const compactorRef = useRef(compactor);
162
+ compactorRef.current = compactor;
163
+ const containerIdRef = useRef(containerId);
164
+ containerIdRef.current = containerId;
165
+ const sessionRef = useRef(null);
166
+ const containerElRef = useRef(null);
167
+ const keyboardRef = useRef(false);
168
+ const dropSpecRef = useRef(null);
169
+ const dropCounterRef = useRef(0);
170
+ const localRegistryRef = useRef(null);
171
+ if (!localRegistryRef.current) localRegistryRef.current = createGridRegistry();
172
+ const registry = props.groupRegistry ?? localRegistryRef.current;
173
+ const registryRef = useRef(registry);
174
+ registryRef.current = registry;
175
+ useEffect(() => registry.register(containerId, () => containerElRef.current?.getBoundingClientRect() ?? null), [registry, containerId]);
176
+ const committedById = useMemo(() => new Map(props.layout.map((it) => [it.i, it])), [props.layout]);
177
+ const committedByIdRef = useRef(committedById);
178
+ committedByIdRef.current = committedById;
179
+ const setSessionBoth = useCallback((next) => {
180
+ sessionRef.current = next;
181
+ setSession(next);
182
+ }, []);
183
+ const setContainerElement = useCallback((element) => {
184
+ containerElRef.current = element;
185
+ }, []);
186
+ /**
187
+ * Is THIS grid the one under `p` (client coords)? Resolved through the
188
+ * registry (not a self-only rect test) so the move-phase preview and the
189
+ * drop-phase commit — which both go through `gridAt` — always agree on which
190
+ * grid wins when grids overlap (`gridAt` returns a single first match).
191
+ */
192
+ const overMe = useCallback((p) => registryRef.current.gridAt(p) === containerIdRef.current, []);
193
+ /** Map a client-space pointer to a grid cell within THIS grid (see {@link receiveCell}). */
194
+ const cellFromPointer = useCallback((p, item) => {
195
+ const el = containerElRef.current;
196
+ if (!el) return null;
197
+ return receiveCell(p, el.getBoundingClientRect(), registryRef.current.getGrabOffset(), item.w, item.h, ppRef.current);
198
+ }, []);
199
+ const ctx = useCallback(() => ({
200
+ positionParams: ppRef.current,
201
+ compactor: compactorRef.current,
202
+ cols: gridRef.current.cols
203
+ }), []);
204
+ const handleDragStart = useCallback((event) => {
205
+ keyboardRef.current = false;
206
+ const data = dragData(event);
207
+ if (!data) {
208
+ const spec = externalDropSpec(event.operation.source, propsRef.current.dropConfig);
209
+ if (spec) {
210
+ dropCounterRef.current += 1;
211
+ dropSpecRef.current = {
212
+ i: spec.i ?? `${containerIdRef.current}-dropped-${dropCounterRef.current}`,
213
+ w: spec.w,
214
+ h: spec.h
215
+ };
216
+ } else dropSpecRef.current = null;
217
+ return;
218
+ }
219
+ dropSpecRef.current = null;
220
+ const layout = propsRef.current.layout;
221
+ const item = layout.find((it) => it.i === data.itemId);
222
+ const p = event.operation.position.current;
223
+ const pointer = {
224
+ x: p.x,
225
+ y: p.y
226
+ };
227
+ if (data.kind === "resize") {
228
+ if (!item) return;
229
+ setSessionBoth(beginResize(layout, {
230
+ item,
231
+ rect: calcGridItemPosition(ppRef.current, item.x, item.y, item.w, item.h),
232
+ pointer
233
+ }, data.handle));
234
+ propsRef.current.onResizeStart?.(layout, item, item, item, event.operation.activatorEvent, null);
235
+ return;
236
+ }
237
+ if (item) {
238
+ keyboardRef.current = event.operation.activatorEvent instanceof KeyboardEvent;
239
+ const rect = calcGridItemPosition(ppRef.current, item.x, item.y, item.w, item.h);
240
+ setSessionBoth(beginDrag(layout, {
241
+ item,
242
+ left: rect.left,
243
+ top: rect.top,
244
+ pointer
245
+ }));
246
+ const cr = (event.operation.source?.element)?.getBoundingClientRect();
247
+ if (cr) {
248
+ registryRef.current.setGrabOffset({
249
+ x: pointer.x - cr.left,
250
+ y: pointer.y - cr.top
251
+ });
252
+ setOverlay({
253
+ item,
254
+ left: cr.left,
255
+ top: cr.top,
256
+ width: cr.width,
257
+ height: cr.height
258
+ });
259
+ }
260
+ propsRef.current.onDragStart?.(layout, item, item, item, event.operation.activatorEvent, null);
261
+ }
262
+ }, [setSessionBoth]);
263
+ const handleDragMove = useCallback((event) => {
264
+ if (keyboardRef.current) return;
265
+ const p = event.operation.position.current;
266
+ const pointer = {
267
+ x: p.x,
268
+ y: p.y
269
+ };
270
+ const current = sessionRef.current;
271
+ if (current?.kind === "resize") {
272
+ const next = dragResize(current, pointer, ctx());
273
+ setSessionBoth(next);
274
+ propsRef.current.onResize?.(next.preview, next.anchor.item, next.placeholder, next.placeholder, event.operation.activatorEvent, null);
275
+ return;
276
+ }
277
+ const data = dragData(event);
278
+ if (!data) {
279
+ const spec = dropSpecRef.current;
280
+ if (spec && overMe(pointer)) {
281
+ const foreign = {
282
+ i: spec.i,
283
+ x: 0,
284
+ y: 0,
285
+ w: spec.w,
286
+ h: spec.h
287
+ };
288
+ const committed = propsRef.current.layout;
289
+ const cell = cellFromPointer(pointer, foreign) ?? {
290
+ x: 0,
291
+ y: 0
292
+ };
293
+ setSessionBoth(beginReceive(committed, foreign, cell.x, cell.y, pointer, ctx()));
294
+ } else if (sessionRef.current) setSessionBoth(null);
295
+ return;
296
+ }
297
+ if (data.kind !== "move") return;
298
+ const here = overMe(pointer);
299
+ if (committedByIdRef.current.has(data.itemId)) {
300
+ const source = current?.kind === "move" ? current : null;
301
+ if (!source) return;
302
+ let next;
303
+ if (here) next = dragTo(source, pointer, ctx());
304
+ else next = {
305
+ ...source,
306
+ preview: source.committed,
307
+ placeholder: null
308
+ };
309
+ setSessionBoth(next);
310
+ const grab = registryRef.current.getGrabOffset();
311
+ let oLeft = pointer.x - grab.x;
312
+ let oTop = pointer.y - grab.y;
313
+ if (propsRef.current.dragConfig?.snapToGrid && here && next.placeholder) {
314
+ const rect = containerElRef.current?.getBoundingClientRect();
315
+ if (rect) {
316
+ const cell = calcGridItemPosition(ppRef.current, next.placeholder.x, next.placeholder.y, next.placeholder.w, next.placeholder.h);
317
+ oLeft = rect.left + cell.left;
318
+ oTop = rect.top + cell.top;
319
+ }
320
+ }
321
+ setOverlay((o) => o ? {
322
+ ...o,
323
+ left: oLeft,
324
+ top: oTop
325
+ } : o);
326
+ propsRef.current.onDrag?.(next.preview, next.anchor.item, next.placeholder, next.placeholder, event.operation.activatorEvent, null);
327
+ return;
328
+ }
329
+ if (here) {
330
+ const foreign = data.item;
331
+ const committed = propsRef.current.layout;
332
+ const cell = cellFromPointer(pointer, foreign) ?? {
333
+ x: 0,
334
+ y: 0
335
+ };
336
+ setSessionBoth(beginReceive(committed, foreign, cell.x, cell.y, pointer, ctx()));
337
+ } else if (sessionRef.current) setSessionBoth(null);
338
+ }, [
339
+ setSessionBoth,
340
+ overMe,
341
+ cellFromPointer,
342
+ ctx
343
+ ]);
344
+ const handleDragEnd = useCallback((event) => {
345
+ const current = sessionRef.current;
346
+ const data = dragData(event);
347
+ const p = event.operation.position.current;
348
+ const myId = containerIdRef.current;
349
+ const dest = keyboardRef.current ? myId : registryRef.current.gridAt({
350
+ x: p.x,
351
+ y: p.y
352
+ });
353
+ const ownsItem = data ? committedByIdRef.current.has(data.itemId) : false;
354
+ setOverlay(null);
355
+ registryRef.current.setGrabOffset(null);
356
+ const native = event.nativeEvent ?? null;
357
+ const p2 = propsRef.current;
358
+ const action = classifyDrop({
359
+ kind: current?.kind ?? null,
360
+ canceled: event.canceled,
361
+ ownsItem,
362
+ hasData: !!data,
363
+ dest,
364
+ myId
365
+ });
366
+ switch (action) {
367
+ case "cancel-resize":
368
+ p2.onResizeStop?.(p2.layout, current?.anchor.item ?? null, null, null, native, null);
369
+ break;
370
+ case "cancel-move":
371
+ p2.onDragStop?.(p2.layout, current?.anchor.item ?? null, null, null, native, null);
372
+ break;
373
+ case "commit-resize":
374
+ if (current) {
375
+ p2.onLayoutChange?.(commitLayout(current));
376
+ p2.onResizeStop?.(current.preview, current.anchor.item, current.placeholder, current.placeholder, native, null);
377
+ }
378
+ break;
379
+ case "commit-in-grid":
380
+ case "remove-source":
381
+ case "revert":
382
+ if (action === "commit-in-grid" && current) p2.onLayoutChange?.(commitLayout(current));
383
+ else if (action === "remove-source" && data) {
384
+ const { compactor: c, cols } = ctx();
385
+ p2.onLayoutChange?.(removeItemWithCompactor(p2.layout, data.itemId, {
386
+ compactor: c,
387
+ cols
388
+ }));
389
+ }
390
+ p2.onDragStop?.(current?.preview ?? p2.layout, current?.anchor.item ?? null, current?.placeholder ?? null, current?.placeholder ?? null, native, null);
391
+ break;
392
+ case "commit-dest":
393
+ if (current) p2.onLayoutChange?.(commitLayout(current));
394
+ break;
395
+ case "external-drop":
396
+ if (current) {
397
+ const committed = commitLayout(current);
398
+ const dropped = committed.find((it) => it.i === current.activeId);
399
+ if (dropped) p2.onDrop?.(committed, dropped, native);
400
+ }
401
+ break;
402
+ }
403
+ dropSpecRef.current = null;
404
+ keyboardRef.current = false;
405
+ setSessionBoth(null);
406
+ }, [setSessionBoth, ctx]);
407
+ useEffect(() => {
408
+ const STEP = {
409
+ ArrowLeft: [-1, 0],
410
+ ArrowRight: [1, 0],
411
+ ArrowUp: [0, -1],
412
+ ArrowDown: [0, 1]
413
+ };
414
+ const onKeyDown = (e) => {
415
+ if (!keyboardRef.current) return;
416
+ const session = sessionRef.current;
417
+ if (!session || session.kind !== "move") return;
418
+ const step = STEP[e.key];
419
+ if (!step) return;
420
+ e.preventDefault();
421
+ e.stopImmediatePropagation();
422
+ const next = nudge(session, step[0], step[1], ctx());
423
+ setSessionBoth(next);
424
+ const cell = next.placeholder;
425
+ const rect = containerElRef.current?.getBoundingClientRect();
426
+ if (cell && rect) {
427
+ const pos = calcGridItemPosition(ppRef.current, cell.x, cell.y, cell.w, cell.h);
428
+ setOverlay((o) => o ? {
429
+ ...o,
430
+ left: rect.left + pos.left,
431
+ top: rect.top + pos.top,
432
+ width: pos.width,
433
+ height: pos.height
434
+ } : o);
435
+ }
436
+ };
437
+ window.addEventListener("keydown", onKeyDown, true);
438
+ return () => window.removeEventListener("keydown", onKeyDown, true);
439
+ }, [ctx, setSessionBoth]);
440
+ useDragDropMonitor(useMemo(() => ({
441
+ onDragStart: handleDragStart,
442
+ onDragMove: handleDragMove,
443
+ onDragEnd: handleDragEnd
444
+ }), [
445
+ handleDragStart,
446
+ handleDragMove,
447
+ handleDragEnd
448
+ ]));
449
+ const dragThreshold = props.dragConfig?.threshold ?? 3;
450
+ const itemSensors = useMemo(() => buildItemSensors(dragThreshold, () => propsRef.current.dragConfig), [dragThreshold]);
451
+ const renderedLayout = session ? session.preview : props.layout;
452
+ const itemsById = useMemo(() => new Map(renderedLayout.map((it) => [it.i, it])), [renderedLayout]);
453
+ const gridDraggable = props.isDraggable ?? true;
454
+ const dragEnabled = props.dragConfig?.enabled ?? true;
455
+ const isItemDraggable = useCallback((id) => {
456
+ const it = committedById.get(id);
457
+ if (!it) return false;
458
+ return gridDraggable && dragEnabled && (it.isDraggable ?? true) && !it.static;
459
+ }, [
460
+ committedById,
461
+ gridDraggable,
462
+ dragEnabled
463
+ ]);
464
+ const gridResizable = props.isResizable ?? true;
465
+ const resizeEnabled = props.resizeConfig?.enabled ?? true;
466
+ const isItemResizable = useCallback((id) => {
467
+ const it = committedById.get(id);
468
+ if (!it) return false;
469
+ return gridResizable && resizeEnabled && (it.isResizable ?? true) && !it.static;
470
+ }, [
471
+ committedById,
472
+ gridResizable,
473
+ resizeEnabled
474
+ ]);
475
+ const defaultHandles = props.resizeConfig?.handles;
476
+ const resizeHandlesFor = useCallback((id) => committedById.get(id)?.resizeHandles ?? defaultHandles ?? DEFAULT_HANDLES, [committedById, defaultHandles]);
477
+ const runtime = useMemo(() => ({
478
+ containerId,
479
+ width: props.width,
480
+ autoSize: props.autoSize ?? true,
481
+ gridConfig,
482
+ positionParams,
483
+ renderedLayout,
484
+ itemsById,
485
+ session,
486
+ isItemDraggable,
487
+ isItemResizable,
488
+ resizeHandlesFor,
489
+ itemSensors,
490
+ setContainerElement,
491
+ overlay
492
+ }), [
493
+ containerId,
494
+ props.width,
495
+ props.autoSize,
496
+ gridConfig,
497
+ positionParams,
498
+ renderedLayout,
499
+ itemsById,
500
+ session,
501
+ isItemDraggable,
502
+ isItemResizable,
503
+ resizeHandlesFor,
504
+ itemSensors,
505
+ setContainerElement,
506
+ overlay
507
+ ]);
508
+ return /* @__PURE__ */ jsx(GridContext.Provider, {
509
+ value: runtime,
510
+ children: props.children
511
+ });
512
+ }
513
+ //#endregion
514
+ //#region src/SnapGridGroup.tsx
515
+ /**
516
+ * Wrap multiple grids to let tiles be dragged **between** them. Provides one
517
+ * shared dnd-kit `DragDropProvider` and a registry so each grid can tell which
518
+ * grid the pointer is over.
519
+ *
520
+ * Item ids must be unique across all grids in a group (they share one manager).
521
+ */
522
+ function SnapGridGroup({ children }) {
523
+ const registryRef = useRef(null);
524
+ if (!registryRef.current) registryRef.current = createGridRegistry();
525
+ return /* @__PURE__ */ jsx(DragDropProvider, { children: /* @__PURE__ */ jsx(SnapGridGroupContext.Provider, {
526
+ value: registryRef.current,
527
+ children
528
+ }) });
529
+ }
530
+ //#endregion
531
+ //#region src/hooks/useGridContainer.ts
532
+ /** Total container height in pixels for the given number of occupied rows. */
533
+ function containerHeight(rows, grid) {
534
+ const padY = (grid.containerPadding ?? grid.margin)[1];
535
+ if (rows <= 0) return padY * 2;
536
+ return padY * 2 + rows * grid.rowHeight + (rows - 1) * grid.margin[1];
537
+ }
538
+ /**
539
+ * Headless hook for the grid container. Registers the droppable surface (the
540
+ * seam for cross-grid drops) and returns props (ref + sizing style) to spread
541
+ * onto your own container element.
542
+ */
543
+ function useGridContainer() {
544
+ const rt = useGridRuntime();
545
+ const { ref, isDropTarget } = useDroppable$1({
546
+ id: rt.containerId,
547
+ type: "grid",
548
+ accept: "grid-item"
549
+ });
550
+ const setContainerElement = rt.setContainerElement;
551
+ const setRef = useCallback((element) => {
552
+ ref(element);
553
+ setContainerElement(element);
554
+ }, [ref, setContainerElement]);
555
+ const height = rt.autoSize ? containerHeight(bottom(rt.renderedLayout), rt.gridConfig) : void 0;
556
+ return {
557
+ containerProps: {
558
+ ref: setRef,
559
+ style: {
560
+ position: "relative",
561
+ width: rt.width,
562
+ height
563
+ },
564
+ "data-drop-target": isDropTarget || void 0
565
+ },
566
+ isDropTarget
567
+ };
568
+ }
569
+ //#endregion
570
+ //#region src/hooks/useGridItem.ts
571
+ const REFLOW_TRANSITION = "transform 150ms ease, width 150ms ease, height 150ms ease";
572
+ /**
573
+ * Headless hook for a single grid item. Returns a ref, a positioning `style`,
574
+ * and drag state — spread them onto whatever element you render. You own the
575
+ * tag, className, content, and any cosmetic styling.
576
+ */
577
+ function useGridItem(id) {
578
+ const rt = useGridRuntime();
579
+ const item = rt.itemsById.get(id);
580
+ const { ref, isDragging } = useDraggable$1({
581
+ id,
582
+ type: "grid-item",
583
+ disabled: !rt.isItemDraggable(id),
584
+ sensors: rt.itemSensors,
585
+ plugins: NO_FEEDBACK,
586
+ data: { snapGrid: {
587
+ kind: "move",
588
+ itemId: id,
589
+ item
590
+ } }
591
+ });
592
+ const active = rt.session?.activeId === id;
593
+ let style = {
594
+ position: "absolute",
595
+ touchAction: "none"
596
+ };
597
+ if (item) {
598
+ const pos = calcGridItemPosition(rt.positionParams, item.x, item.y, item.w, item.h);
599
+ style = {
600
+ position: "absolute",
601
+ left: 0,
602
+ top: 0,
603
+ width: pos.width,
604
+ height: pos.height,
605
+ transform: `translate(${pos.left}px, ${pos.top}px)`,
606
+ visibility: active ? "hidden" : void 0,
607
+ transition: active ? "none" : REFLOW_TRANSITION,
608
+ touchAction: "none"
609
+ };
610
+ }
611
+ return {
612
+ ref,
613
+ style,
614
+ isDragging,
615
+ item
616
+ };
617
+ }
618
+ //#endregion
619
+ //#region src/hooks/useGridPlaceholder.ts
620
+ /**
621
+ * Headless hook returning where the drag placeholder should be rendered, or
622
+ * `null` when no drag is in progress. You render the element however you like.
623
+ */
624
+ function useGridPlaceholder() {
625
+ const rt = useGridRuntime();
626
+ const placeholder = rt.session?.placeholder;
627
+ if (!placeholder) return null;
628
+ const pos = calcGridItemPosition(rt.positionParams, placeholder.x, placeholder.y, placeholder.w, placeholder.h);
629
+ return {
630
+ item: placeholder,
631
+ style: {
632
+ position: "absolute",
633
+ left: 0,
634
+ top: 0,
635
+ width: pos.width,
636
+ height: pos.height,
637
+ transform: `translate(${pos.left}px, ${pos.top}px)`,
638
+ pointerEvents: "none"
639
+ }
640
+ };
641
+ }
642
+ //#endregion
643
+ //#region src/hooks/useGridResizeHandle.ts
644
+ /**
645
+ * Headless hook for a single resize handle. Model a handle as its own draggable;
646
+ * dragging it resizes the item from the given edge/corner. Position and style
647
+ * the handle however you like — spread `ref` and `handleProps` onto it.
648
+ */
649
+ function useGridResizeHandle(itemId, handle) {
650
+ const rt = useGridRuntime();
651
+ const { ref } = useDraggable$1({
652
+ id: `${itemId}::resize::${handle}`,
653
+ disabled: !rt.isItemResizable(itemId),
654
+ plugins: NO_FEEDBACK,
655
+ data: { snapGrid: {
656
+ kind: "resize",
657
+ itemId,
658
+ handle
659
+ } }
660
+ });
661
+ const isResizing = rt.session?.kind === "resize" && rt.session.activeId === itemId;
662
+ return {
663
+ ref,
664
+ handleProps: { [RESIZE_HANDLE_ATTR]: true },
665
+ isResizing
666
+ };
667
+ }
668
+ //#endregion
669
+ //#region src/hooks/useGridDragOverlay.ts
670
+ /**
671
+ * Headless hook for the floating drag preview. Returns `null` unless this grid
672
+ * is the source of an in-progress drag. Render the returned `item` with `style`
673
+ * in a portal at `document.body` so it can float across grids unclipped (see
674
+ * {@link GridDragOverlay} for the convenience component).
675
+ */
676
+ function useGridDragOverlay() {
677
+ const o = useGridRuntime().overlay;
678
+ if (!o) return null;
679
+ return {
680
+ item: o.item,
681
+ style: {
682
+ position: "fixed",
683
+ left: o.left,
684
+ top: o.top,
685
+ width: o.width,
686
+ height: o.height,
687
+ pointerEvents: "none",
688
+ zIndex: 1e3
689
+ }
690
+ };
691
+ }
692
+ //#endregion
693
+ //#region src/hooks/useResponsiveLayout.ts
694
+ /** react-grid-layout's default breakpoints (px) and column counts. */
695
+ const DEFAULT_BREAKPOINTS = {
696
+ lg: 1200,
697
+ md: 996,
698
+ sm: 768,
699
+ xs: 480,
700
+ xxs: 0
701
+ };
702
+ const DEFAULT_BREAKPOINT_COLS = {
703
+ lg: 12,
704
+ md: 10,
705
+ sm: 6,
706
+ xs: 4,
707
+ xxs: 2
708
+ };
709
+ /**
710
+ * Headless responsive layout engine: resolves the active breakpoint and its
711
+ * column count/layout from the container width, generating a layout for the
712
+ * active breakpoint from the nearest one when missing.
713
+ */
714
+ function useResponsiveLayout(options) {
715
+ const { width, layouts, breakpoints = DEFAULT_BREAKPOINTS, cols = DEFAULT_BREAKPOINT_COLS, compactor = verticalCompactor$1, onLayoutChange, onBreakpointChange } = options;
716
+ const breakpoint = getBreakpointFromWidth(breakpoints, width);
717
+ const colCount = getColsFromBreakpoint(breakpoint, cols);
718
+ const layout = useMemo(() => {
719
+ let source = breakpoint;
720
+ let sourceWidth = Number.NEGATIVE_INFINITY;
721
+ for (const [bp, minWidth] of Object.entries(breakpoints)) if (layouts[bp] && minWidth > sourceWidth) {
722
+ sourceWidth = minWidth;
723
+ source = bp;
724
+ }
725
+ return findOrGenerateResponsiveLayout(layouts, breakpoints, breakpoint, source, colCount, compactor);
726
+ }, [
727
+ layouts,
728
+ breakpoints,
729
+ breakpoint,
730
+ colCount,
731
+ compactor
732
+ ]);
733
+ const onBreakpointChangeRef = useRef(onBreakpointChange);
734
+ onBreakpointChangeRef.current = onBreakpointChange;
735
+ const firedBreakpointRef = useRef(breakpoint);
736
+ useEffect(() => {
737
+ if (firedBreakpointRef.current !== breakpoint) {
738
+ firedBreakpointRef.current = breakpoint;
739
+ onBreakpointChangeRef.current?.(breakpoint, colCount);
740
+ }
741
+ }, [breakpoint, colCount]);
742
+ return {
743
+ breakpoint,
744
+ cols: colCount,
745
+ layout,
746
+ onLayoutChange: useCallback((next) => {
747
+ onLayoutChange?.(next, {
748
+ ...layouts,
749
+ [breakpoint]: next
750
+ });
751
+ }, [
752
+ onLayoutChange,
753
+ layouts,
754
+ breakpoint
755
+ ])
756
+ };
757
+ }
758
+ //#endregion
759
+ //#region src/GridDragOverlay.tsx
760
+ /**
761
+ * Renders the floating drag preview in a portal at `document.body` — so it
762
+ * follows the pointer across grids without being clipped by any container.
763
+ * Renders nothing when this grid isn't the drag source.
764
+ */
765
+ function GridDragOverlay({ children, className, style }) {
766
+ const overlay = useGridDragOverlay();
767
+ if (typeof document === "undefined" || !overlay) return null;
768
+ return createPortal(/* @__PURE__ */ jsx("div", {
769
+ className: className ? `snapgrid-overlay ${className}` : "snapgrid-overlay",
770
+ style: style ? {
771
+ ...overlay.style,
772
+ ...style
773
+ } : overlay.style,
774
+ children: children(overlay.item)
775
+ }), document.body);
776
+ }
777
+ //#endregion
778
+ //#region src/GridItem.tsx
779
+ const HANDLE_CURSOR = {
780
+ n: "ns-resize",
781
+ s: "ns-resize",
782
+ e: "ew-resize",
783
+ w: "ew-resize",
784
+ se: "nwse-resize",
785
+ nw: "nwse-resize",
786
+ ne: "nesw-resize",
787
+ sw: "nesw-resize"
788
+ };
789
+ const SIDE = 14;
790
+ function handleStyle(handle) {
791
+ const s = {
792
+ position: "absolute",
793
+ width: SIDE,
794
+ height: SIDE,
795
+ cursor: HANDLE_CURSOR[handle],
796
+ touchAction: "none",
797
+ zIndex: 4
798
+ };
799
+ if (handle.includes("n")) s.top = -14 / 2;
800
+ if (handle.includes("s")) s.bottom = -14 / 2;
801
+ if (handle.includes("e")) s.right = -14 / 2;
802
+ if (handle.includes("w")) s.left = -14 / 2;
803
+ if (handle === "n" || handle === "s") s.left = `calc(50% - ${SIDE / 2}px)`;
804
+ if (handle === "e" || handle === "w") s.top = `calc(50% - ${SIDE / 2}px)`;
805
+ return s;
806
+ }
807
+ function DefaultResizeHandle({ itemId, handle }) {
808
+ const { ref, handleProps } = useGridResizeHandle(itemId, handle);
809
+ return /* @__PURE__ */ jsx("span", {
810
+ ref,
811
+ ...handleProps,
812
+ className: `snapgrid-resize-handle snapgrid-resize-handle--${handle}`,
813
+ style: handleStyle(handle)
814
+ });
815
+ }
816
+ /**
817
+ * Convenience wrapper over {@link useGridItem}: an absolutely-positioned `<div>`
818
+ * with stable hooks (`.snapgrid-item`, `data-grid-id`, `data-dragging`) and the
819
+ * configured resize handles. For full control, use the hooks directly.
820
+ */
821
+ function GridItem({ id, children, className, style }) {
822
+ const rt = useGridRuntime();
823
+ const { ref, style: positionStyle, isDragging } = useGridItem(id);
824
+ const handles = rt.isItemResizable(id) ? rt.resizeHandlesFor(id) : [];
825
+ return /* @__PURE__ */ jsxs("div", {
826
+ ref,
827
+ "data-grid-id": id,
828
+ "data-dragging": isDragging || void 0,
829
+ className: className ? `snapgrid-item ${className}` : "snapgrid-item",
830
+ style: style ? {
831
+ ...positionStyle,
832
+ ...style
833
+ } : positionStyle,
834
+ children: [children, handles.map((handle) => /* @__PURE__ */ jsx(DefaultResizeHandle, {
835
+ itemId: id,
836
+ handle
837
+ }, handle))]
838
+ });
839
+ }
840
+ //#endregion
841
+ //#region src/GridPlaceholder.tsx
842
+ const DEFAULT_LOOK = {
843
+ background: "rgba(99, 102, 241, 0.2)",
844
+ border: "1px dashed rgba(99, 102, 241, 0.6)",
845
+ borderRadius: 4,
846
+ boxSizing: "border-box",
847
+ zIndex: 2,
848
+ transition: "transform 150ms ease, width 150ms ease, height 150ms ease"
849
+ };
850
+ /**
851
+ * Convenience placeholder rendered from {@link useGridPlaceholder}. Renders
852
+ * nothing when no drag is active. For a custom placeholder, call the hook
853
+ * directly and render your own element with the returned `style`.
854
+ */
855
+ function GridPlaceholder({ className, style }) {
856
+ const placeholder = useGridPlaceholder();
857
+ if (!placeholder) return null;
858
+ return /* @__PURE__ */ jsx("div", {
859
+ "aria-hidden": "true",
860
+ className: className ? `snapgrid-placeholder ${className}` : "snapgrid-placeholder",
861
+ style: {
862
+ ...placeholder.style,
863
+ ...DEFAULT_LOOK,
864
+ ...style
865
+ }
866
+ });
867
+ }
868
+ //#endregion
869
+ //#region src/GridLayout.tsx
870
+ /** Strip the namespacing prefix React applies to keys inside `Children.map`. */
871
+ function keyToId(key) {
872
+ return key.startsWith(".$") ? key.slice(2) : key;
873
+ }
874
+ /** The default surface: positioned container + mapped items + placeholder. */
875
+ function GridSurface({ className, style, children }) {
876
+ const { containerProps } = useGridContainer();
877
+ const childById = /* @__PURE__ */ new Map();
878
+ Children.forEach(children, (child) => {
879
+ if (isValidElement(child) && child.key != null) childById.set(keyToId(String(child.key)), child);
880
+ });
881
+ return /* @__PURE__ */ jsxs("div", {
882
+ ...containerProps,
883
+ className: className ? `snapgrid ${className}` : "snapgrid",
884
+ style: style ? {
885
+ ...containerProps.style,
886
+ ...style
887
+ } : containerProps.style,
888
+ children: [
889
+ Children.map(children, (child) => {
890
+ if (!isValidElement(child) || child.key == null) return child;
891
+ return /* @__PURE__ */ jsx(GridItem, {
892
+ id: keyToId(String(child.key)),
893
+ children: child
894
+ }, child.key);
895
+ }),
896
+ /* @__PURE__ */ jsx(GridPlaceholder, {}),
897
+ /* @__PURE__ */ jsx(GridDragOverlay, { children: (item) => childById.get(item.i) ?? null })
898
+ ]
899
+ });
900
+ }
901
+ /**
902
+ * Drop-in grid component: a controlled, react-grid-layout v2-compatible layout
903
+ * backed by dnd-kit. A thin shell over {@link SnapGridProvider} and the headless
904
+ * hooks — children are keyed by their layout item's `i`. For full control over
905
+ * markup/styling, use the provider + hooks directly.
906
+ */
907
+ function GridLayout(props) {
908
+ const { className, style, children, ...providerProps } = props;
909
+ return /* @__PURE__ */ jsx(SnapGridProvider, {
910
+ ...providerProps,
911
+ children: /* @__PURE__ */ jsx(GridSurface, {
912
+ className,
913
+ style,
914
+ children
915
+ })
916
+ });
917
+ }
918
+ //#endregion
919
+ //#region src/ResponsiveGridLayout.tsx
920
+ /**
921
+ * A responsive grid: switches column count and layout by breakpoint as `width`
922
+ * changes, generating a breakpoint's layout from the nearest one when absent.
923
+ * A thin wrapper over {@link useResponsiveLayout} + {@link GridLayout}; mirrors
924
+ * react-grid-layout v2's `ResponsiveGridLayout`.
925
+ */
926
+ function ResponsiveGridLayout(props) {
927
+ const { cols, layout, onLayoutChange } = useResponsiveLayout({
928
+ width: props.width,
929
+ layouts: props.layouts,
930
+ breakpoints: props.breakpoints,
931
+ cols: props.cols,
932
+ compactor: props.compactor,
933
+ onLayoutChange: props.onLayoutChange,
934
+ onBreakpointChange: props.onBreakpointChange
935
+ });
936
+ const gridConfig = useMemo(() => ({
937
+ cols,
938
+ rowHeight: props.rowHeight ?? 150,
939
+ margin: props.margin ?? [10, 10],
940
+ containerPadding: props.containerPadding ?? null
941
+ }), [
942
+ cols,
943
+ props.rowHeight,
944
+ props.margin,
945
+ props.containerPadding
946
+ ]);
947
+ return /* @__PURE__ */ jsx(GridLayout, {
948
+ layout,
949
+ width: props.width,
950
+ onLayoutChange,
951
+ gridConfig,
952
+ compactor: props.compactor,
953
+ dragConfig: props.dragConfig,
954
+ resizeConfig: props.resizeConfig,
955
+ isDraggable: props.isDraggable,
956
+ isResizable: props.isResizable,
957
+ autoSize: props.autoSize,
958
+ className: props.className,
959
+ style: props.style,
960
+ children: props.children
961
+ });
962
+ }
963
+ //#endregion
964
+ //#region src/hooks/useContainerWidth.ts
965
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
966
+ /**
967
+ * Measure a container's width with a `ResizeObserver`. Replaces react-grid-layout's
968
+ * `WidthProvider` HOC with a hook, mirroring RGL v2's `useContainerWidth`.
969
+ */
970
+ function useContainerWidth(options = {}) {
971
+ const { initialWidth = 1280 } = options;
972
+ const [width, setWidth] = useState(initialWidth);
973
+ const [mounted, setMounted] = useState(false);
974
+ const [element, setElement] = useState(null);
975
+ const containerRef = useCallback((node) => setElement(node), []);
976
+ useIsomorphicLayoutEffect(() => {
977
+ if (!element || typeof ResizeObserver === "undefined") return;
978
+ const measure = () => {
979
+ const next = element.getBoundingClientRect().width;
980
+ if (next > 0) {
981
+ setWidth(next);
982
+ setMounted(true);
983
+ }
984
+ };
985
+ measure();
986
+ const observer = new ResizeObserver(measure);
987
+ observer.observe(element);
988
+ return () => observer.disconnect();
989
+ }, [element]);
990
+ return {
991
+ width,
992
+ mounted,
993
+ containerRef
994
+ };
995
+ }
996
+ //#endregion
997
+ export { DEFAULT_BREAKPOINTS, DEFAULT_BREAKPOINT_COLS, Feedback, GridDragOverlay, GridItem, GridLayout, GridPlaceholder, KeyboardSensor, PointerSensor, ResponsiveGridLayout, SnapGridGroup, SnapGridProvider, getCompactor, horizontalCompactor, noCompactor, useContainerWidth, useDraggable, useDroppable, useGridContainer, useGridDragOverlay, useGridItem, useGridPlaceholder, useGridResizeHandle, useResponsiveLayout, verticalCompactor };
998
+
999
+ //# sourceMappingURL=index.mjs.map