@ai-matrx/summon 0.1.2 → 0.1.4

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/CHANGELOG.md CHANGED
@@ -1,9 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4
4
+ ### Unreleased
5
+
6
+ - **New: `./dock`, the one primitive for floating chrome-less cards, pills and docks** (design R5 wave 2). `<Dock>` / `<DockHandle>` / `useDock()` / `useDockContext()` / `<DockProvider>`: pointer drag that never re-renders while moving (CSS `translate` in one animation frame, one commit on release), corner-relative positions clamped to the viewport with an optional nearest-corner snap and fixed-control avoidance, the position remembered per dock id (own store, `DockProvider`, else localStorage) or reported to a controlled host, the window manager's z-order lane on press, the design-system drag cursor hold, a click-vs-drag threshold that swallows the trailing click. Replaces the copies in AI Matrx (`useDraggableFloat`, the assists `useDockDrag`) and in `@ai-matrx/chat` (`useDraggablePanel`, the agent widget drags). Guard: `src/dock/__tests__/dock.test.tsx`. Consumer action: none.
7
+ - **New: the render map can live on the host.** `render` is now optional on `defineOverlay`, and `<SummonHost renders={…}>` takes an id → `() => import("./<id>.render")` map (`OverlayRenderMap`, data type erased, no `any`) for definitions declared without one. A definition's own `render` still wins. Why: every file that imports a definition to open it also inherited an `import()` edge to that overlay's window subtree; with ~220 overlays and ~450 opener files in AI Matrx that multiplied the build graph (a sub-app build went from ~10 s to an out-of-memory kill). An opened overlay with no render source shows its error card (`OverlayRenderMissingError`, `MISSING_RENDER_ERROR`) and logs in development. Guard: `src/react/__tests__/hostRenders.test.tsx`. Consumer action: none for existing users.
8
+
3
9
  ## 0.1.2
4
10
 
5
- UNVERIFIED: this release has no Unreleased entry and the source comparison could not be made (the caller supplied no source comparison). Whether source changed is unknown; consumer action is unknown.
6
- Check the package's gitHead on npm against this version's gitHead: `git diff <previous gitHead> <this gitHead> -- apps/shared/summon/src`.
11
+ SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
12
+ so they may include breaking API changes. Consumer action is UNKNOWN: read the diff before upgrading.
13
+ Diff: `git diff b7bc6ffc86 19d4c1f55d -- apps/shared/summon/src`
14
+
15
+ Changed shipped source files (4):
16
+ - `apps/shared/summon/src/persistence/controller.ts`
17
+ - `apps/shared/summon/src/persistence/data/createDataPersistence.ts`
18
+ - `apps/shared/summon/src/sheet/OverlaySheets.tsx`
19
+ - `apps/shared/summon/src/window/useWindowPanel.ts`
7
20
 
8
21
  ## 0.1.1
9
22
  ### Unreleased
package/README.md CHANGED
@@ -25,7 +25,7 @@ pnpm add @ai-matrx/summon react react-dom react-redux @reduxjs/toolkit
25
25
  | `html-to-image` (optional) | tray thumbnails of minimized windows (without it the tile shows no preview) |
26
26
  | `@supabase/supabase-js` | only `./persistence/data`, through `@ai-matrx/data` |
27
27
 
28
- Entries: `.` (core, no React), `./handles` (no React, no Redux), `./react`, `./window`, `./window-host` (the root-mountable half of the window section: `WindowsProvider`, `WindowTraySync`, `registerTrayPreview` — never loads WindowPanel), `./dialog`, `./sheet`, `./lite`, `./url`, `./persistence`, `./persistence/data`, `./next`, `./testing`, `./styles.css`.
28
+ Entries: `.` (core, no React), `./handles` (no React, no Redux), `./react`, `./window`, `./window-host` (the root-mountable half of the window section: `WindowsProvider`, `WindowTraySync`, `registerTrayPreview` — never loads WindowPanel), `./dialog`, `./sheet`, `./lite`, `./dock`, `./url`, `./persistence`, `./persistence/data`, `./next`, `./testing`, `./styles.css`.
29
29
 
30
30
  ## Quick start
31
31
 
@@ -121,6 +121,7 @@ const Always = () => <notesWindow.Controller noteId="42" />;
121
121
  | `window` | `{ mobile, mobileSidebarAs, ephemeral, preservation, restoreDefaults, windowId, deprecated, unbiddenHome }` for window overlays |
