@ai-matrx/summon 0.1.3 → 0.1.5

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +4 -1
  2. package/README.md +22 -2
  3. package/dist/dock.cjs +552 -0
  4. package/dist/dock.cjs.map +1 -0
  5. package/dist/dock.d.cts +224 -0
  6. package/dist/dock.d.ts +224 -0
  7. package/dist/dock.js +537 -0
  8. package/dist/dock.js.map +1 -0
  9. package/dist/index.cjs +4 -1
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +6 -0
  12. package/dist/index.d.ts +6 -0
  13. package/dist/index.js +4 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/persistence-data.cjs +9 -0
  16. package/dist/persistence-data.cjs.map +1 -1
  17. package/dist/persistence-data.d.cts +2 -1
  18. package/dist/persistence-data.d.ts +2 -1
  19. package/dist/persistence-data.js +9 -0
  20. package/dist/persistence-data.js.map +1 -1
  21. package/dist/persistence.cjs +6 -4
  22. package/dist/persistence.cjs.map +1 -1
  23. package/dist/persistence.js +6 -4
  24. package/dist/persistence.js.map +1 -1
  25. package/dist/react.cjs +19 -0
  26. package/dist/react.cjs.map +1 -1
  27. package/dist/react.d.cts +8 -1
  28. package/dist/react.d.ts +8 -1
  29. package/dist/react.js +19 -0
  30. package/dist/react.js.map +1 -1
  31. package/dist/url.cjs +32 -50
  32. package/dist/url.cjs.map +1 -1
  33. package/dist/url.d.cts +2 -8
  34. package/dist/url.d.ts +2 -8
  35. package/dist/url.js +8 -24
  36. package/dist/url.js.map +1 -1
  37. package/dist/window-host.cjs +43 -1
  38. package/dist/window-host.cjs.map +1 -1
  39. package/dist/window-host.d.cts +45 -1
  40. package/dist/window-host.d.ts +45 -1
  41. package/dist/window-host.js +44 -2
  42. package/dist/window-host.js.map +1 -1
  43. package/dist/window.cjs +127 -149
  44. package/dist/window.cjs.map +1 -1
  45. package/dist/window.d.cts +2 -46
  46. package/dist/window.d.ts +2 -46
  47. package/dist/window.js +14 -34
  48. package/dist/window.js.map +1 -1
  49. package/package.json +14 -4
package/CHANGELOG.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.3
3
+ ## 0.1.5
4
4
  ### Unreleased
5
5
 
6
+ - **Light imports for the boot graph.** `usePopout`, `PopoutContext`, `usePopoutContainer` and `useOverlaySurfaceRenderAck` are now on `./window-host` (the popout context lives there, one identity; `./window` re-exports them), and `useOverlayLink` is on `./react` (`./url` re-exports it). Why: a section ships as one module, so an app that imported one of these hooks from `./window` or `./url` at boot carried the whole window section (WindowPanel, popout, mobile, tray: ~100 KB minified) or the link manager in every route's first load (AI Matrx: `/pricing`, `/notes`, `/dashboard`). Consumer action: none required; import these hooks from `./window-host` / `./react` to drop the heavy sections from a static graph.
7
+ - **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.
8
+ - **Idle cost: the root-mounted pieces do nothing until used.** `WindowTraySync` no longer dispatches two actions on mount and on every resize of a page with no windows: it attaches its resize listener only while a window is registered or waiting to restore (one boolean selector), and `recomputeTrayPositions` writes nothing when the tray is empty (a resize with no tray hands subscribers the same state). Persistence boot is one read: a port may implement `loadAll()` (key → stored value), and when it does the controller restores the workspace and every saved component state from that single call instead of `load` + `list` + one `load` per state key; the `./persistence/data` adapter implements it (one `users.user_surface_state` select). Guards: `src/window/__tests__/windowTraySyncIdle.test.tsx`, `controller.test.ts` (boot = exactly `["loadAll"]`), `src/importCost.test.ts` (the root-mounted entries `.`, `./react`, `./window-host`, `./next` never reach WindowPanel, links, persistence, dialog/sheet/lite or testing). Consumer action: none.
6
9
  - **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.
7
10
 
8
11
  ## 0.1.2
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`, and the in-window hooks `usePopout`, `usePopoutContainer`, `useOverlaySurfaceRenderAck` — never loads WindowPanel), `./dialog`, `./sheet`, `./lite`, `./dock`, `./url`, `./persistence`, `./persistence/data`, `./next`, `./testing`, `./styles.css`.
29
29
 
30
30
  ## Quick start
31
31
 
@@ -239,7 +239,7 @@ defineOverlay<NotesWindowData>()({
239
239
  ```
240
240
 
241
241
  - Opening `/page?panels=notes:42` runs `hydrate({ id: "42", args, open, dispatch })`. `hydrate` may be async (load a record, then `open`).
242
- - A mounted overlay publishes its own address with `useOverlayLink("notes", noteId, { tab: "history" })`. `WindowPanel` does it for you from its `urlSyncKey`, `urlSyncId` and `urlSyncArgs` props.
242
+ - A mounted overlay publishes its own address with `useOverlayLink("notes", noteId, { tab: "history" })` (also exported from `./react`, so a render module can publish without loading the link section). `WindowPanel` does it for you from its `urlSyncKey`, `urlSyncId` and `urlSyncArgs` props.
243
243
  - Unknown keys are announced at once; tokens whose overlay never registers are announced at a deadline and kept verbatim.
244
244
  - **Grammar v2**: `panels = token ("," token)*`, `token = key[:id[:k-v(_k-v)*]]`. Ids are percent-encoded for `% , :`, arg keys and values for `% , : _ -`, so any value survives; v1 links still parse. Helpers: `parseLinkToken`, `parseLinkParam`, `formatLinkToken`, `readPanelsParam`, `withPanelsParam`.
245
245
  - **One door**: `openFromLink(searchOrUrl, { dispatch, requireAll?, onError? })` returns `{ opened, refused, unknown, failed }` and serves the address bar, notifications and any bridge. `requireAll: true` opens nothing if any key is unknown. `openPanelRequest(payload, { dispatch })` validates and opens a `{ panelId, instanceId?, data? }` request from a bridge (returns `{ ok: true, … }` or `{ ok: false, error: "invalid_payload" | "unknown_panel" }`).
@@ -372,6 +372,26 @@ import { LiteWindow } from "@ai-matrx/summon/lite";
372
372
 
373
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.
374
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
+
375
395
  ## Styling
376
396
 
377
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