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