122
122
  | `link` | `{ key, aliases?, hydrate }`, see Links |
123
123
  | `parse` | narrow raw data once; `null` refuses |
124
+ | `render` | `() => import("./x.render")`; optional when the host supplies it (see below), and wins over the host map when present |
124
125
 
125
126
  Minimizing a window always keeps its component mounted, whatever `keepAlive` says.
126
127
 
@@ -159,6 +160,26 @@ emit("uploaded", { url });
159
160
 
160
161
  Closing the instance disposes the channel. Emitting after that is a silent no-op.
161
162
 
163
+ ### Large apps: the render map lives on the host
164
+
165
+ Every file that imports a definition to open it also imports that definition's `render: () => import(…)`. To a bundler, each such file gains an edge to the overlay's whole window subtree. With a few overlays that is free. With hundreds of overlays and hundreds of opener files it multiplies the build graph: in AI Matrx, about 450 opener files each reached about 220 window subtrees, and a sub-app build went from about 10 s to an out-of-memory kill. This is the host-build law: never fan out `import()` edges.
166
+
167
+ The fix keeps "declare once" at two files plus one line. Leave `render` off the definitions, and give the host one id → import map, generated from the file names, that only the host imports:
168
+
169
+ ```ts
170
+ // definitions/notes.overlay.ts — no render: importing it costs nothing
171
+ export const notes = defineOverlay<{ noteId?: string }>()({ id: "notes", label: "Notes" });
172
+
173
+ // definitions/render-map.ts — generated, imported ONLY by the host (behind its own lazy boundary)
174
+ import type { OverlayRenderMap } from "@ai-matrx/summon/react";
175
+ export const RENDER_MAP: OverlayRenderMap = { notes: () => import("./notes.render") };
176
+
177
+ // the host
178
+ <SummonHost definitions={APP_OVERLAYS} renders={RENDER_MAP} />
179
+ ```
180
+
181
+ `OverlayRenderMap` erases each module's data type (`OverlayRenderProps<never>`), so differently typed render modules share one map without `any`. An overlay that is opened with no render source at all (no `render`, no map entry) shows its error card (`OverlayRenderMissingError`, exported as `MISSING_RENDER_ERROR`) and logs in development. It is never a silent blank.
182
+
162
183
  ## Mounting into an existing Redux store
163
184
 
