@snapgridjs/react 0.4.0 → 0.6.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 CHANGED
@@ -1,385 +1,27 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _dnd_kit_react = require("@dnd-kit/react");
3
3
  let _snapgridjs_core = require("@snapgridjs/core");
4
+ let _snapgridjs_dnd = require("@snapgridjs/dnd");
4
5
  let react = require("react");
5
- let _dnd_kit_collision = require("@dnd-kit/collision");
6
- let _dnd_kit_abstract = require("@dnd-kit/abstract");
7
6
  let _dnd_kit_dom = require("@dnd-kit/dom");
8
7
  let _dnd_kit_dom_utilities = require("@dnd-kit/dom/utilities");
9
8
  let _dnd_kit_react_sortable = require("@dnd-kit/react/sortable");
10
9
  let react_jsx_runtime = require("react/jsx-runtime");
11
- //#region src/dnd/entity.ts
12
- /**
13
- * The DOM element of a dnd-kit entity (a draggable, droppable, or drag source).
14
- * dnd-kit's abstract types don't expose `element`, but the DOM layer snapgrid
15
- * runs on always sets it — this centralizes that one assumption (and the cast)
16
- * in a single place instead of scattering it across the drag code.
17
- */
18
- function domElement(entity) {
19
- return entity?.element ?? null;
20
- }
21
- //#endregion
22
- //#region src/dnd/collision.ts
23
- /**
24
- * Marker attribute set on every grid container element. Used by {@link gridDepth}
25
- * to measure how deeply a grid is nested, purely from the DOM.
26
- */
27
- const SNAPGRID_GRID_ATTR = "data-snapgrid-grid";
28
- const GRID_COLLISION_PRIORITY = 10;
29
- /**
30
- * How deeply `el`'s grid is nested: the number of ancestor grid containers above
31
- * it. A top-level grid is 0; a grid rendered inside another grid's tile is 1; and
32
- * so on. DOM containment is the ground truth, so this is correct regardless of the
33
- * React tree shape or how priorities are assigned elsewhere.
34
- */
35
- function gridDepth(el) {
36
- let depth = 0;
37
- let node = el?.parentElement ?? null;
38
- while (node) {
39
- if (node.hasAttribute("data-snapgrid-grid")) depth++;
40
- node = node.parentElement;
41
- }
42
- return depth;
43
- }
44
- /**
45
- * Collision detector for grid droppables. Runs dnd-kit's default detector, then —
46
- * when nested grid rects overlap (the pointer is over both an inner grid and its
47
- * outer one) — ranks the **innermost** grid highest by boosting priority with the
48
- * grid's nesting depth. Without this, overlapping grids tie on priority and the
49
- * winner is arbitrary. For non-nested grids depth is 0, so priority is unchanged.
50
- */
51
- const gridCollisionDetector = (input) => {
52
- const collision = (0, _dnd_kit_collision.defaultCollisionDetection)(input);
53
- if (!collision) return null;
54
- return {
55
- ...collision,
56
- priority: GRID_COLLISION_PRIORITY + gridDepth(domElement(input.droppable))
57
- };
58
- };
59
- //#endregion
60
- //#region src/controller/GridController.ts
61
- function sameItem(a, b) {
62
- if (a === b) return true;
63
- if (!a || !b) return false;
64
- return a.i === b.i && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
65
- }
66
- /**
67
- * Live per-grid drag/resize state as a plain observable: the provider writes
68
- * (`setSession`/`setKeyboard`/`setCommitted`), hooks subscribe to just their own
69
- * slice via `useSyncExternalStore`. Value-cached snapshots mean a drag re-renders
70
- * only the tiles whose slice changed, not the whole subtree (the old
71
- * context-value model re-rendered every tile every frame).
72
- */
73
- var GridController = class {
74
- id;
75
- #committed;
76
- #session = null;
77
- #keyboard = false;
78
- #listeners = /* @__PURE__ */ new Set();
79
- config = null;
80
- #itemCache = /* @__PURE__ */ new Map();
81
- #resizeCache = /* @__PURE__ */ new Map();
82
- #placeholderCache = null;
83
- #renderedMap = null;
84
- #renderedMapSource = null;
85
- #indexById = /* @__PURE__ */ new Map();
86
- #nextIndex = 0;
87
- /** The dnd-kit manager this grid is registered with (set by useInstance). */
88
- manager;
89
- constructor(id, committed = [], manager) {
90
- this.id = id;
91
- this.#committed = committed;
92
- this.manager = manager;
93
- }
94
- /** Replace the per-grid config (called by the container host during render). */
95
- setConfig(config) {
96
- this.config = config;
97
- }
98
- /**
99
- * Re-point this grid's id. The container host syncs it (during render, before
100
- * the droppable/group read it) when the controlled `id` prop changes, so the
101
- * returned `group`, the droppable id, and the registry key never drift apart.
102
- */
103
- setId(id) {
104
- this.id = id;
105
- }
106
- register = () => {};
107
- subscribe = (listener) => {
108
- this.#listeners.add(listener);
109
- return () => {
110
- this.#listeners.delete(listener);
111
- };
112
- };
113
- #emit() {
114
- for (const listener of this.#listeners) listener();
115
- }
116
- /** The layout currently shown: the drag preview while dragging, else committed. */
117
- #rendered() {
118
- return this.#session ? this.#session.preview : this.#committed;
119
- }
120
- #renderedById() {
121
- const rendered = this.#rendered();
122
- if (this.#renderedMapSource !== rendered) {
123
- this.#renderedMap = new Map(rendered.map((it) => [it.i, it]));
124
- this.#renderedMapSource = rendered;
125
- }
126
- return this.#renderedMap;
127
- }
128
- /**
129
- * Sync the committed layout from the controlled `layout` prop. Called during
130
- * the provider's render, so it must NOT notify — emitting here would update
131
- * subscribed GridItems mid-render (a React "setState while rendering" error).
132
- * No notify is needed: a `layout` prop change already re-renders the whole
133
- * provider subtree, so every GridItem re-reads its snapshot on that pass.
134
- */
135
- setCommitted(layout) {
136
- if (this.#committed === layout) return;
137
- this.#committed = layout;
138
- const present = new Set(layout.map((it) => it.i));
139
- for (const id of this.#indexById.keys()) if (!present.has(id)) this.#indexById.delete(id);
140
- for (const id of this.#itemCache.keys()) if (!present.has(id)) this.#itemCache.delete(id);
141
- for (const id of this.#resizeCache.keys()) if (!present.has(id)) this.#resizeCache.delete(id);
142
- }
143
- setSession(next) {
144
- this.#session = next;
145
- this.#emit();
146
- }
147
- getSession() {
148
- return this.#session;
149
- }
150
- /** Record whether the active drag is keyboard-driven (drives `hidden`). */
151
- setKeyboard(value) {
152
- if (this.#keyboard === value) return;
153
- this.#keyboard = value;
154
- this.#emit();
155
- }
156
- itemSnapshot = (id) => {
157
- const item = this.#renderedById().get(id);
158
- const isDragging = this.#session?.activeId === id;
159
- const hidden = isDragging && this.#session?.kind === "move" && !this.#keyboard;
160
- const prev = this.#itemCache.get(id);
161
- if (prev && prev.isDragging === isDragging && prev.hidden === hidden && sameItem(prev.item, item)) return prev;
162
- const snap = {
163
- item,
164
- isDragging,
165
- hidden
166
- };
167
- this.#itemCache.set(id, snap);
168
- return snap;
169
- };
170
- placeholderSnapshot = () => {
171
- const next = this.#session?.placeholder ?? null;
172
- if (sameItem(this.#placeholderCache ?? void 0, next ?? void 0)) return this.#placeholderCache;
173
- this.#placeholderCache = next;
174
- return next;
175
- };
176
- resizeSnapshot = (itemId) => {
177
- const isResizing = this.#session?.kind === "resize" && this.#session.activeId === itemId;
178
- const prev = this.#resizeCache.get(itemId);
179
- if (prev && prev.isResizing === isResizing) return prev;
180
- const snap = { isResizing };
181
- this.#resizeCache.set(itemId, snap);
182
- return snap;
183
- };
184
- renderedSnapshot = () => this.#rendered();
185
- /** A stable index for `id` (see {@link GridController.#indexById}). */
186
- itemIndex(id) {
187
- let i = this.#indexById.get(id);
188
- if (i === void 0) {
189
- i = this.#nextIndex++;
190
- this.#indexById.set(id, i);
191
- }
192
- return i;
193
- }
194
- };
195
- //#endregion
196
- //#region src/controller/registry.ts
197
- /**
198
- * Resolves a grid's {@link GridController} by its id, scoped to the dnd-kit
199
- * manager the grid is registered with. A container registers its controller
200
- * here (during render, so child items resolve it on their first render); items
201
- * look it up by their `group` (= the grid id). Replaces the old geometry
202
- * `GridRegistry` — which grid the pointer is over now comes from the collision
203
- * target, so the registry's only job is id → controller resolution.
204
- *
205
- * Keyed by manager so two apps (or two providers) never collide, and grids in
206
- * one provider share a map (the cross-grid seam).
207
- */
208
- const byManager = /* @__PURE__ */ new WeakMap();
209
- const noManager = /* @__PURE__ */ new Map();
210
- function mapFor(manager) {
211
- if (!manager) return noManager;
212
- let map = byManager.get(manager);
213
- if (!map) {
214
- map = /* @__PURE__ */ new Map();
215
- byManager.set(manager, map);
216
- }
217
- return map;
218
- }
219
- /** Register a controller under `id` for `manager`. Returns an unregister fn. */
220
- function registerController(manager, id, controller) {
221
- const map = mapFor(manager);
222
- map.set(id, controller);
223
- return () => {
224
- if (map.get(id) === controller) map.delete(id);
225
- };
226
- }
227
- /** The controller registered under `id` for `manager`, or undefined. */
228
- function getController(manager, id) {
229
- return mapFor(manager).get(id);
230
- }
231
- const grabOffsets = /* @__PURE__ */ new WeakMap();
232
- const noManagerGrab = { current: null };
233
- function setGrabOffset(manager, offset) {
234
- if (!manager) {
235
- noManagerGrab.current = offset;
236
- return;
237
- }
238
- if (offset) grabOffsets.set(manager, offset);
239
- else grabOffsets.delete(manager);
240
- }
241
- function getGrabOffset(manager) {
242
- return (manager ? grabOffsets.get(manager) : noManagerGrab.current) ?? {
243
- x: 0,
244
- y: 0
245
- };
246
- }
247
- //#endregion
248
- //#region src/dnd/dragFlow.ts
249
- /**
250
- * Pure decision helpers for the drag interaction so the tricky bits — grab-offset
251
- * cell mapping, the cross-grid drop lifecycle, and external-drop acceptance — are
252
- * unit-testable without a DOM or dnd-kit.
253
- */
254
- /** Read snapgrid's payload off a dnd-kit drag source. */
255
- function dragData(event) {
256
- return (event.operation.source?.data)?.snapGrid;
257
- }
258
- /** Size/id spec for an external (non-grid) draggable the grid may accept, or null. */
259
- function externalDropSpec(source, dropConfig) {
260
- if (!dropConfig?.enabled || !source) return null;
261
- const data = source.data;
262
- if (data?.snapGrid) return null;
263
- if (dropConfig.accept && !dropConfig.accept(source)) return null;
264
- const spec = data?.snapGridDrop;
265
- return {
266
- i: spec?.i,
267
- w: spec?.w ?? dropConfig.defaultItem?.w ?? 1,
268
- h: spec?.h ?? dropConfig.defaultItem?.h ?? 1
269
- };
270
- }
271
- /**
272
- * Map a client-space pointer to a grid cell, accounting for where *within* the
273
- * dragged tile the pointer grabbed it. Subtracting the grab offset means the
274
- * tile's top-left (not the cursor) maps to the cell, so a received tile's
275
- * placeholder aligns with the floating overlay instead of jumping its corner to
276
- * the cursor. External drops pass `{ x: 0, y: 0 }` (no meaningful grab point).
277
- */
278
- function receiveCell(pointer, gridRect, grabOffset, w, h, pp) {
279
- return (0, _snapgridjs_core.calcXY)(pp, pointer.y - grabOffset.y - gridRect.top, pointer.x - grabOffset.x - gridRect.left, w, h);
280
- }
281
- /**
282
- * Map a keyboard event key to a one-cell grid step while a keyboard drag is
283
- * active, or null for keys snapgrid doesn't own — Enter/Space (drop) and Escape
284
- * (cancel) fall through to dnd-kit's KeyboardSensor.
285
- */
286
- function arrowStep(key) {
287
- switch (key) {
288
- case "ArrowLeft": return [-1, 0];
289
- case "ArrowRight": return [1, 0];
290
- case "ArrowUp": return [0, -1];
291
- case "ArrowDown": return [0, 1];
292
- default: return null;
293
- }
294
- }
295
- /**
296
- * Which grid a drop commits to, as fed to {@link classifyDrop} as `dest`. A
297
- * keyboard drag has no pointer, so it can only ever land in its own grid; a
298
- * pointer drag lands in whichever grid the collision observer resolved (or none).
299
- */
300
- function dropDestination(opts) {
301
- if (opts.keyboard) return opts.myId;
302
- return opts.targetId != null ? String(opts.targetId) : null;
303
- }
304
- /** Pure classification of a drag end. See {@link DropAction}. */
305
- function classifyDrop(s) {
306
- if (s.canceled) {
307
- if (s.kind === "resize") return "cancel-resize";
308
- if (s.ownsItem) return "cancel-move";
309
- return "noop";
310
- }
311
- if (s.kind === "resize") return "commit-resize";
312
- if (s.ownsItem && s.hasData) {
313
- if (s.dest === s.myId && s.kind === "move") return "commit-in-grid";
314
- if (s.dest) return "remove-source";
315
- return "revert";
316
- }
317
- if (s.dest === s.myId && s.kind === "move") return s.hasData ? "commit-dest" : "external-drop";
318
- return "noop";
319
- }
320
- //#endregion
321
- //#region src/dnd/snapToGrid.ts
322
- /**
323
- * Quantizes the dragged item's transform to whole grid cells, so the floating
324
- * <DragOverlay> clone jumps cell-to-cell in lockstep with the (always-snapped)
325
- * placeholder instead of tracking the pointer smoothly. Applied on the item
326
- * draggable; a no-op unless `dragConfig.snapToGrid` is set.
327
- */
328
- var SnapToGrid = class extends _dnd_kit_abstract.Modifier {
329
- apply({ transform }) {
330
- const opts = this.options;
331
- if (!opts?.isEnabled()) return transform;
332
- const pp = opts.getPositionParams();
333
- const colStep = (0, _snapgridjs_core.calcGridColWidth)(pp) + pp.margin[0];
334
- const rowStep = pp.rowHeight + pp.margin[1];
335
- if (colStep <= 0 || rowStep <= 0) return transform;
336
- return {
337
- x: Math.round(transform.x / colStep) * colStep,
338
- y: Math.round(transform.y / rowStep) * rowStep
339
- };
340
- }
341
- };
342
- //#endregion
343
- //#region src/hooks/dndShared.ts
344
- /** Marker attribute placed on resize-handle elements. */
345
- const RESIZE_HANDLE_ATTR = "data-snapgrid-resize-handle";
346
- const NO_FEEDBACK = [_dnd_kit_dom.Feedback.configure({ feedback: "none" })];
347
- /**
348
- * Whether a pointer-down on `target` should NOT start an item move. Pure and
349
- * exported for testing. Honors three rules, in order:
350
- * - never start a move from a resize handle;
351
- * - never start from a region matching `dragConfig.cancel`;
352
- * - if `dragConfig.handle` is set, only start from within it.
353
- */
354
- function shouldPreventItemDrag(target, cfg) {
355
- if (!(target instanceof Element)) return false;
356
- if (target.closest(`[data-snapgrid-resize-handle]`)) return true;
357
- if (cfg?.cancel && target.closest(cfg.cancel)) return true;
358
- if (cfg?.handle && !target.closest(cfg.handle)) return true;
359
- return false;
360
- }
361
- /**
362
- * Sensors for item (move) draggables, built from the drag config: a distance
363
- * activation threshold (so clicks don't start drags) plus handle/cancel/resize
364
- * gating, with the keyboard sensor kept for accessibility.
365
- */
366
- function buildItemSensors(threshold, getDragConfig) {
367
- return [_dnd_kit_dom.PointerSensor.configure({
368
- activationConstraints: () => threshold > 0 ? [new _dnd_kit_dom.PointerActivationConstraints.Distance({ value: threshold })] : void 0,
369
- preventActivation: (event) => shouldPreventItemDrag(event.target, getDragConfig())
370
- }), _dnd_kit_dom.KeyboardSensor];
371
- }
372
- //#endregion
373
10
  //#region src/hooks/useGridController.ts
374
11
  const DEFAULT_HANDLES = ["se"];
375
12
  function itemGateOpen(flag, isStatic) {
376
13
  return isStatic ? flag === true : flag ?? true;
377
14
  }
378
15
  /**
379
- * The grid's brain: owns the {@link GridController}, runs the dnd-kit drag/resize
380
- * monitor for this grid, and writes per-grid config to the controller each render.
381
- * Created by {@link useGridContainer}; items resolve the same controller by their
382
- * `group` (= this grid's id) from the per-manager registry. Consumes the ambient
16
+ * The grid's React seam: owns the {@link GridController} (an observable render
17
+ * bridge), publishes per-grid config to it each render, registers it for id →
18
+ * controller resolution, and attaches the manager-wide {@link SnapGridEngine}.
19
+ *
20
+ * The drag/resize *orchestration* lives in the engine (one per manager), not
21
+ * here — this hook only wires the React-specific parts: the controller, the item
22
+ * sensors/modifiers descriptors, the draggable/resizable gates, and the config
23
+ * the engine reads. Created by {@link useGridContainer}; items resolve the same
24
+ * controller by their `group` (= this grid's id). Consumes the ambient
383
25
  * `DragDropProvider` — it does not mint one.
384
26
  */
385
27
  function useGridController(opts) {
@@ -392,271 +34,27 @@ function useGridController(opts) {
392
34
  const positionParams = (0, react.useMemo)(() => (0, _snapgridjs_core.toPositionParams)(gridConfig, opts.width), [gridConfig, opts.width]);
393
35
  const compactor = opts.compactor ?? _snapgridjs_core.verticalCompactor;
394
36
  const manager = (0, _dnd_kit_react.useDragDropManager)();
395
- const controller = (0, _dnd_kit_react.useInstance)((m) => new GridController(containerId, opts.layout, m ?? void 0));
37
+ const controller = (0, _dnd_kit_react.useInstance)((m) => new _snapgridjs_dnd.GridController(containerId, opts.layout, m ?? void 0));
396
38
  controller.setCommitted(opts.layout);
397
39
  if (controller.id !== containerId) controller.setId(containerId);
398
40
  const optsRef = (0, react.useRef)(opts);
399
41
  optsRef.current = opts;
400
42
  const ppRef = (0, react.useRef)(positionParams);
401
43
  ppRef.current = positionParams;
402
- const gridRef = (0, react.useRef)(gridConfig);
403
- gridRef.current = gridConfig;
404
- const compactorRef = (0, react.useRef)(compactor);
405
- compactorRef.current = compactor;
406
- const containerIdRef = (0, react.useRef)(containerId);
407
- containerIdRef.current = containerId;
408
- const managerRef = (0, react.useRef)(manager);
409
- managerRef.current = manager;
410
- const sessionRef = (0, react.useRef)(null);
411
- const containerElRef = (0, react.useRef)(null);
412
- const keyboardRef = (0, react.useRef)(false);
413
- const dropSpecRef = (0, react.useRef)(null);
414
- const dropCounterRef = (0, react.useRef)(0);
415
- registerController(manager, containerId, controller);
416
- (0, react.useEffect)(() => registerController(manager, containerId, controller), [
44
+ (0, _snapgridjs_dnd.registerController)(manager, containerId, controller);
45
+ (0, react.useEffect)(() => (0, _snapgridjs_dnd.registerController)(manager, containerId, controller), [
417
46
  manager,
418
47
  containerId,
419
48
  controller
420
49
  ]);
421
- const committedById = (0, react.useMemo)(() => new Map(opts.layout.map((it) => [it.i, it])), [opts.layout]);
422
- const committedByIdRef = (0, react.useRef)(committedById);
423
- committedByIdRef.current = committedById;
424
- const setSessionBoth = (0, react.useCallback)((next) => {
425
- sessionRef.current = next;
426
- controller.setSession(next);
427
- }, [controller]);
428
- const setKeyboard = (0, react.useCallback)((value) => {
429
- keyboardRef.current = value;
430
- controller.setKeyboard(value);
431
- }, [controller]);
432
- const setContainerElement = (0, react.useCallback)((element) => {
433
- containerElRef.current = element;
434
- }, []);
435
- /**
436
- * Is THIS grid the drop target dnd-kit's collision observer resolved? Both the
437
- * move-phase preview and the drop-phase commit read `operation.target`, so they
438
- * always agree on which grid wins (one oracle), including when grids overlap.
439
- */
440
- const overMe = (0, react.useCallback)((target) => target?.id === containerIdRef.current, []);
441
- /** Map a client-space pointer to a grid cell within THIS grid (see {@link receiveCell}). */
442
- const cellFromPointer = (0, react.useCallback)((p, item) => {
443
- const el = containerElRef.current;
444
- if (!el) return null;
445
- return receiveCell(p, el.getBoundingClientRect(), getGrabOffset(managerRef.current), item.w, item.h, ppRef.current);
446
- }, []);
447
- const ctx = (0, react.useCallback)(() => ({
448
- positionParams: ppRef.current,
449
- compactor: compactorRef.current,
450
- cols: gridRef.current.cols
451
- }), []);
452
- const handleDragStart = (0, react.useCallback)((event) => {
453
- setKeyboard(false);
454
- const data = dragData(event);
455
- if (!data) {
456
- const spec = externalDropSpec(event.operation.source, optsRef.current.dropConfig);
457
- if (spec) {
458
- dropCounterRef.current += 1;
459
- dropSpecRef.current = {
460
- i: spec.i ?? `${containerIdRef.current}-dropped-${dropCounterRef.current}`,
461
- w: spec.w,
462
- h: spec.h
463
- };
464
- } else dropSpecRef.current = null;
465
- return;
466
- }
467
- dropSpecRef.current = null;
468
- const layout = optsRef.current.layout;
469
- const item = layout.find((it) => it.i === data.itemId);
470
- const p = event.operation.position.current;
471
- const pointer = {
472
- x: p.x,
473
- y: p.y
474
- };
475
- if (data.kind === "resize") {
476
- if (!item) return;
477
- setSessionBoth((0, _snapgridjs_core.beginResize)(layout, {
478
- item,
479
- rect: (0, _snapgridjs_core.calcGridItemPosition)(ppRef.current, item.x, item.y, item.w, item.h),
480
- pointer
481
- }, data.handle));
482
- optsRef.current.onResizeStart?.(layout, item, item, item, event.operation.activatorEvent, null);
483
- return;
484
- }
485
- if (item) {
486
- setKeyboard(event.operation.activatorEvent instanceof KeyboardEvent);
487
- const rect = (0, _snapgridjs_core.calcGridItemPosition)(ppRef.current, item.x, item.y, item.w, item.h);
488
- setSessionBoth((0, _snapgridjs_core.beginDrag)(layout, {
489
- item,
490
- left: rect.left,
491
- top: rect.top,
492
- pointer
493
- }));
494
- const cr = domElement(event.operation.source)?.getBoundingClientRect();
495
- if (cr) setGrabOffset(managerRef.current, {
496
- x: pointer.x - cr.left,
497
- y: pointer.y - cr.top
498
- });
499
- optsRef.current.onDragStart?.(layout, item, item, item, event.operation.activatorEvent, null);
500
- }
501
- }, [setSessionBoth, setKeyboard]);
502
- const handleDragMove = (0, react.useCallback)((event) => {
503
- if (keyboardRef.current) return;
504
- const p = event.operation.position.current;
505
- const pointer = {
506
- x: p.x,
507
- y: p.y
508
- };
509
- const current = sessionRef.current;
510
- if (current?.kind === "resize") {
511
- const next = (0, _snapgridjs_core.dragResize)(current, pointer, ctx());
512
- setSessionBoth(next);
513
- optsRef.current.onResize?.(next.preview, next.anchor.item, next.placeholder, next.placeholder, event.operation.activatorEvent, null);
514
- return;
515
- }
516
- const target = event.operation.target;
517
- const data = dragData(event);
518
- if (!data) {
519
- const spec = dropSpecRef.current;
520
- if (spec && overMe(target)) {
521
- const foreign = {
522
- i: spec.i,
523
- x: 0,
524
- y: 0,
525
- w: spec.w,
526
- h: spec.h
527
- };
528
- const committed = optsRef.current.layout;
529
- const cell = cellFromPointer(pointer, foreign) ?? {
530
- x: 0,
531
- y: 0
532
- };
533
- setSessionBoth((0, _snapgridjs_core.beginReceive)(committed, foreign, cell.x, cell.y, pointer, ctx()));
534
- } else if (sessionRef.current) setSessionBoth(null);
535
- return;
536
- }
537
- if (data.kind !== "move") return;
538
- const here = overMe(target);
539
- if (committedByIdRef.current.has(data.itemId)) {
540
- const source = current?.kind === "move" ? current : null;
541
- if (!source) return;
542
- let next;
543
- if (here) next = (0, _snapgridjs_core.dragTo)(source, pointer, ctx());
544
- else next = {
545
- ...source,
546
- preview: source.committed,
547
- placeholder: null
548
- };
549
- setSessionBoth(next);
550
- optsRef.current.onDrag?.(next.preview, next.anchor.item, next.placeholder, next.placeholder, event.operation.activatorEvent, null);
551
- return;
552
- }
553
- if (here) {
554
- const foreign = data.item;
555
- const committed = optsRef.current.layout;
556
- const cell = cellFromPointer(pointer, foreign) ?? {
557
- x: 0,
558
- y: 0
559
- };
560
- setSessionBoth((0, _snapgridjs_core.beginReceive)(committed, foreign, cell.x, cell.y, pointer, ctx()));
561
- } else if (sessionRef.current) setSessionBoth(null);
562
- }, [
563
- setSessionBoth,
564
- overMe,
565
- cellFromPointer,
566
- ctx
567
- ]);
568
- const handleDragEnd = (0, react.useCallback)((event) => {
569
- const current = sessionRef.current;
570
- const data = dragData(event);
571
- const myId = containerIdRef.current;
572
- const dest = dropDestination({
573
- keyboard: keyboardRef.current,
574
- targetId: event.operation.target?.id,
575
- myId
576
- });
577
- const ownsItem = data ? committedByIdRef.current.has(data.itemId) : false;
578
- setGrabOffset(managerRef.current, null);
579
- const native = event.nativeEvent ?? null;
580
- const o = optsRef.current;
581
- const action = classifyDrop({
582
- kind: current?.kind ?? null,
583
- canceled: event.canceled,
584
- ownsItem,
585
- hasData: !!data,
586
- dest,
587
- myId
588
- });
589
- switch (action) {
590
- case "cancel-resize":
591
- o.onResizeStop?.(o.layout, current?.anchor.item ?? null, null, null, native, null);
592
- break;
593
- case "cancel-move":
594
- o.onDragStop?.(o.layout, current?.anchor.item ?? null, null, null, native, null);
595
- break;
596
- case "commit-resize":
597
- if (current) {
598
- o.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(current));
599
- o.onResizeStop?.(current.preview, current.anchor.item, current.placeholder, current.placeholder, native, null);
600
- }
601
- break;
602
- case "commit-in-grid":
603
- case "remove-source":
604
- case "revert":
605
- if (action === "commit-in-grid" && current) o.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(current));
606
- else if (action === "remove-source" && data) {
607
- const { compactor: c, cols } = ctx();
608
- o.onLayoutChange?.((0, _snapgridjs_core.removeItemWithCompactor)(o.layout, data.itemId, {
609
- compactor: c,
610
- cols
611
- }));
612
- }
613
- o.onDragStop?.(current?.preview ?? o.layout, current?.anchor.item ?? null, current?.placeholder ?? null, current?.placeholder ?? null, native, null);
614
- break;
615
- case "commit-dest":
616
- if (current) o.onLayoutChange?.((0, _snapgridjs_core.commitLayout)(current));
617
- break;
618
- case "external-drop":
619
- if (current) {
620
- const committed = (0, _snapgridjs_core.commitLayout)(current);
621
- const dropped = committed.find((it) => it.i === current.activeId);
622
- if (dropped) o.onDrop?.(committed, dropped, native);
623
- }
624
- break;
625
- }
626
- dropSpecRef.current = null;
627
- setKeyboard(false);
628
- setSessionBoth(null);
629
- }, [
630
- setSessionBoth,
631
- setKeyboard,
632
- ctx
633
- ]);
634
50
  (0, react.useEffect)(() => {
635
- const onKeyDown = (e) => {
636
- if (!keyboardRef.current) return;
637
- const session = sessionRef.current;
638
- if (!session || session.kind !== "move") return;
639
- const step = arrowStep(e.key);
640
- if (!step) return;
641
- e.preventDefault();
642
- e.stopImmediatePropagation();
643
- setSessionBoth((0, _snapgridjs_core.nudge)(session, step[0], step[1], ctx()));
644
- };
645
- window.addEventListener("keydown", onKeyDown, true);
646
- return () => window.removeEventListener("keydown", onKeyDown, true);
647
- }, [ctx, setSessionBoth]);
648
- (0, _dnd_kit_react.useDragDropMonitor)((0, react.useMemo)(() => ({
649
- onDragStart: handleDragStart,
650
- onDragMove: handleDragMove,
651
- onDragEnd: handleDragEnd
652
- }), [
653
- handleDragStart,
654
- handleDragMove,
655
- handleDragEnd
656
- ]));
51
+ if (!manager) return;
52
+ return (0, _snapgridjs_dnd.attachEngine)(manager);
53
+ }, [manager]);
54
+ const committedById = (0, react.useMemo)(() => new Map(opts.layout.map((it) => [it.i, it])), [opts.layout]);
657
55
  const dragThreshold = opts.dragConfig?.threshold ?? 3;
658
- const itemSensors = (0, react.useMemo)(() => buildItemSensors(dragThreshold, () => optsRef.current.dragConfig), [dragThreshold]);
659
- const itemModifiers = (0, react.useMemo)(() => [SnapToGrid.configure({
56
+ const itemSensors = (0, react.useMemo)(() => (0, _snapgridjs_dnd.buildItemSensors)(dragThreshold, () => optsRef.current.dragConfig), [dragThreshold]);
57
+ const itemModifiers = (0, react.useMemo)(() => [_snapgridjs_dnd.SnapToGrid.configure({
660
58
  getPositionParams: () => ppRef.current,
661
59
  isEnabled: () => optsRef.current.dragConfig?.snapToGrid ?? false
662
60
  })], []);
@@ -684,6 +82,25 @@ function useGridController(opts) {
684
82
  ]);
685
83
  const defaultHandles = opts.resizeConfig?.handles;
686
84
  const resizeHandlesFor = (0, react.useCallback)((id) => committedById.get(id)?.resizeHandles ?? defaultHandles ?? DEFAULT_HANDLES, [committedById, defaultHandles]);
85
+ const callbacks = (0, react.useMemo)(() => ({
86
+ onDragStart: opts.onDragStart,
87
+ onDrag: opts.onDrag,
88
+ onDragStop: opts.onDragStop,
89
+ onResizeStart: opts.onResizeStart,
90
+ onResize: opts.onResize,
91
+ onResizeStop: opts.onResizeStop,
92
+ onLayoutChange: opts.onLayoutChange,
93
+ onDrop: opts.onDrop
94
+ }), [
95
+ opts.onDragStart,
96
+ opts.onDrag,
97
+ opts.onDragStop,
98
+ opts.onResizeStart,
99
+ opts.onResize,
100
+ opts.onResizeStop,
101
+ opts.onLayoutChange,
102
+ opts.onDrop
103
+ ]);
687
104
  controller.setConfig({
688
105
  positionParams,
689
106
  gridConfig,
@@ -694,7 +111,10 @@ function useGridController(opts) {
694
111
  isItemDraggable,
695
112
  isItemResizable,
696
113
  resizeHandlesFor,
697
- setContainerElement
114
+ compactor,
115
+ dragConfig: opts.dragConfig,
116
+ dropConfig: opts.dropConfig,
117
+ callbacks
698
118
  });
699
119
  return controller;
700
120
  }
@@ -714,25 +134,26 @@ function containerHeight(rows, grid) {
714
134
  */
715
135
  function useGridContainer(opts) {
716
136
  const controller = useGridController(opts);
717
- const { width, autoSize, gridConfig, setContainerElement } = controller.config;
137
+ const { width, autoSize, gridConfig } = controller.config;
718
138
  const gridElRef = (0, react.useRef)(null);
719
139
  const { ref, isDropTarget } = (0, _dnd_kit_react.useDroppable)({
720
140
  id: controller.id,
721
141
  type: "grid",
722
142
  accept: (source) => {
723
- const srcEl = domElement(source);
143
+ const srcEl = (0, _snapgridjs_dnd.domElement)(source);
724
144
  if (srcEl && gridElRef.current && srcEl.contains(gridElRef.current)) return false;
725
145
  if (source.type === "grid-item") return true;
726
- return source.data?.snapGridDrop != null;
146
+ if (source.data?.snapGridDrop != null) return true;
147
+ return opts.accept?.(source) ?? false;
727
148
  },
728
- collisionDetector: gridCollisionDetector
149
+ collisionDetector: _snapgridjs_dnd.gridCollisionDetector
729
150
  });
730
151
  const setRef = (0, react.useCallback)((element) => {
731
152
  ref(element);
732
- setContainerElement(element);
153
+ controller.element = element;
733
154
  gridElRef.current = element;
734
- if (element) element.setAttribute(SNAPGRID_GRID_ATTR, "");
735
- }, [ref, setContainerElement]);
155
+ if (element) element.setAttribute(_snapgridjs_dnd.SNAPGRID_GRID_ATTR, "");
156
+ }, [ref, controller]);
736
157
  const renderedLayout = (0, react.useSyncExternalStore)(controller.subscribe, controller.renderedSnapshot, controller.renderedSnapshot);
737
158
  return {
738
159
  containerProps: {
@@ -751,6 +172,7 @@ function useGridContainer(opts) {
751
172
  }
752
173
  const REFLOW_EASING = "ease";
753
174
  const REFLOW_TRANSITION = `transform 150ms ${REFLOW_EASING}, width 150ms ${REFLOW_EASING}, height 150ms ${REFLOW_EASING}`;
175
+ const TILE_TRANSITION = `width 150ms ${REFLOW_EASING}, height 150ms ${REFLOW_EASING}`;
754
176
  //#endregion
755
177
  //#region src/hooks/useResolveController.ts
756
178
  /**
@@ -761,7 +183,7 @@ const REFLOW_TRANSITION = `transform 150ms ${REFLOW_EASING}, width 150ms ${REFLO
761
183
  * grid / `DragDropProvider`.
762
184
  */
763
185
  function useResolveController(group) {
764
- const controller = getController((0, _dnd_kit_react.useDragDropManager)(), group);
186
+ const controller = (0, _snapgridjs_dnd.getController)((0, _dnd_kit_react.useDragDropManager)(), group);
765
187
  if (!controller) throw new Error(`snapgrid: no grid found for group "${group}". A grid item must pass the group returned by its grid's useGridContainer, and render inside a <DragDropProvider> (or use <GridLayout>, which wires this for you).`);
766
188
  return controller;
767
189
  }
@@ -771,6 +193,7 @@ const ITEM_FEEDBACK = [_dnd_kit_dom.Feedback.configure({
771
193
  feedback: (_source, manager) => (0, _dnd_kit_dom_utilities.isKeyboardEvent)(manager.dragOperation.activatorEvent) ? "none" : "default",
772
194
  dropAnimation: null
773
195
  })];
196
+ const tileNeverTarget = () => null;
774
197
  /**
775
198
  * Headless hook for a single grid tile. The tile is a real `useSortable` (a
776
199
  * draggable + droppable carrying `group`/`index`/`type`/`accept`), so it
@@ -781,10 +204,18 @@ const ITEM_FEEDBACK = [_dnd_kit_dom.Feedback.configure({
781
204
  * element you render — you own the tag, className, content, and cosmetic styling.
782
205
  *
783
206
  * The dragged tile floats itself via dnd-kit's default feedback (no `<DragOverlay>`):
784
- * the active tile renders at its committed origin so the float offset composes, and
785
- * reflow is animated on the compositor via the Web Animations API — both so it stays
786
- * smooth in Safari, where the float's popover top-layer repaint would jank a
787
- * CSS-transition reflow.
207
+ * the active tile renders at its committed origin and dnd-kit's float follows the
208
+ * pointer from there, while reflow is animated on the compositor via the Web
209
+ * Animations API (a FLIP) — both so it stays smooth in Safari, where the float's
210
+ * popover top-layer repaint would jank a CSS-transition reflow.
211
+ *
212
+ * Tiles are positioned with `left`/`top` (not `transform`). dnd-kit's self-float
213
+ * measures the source element's rect ignoring transforms and re-applies its current
214
+ * transform each frame; a transform-positioned tile leans on that, but the
215
+ * compensation is lost the instant the dragged element is swapped for a foreign one
216
+ * mid-drag (grid → sortable interop — the tile becomes a flow card), which would
217
+ * make the float jump by the tile's grid offset. Plain left/top has nothing to lose
218
+ * on the swap, matching how dnd-kit's own flow-positioned sortables hand off cleanly.
788
219
  */
789
220
  function useGridItem(id, group) {
790
221
  const controller = useResolveController(group);
@@ -799,14 +230,20 @@ function useGridItem(id, group) {
799
230
  const data = (0, react.useMemo)(() => ({ snapGrid: {
800
231
  kind: "move",
801
232
  itemId: id,
802
- item
803
- } }), [id, item]);
233
+ item,
234
+ group
235
+ } }), [
236
+ id,
237
+ item,
238
+ group
239
+ ]);
804
240
  const { ref: sortableRef, handleRef, isDragging } = (0, _dnd_kit_react_sortable.useSortable)({
805
241
  id,
806
242
  index: controller.itemIndex(id),
807
243
  group,
808
244
  type: "grid-item",
809
245
  accept: "grid-item",
246
+ collisionDetector: tileNeverTarget,
810
247
  disabled: !config.isItemDraggable(id),
811
248
  sensors: config.itemSensors,
812
249
  modifiers: config.itemModifiers,
@@ -826,6 +263,7 @@ function useGridItem(id, group) {
826
263
  const posTop = pos?.top;
827
264
  const prev = (0, react.useRef)(null);
828
265
  const reflowAnim = (0, react.useRef)(null);
266
+ const settleAnchor = (0, react.useRef)(null);
829
267
  (0, react.useLayoutEffect)(() => {
830
268
  const cur = posLeft != null && posTop != null ? {
831
269
  left: posLeft,
@@ -834,17 +272,31 @@ function useGridItem(id, group) {
834
272
  const before = prev.current;
835
273
  prev.current = cur;
836
274
  const el = elRef.current;
837
- if (!el || !cur || !before || active || justDropped || !dragging) return;
275
+ if (!el || !cur) return;
276
+ if (active) {
277
+ settleAnchor.current = cur;
278
+ reflowAnim.current?.cancel();
279
+ return;
280
+ }
281
+ if (dragging) settleAnchor.current = null;
282
+ else if (settleAnchor.current) {
283
+ const a = settleAnchor.current;
284
+ reflowAnim.current?.cancel();
285
+ if (cur.left !== a.left || cur.top !== a.top) settleAnchor.current = null;
286
+ return;
287
+ }
288
+ if (!before || justDropped) return;
838
289
  if (before.left === cur.left && before.top === cur.top) return;
290
+ if (typeof el.animate !== "function") return;
839
291
  let fromX = before.left;
840
292
  let fromY = before.top;
841
293
  if (reflowAnim.current?.playState === "running") {
842
294
  const m = new DOMMatrix(getComputedStyle(el).transform);
843
- fromX = m.m41;
844
- fromY = m.m42;
295
+ fromX = before.left + m.m41;
296
+ fromY = before.top + m.m42;
845
297
  }
846
298
  reflowAnim.current?.cancel();
847
- reflowAnim.current = el.animate([{ transform: `translate(${fromX}px, ${fromY}px)` }, { transform: `translate(${cur.left}px, ${cur.top}px)` }], {
299
+ reflowAnim.current = el.animate([{ transform: `translate(${fromX - cur.left}px, ${fromY - cur.top}px)` }, { transform: "translate(0px, 0px)" }], {
848
300
  duration: 150,
849
301
  easing: REFLOW_EASING
850
302
  });
@@ -861,12 +313,11 @@ function useGridItem(id, group) {
861
313
  handleRef,
862
314
  style: pos ? {
863
315
  position: "absolute",
864
- left: 0,
865
- top: 0,
316
+ left: pos.left,
317
+ top: pos.top,
866
318
  width: pos.width,
867
319
  height: pos.height,
868
- transform: `translate(${pos.left}px, ${pos.top}px)`,
869
- transition: active || justDropped || dragging ? "none" : REFLOW_TRANSITION,
320
+ transition: justDropped || dragging ? "none" : TILE_TRANSITION,
870
321
  touchAction: "none"
871
322
  } : {
872
323
  position: "absolute",
@@ -914,17 +365,18 @@ function useGridResizeHandle(itemId, handle, group) {
914
365
  const { ref } = (0, _dnd_kit_react.useDraggable)({
915
366
  id: `${itemId}::resize::${handle}`,
916
367
  disabled: !controller.config?.isItemResizable(itemId),
917
- plugins: NO_FEEDBACK,
368
+ plugins: _snapgridjs_dnd.NO_FEEDBACK,
918
369
  data: { snapGrid: {
919
370
  kind: "resize",
920
371
  itemId,
921
- handle
372
+ handle,
373
+ group
922
374
  } }
923
375
  });
924
376
  const { isResizing } = (0, react.useSyncExternalStore)(controller.subscribe, () => controller.resizeSnapshot(itemId), () => controller.resizeSnapshot(itemId));
925
377
  return {
926
378
  ref,
927
- handleProps: { [RESIZE_HANDLE_ATTR]: true },
379
+ handleProps: { [_snapgridjs_dnd.RESIZE_HANDLE_ATTR]: true },
928
380
  isResizing
929
381
  };
930
382
  }
@@ -1263,6 +715,12 @@ Object.defineProperty(exports, "PointerSensor", {
1263
715
  });
1264
716
  exports.ResponsiveGridLayout = ResponsiveGridLayout;
1265
717
  exports.SnapGridGroup = SnapGridGroup;
718
+ Object.defineProperty(exports, "defaultGridConfig", {
719
+ enumerable: true,
720
+ get: function() {
721
+ return _snapgridjs_core.defaultGridConfig;
722
+ }
723
+ });
1266
724
  Object.defineProperty(exports, "getCompactor", {
1267
725
  enumerable: true,
1268
726
  get: function() {
@@ -1275,12 +733,36 @@ Object.defineProperty(exports, "horizontalCompactor", {
1275
733
  return _snapgridjs_core.horizontalCompactor;
1276
734
  }
1277
735
  });
736
+ Object.defineProperty(exports, "insertItemWithCompactor", {
737
+ enumerable: true,
738
+ get: function() {
739
+ return _snapgridjs_core.insertItemWithCompactor;
740
+ }
741
+ });
1278
742
  Object.defineProperty(exports, "noCompactor", {
1279
743
  enumerable: true,
1280
744
  get: function() {
1281
745
  return _snapgridjs_core.noCompactor;
1282
746
  }
1283
747
  });
748
+ Object.defineProperty(exports, "removeItemWithCompactor", {
749
+ enumerable: true,
750
+ get: function() {
751
+ return _snapgridjs_core.removeItemWithCompactor;
752
+ }
753
+ });
754
+ Object.defineProperty(exports, "snapMove", {
755
+ enumerable: true,
756
+ get: function() {
757
+ return _snapgridjs_dnd.snapMove;
758
+ }
759
+ });
760
+ Object.defineProperty(exports, "toPositionParams", {
761
+ enumerable: true,
762
+ get: function() {
763
+ return _snapgridjs_core.toPositionParams;
764
+ }
765
+ });
1284
766
  exports.useContainerWidth = useContainerWidth;
1285
767
  Object.defineProperty(exports, "useDraggable", {
1286
768
  enumerable: true,