@ai-matrx/summon 0.1.3 → 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 +2 -1
- package/README.md +21 -1
- package/dist/dock.cjs +552 -0
- package/dist/dock.cjs.map +1 -0
- package/dist/dock.d.cts +224 -0
- package/dist/dock.d.ts +224 -0
- package/dist/dock.js +537 -0
- package/dist/dock.js.map +1 -0
- package/package.json +13 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.4
|
|
4
4
|
### Unreleased
|
|
5
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.
|
|
6
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.
|
|
7
8
|
|
|
8
9
|
## 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` — never loads WindowPanel), `./dialog`, `./sheet`, `./lite`, `./dock`, `./url`, `./persistence`, `./persistence/data`, `./next`, `./testing`, `./styles.css`.
|
|
29
29
|
|
|
30
30
|
## Quick start
|
|
31
31
|
|
|
@@ -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
|