164
185
  ```ts
@@ -351,6 +372,26 @@ import { LiteWindow } from "@ai-matrx/summon/lite";
351
372
 
352
373
  `OverlayDialog`, `OverlaySheet`, `OverlayDrawer` and `OverlayBottomSheet` take the design-system component's props plus the close binding: `boundToOverlay` (open while the instance is shown) or `onClose` (+ `open`). `LiteWindow` has drag, edge resize, Escape when topmost, focus in and out, and a bottom drawer on phones; no minimize, tray, persistence or links.
353
374
 
375
+ ### Dock (floating chrome-less cards, pills and docks)
376
+
377
+ ```tsx
378
+ import { Dock, DockHandle, DockProvider, useDock, useDockContext } from "@ai-matrx/summon/dock";
379
+
380
+ <Dock id="attention" corner="bottom-right" defaultPosition={{ x: 16, y: 16 }} zIndex={50} className="w-80">
381
+ <DockHandle>⋮⋮ Alerts <button onClick={…}>Close</button></DockHandle>
382
+ …
383
+ </Dock>
384
+
385
+ // Your own markup: spread the bindings.
386
+ const dock = useDock({ id: "assists", corner: "bottom-right", position, onPositionChange: save, touch: false, clamp: { margin: 0, minVisible: { width: 120, height: 32 } } });
387
+ <div ref={dock.ref} style={dock.style}><button {...dock.handleProps}>pill</button></div>
388
+
389
+ // Hand every dock the host's storage (declared keys, per-user sync).
390
+ <DockProvider store={{ read: (id) => …, write: (id, position) => … }}>…</DockProvider>
391
+ ```
392
+
393
+ Pointer drag that never re-renders while moving: each move paints the CSS `translate` property in one animation frame and the position is committed to React state once, on release (`dragging` flips twice per gesture, never per move), so a dock with a streaming answer inside stays cheap. Positions are offsets from a corner (`corner`, default bottom-right), clamped to the viewport on every render (`clamp: { margin = 8, minVisible }`; a dock bigger than the room keeps its top-left reachable); `snapToCorner` re-anchors to the nearest corner on release; `avoid: { selector, gap }` keeps it off fixed controls. Without a position it rests on `defaultPosition`, or on a CSS `anchor` (`top/bottom/left/right/centerX`). Remembered per id: the dock's `store`, else the nearest `DockProvider`, else localStorage (`matrx:summon:dock:<id>`); `persist={false}` for per-moment docks; a controlled host passes `position` + `onPositionChange` (told once per drag, and `null` on `reset()`). A press passes a 4px `threshold` before it is a drag, and a drag swallows its trailing click; controls inside the handle (`ignore`, default buttons/links/fields/`[data-no-drag]`) never start one; `touch={false}` leaves touch to page scroll; `draggable={false}` pins it (phones). On press it joins the window manager's z-order lane (`raiseTransientWindow`, above windows, below modals) when a `SummonProvider` is present, and the drag holds `grabbing` through design-system `holdDragCursor`. No listener exists outside an active press.
394
+
354
395
  ## Styling
355
396
 
356
397
  Import the structural CSS once, and make Tailwind v4 scan the package, since the components use utility classes:
package/dist/dock.cjs ADDED
@@ -0,0 +1,552 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/dock.ts
22
+ var dock_exports = {};
23
+ __export(dock_exports, {
24
+ DEFAULT_DOCK_IGNORE: () => DEFAULT_DOCK_IGNORE,
25
+ DEFAULT_DOCK_MARGIN: () => DEFAULT_DOCK_MARGIN,
26
+ DEFAULT_DRAG_THRESHOLD: () => DEFAULT_DRAG_THRESHOLD,
27
+ DOCK_STORAGE_PREFIX: () => DOCK_STORAGE_PREFIX,
28
+ Dock: () => Dock,
29
+ DockHandle: () => DockHandle,
30
+ DockProvider: () => DockProvider,
31
+ avoidExclusions: () => avoidExclusions,
32
+ clampOffset: () => clampOffset,
33
+ createLocalDockStore: () => createLocalDockStore,
34
+ fromScreen: () => fromScreen,
35
+ memoryOnlyDockStore: () => memoryOnlyDockStore,
36
+ nearestCorner: () => nearestCorner,
37
+ parseDockPosition: () => parseDockPosition,
38
+ toScreen: () => toScreen,
39
+ useDock: () => useDock,
40
+ useDockContext: () => useDockContext,
41
+ useDockStore: () => useDockStore
42
+ });
43
+ module.exports = __toCommonJS(dock_exports);
44
+
45
+ // src/dock/Dock.tsx
46
+ var import_react4 = require("react");
47
+
48
+ // src/dock/useDock.ts
49
+ var import_react2 = require("react");
50
+ var import_design_system = require("@ai-matrx/design-system");
51
+ var import_summon = require("@ai-matrx/summon");
52
+ var import_react3 = require("@ai-matrx/summon/react");
53
+
54
+ // src/dock/geometry.ts
55
+ var DEFAULT_DOCK_MARGIN = 8;
56
+ var DEFAULT_DRAG_THRESHOLD = 4;
57
+ var fromLeft = (corner) => corner.endsWith("left");
58
+ var fromTop = (corner) => corner.startsWith("top");
59
+ function toScreen(offset, corner, size, viewport) {
60
+ return {
61
+ left: fromLeft(corner) ? offset.x : viewport.width - size.width - offset.x,
62
+ top: fromTop(corner) ? offset.y : viewport.height - size.height - offset.y
63
+ };
64
+ }
65
+ function fromScreen(point, corner, size, viewport) {
66
+ return {
67
+ x: fromLeft(corner) ? point.left : viewport.width - size.width - point.left,
68
+ y: fromTop(corner) ? point.top : viewport.height - size.height - point.top
69
+ };
70
+ }
71
+ function clampOffset(offset, corner, size, viewport, clamp = {}) {
72
+ const margin = clamp.margin ?? DEFAULT_DOCK_MARGIN;
73
+ const visible = clamp.minVisible ?? size;
74
+ const axis = (value, room, extent, nearIsStart) => {
75
+ const min = margin;
76
+ const max = room - extent - margin;
77
+ if (max < min) return Math.round(nearIsStart ? min : max);
78
+ return Math.round(Math.min(Math.max(value, min), max));
79
+ };
80
+ return {
81
+ x: axis(offset.x, viewport.width, visible.width, fromLeft(corner)),
82
+ y: axis(offset.y, viewport.height, visible.height, fromTop(corner))
83
+ };
84
+ }
85
+ function nearestCorner(point, size, viewport) {
86
+ const top = point.top + size.height / 2 < viewport.height / 2;
87
+ const left = point.left + size.width / 2 < viewport.width / 2;
88
+ return `${top ? "top" : "bottom"}-${left ? "left" : "right"}`;
89
+ }
90
+ function overlaps(a, b) {
91
+ return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
92
+ }
93
+ function avoidExclusions(point, size, excluded, gap, keepOnScreen) {
94
+ const bounded = keepOnScreen(point);
95
+ if (excluded.length === 0) return bounded;
96
+ const rectFor = (candidate) => ({
97
+ left: candidate.left,
98
+ top: candidate.top,
99
+ right: candidate.left + size.width,
100
+ bottom: candidate.top + size.height
101
+ });
102
+ const isClear = (candidate) => !excluded.some((target) => overlaps(rectFor(candidate), target));
103
+ if (isClear(bounded)) return bounded;
104
+ const clear = excluded.flatMap((target) => [
105
+ { left: bounded.left, top: target.top - size.height - gap },
106
+ { left: bounded.left, top: target.bottom + gap },
107
+ { left: target.left - size.width - gap, top: bounded.top },
108
+ { left: target.right + gap, top: bounded.top }
109
+ ]).map(keepOnScreen).filter(isClear);
110
+ if (clear.length === 0) return bounded;
111
+ const distance = (candidate) => Math.hypot(candidate.left - bounded.left, candidate.top - bounded.top);
112
+ return clear.reduce((nearest, candidate) => distance(candidate) < distance(nearest) ? candidate : nearest);
113
+ }
114
+ function samePosition(a, b) {
115
+ if (!a || !b) return a === b || !a && !b;
116
+ return a.x === b.x && a.y === b.y && a.corner === b.corner;
117
+ }
118
+
119
+ // src/dock/store.ts
120
+ var import_react = require("react");
121
+ var DOCK_STORAGE_PREFIX = "matrx:summon:dock:";
122
+ var CORNERS = ["top-left", "top-right", "bottom-left", "bottom-right"];
123
+ function parseDockPosition(value) {
124
+ if (!value || typeof value !== "object") return null;
125
+ const { x, y, corner } = value;
126
+ if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y)) return null;
127
+ if (corner !== void 0 && !CORNERS.includes(corner)) return null;
128
+ return corner === void 0 ? { x, y } : { x, y, corner };
129
+ }
130
+ function storage() {
131
+ try {
132
+ return typeof window === "undefined" ? null : window.localStorage;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+ function createLocalDockStore(prefix = DOCK_STORAGE_PREFIX) {
138
+ return {
139
+ read(id) {
140
+ try {
141
+ const raw = storage()?.getItem(prefix + id);
142
+ return raw ? parseDockPosition(JSON.parse(raw)) : null;
143
+ } catch {
144
+ return null;
145
+ }
146
+ },
147
+ write(id, position) {
148
+ try {
149
+ const area = storage();
150
+ if (!area) return;
151
+ if (position) area.setItem(prefix + id, JSON.stringify(position));
152
+ else area.removeItem(prefix + id);
153
+ } catch {
154
+ }
155
+ }
156
+ };
157
+ }
158
+ var memoryOnlyDockStore = { read: () => null, write: () => void 0 };
159
+ var defaultStore = createLocalDockStore();
160
+ var DockStoreContext = (0, import_react.createContext)(null);
161
+ function DockProvider({ store, children }) {
162
+ return (0, import_react.createElement)(DockStoreContext.Provider, { value: store }, children);
163
+ }
164
+ function useDockStore(own) {
165
+ const provided = (0, import_react.useContext)(DockStoreContext);
166
+ return own ?? provided ?? defaultStore;
167
+ }
168
+
169
+ // src/dock/useDock.ts
170
+ var DEFAULT_DOCK_IGNORE = "button,a,input,select,textarea,[contenteditable='true'],[data-no-drag]";
171
+ var SSR_VIEWPORT = { width: 1280, height: 800 };
172
+ var noopSubscribe = () => () => void 0;
173
+ function readViewport() {
174
+ return typeof window === "undefined" ? SSR_VIEWPORT : { width: window.innerWidth, height: window.innerHeight };
175
+ }
176
+ function measure(element) {
177
+ return { width: element?.offsetWidth ?? 0, height: element?.offsetHeight ?? 0 };
178
+ }
179
+ function excludedRects(avoid, self) {
180
+ if (!avoid || typeof document === "undefined") return [];
181
+ return [...document.querySelectorAll(avoid.selector)].filter((target) => target !== self && !self?.contains(target)).map((target) => target.getBoundingClientRect()).filter((rect) => rect.width > 0 && rect.height > 0);
182
+ }
183
+ function useDock(options) {
184
+ const {
185
+ id,
186
+ corner = "bottom-right",
187
+ defaultPosition = null,
188
+ anchor,
189
+ position: controlled,
190
+ onPositionChange,
191
+ persist = true,
192
+ draggable = true,
193
+ touch = true,
194
+ threshold = DEFAULT_DRAG_THRESHOLD,
195
+ clamp,
196
+ avoid,
197
+ snapToCorner = false,
198
+ zIndex: baseZ,
199
+ raiseOnPress = true,
200
+ ignore = DEFAULT_DOCK_IGNORE
201
+ } = options;
202
+ const isControlled = controlled !== void 0;
203
+ const providedStore = useDockStore(options.store);
204
+ const store = persist && !isControlled ? providedStore : memoryOnlyDockStore;
205
+ const [element, setElement] = (0, import_react2.useState)(null);
206
+ const [local, setLocal] = (0, import_react2.useState)(null);
207
+ const [pending, setPending] = (0, import_react2.useState)(null);
208
+ const [geometry, setGeometry] = (0, import_react2.useState)({ viewport: SSR_VIEWPORT, size: { width: 0, height: 0 } });
209
+ const [nudge, setNudge] = (0, import_react2.useState)(null);
210
+ const [dragging, setDragging] = (0, import_react2.useState)(false);
211
+ const pressRef = (0, import_react2.useRef)(null);
212
+ const frameRef = (0, import_react2.useRef)(null);
213
+ const adoptingRef = (0, import_react2.useRef)(false);
214
+ const restoredRef = (0, import_react2.useRef)(null);
215
+ const chosen = isControlled ? pending && samePosition(pending.forProp, controlled) ? pending.value : controlled : local;
216
+ const raw = chosen ?? defaultPosition;
217
+ const rawCorner = raw?.corner ?? corner;
218
+ const offset = raw ? clampOffset(raw, rawCorner, geometry.size, geometry.viewport, clamp) : null;
219
+ const live = (0, import_react2.useRef)({ offset, rawCorner, clamp, avoid, threshold, snapToCorner, onPositionChange, isControlled, controlled, store, id });
220
+ (0, import_react2.useEffect)(() => {
221
+ live.current = { offset, rawCorner, clamp, avoid, threshold, snapToCorner, onPositionChange, isControlled, controlled, store, id };
222
+ });
223
+ (0, import_react2.useLayoutEffect)(() => {
224
+ if (!element || restoredRef.current === id) return;
225
+ restoredRef.current = id;
226
+ if (isControlled) return;
227
+ setLocal(store.read(id));
228
+ }, [element, id, isControlled, store]);
229
+ (0, import_react2.useLayoutEffect)(() => {
230
+ if (!element) return void 0;
231
+ let frame = null;
232
+ const read = () => {
233
+ const next = { viewport: readViewport(), size: measure(element) };
234
+ setGeometry(
235
+ (current) => current.viewport.width === next.viewport.width && current.viewport.height === next.viewport.height && current.size.width === next.size.width && current.size.height === next.size.height ? current : next
236
+ );
237
+ };
238
+ const schedule = () => {
239
+ if (frame !== null) return;
240
+ frame = window.requestAnimationFrame(() => {
241
+ frame = null;
242
+ read();
243
+ });
244
+ };
245
+ read();
246
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
247
+ observer?.observe(element);
248
+ window.addEventListener("resize", schedule);
249
+ return () => {
250
+ observer?.disconnect();
251
+ window.removeEventListener("resize", schedule);
252
+ if (frame !== null) window.cancelAnimationFrame(frame);
253
+ };
254
+ }, [element]);
255
+ const avoidSelector = avoid?.selector;
256
+ const avoidGap = avoid?.gap;
257
+ (0, import_react2.useLayoutEffect)(() => {
258
+ if (!element || !avoidSelector || offset) {
259
+ setNudge((current) => current === null ? current : null);
260
+ return void 0;
261
+ }
262
+ let frame = null;
263
+ let applied = null;
264
+ const reconcile = () => {
265
+ const rect = element.getBoundingClientRect();
266
+ const size = measure(element);
267
+ const viewport = readViewport();
268
+ const base = { left: rect.left - (applied?.left ?? 0), top: rect.top - (applied?.top ?? 0) };
269
+ const keep = (p) => {
270
+ const o = clampOffset(fromScreen(p, "top-left", size, viewport), "top-left", size, viewport, live.current.clamp);
271
+ return toScreen(o, "top-left", size, viewport);
272
+ };
273
+ const clear = avoidExclusions(base, size, excludedRects({ selector: avoidSelector }, element), avoidGap ?? DEFAULT_DOCK_MARGIN, keep);
274
+ const next = { left: Math.round(clear.left - base.left), top: Math.round(clear.top - base.top) };
275
+ applied = next.left === 0 && next.top === 0 ? null : next;
276
+ setNudge(
277
+ (current) => current === null && applied === null || current && applied && current.left === applied.left && current.top === applied.top ? current : applied
278
+ );
279
+ };
280
+ const schedule = () => {
281
+ if (frame !== null) return;
282
+ frame = window.requestAnimationFrame(() => {
283
+ frame = null;
284
+ reconcile();
285
+ });
286
+ };
287
+ reconcile();
288
+ const mutations = typeof MutationObserver === "undefined" ? null : new MutationObserver(schedule);
289
+ mutations?.observe(document.body, { childList: true, subtree: true });
290
+ window.addEventListener("resize", schedule);
291
+ window.addEventListener("scroll", schedule, true);
292
+ return () => {
293
+ mutations?.disconnect();
294
+ window.removeEventListener("resize", schedule);
295
+ window.removeEventListener("scroll", schedule, true);
296
+ if (frame !== null) window.cancelAnimationFrame(frame);
297
+ };
298
+ }, [element, avoidSelector, avoidGap, offset === null]);
299
+ let placed = null;
300
+ if (offset) {
301
+ placed = toScreen(offset, rawCorner, geometry.size, geometry.viewport);
302
+ if (avoid && element) {
303
+ const size = geometry.size;
304
+ const viewport = geometry.viewport;
305
+ const keep = (p) => toScreen(clampOffset(fromScreen(p, "top-left", size, viewport), "top-left", size, viewport, clamp), "top-left", size, viewport);
306
+ placed = avoidExclusions(placed, size, excludedRects(avoid, element), avoid.gap ?? DEFAULT_DOCK_MARGIN, keep);
307
+ }
308
+ }
309
+ const renderedOffset = placed && offset ? fromScreen(placed, rawCorner, geometry.size, geometry.viewport) : offset;
310
+ (0, import_react2.useLayoutEffect)(() => {
311
+ if (!adoptingRef.current) return;
312
+ adoptingRef.current = false;
313
+ element?.style.removeProperty("translate");
314
+ });
315
+ const summonStore = (0, import_react3.useOptionalSummonStore)();
316
+ const laneZ = (0, import_react2.useSyncExternalStore)(
317
+ summonStore ? summonStore.subscribe : noopSubscribe,
318
+ () => summonStore ? summonStore.getState().windowManager?.transientZ?.[id] : void 0,
319
+ () => void 0
320
+ );
321
+ (0, import_react2.useEffect)(() => {
322
+ if (!summonStore) return void 0;
323
+ return () => {
324
+ const state = summonStore.getState();
325
+ if (state.windowManager?.transientZ?.[id] !== void 0) summonStore.dispatch((0, import_summon.releaseTransientWindow)(id));
326
+ };
327
+ }, [summonStore, id]);
328
+ const zIndex = laneZ ?? baseZ;
329
+ (0, import_react2.useEffect)(
330
+ () => () => {
331
+ pressRef.current?.cleanup();
332
+ pressRef.current = null;
333
+ },
334
+ []
335
+ );
336
+ const commit = (0, import_react2.useCallback)((next) => {
337
+ const current = live.current;
338
+ adoptingRef.current = true;
339
+ if (current.isControlled) setPending({ value: next, forProp: current.controlled ?? null });
340
+ else {
341
+ setLocal(next);
342
+ current.store.write(current.id, next);
343
+ }
344
+ current.onPositionChange?.(next);
345
+ }, []);
346
+ const onPointerDown = (0, import_react2.useCallback)(
347
+ (event) => {
348
+ if (!draggable || event.button !== 0 || !element) return;
349
+ if (!touch && event.pointerType === "touch") return;
350
+ const target = event.target;
351
+ if (ignore && target) {
352
+ const hit = target.closest(ignore);
353
+ if (hit && hit !== event.currentTarget && event.currentTarget.contains(hit)) return;
354
+ }
355
+ pressRef.current?.cleanup();
356
+ if (raiseOnPress && summonStore) summonStore.dispatch((0, import_summon.raiseTransientWindow)(id));
357
+ if (!target?.closest("button,a,input,select,textarea,label,[role=button]")) event.preventDefault();
358
+ const rect = element.getBoundingClientRect();
359
+ const viewport = readViewport();
360
+ const size = measure(element);
361
+ const press = {
362
+ pointerId: event.pointerId,
363
+ startX: event.clientX,
364
+ startY: event.clientY,
365
+ origin: { left: rect.left, top: rect.top },
366
+ size,
367
+ viewport,
368
+ active: false,
369
+ latest: { left: rect.left, top: rect.top },
370
+ release: null,
371
+ cleanup: () => void 0
372
+ };
373
+ const startOffset = live.current.offset ?? fromScreen(press.origin, live.current.rawCorner, size, viewport);
374
+ const startCorner = live.current.rawCorner;
375
+ const startScreen = toScreen(startOffset, startCorner, size, viewport);
376
+ const paint = () => {
377
+ frameRef.current = null;
378
+ const active = pressRef.current;
379
+ if (!active || !active.active) return;
380
+ element.style.translate = `${active.latest.left - startScreen.left}px ${active.latest.top - startScreen.top}px`;
381
+ };
382
+ const move = (e) => {
383
+ if (e.pointerId !== press.pointerId) return;
384
+ const dx = e.clientX - press.startX;
385
+ const dy = e.clientY - press.startY;
386
+ const opts = live.current;
387
+ if (!press.active) {
388
+ if (Math.abs(dx) < opts.threshold && Math.abs(dy) < opts.threshold) return;
389
+ press.active = true;
390
+ press.release = (0, import_design_system.holdDragCursor)("grabbing");
391
+ setDragging(true);
392
+ }
393
+ const desired = fromScreen({ left: startScreen.left + dx, top: startScreen.top + dy }, startCorner, size, viewport);
394
+ let next = toScreen(clampOffset(desired, startCorner, size, viewport, opts.clamp), startCorner, size, viewport);
395
+ if (opts.avoid) {
396
+ const keep = (p) => toScreen(clampOffset(fromScreen(p, "top-left", size, viewport), "top-left", size, viewport, opts.clamp), "top-left", size, viewport);
397
+ next = avoidExclusions(next, size, excludedRects(opts.avoid, element), opts.avoid.gap ?? DEFAULT_DOCK_MARGIN, keep);
398
+ }
399
+ press.latest = next;
400
+ if (frameRef.current === null) {
401
+ if (typeof window.requestAnimationFrame === "function") frameRef.current = window.requestAnimationFrame(paint);
402
+ else paint();
403
+ }
404
+ };
405
+ const end = (e) => {
406
+ if (e.pointerId !== press.pointerId) return;
407
+ press.cleanup();
408
+ pressRef.current = null;
409
+ if (!press.active) return;
410
+ press.release?.();
411
+ setDragging(false);
412
+ const opts = live.current;
413
+ const finalCorner = opts.snapToCorner ? nearestCorner(press.latest, size, viewport) : startCorner;
414
+ const finalOffset = clampOffset(fromScreen(press.latest, finalCorner, size, viewport), finalCorner, size, viewport, opts.clamp);
415
+ commit(finalCorner === corner ? { x: finalOffset.x, y: finalOffset.y } : { ...finalOffset, corner: finalCorner });
416
+ const swallow = (click) => {
417
+ click.stopPropagation();
418
+ click.preventDefault();
419
+ };
420
+ window.addEventListener("click", swallow, { capture: true, once: true });
421
+ window.setTimeout(() => window.removeEventListener("click", swallow, { capture: true }), 0);
422
+ };
423
+ press.cleanup = () => {
424
+ window.removeEventListener("pointermove", move);
425
+ window.removeEventListener("pointerup", end);
426
+ window.removeEventListener("pointercancel", end);
427
+ if (frameRef.current !== null) {
428
+ window.cancelAnimationFrame?.(frameRef.current);
429
+ frameRef.current = null;
430
+ }
431
+ if (!press.active) return;
432
+ press.release?.();
433
+ press.release = null;
434
+ };
435
+ pressRef.current = press;
436
+ window.addEventListener("pointermove", move);
437
+ window.addEventListener("pointerup", end);
438
+ window.addEventListener("pointercancel", end);
439
+ },
440
+ [commit, corner, draggable, element, id, ignore, raiseOnPress, summonStore, touch]
441
+ );
442
+ const reset = (0, import_react2.useCallback)(() => {
443
+ adoptingRef.current = true;
444
+ commit(null);
445
+ }, [commit]);
446
+ let style;
447
+ if (renderedOffset) {
448
+ const left = rawCorner.endsWith("left");
449
+ const top = rawCorner.startsWith("top");
450
+ style = {
451
+ position: "fixed",
452
+ left: left ? renderedOffset.x : "auto",
453
+ right: left ? "auto" : renderedOffset.x,
454
+ top: top ? renderedOffset.y : "auto",
455
+ bottom: top ? "auto" : renderedOffset.y
456
+ };
457
+ } else {
458
+ const transform = [anchor?.centerX ? "translateX(-50%)" : null, nudge ? `translate(${nudge.left}px, ${nudge.top}px)` : null].filter(Boolean).join(" ");
459
+ style = {
460
+ position: "fixed",
461
+ top: anchor?.top,
462
+ bottom: anchor?.bottom,
463
+ left: anchor?.centerX ? "50%" : anchor?.left,
464
+ right: anchor?.right,
465
+ ...transform ? { transform } : {}
466
+ };
467
+ }
468
+ if (zIndex !== void 0) style.zIndex = zIndex;
469
+ return {
470
+ ref: setElement,
471
+ element,
472
+ style,
473
+ handleProps: {
474
+ onPointerDown,
475
+ style: draggable ? { cursor: "grab", touchAction: touch ? "none" : "auto" } : {},
476
+ "data-dock-handle": ""
477
+ },
478
+ offset: renderedOffset,
479
+ corner: rawCorner,
480
+ dragging,
481
+ moved: chosen !== null,
482
+ zIndex,
483
+ reset
484
+ };
485
+ }
486
+
487
+ // src/dock/Dock.tsx
488
+ var import_jsx_runtime = require("react/jsx-runtime");
489
+ var DockContext = (0, import_react4.createContext)(null);
490
+ function useDockContext() {
491
+ const dock = (0, import_react4.useContext)(DockContext);
492
+ if (!dock) throw new Error("[summon] useDockContext() must be used inside <Dock>");
493
+ return dock;
494
+ }
495
+ var OPTION_KEYS = [
496
+ "id",
497
+ "corner",
498
+ "defaultPosition",
499
+ "anchor",
500
+ "position",
501
+ "onPositionChange",
502
+ "persist",
503
+ "store",
504
+ "draggable",
505
+ "touch",
506
+ "threshold",
507
+ "clamp",
508
+ "avoid",
509
+ "snapToCorner",
510
+ "zIndex",
511
+ "raiseOnPress",
512
+ "ignore"
513
+ ];
514
+ function Dock(props) {
515
+ const options = {};
516
+ const rest = {};
517
+ for (const [key, value] of Object.entries(props)) {
518
+ if (OPTION_KEYS.includes(key)) options[key] = value;
519
+ else if (key !== "handle" && key !== "children" && key !== "style") rest[key] = value;
520
+ }
521
+ const dock = useDock(options);
522
+ const self = props.handle === "self";
523
+ const style = { ...props.style, ...dock.style, ...self ? dock.handleProps.style : {} };
524
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DockContext.Provider, { value: dock, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
525
+ "div",
526
+ {
527
+ ...rest,
528
+ ref: dock.ref,
529
+ "data-dock": props.id,
530
+ "data-dragging": dock.dragging ? "" : void 0,
531
+ style,
532
+ ...self ? { onPointerDown: dock.handleProps.onPointerDown, "data-dock-handle": "" } : {},
533
+ children: typeof props.children === "function" ? props.children(dock) : props.children
534
+ }
535
+ ) });
536
+ }
537
+ function DockHandle({ style, onPointerDown, ...rest }) {
538
+ const dock = useDockContext();
539
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
540
+ "div",
541
+ {
542
+ ...rest,
543
+ ...dock.handleProps,
544
+ style: { ...style, ...dock.handleProps.style },
545
+ onPointerDown: (event) => {
546
+ onPointerDown?.(event);
547
+ dock.handleProps.onPointerDown(event);
548
+ }
549
+ }
550
+ );
551
+ }
552
+ //# sourceMappingURL=dock.cjs.map