@ai-matrx/summon 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +380 -2
  4. package/dist/closeBinding-C6raC3qJ.d.cts +20 -0
  5. package/dist/closeBinding-C6raC3qJ.d.ts +20 -0
  6. package/dist/dialog.cjs +60 -0
  7. package/dist/dialog.cjs.map +1 -0
  8. package/dist/dialog.d.cts +16 -0
  9. package/dist/dialog.d.ts +16 -0
  10. package/dist/dialog.js +38 -0
  11. package/dist/dialog.js.map +1 -0
  12. package/dist/errors-eJrKXq33.d.cts +30 -0
  13. package/dist/errors-eJrKXq33.d.ts +30 -0
  14. package/dist/handles.cjs +411 -0
  15. package/dist/handles.cjs.map +1 -0
  16. package/dist/handles.d.cts +166 -0
  17. package/dist/handles.d.ts +166 -0
  18. package/dist/handles.js +388 -0
  19. package/dist/handles.js.map +1 -0
  20. package/dist/index.cjs +2383 -0
  21. package/dist/index.cjs.map +1 -0
  22. package/dist/index.d.cts +1259 -0
  23. package/dist/index.d.ts +1259 -0
  24. package/dist/index.js +2363 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/lite.cjs +460 -0
  27. package/dist/lite.cjs.map +1 -0
  28. package/dist/lite.d.cts +56 -0
  29. package/dist/lite.d.ts +56 -0
  30. package/dist/lite.js +444 -0
  31. package/dist/lite.js.map +1 -0
  32. package/dist/next.cjs +61 -0
  33. package/dist/next.cjs.map +1 -0
  34. package/dist/next.d.cts +5 -0
  35. package/dist/next.d.ts +5 -0
  36. package/dist/next.js +39 -0
  37. package/dist/next.js.map +1 -0
  38. package/dist/persistence-data.cjs +143 -0
  39. package/dist/persistence-data.cjs.map +1 -0
  40. package/dist/persistence-data.d.cts +72 -0
  41. package/dist/persistence-data.d.ts +72 -0
  42. package/dist/persistence-data.js +120 -0
  43. package/dist/persistence-data.js.map +1 -0
  44. package/dist/persistence.cjs +1073 -0
  45. package/dist/persistence.cjs.map +1 -0
  46. package/dist/persistence.d.cts +236 -0
  47. package/dist/persistence.d.ts +236 -0
  48. package/dist/persistence.js +1077 -0
  49. package/dist/persistence.js.map +1 -0
  50. package/dist/react.cjs +733 -0
  51. package/dist/react.cjs.map +1 -0
  52. package/dist/react.d.cts +261 -0
  53. package/dist/react.d.ts +261 -0
  54. package/dist/react.js +718 -0
  55. package/dist/react.js.map +1 -0
  56. package/dist/sheet.cjs +71 -0
  57. package/dist/sheet.cjs.map +1 -0
  58. package/dist/sheet.d.cts +17 -0
  59. package/dist/sheet.d.ts +17 -0
  60. package/dist/sheet.js +49 -0
  61. package/dist/sheet.js.map +1 -0
  62. package/dist/styles.css +78 -0
  63. package/dist/testing.cjs +132 -0
  64. package/dist/testing.cjs.map +1 -0
  65. package/dist/testing.d.cts +44 -0
  66. package/dist/testing.d.ts +44 -0
  67. package/dist/testing.js +115 -0
  68. package/dist/testing.js.map +1 -0
  69. package/dist/url.cjs +362 -0
  70. package/dist/url.cjs.map +1 -0
  71. package/dist/url.d.cts +153 -0
  72. package/dist/url.d.ts +153 -0
  73. package/dist/url.js +352 -0
  74. package/dist/url.js.map +1 -0
  75. package/dist/window-host.cjs +123 -0
  76. package/dist/window-host.cjs.map +1 -0
  77. package/dist/window-host.d.cts +72 -0
  78. package/dist/window-host.d.ts +72 -0
  79. package/dist/window-host.js +104 -0
  80. package/dist/window-host.js.map +1 -0
  81. package/dist/window.cjs +3978 -0
  82. package/dist/window.cjs.map +1 -0
  83. package/dist/window.d.cts +796 -0
  84. package/dist/window.d.ts +796 -0
  85. package/dist/window.js +4003 -0
  86. package/dist/window.js.map +1 -0
  87. package/package.json +237 -4
package/dist/lite.js ADDED
@@ -0,0 +1,444 @@
1
+ "use client";
2
+
3
+ // src/lite/LiteWindow.tsx
4
+ import {
5
+ useCallback,
6
+ useEffect,
7
+ useId,
8
+ useLayoutEffect,
9
+ useState
10
+ } from "react";
11
+ import { createPortal } from "react-dom";
12
+ import { PortalContainerProvider } from "@ai-matrx/design-system";
13
+ import { cn } from "@ai-matrx/design-system/cn";
14
+ import { globalSlot } from "@ai-matrx/kit/global-slot";
15
+ import { useIsMobile } from "@ai-matrx/kit/media-query";
16
+ import { raiseTransientWindow, releaseTransientWindow, selectTransientZIndex } from "@ai-matrx/summon";
17
+ import { useOptionalOverlayInstance, useSummonDispatch, useSummonPorts, useSummonSelector } from "@ai-matrx/summon/react";
18
+
19
+ // src/lite/focusOnOpen.ts
20
+ var KEEP_FOCUS_ATTRIBUTE = "data-keep-focus-on-open";
21
+ var EDITABLE_INPUT_TYPES = /* @__PURE__ */ new Set([
22
+ "",
23
+ "text",
24
+ "search",
25
+ "email",
26
+ "url",
27
+ "tel",
28
+ "password",
29
+ "number",
30
+ "date",
31
+ "datetime-local",
32
+ "month",
33
+ "time",
34
+ "week"
35
+ ]);
36
+ function isEditableField(el) {
37
+ if (!(el instanceof HTMLElement)) return false;
38
+ if (el instanceof HTMLTextAreaElement) return !el.disabled && !el.readOnly;
39
+ if (el instanceof HTMLInputElement) {
40
+ return !el.disabled && !el.readOnly && EDITABLE_INPUT_TYPES.has((el.getAttribute("type") ?? "").toLowerCase());
41
+ }
42
+ return el.isContentEditable === true;
43
+ }
44
+ function mayTakeFocusOnOpen(container) {
45
+ const active = typeof document === "undefined" ? null : document.activeElement;
46
+ if (!isEditableField(active)) return true;
47
+ if (container != null && container.contains(active)) return true;
48
+ return active.closest(`[${KEEP_FOCUS_ATTRIBUTE}]`) == null;
49
+ }
50
+
51
+ // src/lite/LiteWindow.tsx
52
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
53
+ function X(props) {
54
+ return /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className: props.className, children: [
55
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
56
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
57
+ ] });
58
+ }
59
+ var SIZE_PX = {
60
+ sm: { width: 380, height: 420 },
61
+ md: { width: 560, height: 560 },
62
+ lg: { width: 780, height: 680 }
63
+ };
64
+ var VIEWPORT_PADDING = 8;
65
+ var DEFAULT_MIN_WIDTH = 320;
66
+ var DEFAULT_MIN_HEIGHT = 200;
67
+ var KEY_DRAG_STEP = 20;
68
+ var FIELD_SELECTOR = [
69
+ "[data-panel-initial-focus]",
70
+ "input:not([disabled]):not([type='hidden'])",
71
+ "textarea:not([disabled])",
72
+ "select:not([disabled])",
73
+ "[contenteditable='true']"
74
+ ].join(",");
75
+ var FALLBACK_SELECTOR = [
76
+ "button:not([disabled])",
77
+ "a[href]",
78
+ "[tabindex]:not([tabindex='-1'])"
79
+ ].join(",");
80
+ function firstVisibleMatch(root, selector) {
81
+ return Array.from(root.querySelectorAll(selector)).find(
82
+ (el) => el.getAttribute("aria-hidden") !== "true" && el.getClientRects().length > 0
83
+ ) ?? null;
84
+ }
85
+ var openStack = () => globalSlot("ai-matrx.summon.lite.openStack", () => []);
86
+ function aDismissableLayerIsOpen() {
87
+ return document.querySelector("[data-radix-popper-content-wrapper]") !== null;
88
+ }
89
+ function clampRect(rect, vw, vh) {
90
+ const width = Math.min(rect.width, Math.max(240, vw - VIEWPORT_PADDING * 2));
91
+ const height = Math.min(
92
+ rect.height,
93
+ Math.max(180, vh - VIEWPORT_PADDING * 2)
94
+ );
95
+ return {
96
+ width,
97
+ height,
98
+ x: Math.min(Math.max(rect.x, VIEWPORT_PADDING), vw - width - VIEWPORT_PADDING),
99
+ y: Math.min(
100
+ Math.max(rect.y, VIEWPORT_PADDING),
101
+ vh - height - VIEWPORT_PADDING
102
+ )
103
+ };
104
+ }
105
+ function LiteWindow({
106
+ id,
107
+ title,
108
+ description,
109
+ children,
110
+ onClose: onCloseProp,
111
+ size = "md",
112
+ width,
113
+ height,
114
+ minWidth = DEFAULT_MIN_WIDTH,
115
+ minHeight = DEFAULT_MIN_HEIGHT,
116
+ draggable = true,
117
+ resizable = true,
118
+ dismissDisabled = false,
119
+ headerActions,
120
+ footer,
121
+ className,
122
+ contentClassName = "px-4 py-3",
123
+ initialFocus = true,
124
+ container
125
+ }) {
126
+ const instance = useOptionalOverlayInstance();
127
+ const ports = useSummonPorts();
128
+ const onClose = useCallback(
129
+ (reason) => {
130
+ if (onCloseProp) onCloseProp(reason);
131
+ else if (instance) instance.close();
132
+ else ports.errors.capture(new Error("[summon] LiteWindow has no onClose and renders outside an overlay instance"), { id, reason });
133
+ },
134
+ [id, instance, onCloseProp, ports]
135
+ );
136
+ const bounded = container !== void 0;
137
+ const dispatch = useSummonDispatch();
138
+ const zIndex = useSummonSelector(selectTransientZIndex(id));
139
+ const isNarrow = useIsMobile();
140
+ const [opener] = useState(
141
+ () => typeof document !== "undefined" && document.activeElement instanceof HTMLElement && document.activeElement !== document.body ? document.activeElement : null
142
+ );
143
+ const [portalTarget, setPortalTarget] = useState(null);
144
+ const [frameEl, setFrameEl] = useState(null);
145
+ const titleId = useId();
146
+ const descriptionId = useId();
147
+ const [rect, setRect] = useState(() => {
148
+ const base = SIZE_PX[size];
149
+ return {
150
+ width: width ?? base.width,
151
+ height: height ?? base.height,
152
+ x: 0,
153
+ y: 0
154
+ };
155
+ });
156
+ const [placed, setPlaced] = useState(false);
157
+ const [viewport, setViewport] = useState(() => ({
158
+ w: typeof window === "undefined" ? 1280 : window.innerWidth,
159
+ h: typeof window === "undefined" ? 800 : window.innerHeight
160
+ }));
161
+ const view = clampRect(rect, viewport.w, viewport.h);
162
+ useEffect(() => {
163
+ if (bounded) {
164
+ setPortalTarget(container ?? null);
165
+ return;
166
+ }
167
+ setPortalTarget(document.getElementById("glass-layer") ?? document.body);
168
+ }, [bounded, container]);
169
+ useEffect(() => {
170
+ dispatch(raiseTransientWindow(id));
171
+ openStack().push(id);
172
+ return () => {
173
+ dispatch(releaseTransientWindow(id));
174
+ const at = openStack().lastIndexOf(id);
175
+ if (at !== -1) openStack().splice(at, 1);
176
+ };
177
+ }, [dispatch, id]);
178
+ useLayoutEffect(() => {
179
+ if (typeof window === "undefined") return void 0;
180
+ if (bounded) {
181
+ if (!container) return void 0;
182
+ const measure = () => ({ w: container.clientWidth, h: container.clientHeight });
183
+ const box = measure();
184
+ setViewport(box);
185
+ if (!placed) {
186
+ setRect((current) => ({
187
+ ...current,
188
+ x: Math.round((box.w - current.width) / 2),
189
+ y: Math.round(Math.max(VIEWPORT_PADDING, (box.h - current.height) / 2.4))
190
+ }));
191
+ setPlaced(true);
192
+ }
193
+ const observer = new ResizeObserver(() => setViewport(measure()));
194
+ observer.observe(container);
195
+ return () => observer.disconnect();
196
+ }
197
+ if (!placed) {
198
+ setRect((current) => ({
199
+ ...current,
200
+ x: Math.round((window.innerWidth - current.width) / 2),
201
+ y: Math.round(
202
+ Math.max(56, (window.innerHeight - current.height) / 2.4)
203
+ )
204
+ }));
205
+ setPlaced(true);
206
+ }
207
+ const onResize = () => setViewport({ w: window.innerWidth, h: window.innerHeight });
208
+ window.addEventListener("resize", onResize);
209
+ return () => window.removeEventListener("resize", onResize);
210
+ }, [placed, bounded, container]);
211
+ useEffect(() => {
212
+ if (dismissDisabled) return void 0;
213
+ const onKeyDown = (e) => {
214
+ if (e.key !== "Escape" || e.defaultPrevented) return;
215
+ if (openStack()[openStack().length - 1] !== id) return;
216
+ if (aDismissableLayerIsOpen()) return;
217
+ e.stopPropagation();
218
+ onClose("escape");
219
+ };
220
+ document.addEventListener("keydown", onKeyDown);
221
+ return () => document.removeEventListener("keydown", onKeyDown);
222
+ }, [dismissDisabled, id, onClose]);
223
+ useEffect(() => {
224
+ if (!initialFocus || !frameEl) return void 0;
225
+ const frame = requestAnimationFrame(() => {
226
+ if (!mayTakeFocusOnOpen(frameEl)) return;
227
+ const content = frameEl.querySelector("[data-panel-content]") ?? frameEl;
228
+ const target = firstVisibleMatch(content, FIELD_SELECTOR) ?? firstVisibleMatch(content, FALLBACK_SELECTOR) ?? frameEl;
229
+ target.focus({ preventScroll: true });
230
+ });
231
+ return () => {
232
+ cancelAnimationFrame(frame);
233
+ if (opener?.isConnected && !frameEl.contains(opener)) {
234
+ requestAnimationFrame(() => opener.focus({ preventScroll: true }));
235
+ }
236
+ };
237
+ }, [initialFocus, frameEl, opener]);
238
+ const raise = useCallback(() => {
239
+ dispatch(raiseTransientWindow(id));
240
+ const at = openStack().lastIndexOf(id);
241
+ if (at !== -1 && at !== openStack().length - 1) {
242
+ openStack().splice(at, 1);
243
+ openStack().push(id);
244
+ }
245
+ }, [dispatch, id]);
246
+ const startDrag = useCallback(
247
+ (e) => {
248
+ if (!draggable || isNarrow) return;
249
+ if (e.button !== 0) return;
250
+ const target = e.target;
251
+ if (target.closest("button,a,input,[role='button']")) return;
252
+ e.preventDefault();
253
+ raise();
254
+ const startX = e.clientX;
255
+ const startY = e.clientY;
256
+ const origin = { ...view };
257
+ const onMove = (move) => {
258
+ setRect({
259
+ ...origin,
260
+ x: origin.x + (move.clientX - startX),
261
+ y: origin.y + (move.clientY - startY)
262
+ });
263
+ };
264
+ const onUp = () => {
265
+ window.removeEventListener("pointermove", onMove);
266
+ window.removeEventListener("pointerup", onUp);
267
+ };
268
+ window.addEventListener("pointermove", onMove);
269
+ window.addEventListener("pointerup", onUp);
270
+ },
271
+ [draggable, isNarrow, raise, view]
272
+ );
273
+ const startResize = useCallback(
274
+ (e, edge) => {
275
+ if (!resizable || isNarrow || e.button !== 0) return;
276
+ e.preventDefault();
277
+ e.stopPropagation();
278
+ raise();
279
+ const startX = e.clientX;
280
+ const startY = e.clientY;
281
+ const origin = { ...view };
282
+ const onMove = (move) => {
283
+ const next = { ...origin };
284
+ if (edge === "e" || edge === "se") {
285
+ next.width = Math.max(minWidth, origin.width + (move.clientX - startX));
286
+ }
287
+ if (edge === "s" || edge === "se") {
288
+ next.height = Math.max(
289
+ minHeight,
290
+ origin.height + (move.clientY - startY)
291
+ );
292
+ }
293
+ setRect(next);
294
+ };
295
+ const onUp = () => {
296
+ window.removeEventListener("pointermove", onMove);
297
+ window.removeEventListener("pointerup", onUp);
298
+ };
299
+ window.addEventListener("pointermove", onMove);
300
+ window.addEventListener("pointerup", onUp);
301
+ },
302
+ [isNarrow, minHeight, minWidth, raise, resizable, view]
303
+ );
304
+ const onHandleKeyDown = useCallback(
305
+ (e) => {
306
+ if (isNarrow || !draggable) return;
307
+ const step = e.shiftKey || e.altKey ? 1 : KEY_DRAG_STEP;
308
+ const delta = e.key === "ArrowLeft" ? { x: -step, y: 0 } : e.key === "ArrowRight" ? { x: step, y: 0 } : e.key === "ArrowUp" ? { x: 0, y: -step } : e.key === "ArrowDown" ? { x: 0, y: step } : null;
309
+ if (!delta) return;
310
+ e.preventDefault();
311
+ setRect((current) => ({
312
+ ...current,
313
+ x: current.x + delta.x,
314
+ y: current.y + delta.y
315
+ }));
316
+ },
317
+ [draggable, isNarrow]
318
+ );
319
+ if (!portalTarget) return null;
320
+ const style = isNarrow ? { zIndex } : {
321
+ zIndex,
322
+ left: view.x,
323
+ top: view.y,
324
+ width: view.width,
325
+ height: view.height
326
+ };
327
+ const frameClass = isNarrow ? "fixed inset-x-0 bottom-0 max-h-[88dvh] rounded-t-xl border-t" : bounded ? "absolute rounded-xl border" : "fixed rounded-xl border";
328
+ const canDismiss = !dismissDisabled;
329
+ const panel = /* @__PURE__ */ jsxs(
330
+ "div",
331
+ {
332
+ ref: setFrameEl,
333
+ role: "dialog",
334
+ "aria-modal": "false",
335
+ "aria-labelledby": titleId,
336
+ "aria-describedby": description ? descriptionId : void 0,
337
+ tabIndex: -1,
338
+ "data-matrx-lite-window": id,
339
+ "data-presentation": isNarrow ? "drawer" : "floating",
340
+ onPointerDownCapture: raise,
341
+ style,
342
+ className: cn(
343
+ "flex flex-col overflow-hidden border-border bg-card text-card-foreground shadow-2xl outline-none",
344
+ "transition-[border-radius] duration-150",
345
+ frameClass,
346
+ className
347
+ ),
348
+ children: [
349
+ /* @__PURE__ */ jsxs(
350
+ "div",
351
+ {
352
+ onPointerDown: startDrag,
353
+ className: cn(
354
+ "flex shrink-0 items-center gap-2 border-b border-border bg-muted/40 px-3 py-2",
355
+ !isNarrow && draggable && "cursor-grab active:cursor-grabbing"
356
+ ),
357
+ children: [
358
+ !isNarrow && draggable ? /* @__PURE__ */ jsx(
359
+ "span",
360
+ {
361
+ role: "button",
362
+ tabIndex: 0,
363
+ "aria-label": `Move ${typeof title === "string" ? title : "window"} (arrow keys)`,
364
+ onKeyDown: onHandleKeyDown,
365
+ className: "h-4 w-1.5 shrink-0 rounded-full bg-border outline-none focus-visible:ring-2 focus-visible:ring-ring"
366
+ }
367
+ ) : null,
368
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
369
+ /* @__PURE__ */ jsx(
370
+ "h2",
371
+ {
372
+ id: titleId,
373
+ className: "truncate text-sm font-semibold text-foreground",
374
+ children: title
375
+ }
376
+ ),
377
+ description ? /* @__PURE__ */ jsx(
378
+ "p",
379
+ {
380
+ id: descriptionId,
381
+ className: "mt-0.5 line-clamp-2 text-xs text-muted-foreground",
382
+ children: description
383
+ }
384
+ ) : null
385
+ ] }),
386
+ headerActions ? /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-1", children: headerActions }) : null,
387
+ canDismiss ? /* @__PURE__ */ jsx(
388
+ "button",
389
+ {
390
+ type: "button",
391
+ onClick: () => onClose("close-button"),
392
+ "aria-label": "Close",
393
+ className: "shrink-0 rounded-md p-1 text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring",
394
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
395
+ }
396
+ ) : null
397
+ ]
398
+ }
399
+ ),
400
+ /* @__PURE__ */ jsx(
401
+ "div",
402
+ {
403
+ "data-panel-content": true,
404
+ className: cn("min-h-0 flex-1 overflow-auto", contentClassName),
405
+ children: /* @__PURE__ */ jsx(PortalContainerProvider, { container: frameEl, children })
406
+ }
407
+ ),
408
+ footer ? /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t border-border bg-muted/30 px-4 py-2", children: footer }) : null,
409
+ resizable && !isNarrow ? /* @__PURE__ */ jsxs(Fragment, { children: [
410
+ /* @__PURE__ */ jsx(
411
+ "span",
412
+ {
413
+ onPointerDown: (e) => startResize(e, "e"),
414
+ className: "absolute right-0 top-8 h-[calc(100%-3rem)] w-1.5 cursor-ew-resize"
415
+ }
416
+ ),
417
+ /* @__PURE__ */ jsx(
418
+ "span",
419
+ {
420
+ onPointerDown: (e) => startResize(e, "s"),
421
+ className: "absolute bottom-0 left-4 h-1.5 w-[calc(100%-2rem)] cursor-ns-resize"
422
+ }
423
+ ),
424
+ /* @__PURE__ */ jsx(
425
+ "span",
426
+ {
427
+ onPointerDown: (e) => startResize(e, "se"),
428
+ "aria-hidden": true,
429
+ className: "absolute bottom-0 right-0 h-4 w-4 cursor-nwse-resize"
430
+ }
431
+ )
432
+ ] }) : null
433
+ ]
434
+ }
435
+ );
436
+ return createPortal(panel, portalTarget);
437
+ }
438
+ export {
439
+ KEEP_FOCUS_ATTRIBUTE,
440
+ LiteWindow,
441
+ isEditableField,
442
+ mayTakeFocusOnOpen
443
+ };
444
+ //# sourceMappingURL=lite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lite/LiteWindow.tsx","../src/lite/focusOnOpen.ts"],"sourcesContent":["\"use client\";\n\n/**\n * LiteWindow (ported from matrx-frontend `MatrxFloatingFrame`, the FLOATING presentation of\n * `MatrxDynamicPanelHost`) — the light floating window.\n *\n * This is the \"lightweight window\" Arman asked for on 2026-09-18: *\"a slightly\n * more lightweight version of the window panel … doesn't minimize and stuff\n * like that but carries the basics of dragging\"*. It exists so a picker or a\n * form never has to live in a blocking modal\n * (`common-docs/policies/no-dead-ends.md` §4).\n *\n * It is NOT a second floating-surface family. It is one more presentation of\n * the panel host this repo already has in 59 files; the host routes to it on\n * `presentation=\"floating\"` and keeps every other call site on the docked\n * branch untouched.\n *\n * CARRIES: drag by header, edge/corner resize, Escape when topmost, focus in\n * on open and back to the opener on close, z-order shared with `WindowPanel`\n * (same `nextZIndex` counter — see `windowManagerSlice.transientZ`), a bottom\n * drawer below 768, and a nested portal container so a `CreatablePicker`\n * popover mounts INSIDE the window instead of behind it.\n *\n * NEVER GETS: minimize, the tray, persistence/restore, pop-out, URL sync,\n * sidebars, a focus trap, `aria-modal`, `inert` on the page, or a body scroll\n * lock. Each one is either the modal disease or a lost-work trap.\n *\n * 🚨 THE NEVER-LOSE-INPUT CONTRACT (the law's §5, and finding C1 of\n * `common-docs/projects/every-picker-takes-new-input/recon/plan-attack-1.md`,\n * where crossing 768 with the agent-variable editor open destroyed the editor\n * and everything typed into it). The floating frame and the drawer are THE\n * SAME DOM NODE and the same React subtree: crossing the breakpoint changes\n * `className` and `style` on one `<div>` and nothing else. There is no\n * `isNarrow ? <Drawer>{children}</Drawer> : <Floating>{children}</Floating>`\n * anywhere in this file, and there must never be — that ternary is exactly\n * what unmounts the body and eats the person's work. `children` is rendered\n * in one place, once.\n */\n\nimport {\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useState,\n type CSSProperties,\n type KeyboardEvent as ReactKeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { PortalContainerProvider } from \"@ai-matrx/design-system\";\nimport { cn } from \"@ai-matrx/design-system/cn\";\nimport { globalSlot } from \"@ai-matrx/kit/global-slot\";\nimport { useIsMobile } from \"@ai-matrx/kit/media-query\";\nimport { raiseTransientWindow, releaseTransientWindow, selectTransientZIndex } from \"@ai-matrx/summon\";\nimport { useOptionalOverlayInstance, useSummonDispatch, useSummonPorts, useSummonSelector } from \"@ai-matrx/summon/react\";\nimport { mayTakeFocusOnOpen } from \"./focusOnOpen\";\n\nfunction X(props: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className={props.className}>\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n );\n}\n\nexport type FloatingFrameSize = \"sm\" | \"md\" | \"lg\";\n\nconst SIZE_PX: Record<FloatingFrameSize, { width: number; height: number }> = {\n sm: { width: 380, height: 420 },\n md: { width: 560, height: 560 },\n lg: { width: 780, height: 680 },\n};\n\nconst VIEWPORT_PADDING = 8;\nconst DEFAULT_MIN_WIDTH = 320;\nconst DEFAULT_MIN_HEIGHT = 200;\n/** Keyboard drag step; a modifier drops it to 1px. */\nconst KEY_DRAG_STEP = 20;\n\ninterface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ntype ResizeEdge = \"e\" | \"s\" | \"se\";\n\nexport type FloatingCloseReason = \"escape\" | \"close-button\" | \"programmatic\";\n\nexport interface LiteWindowProps {\n /** Stable id — the key this window holds in the shared z-order lane. */\n id: string;\n title: ReactNode;\n description?: ReactNode;\n children: ReactNode;\n /**\n * Close request. Omit inside an SummonHost-rendered overlay: the window then closes its own\n * instance. A page-local window must pass it.\n */\n onClose?: ((reason: FloatingCloseReason) => void) | undefined;\n size?: FloatingFrameSize;\n width?: number;\n height?: number;\n minWidth?: number;\n minHeight?: number;\n draggable?: boolean;\n resizable?: boolean;\n /** Blocks Escape and the header close button while true. */\n dismissDisabled?: boolean;\n headerActions?: ReactNode;\n footer?: ReactNode;\n className?: string;\n contentClassName?: string;\n /** Move focus into the body on open, and back to the opener on close. */\n initialFocus?: boolean;\n /**\n * CONTAINER-BOUNDED mode. When set, the window lives INSIDE this element\n * instead of over the whole viewport: it portals into it (position\n * absolute — the element must be positioned), centres in it on open, and\n * is dragged and clamped within its box (it follows the box when it\n * resizes). Used by the canvas workspace's floating chat, which belongs to\n * the canvas region, not the window. `null` = the element is not mounted\n * yet; the frame waits for it. Omit for the viewport-wide default.\n */\n container?: HTMLElement | null;\n}\n\nconst FIELD_SELECTOR = [\n \"[data-panel-initial-focus]\",\n \"input:not([disabled]):not([type='hidden'])\",\n \"textarea:not([disabled])\",\n \"select:not([disabled])\",\n \"[contenteditable='true']\",\n].join(\",\");\n\nconst FALLBACK_SELECTOR = [\n \"button:not([disabled])\",\n \"a[href]\",\n \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\nfunction firstVisibleMatch(root: HTMLElement, selector: string) {\n return (\n Array.from(root.querySelectorAll<HTMLElement>(selector)).find(\n (el) =>\n el.getAttribute(\"aria-hidden\") !== \"true\" &&\n el.getClientRects().length > 0,\n ) ?? null\n );\n}\n\n/**\n * The open lite windows, newest last. Escape belongs to the TOPMOST one only —\n * two open windows must not both close on one key. Module scope, because a\n * per-component ref cannot see its siblings (the same mistake that opened two\n * Google consent windows for one intent, `check:google-auth-gate`).\n */\nconst openStack = (): string[] => globalSlot<string[]>(\"ai-matrx.summon.lite.openStack\", () => []);\n\n/**\n * True when a Radix popper (a `CreatablePicker` list, a Select, a Popover) is\n * open anywhere. That layer owns the Escape; we must not close the window out\n * from under a person who was only dismissing a dropdown.\n */\nfunction aDismissableLayerIsOpen(): boolean {\n return document.querySelector(\"[data-radix-popper-content-wrapper]\") !== null;\n}\n\nfunction clampRect(rect: Rect, vw: number, vh: number): Rect {\n const width = Math.min(rect.width, Math.max(240, vw - VIEWPORT_PADDING * 2));\n const height = Math.min(\n rect.height,\n Math.max(180, vh - VIEWPORT_PADDING * 2),\n );\n return {\n width,\n height,\n x: Math.min(Math.max(rect.x, VIEWPORT_PADDING), vw - width - VIEWPORT_PADDING),\n y: Math.min(\n Math.max(rect.y, VIEWPORT_PADDING),\n vh - height - VIEWPORT_PADDING,\n ),\n };\n}\n\nexport function LiteWindow({\n id,\n title,\n description,\n children,\n onClose: onCloseProp,\n size = \"md\",\n width,\n height,\n minWidth = DEFAULT_MIN_WIDTH,\n minHeight = DEFAULT_MIN_HEIGHT,\n draggable = true,\n resizable = true,\n dismissDisabled = false,\n headerActions,\n footer,\n className,\n contentClassName = \"px-4 py-3\",\n initialFocus = true,\n container,\n}: LiteWindowProps) {\n // Close: the explicit prop, else the enclosing SummonHost instance; neither is reported.\n const instance = useOptionalOverlayInstance();\n const ports = useSummonPorts();\n const onClose = useCallback(\n (reason: FloatingCloseReason) => {\n if (onCloseProp) onCloseProp(reason);\n else if (instance) instance.close();\n else ports.errors.capture(new Error(\"[summon] LiteWindow has no onClose and renders outside an overlay instance\"), { id, reason });\n },\n [id, instance, onCloseProp, ports],\n );\n const bounded = container !== undefined;\n const dispatch = useSummonDispatch();\n const zIndex = useSummonSelector(selectTransientZIndex(id));\n const isNarrow = useIsMobile();\n /**\n * THE OPENER — captured in the very first render, before this window's body\n * has mounted. It cannot be read later: a form whose first field carries\n * `autoFocus` (the real Add-a-scope-type form does) has already taken focus\n * by the time an effect runs, and the \"opener\" we would return focus to is\n * then a field inside the window that is about to be destroyed. That is\n * exactly how the first live pass closed with focus on nothing.\n */\n const [opener] = useState<HTMLElement | null>(() =>\n typeof document !== \"undefined\" &&\n document.activeElement instanceof HTMLElement &&\n document.activeElement !== document.body\n ? document.activeElement\n : null,\n );\n const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);\n const [frameEl, setFrameEl] = useState<HTMLDivElement | null>(null);\n const titleId = useId();\n const descriptionId = useId();\n\n const [rect, setRect] = useState<Rect>(() => {\n const base = SIZE_PX[size];\n return {\n width: width ?? base.width,\n height: height ?? base.height,\n x: 0,\n y: 0,\n };\n });\n const [placed, setPlaced] = useState(false);\n const [viewport, setViewport] = useState(() => ({\n w: typeof window === \"undefined\" ? 1280 : window.innerWidth,\n h: typeof window === \"undefined\" ? 800 : window.innerHeight,\n }));\n\n /** What actually gets drawn: the person's rect, kept on screen. */\n const view = clampRect(rect, viewport.w, viewport.h);\n\n useEffect(() => {\n if (bounded) {\n setPortalTarget(container ?? null);\n return;\n }\n setPortalTarget(document.getElementById(\"glass-layer\") ?? document.body);\n }, [bounded, container]);\n\n // Join the shared z-order on mount, leave it on unmount.\n useEffect(() => {\n dispatch(raiseTransientWindow(id));\n openStack().push(id);\n return () => {\n dispatch(releaseTransientWindow(id));\n const at = openStack().lastIndexOf(id);\n if (at !== -1) openStack().splice(at, 1);\n };\n }, [dispatch, id]);\n\n /**\n * Centre on first paint, and track the viewport.\n *\n * `rect` is the size and place the PERSON chose; it is never shrunk by a\n * viewport change. The clamp happens at render time only, so a window\n * squeezed at 375 comes back at its own size when there is room again —\n * a window that returned from a phone width permanently 359px wide was\n * the first thing this cost us.\n */\n useLayoutEffect(() => {\n if (typeof window === \"undefined\") return undefined;\n if (bounded) {\n // Bounded: the \"viewport\" is the container's box.\n if (!container) return undefined;\n const measure = () => ({ w: container.clientWidth, h: container.clientHeight });\n const box = measure();\n setViewport(box);\n if (!placed) {\n setRect((current) => ({\n ...current,\n x: Math.round((box.w - current.width) / 2),\n y: Math.round(Math.max(VIEWPORT_PADDING, (box.h - current.height) / 2.4)),\n }));\n setPlaced(true);\n }\n const observer = new ResizeObserver(() => setViewport(measure()));\n observer.observe(container);\n return () => observer.disconnect();\n }\n if (!placed) {\n setRect((current) => ({\n ...current,\n x: Math.round((window.innerWidth - current.width) / 2),\n y: Math.round(\n Math.max(56, (window.innerHeight - current.height) / 2.4),\n ),\n }));\n setPlaced(true);\n }\n const onResize = () =>\n setViewport({ w: window.innerWidth, h: window.innerHeight });\n window.addEventListener(\"resize\", onResize);\n return () => window.removeEventListener(\"resize\", onResize);\n }, [placed, bounded, container]);\n\n // Escape closes the TOPMOST lite window, and only when no inner dismissable\n // layer is holding the key.\n useEffect(() => {\n if (dismissDisabled) return undefined;\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\" || e.defaultPrevented) return;\n if (openStack()[openStack().length - 1] !== id) return;\n if (aDismissableLayerIsOpen()) return;\n e.stopPropagation();\n onClose(\"escape\");\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [dismissDisabled, id, onClose]);\n\n // Focus in on open; back to the opener on close.\n useEffect(() => {\n if (!initialFocus || !frameEl) return undefined;\n const frame = requestAnimationFrame(() => {\n // Never out of a field she is typing in outside this window\n // (focusOnOpen.ts — the same rule as the docked host).\n if (!mayTakeFocusOnOpen(frameEl)) return;\n const content =\n frameEl.querySelector<HTMLElement>(\"[data-panel-content]\") ?? frameEl;\n const target =\n firstVisibleMatch(content, FIELD_SELECTOR) ??\n firstVisibleMatch(content, FALLBACK_SELECTOR) ??\n frameEl;\n target.focus({ preventScroll: true });\n });\n return () => {\n cancelAnimationFrame(frame);\n if (opener?.isConnected && !frameEl.contains(opener)) {\n requestAnimationFrame(() => opener.focus({ preventScroll: true }));\n }\n };\n }, [initialFocus, frameEl, opener]);\n\n const raise = useCallback(() => {\n dispatch(raiseTransientWindow(id));\n const at = openStack().lastIndexOf(id);\n if (at !== -1 && at !== openStack().length - 1) {\n openStack().splice(at, 1);\n openStack().push(id);\n }\n }, [dispatch, id]);\n\n const startDrag = useCallback(\n (e: ReactPointerEvent<HTMLElement>) => {\n if (!draggable || isNarrow) return;\n if (e.button !== 0) return;\n const target = e.target as HTMLElement;\n // A control in the header bar is a control, not a drag handle.\n if (target.closest(\"button,a,input,[role='button']\")) return;\n e.preventDefault();\n raise();\n const startX = e.clientX;\n const startY = e.clientY;\n const origin = { ...view };\n const onMove = (move: PointerEvent) => {\n setRect({\n ...origin,\n x: origin.x + (move.clientX - startX),\n y: origin.y + (move.clientY - startY),\n });\n };\n const onUp = () => {\n window.removeEventListener(\"pointermove\", onMove);\n window.removeEventListener(\"pointerup\", onUp);\n };\n window.addEventListener(\"pointermove\", onMove);\n window.addEventListener(\"pointerup\", onUp);\n },\n [draggable, isNarrow, raise, view],\n );\n\n const startResize = useCallback(\n (e: ReactPointerEvent<HTMLElement>, edge: ResizeEdge) => {\n if (!resizable || isNarrow || e.button !== 0) return;\n e.preventDefault();\n e.stopPropagation();\n raise();\n const startX = e.clientX;\n const startY = e.clientY;\n const origin = { ...view };\n const onMove = (move: PointerEvent) => {\n const next = { ...origin };\n if (edge === \"e\" || edge === \"se\") {\n next.width = Math.max(minWidth, origin.width + (move.clientX - startX));\n }\n if (edge === \"s\" || edge === \"se\") {\n next.height = Math.max(\n minHeight,\n origin.height + (move.clientY - startY),\n );\n }\n setRect(next);\n };\n const onUp = () => {\n window.removeEventListener(\"pointermove\", onMove);\n window.removeEventListener(\"pointerup\", onUp);\n };\n window.addEventListener(\"pointermove\", onMove);\n window.addEventListener(\"pointerup\", onUp);\n },\n [isNarrow, minHeight, minWidth, raise, resizable, view],\n );\n\n /** Drag is never mouse-only: the handle is focusable and arrow keys move it. */\n const onHandleKeyDown = useCallback(\n (e: ReactKeyboardEvent<HTMLElement>) => {\n if (isNarrow || !draggable) return;\n const step = e.shiftKey || e.altKey ? 1 : KEY_DRAG_STEP;\n const delta =\n e.key === \"ArrowLeft\"\n ? { x: -step, y: 0 }\n : e.key === \"ArrowRight\"\n ? { x: step, y: 0 }\n : e.key === \"ArrowUp\"\n ? { x: 0, y: -step }\n : e.key === \"ArrowDown\"\n ? { x: 0, y: step }\n : null;\n if (!delta) return;\n e.preventDefault();\n setRect((current) => ({\n ...current,\n x: current.x + delta.x,\n y: current.y + delta.y,\n }));\n },\n [draggable, isNarrow],\n );\n\n if (!portalTarget) return null;\n\n // ── THE ONE NODE ──────────────────────────────────────────────────────────\n // Floating and drawer differ ONLY in these two values. Same element, same\n // subtree, same children — so crossing 768 is a re-render, never an unmount.\n const style: CSSProperties = isNarrow\n ? { zIndex }\n : {\n zIndex,\n left: view.x,\n top: view.y,\n width: view.width,\n height: view.height,\n };\n\n const frameClass = isNarrow\n ? \"fixed inset-x-0 bottom-0 max-h-[88dvh] rounded-t-xl border-t\"\n : bounded\n ? \"absolute rounded-xl border\"\n : \"fixed rounded-xl border\";\n\n const canDismiss = !dismissDisabled;\n\n const panel = (\n <div\n ref={setFrameEl}\n role=\"dialog\"\n aria-modal=\"false\"\n aria-labelledby={titleId}\n aria-describedby={description ? descriptionId : undefined}\n tabIndex={-1}\n data-matrx-lite-window={id}\n data-presentation={isNarrow ? \"drawer\" : \"floating\"}\n onPointerDownCapture={raise}\n style={style}\n className={cn(\n \"flex flex-col overflow-hidden border-border bg-card text-card-foreground shadow-2xl outline-none\",\n \"transition-[border-radius] duration-150\",\n frameClass,\n className,\n )}\n >\n <div\n onPointerDown={startDrag}\n className={cn(\n \"flex shrink-0 items-center gap-2 border-b border-border bg-muted/40 px-3 py-2\",\n !isNarrow && draggable && \"cursor-grab active:cursor-grabbing\",\n )}\n >\n {!isNarrow && draggable ? (\n <span\n role=\"button\"\n tabIndex={0}\n aria-label={`Move ${typeof title === \"string\" ? title : \"window\"} (arrow keys)`}\n onKeyDown={onHandleKeyDown}\n className=\"h-4 w-1.5 shrink-0 rounded-full bg-border outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n />\n ) : null}\n <div className=\"min-w-0 flex-1\">\n <h2\n id={titleId}\n className=\"truncate text-sm font-semibold text-foreground\"\n >\n {title}\n </h2>\n {description ? (\n <p\n id={descriptionId}\n className=\"mt-0.5 line-clamp-2 text-xs text-muted-foreground\"\n >\n {description}\n </p>\n ) : null}\n </div>\n {headerActions ? (\n <div className=\"flex shrink-0 items-center gap-1\">{headerActions}</div>\n ) : null}\n {canDismiss ? (\n <button\n type=\"button\"\n onClick={() => onClose(\"close-button\")}\n aria-label=\"Close\"\n className=\"shrink-0 rounded-md p-1 text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n ) : null}\n </div>\n\n <div\n data-panel-content\n className={cn(\"min-h-0 flex-1 overflow-auto\", contentClassName)}\n >\n {/*\n The nested portal container: a CreatablePicker popover, a Select or a\n Tooltip opened in here mounts INSIDE this window instead of at\n document.body, where it would land behind the window that opened it.\n */}\n <PortalContainerProvider container={frameEl}>\n {children}\n </PortalContainerProvider>\n </div>\n\n {footer ? (\n <div className=\"shrink-0 border-t border-border bg-muted/30 px-4 py-2\">\n {footer}\n </div>\n ) : null}\n\n {resizable && !isNarrow ? (\n <>\n <span\n onPointerDown={(e) => startResize(e, \"e\")}\n className=\"absolute right-0 top-8 h-[calc(100%-3rem)] w-1.5 cursor-ew-resize\"\n />\n <span\n onPointerDown={(e) => startResize(e, \"s\")}\n className=\"absolute bottom-0 left-4 h-1.5 w-[calc(100%-2rem)] cursor-ns-resize\"\n />\n <span\n onPointerDown={(e) => startResize(e, \"se\")}\n aria-hidden\n className=\"absolute bottom-0 right-0 h-4 w-4 cursor-nwse-resize\"\n />\n </>\n ) : null}\n </div>\n );\n\n return createPortal(panel, portalTarget);\n}\n","// components/matrx/resizable/focusOnOpen.ts\n//\n// 🚨 AN OPENING WINDOW NEVER TAKES FOCUS OUT OF A FIELD SHE IS TYPING IN\n// (Masterwork cold walk 23, 2026-09-30).\n//\n// `/masterwork/<id>?interview=1&rename=1` opened the Rulebook title's inline\n// rename AND the interview's docked panel on the same load. The rename field\n// took focus first; ~60ms later the panel's focus-on-open moved focus into\n// the panel, the rename field blurred, and EditableLabel commits-and-closes\n// on blur — so the rename the duplicate-name notice sent her to was gone\n// before she could type. Measured headless on localhost: focus() from the\n// rename field at t=2856ms, then from MatrxDynamicPanelHost's\n// focusPreferredTarget at 2921/2930/3043ms.\n//\n// The rule, shared by every window primitive in this folder (the docked host\n// and the floating frame): a window that opens takes focus, EXCEPT out of an\n// editable field that sits inside an element carrying KEEP_FOCUS_ATTRIBUTE —\n// an inline edit the page itself opened (the Rulebook title's rename on\n// `?rename=1`). A panel she opens with Enter or a hotkey from any other field\n// still takes focus: that is the panel she asked to type into.\n//\n// Why a mark and not `navigator.userActivation`: measured headless on\n// localhost, a cold load of the deep link reported `isActive === true` when\n// the panel opened, and a client navigation right after a click (Start → the\n// new Rulebook) is inside the click's activation window too — the gesture\n// cannot tell \"she opened this panel\" from \"she clicked something earlier\".\n// A <select> is never a field she is typing in.\n\n/** Put on an inline-edit container: a field inside it keeps focus when a window opens. */\nexport const KEEP_FOCUS_ATTRIBUTE = \"data-keep-focus-on-open\";\n\nconst EDITABLE_INPUT_TYPES = new Set([\n \"\",\n \"text\",\n \"search\",\n \"email\",\n \"url\",\n \"tel\",\n \"password\",\n \"number\",\n \"date\",\n \"datetime-local\",\n \"month\",\n \"time\",\n \"week\",\n]);\n\n/** Is `el` a field a person types into? */\nexport function isEditableField(el: Element | null): el is HTMLElement {\n if (!(el instanceof HTMLElement)) return false;\n if (el instanceof HTMLTextAreaElement) return !el.disabled && !el.readOnly;\n if (el instanceof HTMLInputElement) {\n return (\n !el.disabled &&\n !el.readOnly &&\n EDITABLE_INPUT_TYPES.has((el.getAttribute(\"type\") ?? \"\").toLowerCase())\n );\n }\n return el.isContentEditable === true;\n}\n\n/**\n * May a window that is opening (`container`) move focus into itself right\n * now? False only while focus is in an editable field outside it that sits in\n * a KEEP_FOCUS_ATTRIBUTE container.\n */\nexport function mayTakeFocusOnOpen(container: HTMLElement | null): boolean {\n const active = typeof document === \"undefined\" ? null : document.activeElement;\n if (!isEditableField(active)) return true;\n if (container != null && container.contains(active)) return true;\n return active.closest(`[${KEEP_FOCUS_ATTRIBUTE}]`) == null;\n}\n"],"mappings":"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","names":[]}
package/dist/next.cjs ADDED
@@ -0,0 +1,61 @@
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/next.ts
22
+ var next_exports = {};
23
+ __export(next_exports, {
24
+ useNextRouterPort: () => useNextRouterPort
25
+ });
26
+ module.exports = __toCommonJS(next_exports);
27
+
28
+ // src/next/useNextRouterPort.ts
29
+ var import_react = require("react");
30
+ var import_navigation = require("next/navigation");
31
+ function useNextRouterPort() {
32
+ const pathname = (0, import_navigation.usePathname)();
33
+ const searchParams = (0, import_navigation.useSearchParams)();
34
+ const query = searchParams?.toString() ?? "";
35
+ const snapshot = (0, import_react.useRef)({ pathname: pathname ?? "/", search: query ? `?${query}` : "" });
36
+ const listeners = (0, import_react.useRef)(/* @__PURE__ */ new Set());
37
+ const [port] = (0, import_react.useState)(() => ({
38
+ // The live address when there is one: a history write lands there before React re-renders.
39
+ pathname: () => typeof window === "undefined" ? snapshot.current.pathname : window.location.pathname,
40
+ search: () => typeof window === "undefined" ? snapshot.current.search : window.location.search,
41
+ replaceSearch: (nextSearch) => {
42
+ if (typeof window === "undefined") return;
43
+ const search = nextSearch && !nextSearch.startsWith("?") ? `?${nextSearch}` : nextSearch;
44
+ window.history.replaceState(window.history.state, "", `${window.location.pathname}${search}${window.location.hash}`);
45
+ },
46
+ subscribe: (listener) => {
47
+ listeners.current.add(listener);
48
+ return () => {
49
+ listeners.current.delete(listener);
50
+ };
51
+ }
52
+ }));
53
+ (0, import_react.useEffect)(() => {
54
+ const next = { pathname: pathname ?? "/", search: query ? `?${query}` : "" };
55
+ const changed = next.pathname !== snapshot.current.pathname || next.search !== snapshot.current.search;
56
+ snapshot.current = next;
57
+ if (changed) for (const listener of [...listeners.current]) listener();
58
+ }, [pathname, query]);
59
+ return port;
60
+ }
61
+ //# sourceMappingURL=next.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next.ts","../src/next/useNextRouterPort.ts"],"sourcesContent":["// @ai-matrx/summon/next — Next.js App Router adapters (the only section that imports `next`).\nexport { useNextRouterPort } from \"./next/useNextRouterPort\";\n","/**\n * useNextRouterPort — the overlays RouterPort for a Next.js App Router host. `next` is an optional\n * peer; this is the only module in the package that imports it.\n *\n * replaceSearch writes through `window.history.replaceState` — NEVER `router.replace`, which is an\n * App Router navigation (fresh RSC payload, page remount). Next keeps `useSearchParams` in sync with\n * history writes, and that change is what re-notifies subscribers.\n */\nimport { useEffect, useRef, useState } from \"react\";\nimport { usePathname, useSearchParams } from \"next/navigation\";\nimport type { RouterPort } from \"@ai-matrx/summon\";\n\nexport function useNextRouterPort(): RouterPort {\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const query = searchParams?.toString() ?? \"\";\n const snapshot = useRef({ pathname: pathname ?? \"/\", search: query ? `?${query}` : \"\" });\n const listeners = useRef(new Set<() => void>());\n\n const [port] = useState<RouterPort>(() => ({\n // The live address when there is one: a history write lands there before React re-renders.\n pathname: () => (typeof window === \"undefined\" ? snapshot.current.pathname : window.location.pathname),\n search: () => (typeof window === \"undefined\" ? snapshot.current.search : window.location.search),\n replaceSearch: (nextSearch) => {\n if (typeof window === \"undefined\") return;\n const search = nextSearch && !nextSearch.startsWith(\"?\") ? `?${nextSearch}` : nextSearch;\n window.history.replaceState(window.history.state, \"\", `${window.location.pathname}${search}${window.location.hash}`);\n },\n subscribe: (listener) => {\n listeners.current.add(listener);\n return () => {\n listeners.current.delete(listener);\n };\n },\n }));\n\n useEffect(() => {\n const next = { pathname: pathname ?? \"/\", search: query ? `?${query}` : \"\" };\n const changed = next.pathname !== snapshot.current.pathname || next.search !== snapshot.current.search;\n snapshot.current = next;\n if (changed) for (const listener of [...listeners.current]) listener();\n }, [pathname, query]);\n\n return port;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACQA,mBAA4C;AAC5C,wBAA6C;AAGtC,SAAS,oBAAgC;AAC9C,QAAM,eAAW,+BAAY;AAC7B,QAAM,mBAAe,mCAAgB;AACrC,QAAM,QAAQ,cAAc,SAAS,KAAK;AAC1C,QAAM,eAAW,qBAAO,EAAE,UAAU,YAAY,KAAK,QAAQ,QAAQ,IAAI,KAAK,KAAK,GAAG,CAAC;AACvF,QAAM,gBAAY,qBAAO,oBAAI,IAAgB,CAAC;AAE9C,QAAM,CAAC,IAAI,QAAI,uBAAqB,OAAO;AAAA;AAAA,IAEzC,UAAU,MAAO,OAAO,WAAW,cAAc,SAAS,QAAQ,WAAW,OAAO,SAAS;AAAA,IAC7F,QAAQ,MAAO,OAAO,WAAW,cAAc,SAAS,QAAQ,SAAS,OAAO,SAAS;AAAA,IACzF,eAAe,CAAC,eAAe;AAC7B,UAAI,OAAO,WAAW,YAAa;AACnC,YAAM,SAAS,cAAc,CAAC,WAAW,WAAW,GAAG,IAAI,IAAI,UAAU,KAAK;AAC9E,aAAO,QAAQ,aAAa,OAAO,QAAQ,OAAO,IAAI,GAAG,OAAO,SAAS,QAAQ,GAAG,MAAM,GAAG,OAAO,SAAS,IAAI,EAAE;AAAA,IACrH;AAAA,IACA,WAAW,CAAC,aAAa;AACvB,gBAAU,QAAQ,IAAI,QAAQ;AAC9B,aAAO,MAAM;AACX,kBAAU,QAAQ,OAAO,QAAQ;AAAA,MACnC;AAAA,IACF;AAAA,EACF,EAAE;AAEF,8BAAU,MAAM;AACd,UAAM,OAAO,EAAE,UAAU,YAAY,KAAK,QAAQ,QAAQ,IAAI,KAAK,KAAK,GAAG;AAC3E,UAAM,UAAU,KAAK,aAAa,SAAS,QAAQ,YAAY,KAAK,WAAW,SAAS,QAAQ;AAChG,aAAS,UAAU;AACnB,QAAI,QAAS,YAAW,YAAY,CAAC,GAAG,UAAU,OAAO,EAAG,UAAS;AAAA,EACvE,GAAG,CAAC,UAAU,KAAK,CAAC;AAEpB,SAAO;AACT;","names":[]}
@@ -0,0 +1,5 @@
1
+ import { RouterPort } from '@ai-matrx/summon';
2
+
3
+ declare function useNextRouterPort(): RouterPort;
4
+
5
+ export { useNextRouterPort };
package/dist/next.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { RouterPort } from '@ai-matrx/summon';
2
+
3
+ declare function useNextRouterPort(): RouterPort;
4
+
5
+ export { useNextRouterPort };
package/dist/next.js ADDED
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ // src/next/useNextRouterPort.ts
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { usePathname, useSearchParams } from "next/navigation";
6
+ function useNextRouterPort() {
7
+ const pathname = usePathname();
8
+ const searchParams = useSearchParams();
9
+ const query = searchParams?.toString() ?? "";
10
+ const snapshot = useRef({ pathname: pathname ?? "/", search: query ? `?${query}` : "" });
11
+ const listeners = useRef(/* @__PURE__ */ new Set());
12
+ const [port] = useState(() => ({
13
+ // The live address when there is one: a history write lands there before React re-renders.
14
+ pathname: () => typeof window === "undefined" ? snapshot.current.pathname : window.location.pathname,
15
+ search: () => typeof window === "undefined" ? snapshot.current.search : window.location.search,
16
+ replaceSearch: (nextSearch) => {
17
+ if (typeof window === "undefined") return;
18
+ const search = nextSearch && !nextSearch.startsWith("?") ? `?${nextSearch}` : nextSearch;
19
+ window.history.replaceState(window.history.state, "", `${window.location.pathname}${search}${window.location.hash}`);
20
+ },
21
+ subscribe: (listener) => {
22
+ listeners.current.add(listener);
23
+ return () => {
24
+ listeners.current.delete(listener);
25
+ };
26
+ }
27
+ }));
28
+ useEffect(() => {
29
+ const next = { pathname: pathname ?? "/", search: query ? `?${query}` : "" };
30
+ const changed = next.pathname !== snapshot.current.pathname || next.search !== snapshot.current.search;
31
+ snapshot.current = next;
32
+ if (changed) for (const listener of [...listeners.current]) listener();
33
+ }, [pathname, query]);
34
+ return port;
35
+ }
36
+ export {
37
+ useNextRouterPort
38
+ };
39
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next/useNextRouterPort.ts"],"sourcesContent":["/**\n * useNextRouterPort — the overlays RouterPort for a Next.js App Router host. `next` is an optional\n * peer; this is the only module in the package that imports it.\n *\n * replaceSearch writes through `window.history.replaceState` — NEVER `router.replace`, which is an\n * App Router navigation (fresh RSC payload, page remount). Next keeps `useSearchParams` in sync with\n * history writes, and that change is what re-notifies subscribers.\n */\nimport { useEffect, useRef, useState } from \"react\";\nimport { usePathname, useSearchParams } from \"next/navigation\";\nimport type { RouterPort } from \"@ai-matrx/summon\";\n\nexport function useNextRouterPort(): RouterPort {\n const pathname = usePathname();\n const searchParams = useSearchParams();\n const query = searchParams?.toString() ?? \"\";\n const snapshot = useRef({ pathname: pathname ?? \"/\", search: query ? `?${query}` : \"\" });\n const listeners = useRef(new Set<() => void>());\n\n const [port] = useState<RouterPort>(() => ({\n // The live address when there is one: a history write lands there before React re-renders.\n pathname: () => (typeof window === \"undefined\" ? snapshot.current.pathname : window.location.pathname),\n search: () => (typeof window === \"undefined\" ? snapshot.current.search : window.location.search),\n replaceSearch: (nextSearch) => {\n if (typeof window === \"undefined\") return;\n const search = nextSearch && !nextSearch.startsWith(\"?\") ? `?${nextSearch}` : nextSearch;\n window.history.replaceState(window.history.state, \"\", `${window.location.pathname}${search}${window.location.hash}`);\n },\n subscribe: (listener) => {\n listeners.current.add(listener);\n return () => {\n listeners.current.delete(listener);\n };\n },\n }));\n\n useEffect(() => {\n const next = { pathname: pathname ?? \"/\", search: query ? `?${query}` : \"\" };\n const changed = next.pathname !== snapshot.current.pathname || next.search !== snapshot.current.search;\n snapshot.current = next;\n if (changed) for (const listener of [...listeners.current]) listener();\n }, [pathname, query]);\n\n return port;\n}\n"],"mappings":";;;AAQA,SAAS,WAAW,QAAQ,gBAAgB;AAC5C,SAAS,aAAa,uBAAuB;AAGtC,SAAS,oBAAgC;AAC9C,QAAM,WAAW,YAAY;AAC7B,QAAM,eAAe,gBAAgB;AACrC,QAAM,QAAQ,cAAc,SAAS,KAAK;AAC1C,QAAM,WAAW,OAAO,EAAE,UAAU,YAAY,KAAK,QAAQ,QAAQ,IAAI,KAAK,KAAK,GAAG,CAAC;AACvF,QAAM,YAAY,OAAO,oBAAI,IAAgB,CAAC;AAE9C,QAAM,CAAC,IAAI,IAAI,SAAqB,OAAO;AAAA;AAAA,IAEzC,UAAU,MAAO,OAAO,WAAW,cAAc,SAAS,QAAQ,WAAW,OAAO,SAAS;AAAA,IAC7F,QAAQ,MAAO,OAAO,WAAW,cAAc,SAAS,QAAQ,SAAS,OAAO,SAAS;AAAA,IACzF,eAAe,CAAC,eAAe;AAC7B,UAAI,OAAO,WAAW,YAAa;AACnC,YAAM,SAAS,cAAc,CAAC,WAAW,WAAW,GAAG,IAAI,IAAI,UAAU,KAAK;AAC9E,aAAO,QAAQ,aAAa,OAAO,QAAQ,OAAO,IAAI,GAAG,OAAO,SAAS,QAAQ,GAAG,MAAM,GAAG,OAAO,SAAS,IAAI,EAAE;AAAA,IACrH;AAAA,IACA,WAAW,CAAC,aAAa;AACvB,gBAAU,QAAQ,IAAI,QAAQ;AAC9B,aAAO,MAAM;AACX,kBAAU,QAAQ,OAAO,QAAQ;AAAA,MACnC;AAAA,IACF;AAAA,EACF,EAAE;AAEF,YAAU,MAAM;AACd,UAAM,OAAO,EAAE,UAAU,YAAY,KAAK,QAAQ,QAAQ,IAAI,KAAK,KAAK,GAAG;AAC3E,UAAM,UAAU,KAAK,aAAa,SAAS,QAAQ,YAAY,KAAK,WAAW,SAAS,QAAQ;AAChG,aAAS,UAAU;AACnB,QAAI,QAAS,YAAW,YAAY,CAAC,GAAG,UAAU,OAAO,EAAG,UAAS;AAAA,EACvE,GAAG,CAAC,UAAU,KAAK,CAAC;AAEpB,SAAO;AACT;","names":[]}