@ai-matrx/summon 0.0.0 → 0.1.1

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 +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +379 -1
  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 +738 -0
  51. package/dist/react.cjs.map +1 -0
  52. package/dist/react.d.cts +266 -0
  53. package/dist/react.d.ts +266 -0
  54. package/dist/react.js +723 -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 +3977 -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 +4002 -0
  86. package/dist/window.js.map +1 -0
  87. package/package.json +239 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1
4
+ ### Unreleased
5
+
6
+ - **Fixed: an open overlay re-rendered on every render of the host's parent** (runtime sweep, 2026-10-16: a `shareLinkDialog` opened without its file id logged its refusal 425 times in its first 2 s inside AI Matrx). SummonHost's per-definition slot, its per-instance slot and every definition's instance body are now `memo` components (SummonHost itself stays a plain function), so an open overlay re-renders only when ITS instance changes (data reference, hidden), never because the app shell above the host re-rendered. Guard: `src/react/__tests__/renderStorm.test.tsx` (null-render under host churn, `parse()` refusal closes once, close-on-mount renders once, Controller never reopens a self-closed overlay, keepAlive hidden not re-rendered) — red on 0.1.0 (21 renders vs 1). Consumer action: none.
7
+ - **New: `useOptionalSummonStore()`** (`./react`) — the nearest provider's store or `null`, for a package that mounts its own `SummonProvider` only when the host has none (`@ai-matrx/chat` does). Consumer action: none.
8
+ - Source passes stricter consumer compilers (`verbatimModuleSyntax`, `erasableSyntaxOnly`, `noUnusedLocals`): type-only import in the window-manager slice, no constructor parameter properties in `KeepAliveRegistry` / `LoaderCache`, four unused names removed. Consumer action: none.
9
+
10
+ ## 0.1.0 — 2026-10-16
11
+
12
+ First release (C38): the Redux-driven render system carved out of matrx-frontend.
13
+
14
+ - Core: store, slices (`overlays`, `windowManager`, `urlSync`, `overlayState`), close lifecycle, render watchdog, ports with working defaults, definition registry.
15
+ - Handles: the callbackManager successor (same verbs), event channels, one-shot requests, object handles, owner-scoped disposal on close. Fixed: group membership never shrank after trigger/remove.
16
+ - React: `SummonProvider` (own store context or the host's store), `defineOverlay` + `def.useSummon()` / `def.Controller`, `SummonHost` (one subscriber per overlay; closed = nothing mounted, no chunk fetched), keep-alive none/hidden/restore, `useOverlayState`, lazy loader with timeout, retry and error boundary.
17
+ - Window: the WindowPanel desktop (drag, resize, snap, arrange, tray with thumbnails, minimize/hide all, pop-out, phone presentations). Minimize always keeps the body mounted; pop-out and dock keep the body's identity (fixed: it remounted).
18
+ - window-host (root-mountable config, tray sync, tray previews), lite window, dialog and sheet wrappers.
19
+ - Links: `?panels=` manager, grammar v2 (percent-encoded, v1-compatible), one open-from-link door. Fixed: link vs restore race (link data wins, restored geometry wins).
20
+ - Persistence: browser-local store (per tab or per person) and `@ai-matrx/data` adapter over `users.user_surface_state`; Next.js router port; testing helpers.
21
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI Matrix Engine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,381 @@
1
1
  # @ai-matrx/summon
2
2
 
3
- Name reservation. No usable API yet; do not adopt.
3
+ Summon any React component from anywhere — with its handlers, refs and state — and never mount it until it is called.
4
+
5
+ - **Dispatch to open.** Open a window, dialog, sheet or any component from anywhere with a hook or an action. A closed overlay puts nothing in the React tree and fetches no code.
6
+ - **Single or multi instance** per overlay.
7
+ - **Handles beside Redux.** Callbacks, event channels, one-shot requests and imperative refs live in a registry next to the store, owned by the open instance and disposed when it closes.
8
+ - **Keep-alive.** On close an overlay unmounts (`none`), stays alive hidden (`hidden`), or unmounts and remembers its saved state (`restore`). `useOverlayState` is the saved-state hook.
9
+ - **Address-bar links.** `?panels=notes:42` names an open overlay, loads its data and survives reload.
10
+ - **Persistence**, off or on: browser-local, your own database through a small port, or `@ai-matrx/data`.
11
+ - **Surfaces.** `WindowPanel` is a desktop in the browser (drag, resize, snap, arrange, tray with thumbnails, minimize, pop-out to a real browser window, three phone presentations). `dialog`, `sheet` and `lite` (a light floating window) share the same close binding.
12
+
13
+ The package owns its Redux slices. It runs on a private store, or mounts into the store you already have.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pnpm add @ai-matrx/summon react react-dom react-redux @reduxjs/toolkit
19
+ ```
20
+
21
+ | Peer | Needed for |
22
+ |---|---|
23
+ | `react` ≥ 19, `react-dom`, `react-redux` ≥ 9, `@reduxjs/toolkit` ≥ 2 | everything except the framework-free `.` and `./handles` entries |
24
+ | `next` ≥ 16 (optional) | only `./next` |
25
+ | `html-to-image` (optional) | tray thumbnails of minimized windows (without it the tile shows no preview) |
26
+ | `@supabase/supabase-js` | only `./persistence/data`, through `@ai-matrx/data` |
27
+
28
+ Entries: `.` (core, no React), `./handles` (no React, no Redux), `./react`, `./window`, `./dialog`, `./sheet`, `./lite`, `./url`, `./persistence`, `./persistence/data`, `./next`, `./testing`, `./styles.css`.
29
+
30
+ ## Quick start
31
+
32
+ **1. Provider and host, once, near the root.**
33
+
34
+ ```tsx
35
+ "use client";
36
+ import { SummonProvider, SummonHost } from "@ai-matrx/summon/react";
37
+ import { APP_OVERLAYS } from "./overlays";
38
+
39
+ export function Root({ children }: { children: React.ReactNode }) {
40
+ return (
41
+ <SummonProvider>
42
+ {children}
43
+ <SummonHost definitions={APP_OVERLAYS} />
44
+ </SummonProvider>
45
+ );
46
+ }
47
+ ```
48
+
49
+ **2. Declare an overlay once**: a light definition file (safe to import anywhere) and a render module (the lazy chunk).
50
+
51
+ ```ts
52
+ // notes.overlay.ts
53
+ import { defineOverlay } from "@ai-matrx/summon/react";
54
+
55
+ export interface NotesWindowData {
56
+ noteId?: string;
57
+ }
58
+
59
+ export const notesWindow = defineOverlay<NotesWindowData>()({
60
+ id: "notesWindow",
61
+ label: "Notes",
62
+ render: () => import("./notes.render"),
63
+ });
64
+
65
+ // One line per overlay: opens are type-checked and a mistyped id is a compile error.
66
+ declare module "@ai-matrx/summon" {
67
+ interface OverlayRegistry {
68
+ notesWindow: NotesWindowData;
69
+ }
70
+ }
71
+ ```
72
+
73
+ ```tsx
74
+ // notes.render.tsx — loaded only when the overlay is first opened
75
+ import type { OverlayRenderProps } from "@ai-matrx/summon/react";
76
+ import { WindowPanel } from "@ai-matrx/summon/window";
77
+
78
+ export default function render({ data }: OverlayRenderProps) {
79
+ const noteId = typeof data?.noteId === "string" ? data.noteId : null;
80
+ return (
81
+ <WindowPanel id="notes" title="Notes" boundToOverlay width={480} height={420}>
82
+ <NoteEditor noteId={noteId} />
83
+ </WindowPanel>
84
+ );
85
+ }
86
+ ```
87
+
88
+ `data` arrives as untrusted `Readonly<Record<string, unknown>> | null` (it crossed Redux, a URL or storage). Narrow it in the render module, or give the definition a `parse(raw)` that returns the typed value (or `null` to refuse and close with a warning).
89
+
90
+ ```ts
91
+ // overlays.ts
92
+ import { notesWindow } from "./notes.overlay";
93
+ export const APP_OVERLAYS = [notesWindow];
94
+ ```
95
+
96
+ **3. Open it.**
97
+
98
+ ```tsx
99
+ import { notesWindow } from "./notes.overlay";
100
+
101
+ function OpenNotes() {
102
+ const open = notesWindow.useSummon();
103
+ return <button onClick={() => open({ data: { noteId: "42" } })}>Notes</button>;
104
+ }
105
+
106
+ // Declarative: opens on mount, closes on unmount, re-opens when the (serialisable) props change.
107
+ const Always = () => <notesWindow.Controller noteId="42" />;
108
+ ```
109
+
110
+ `open()` returns `{ overlayId, instanceId, close() }`. The definition also gives you `useIsOpen(instanceId?)`, `useClose()` and, inside the render tree, `useEmit()`.
111
+
112
+ ### Definition options
113
+
114
+ | Field | Meaning |
115
+ |---|---|
116
+ | `id`, `label` | id is the registry key; label shows in the tray, menus and diagnostics |
117
+ | `instance` | `"single"` (default, instance id `"default"`) or `"multi"` (a fresh id per open, or pass `instanceId`) |
118
+ | `keepAlive` | `"none"` (default), `"hidden"` or `"restore"` |
119
+ | `closesOnNavigation` | close when the route changes (the overlay shows a piece of the page that opened it) |
120
+ | `defaults` | merged under every open's data |
121
+ | `window` | `{ mobile, mobileSidebarAs, ephemeral, preservation, restoreDefaults, windowId, deprecated, unbiddenHome }` for window overlays |
122
+ | `link` | `{ key, aliases?, hydrate }`, see Links |
123
+ | `parse` | narrow raw data once; `null` refuses |
124
+
125
+ Minimizing a window always keeps its component mounted, whatever `keepAlive` says.
126
+
127
+ ### Keep-alive and saved state
128
+
129
+ ```tsx
130
+ import { useOverlayState, useOverlayInstance } from "@ai-matrx/summon/react";
131
+
132
+ function Draft() {
133
+ const [text, setText] = useOverlayState("draft", ""); // like useState; scoped to this open instance
134
+ const { close, hidden } = useOverlayInstance();
135
+ return <textarea value={text} onChange={(e) => setText(e.target.value)} />;
136
+ }
137
+ ```
138
+
139
+ - `keepAlive: "hidden"`: closing hides the live component (`<Activity mode="hidden">`); reopening shows the same instance. At most `keepAliveLimit` (default 8) closed instances stay mounted; the oldest is evicted and released.
140
+ - `keepAlive: "restore"`: closing unmounts; `useOverlayState` values come back on reopen (and persist when persistence is on).
141
+ - `useOverlayState` values must be serialisable. Any descendant of the overlay may call it.
142
+
143
+ ### Callbacks without functions in Redux
144
+
145
+ Declare an event map and pass plain functions when opening. They travel through an event channel owned by the instance; only its id rides in the data.
146
+
147
+ ```ts
148
+ export const uploader = defineOverlay<{ folder?: string }, { uploaded: { url: string }; cleared: void }>()({
149
+ id: "uploader", label: "Uploader", render: () => import("./uploader.render"),
150
+ });
151
+
152
+ const open = uploader.useSummon();
153
+ open({ data: { folder: "logos" }, handlers: { uploaded: (r) => setUrl(r.url) } });
154
+
155
+ // inside the overlay's render tree
156
+ const emit = uploader.useEmit();
157
+ emit("uploaded", { url });
158
+ ```
159
+
160
+ Closing the instance disposes the channel. Emitting after that is a silent no-op.
161
+
162
+ ## Mounting into an existing Redux store
163
+
164
+ ```ts
165
+ import { configureStore } from "@reduxjs/toolkit";
166
+ import { summonReducers, createSummonMiddleware } from "@ai-matrx/summon";
167
+
168
+ const ports = { /* see Ports */ };
169
+ export const store = configureStore({
170
+ reducer: { ...appReducers, ...summonReducers }, // keys: overlays, windowManager, urlSync, overlayState
171
+ middleware: (getDefault) => getDefault().concat(createSummonMiddleware(ports)),
172
+ });
173
+ ```
174
+
175
+ ```tsx
176
+ <SummonProvider store={store} ports={ports}>…</SummonProvider>
177
+ ```
178
+
179
+ Pass the same `ports` object to both. The overlays slice keys are fixed. Without `store`, the provider makes a private store (`createSummonStore({ ports })`).
180
+
181
+ Provider knobs: `fallbacks` (`loading`, `error` components), `errorReportExtras` (extra controls in the error card), `renderWrapper` (wrap every rendered overlay), `keepAliveLimit` (8), `importTimeoutMs` (12 s; a render module that has not loaded becomes a `ChunkLoadError` with Try again), `stallWarnMs` (10 s).
182
+
183
+ ## Ports
184
+
185
+ Every port has a working default, so a bare host needs none. Override only what you have; pass them as `ports` on the provider (a partial object).
186
+
187
+ | Port | Shape | Default |
188
+ |---|---|---|
189
+ | `identity` | `current(): { key, ready }`, `subscribe(listener)` | anonymous, always ready. A changing `key` swaps all saved state |
190
+ | `router` | `pathname()`, `search()`, `replaceSearch(next)`, `subscribe(listener)` | `window.location` + `history.replaceState` (inert on the server) |
191
+ | `notify` | `info`, `warn`, `error(message, options?)`, optional `dismiss(id)` | console |
192
+ | `errors` | `capture(error, context)` | console.error |
193
+ | `debug` | `enabled()`, optional `subscribe` | off |
194
+ | `persistence` | an `OverlayPersistencePort`, or `null` | `null` (persistence off) |
195
+
196
+ Next.js: `import { useNextRouterPort } from "@ai-matrx/summon/next"` returns a stable `RouterPort` (call it inside a client component under Suspense; it reads `useSearchParams`). Testing fakes: see Testing.
197
+
198
+ ## Links
199
+
200
+ `OverlayLinkManager` keeps `?panels=` and open overlays in sync through `ports.router`. Mount it once inside the provider.
201
+
202
+ ```tsx
203
+ import { OverlayLinkManager, useOverlayLink } from "@ai-matrx/summon/url";
204
+
205
+ <SummonProvider><OverlayLinkManager /><SummonHost definitions={APP_OVERLAYS} /></SummonProvider>
206
+ ```
207
+
208
+ Give the definition a link:
209
+
210
+ ```ts
211
+ defineOverlay<NotesWindowData>()({
212
+ id: "notesWindow", label: "Notes", render: () => import("./notes.render"),
213
+ link: {
214
+ key: "notes",
215
+ hydrate: ({ id, args, open }) => open({ data: { noteId: id } }), // return "refused" to drop the token
216
+ },
217
+ });
218
+ ```
219
+
220
+ - Opening `/page?panels=notes:42` runs `hydrate({ id: "42", args, open, dispatch })`. `hydrate` may be async (load a record, then `open`).
221
+ - 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.
222
+ - Unknown keys are announced at once; tokens whose overlay never registers are announced at a deadline and kept verbatim.
223
+ - **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`.
224
+ - **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" }`).
225
+
226
+ ## Persistence
227
+
228
+ Off by default. Turn it on with a `persistence` port plus one mounted component:
229
+
230
+ ```tsx
231
+ import { createLocalPersistence, OverlayPersistence } from "@ai-matrx/summon/persistence";
232
+
233
+ const persistence = createLocalPersistence(); // create once, module level
234
+ <SummonProvider ports={{ persistence }}>
235
+ <OverlayPersistence /> {/* mount once */}
236
+ <SummonHost definitions={APP_OVERLAYS} />
237
+ </SummonProvider>
238
+ ```
239
+
240
+ What is saved: the person's open windows and layout (the *workspace*), and the component state of instances that are `keepAlive: "restore"` or preserved windows. Window data is saved only for the keys a window lists in `window.preservation.dataKeys` (default-deny); `window.ephemeral: true` never saves. Caps: 256 KiB workspace, 64 sessions, 32 KiB per window's data and per state entry; overruns report through `ports.errors.capture` (`source: "overlay-persistence"`).
241
+
242
+ `<OverlayPersistence>` options: `scope` (`"tab"` one workspace per browser tab, `"person"` one per person; default `"tab"` for the local store, `"person"` for per-person stores), `seedFromPerson` (a new tab starts from the person's last layout; default true), `saveDebounceMs` (250), `viewport`. `useOverlayPersistence(options)` returns `{ hydrated }`.
243
+
244
+ **Browser-local store.** `createLocalPersistence({ namespace?, backend?, storage?, session?, locks?, idbReadBudgetMs?, isReload?, now? })`: a localStorage mirror plus IndexedDB, per identity, per tab workspace with leases. It also has `purgeIdentity(identityKey)` for sign-out cleanup.
245
+
246
+ **Your own database.** Implement the port:
247
+
248
+ ```ts
249
+ import type { OverlayPersistencePort } from "@ai-matrx/summon";
250
+
251
+ export const myPersistence: OverlayPersistencePort = {
252
+ perPerson: true, // true: rows are already scoped to the signed-in person (plain keys). Omit/false: keys arrive prefixed "<identityKey>/"
253
+ load: async (key) => /* { value, version } | null */ null,
254
+ save: async (key, value, options) => {
255
+ /* if options?.expectedVersion is set and differs from the stored version, throw an error named "PersistenceVersionConflictError" */
256
+ return { version: 1 };
257
+ },
258
+ remove: async (key) => {},
259
+ list: async (prefix) => [], // keys starting with prefix
260
+ };
261
+ ```
262
+
263
+ Keys written: `workspace`, `workspace:<tabWorkspaceId>` (tab scope), `state:<overlayId>:<instanceId>`. Values are JSON. Suggested table:
264
+
265
+ | Column | Type | |
266
+ |---|---|---|
267
+ | `person_id` | uuid | owner; rows are read and written only for the signed-in person |
268
+ | `key` | text | one of the keys above; unique with `person_id` |
269
+ | `value` | jsonb | the saved value |
270
+ | `version` | int | starts at 1, +1 per save (optimistic concurrency) |
271
+
272
+ On a conflict the controller re-reads the version and writes once more, so the latest device wins. Version conflicts are recognised by error name (`isVersionConflict(error)`).
273
+
274
+ **`@ai-matrx/data` hosts.** `createDataPersistence({ db, userId?, table?, feature? })` from `@ai-matrx/summon/persistence/data` maps each key to a row of `users.user_surface_state` (`feature = "summon"`, `surface_key = key`, `state = value`, `version`), through the data package's doors. The organization comes from `createDb({ client, organization })` and the insert is refused when there is none.
275
+
276
+ ```ts
277
+ import { createDb } from "@ai-matrx/data/db";
278
+ import { createDataPersistence } from "@ai-matrx/summon/persistence/data";
279
+
280
+ const persistence = createDataPersistence({ db: createDb({ client: supabase, organization: () => activeOrgId }) });
281
+ ```
282
+
283
+ ## Handles
284
+
285
+ `@ai-matrx/summon/handles` is framework-free. State lives in one global slot, so duplicated bundles share one registry.
286
+
287
+ - `callbackManager` (verbs): `register`, `registerWithContext(callback, { context, groupId, owner })`, `subscribe`, `unsubscribe`, `trigger`, `triggerWithContext`, `triggerGroup`, `triggerGroupCommand`, `updateProgress`, `createGroup`, `removeGroup`, `remove` / `unregister`, `get`, `has`, `registerWidgetHandle`, `ownedBy`, `groupMembers`, `disposeOwner`, `size`, `subscribeToRegistry`.
288
+ - Typed helpers, each accepting `{ owner }`:
289
+
290
+ ```ts
291
+ import { createEventChannel, getEventChannel, createRequest, getRequest, registerHandle, getHandle } from "@ai-matrx/summon/handles";
292
+
293
+ const owner = "notesWindow:default"; // overlayId:instanceId
294
+
295
+ const channel = createEventChannel<{ saved: string }>({ owner }); // many-shot; pass channel.id in open data
296
+ channel.subscribe((e) => console.log(e)); channel.emit({ saved: "42" });
297
+ getEventChannel<{ saved: string }>(channel.id)?.emit({ saved: "43" }); // overlay side
298
+
299
+ const request = createRequest<{ prompt: string }, boolean>({ prompt: "Discard?" }, { owner }); // one-shot
300
+ getRequest<{ prompt: string }, boolean>(request.id)?.resolve(true); // overlay answers
301
+ const answer = await request.promise; // rejects with HandleDisposedError if disposed unanswered
302
+
303
+ const dispose = registerHandle("editor:42", { focus() {} }, { owner }); // imperative ref
304
+ getHandle<{ focus(): void }>("editor:42")?.focus();
305
+ dispose();
306
+ ```
307
+
308
+ **Owner disposal.** When an instance closes, everything registered with its `overlayId:instanceId` as `owner` is disposed: pending requests reject, channels drop subscribers, handles are removed. No opener disposes by hand. Disposing a group member removes it from the group.
309
+
310
+ ## WindowPanel
311
+
312
+ ```tsx
313
+ import { WindowsProvider, WindowPanel } from "@ai-matrx/summon/window";
314
+ ```
315
+
316
+ Slots contract: the **body is content only**. Everything that is window chrome goes in a slot, never in the body.
317
+
318
+ | Slot | Props |
319
+ |---|---|
320
+ | Title | `title`, `titleNode` |
321
+ | Header actions | `actionsLeft`, `actionsRight` |
322
+ | Footer | `footer`, or zoned `footerLeft` / `footerCenter` / `footerRight`; `footerVariant: "bar" \| "rich"` |
323
+ | Left sidebar | `sidebar`, `sidebarDefaultSize`, `sidebarMinSize`, `defaultSidebarOpen`, `sidebarExpandsWindow` |
324
+ | Right panel | `secondaryPanel`, `secondaryPanelOpen`, `secondaryPanelDefaultSize`, `secondaryPanelMinSize` |
325
+
326
+ **Close binding** is required and checked by the type system: exactly one of `overlayId` (close that overlay), `boundToOverlay` (close the enclosing `SummonHost` instance) or `onClose` (page-local). A window with none of them does not compile.
327
+
328
+ Other props: `id`, `width`/`height` (numbers or `"60vw"`), `minWidth`/`minHeight`, `position`, `fitContent`, `closeOnEscape`, `hidePopOutButton`, `urlSyncKey`/`urlSyncId`/`urlSyncArgs`, `onCollectData`, `captureTraySnapshot`.
329
+
330
+ Behaviour you can rely on: maximize, minimize, pop-out and dock never remount the body (state, refs and effects survive; minimize keeps the body mounted and captures the tray thumbnail). Pop-out uses a real browser window (Document Picture-in-Picture where available). On phones a window renders as `fullscreen`, `drawer` or `card` per the definition's `window.mobile`.
331
+
332
+ **`WindowsProvider`** is optional; options: `escapeYieldSelector` (visible elements matching it own the Escape key; default `[data-overlay-escape-yield]`), `mobileTopOffset` (default `var(--header-height, 0px)`), `selectionSurface` (a component wrapping every window body, for a host's selection or right-click menu).
333
+
334
+ Also exported from `./window`: `WindowTraySync`, `LayoutIcon`, `registerTrayPreview` (a semantic preview for a minimized tile), `usePopoutControl`, `MobileDrawerSurface`, `MobileCardSurface`, `useWindowPanel`.
335
+
336
+ ### Dialog, sheet, light window
337
+
338
+ ```tsx
339
+ import { OverlayDialog } from "@ai-matrx/summon/dialog";
340
+ import { OverlaySheet, OverlayDrawer, OverlayBottomSheet } from "@ai-matrx/summon/sheet";
341
+ import { LiteWindow } from "@ai-matrx/summon/lite";
342
+
343
+ // inside a render module: closes through the overlay instance
344
+ <OverlayDialog boundToOverlay>{/* DialogContent, DialogTitle … from @ai-matrx/design-system */}</OverlayDialog>
345
+
346
+ // page-local: you own the state
347
+ <OverlayDialog open={open} onClose={() => setOpen(false)}>…</OverlayDialog>
348
+
349
+ <LiteWindow id="inspector" title="Inspector">…</LiteWindow> {/* inside an SummonHost overlay, onClose can be omitted */}
350
+ ```
351
+
352
+ `OverlayDialog`, `OverlaySheet`, `OverlayDrawer` and `OverlayBottomSheet` take the design-system component's props plus the close binding: `boundToOverlay` (open while the instance is shown) or `onClose` (+ `open`). `LiteWindow` has drag, edge resize, Escape when topmost, focus in and out, and a bottom drawer on phones; no minimize, tray, persistence or links.
353
+
354
+ ## Styling
355
+
356
+ Import the structural CSS once, and make Tailwind v4 scan the package, since the components use utility classes:
357
+
358
+ ```css
359
+ @import "tailwindcss";
360
+ @import "@ai-matrx/summon/styles.css";
361
+ @source "../node_modules/@ai-matrx/summon/dist"; /* path relative to this CSS file */
362
+ ```
363
+
364
+ Colors come only from `@ai-matrx/design-system` tokens, so the host's theme applies. The traffic-light colors are `--matrx-window-light-*` custom properties you can override.
365
+
366
+ ## Testing
367
+
368
+ ```tsx
369
+ import { createTestPorts, createTestSummonStore, createMemoryPersistence } from "@ai-matrx/summon/testing";
370
+
371
+ const ports = createTestPorts({ persistence: createMemoryPersistence() });
372
+ const store = createTestSummonStore(ports);
373
+
374
+ render(<SummonProvider store={store} ports={ports}><SummonHost definitions={[notesWindow]} /></SummonProvider>);
375
+ ```
376
+
377
+ `createTestPorts` gives a controllable `identity` (`set(key, ready?)`), a controllable `router` (`navigate(path, search?)`) and recording `notify` and `errors` (`messages`, `captured`). `createMemoryPersistence` is an in-memory, versioned port. No test-runner dependency.
378
+
379
+ ## Gates
380
+
381
+ `pnpm typecheck` (also compiles the package with a host-augmented registry), `npx vitest run`, `pnpm check:package`.
@@ -0,0 +1,20 @@
1
+ type OverlaySurfaceCloseBinding = {
2
+ /** Page-local: called when the surface asks to close (Escape, scrim, close button). */
3
+ onClose: () => void;
4
+ boundToOverlay?: never;
5
+ /** Page-local open state. Default true (render it only while open, or pass it). */
6
+ open?: boolean | undefined;
7
+ } | {
8
+ /** Close through the enclosing SummonHost instance. */
9
+ boundToOverlay: true;
10
+ /** Optional extra hook run before the instance closes. */
11
+ onClose?: (() => void) | undefined;
12
+ open?: never;
13
+ };
14
+ interface ResolvedSurfaceClose {
15
+ readonly open: boolean;
16
+ readonly onOpenChange: (open: boolean) => void;
17
+ }
18
+ declare function useOverlaySurfaceClose(binding: OverlaySurfaceCloseBinding, surface: string): ResolvedSurfaceClose;
19
+
20
+ export { type OverlaySurfaceCloseBinding as O, type ResolvedSurfaceClose as R, useOverlaySurfaceClose as u };
@@ -0,0 +1,20 @@
1
+ type OverlaySurfaceCloseBinding = {
2
+ /** Page-local: called when the surface asks to close (Escape, scrim, close button). */
3
+ onClose: () => void;
4
+ boundToOverlay?: never;
5
+ /** Page-local open state. Default true (render it only while open, or pass it). */
6
+ open?: boolean | undefined;
7
+ } | {
8
+ /** Close through the enclosing SummonHost instance. */
9
+ boundToOverlay: true;
10
+ /** Optional extra hook run before the instance closes. */
11
+ onClose?: (() => void) | undefined;
12
+ open?: never;
13
+ };
14
+ interface ResolvedSurfaceClose {
15
+ readonly open: boolean;
16
+ readonly onOpenChange: (open: boolean) => void;
17
+ }
18
+ declare function useOverlaySurfaceClose(binding: OverlaySurfaceCloseBinding, surface: string): ResolvedSurfaceClose;
19
+
20
+ export { type OverlaySurfaceCloseBinding as O, type ResolvedSurfaceClose as R, useOverlaySurfaceClose as u };
@@ -0,0 +1,60 @@
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/dialog.ts
22
+ var dialog_exports = {};
23
+ __export(dialog_exports, {
24
+ OverlayDialog: () => OverlayDialog,
25
+ useOverlaySurfaceClose: () => useOverlaySurfaceClose
26
+ });
27
+ module.exports = __toCommonJS(dialog_exports);
28
+
29
+ // src/dialog/OverlayDialog.tsx
30
+ var import_design_system = require("@ai-matrx/design-system");
31
+
32
+ // src/dialog/closeBinding.ts
33
+ var import_react = require("react");
34
+ var import_react2 = require("@ai-matrx/summon/react");
35
+ function useOverlaySurfaceClose(binding, surface) {
36
+ const instance = (0, import_react2.useOptionalOverlayInstance)();
37
+ const ports = (0, import_react2.useOptionalSummonPorts)();
38
+ const { onClose, boundToOverlay } = binding;
39
+ const open = boundToOverlay ? instance ? !instance.hidden : false : binding.open ?? true;
40
+ const onOpenChange = (0, import_react.useCallback)(
41
+ (next) => {
42
+ if (next) return;
43
+ onClose?.();
44
+ if (!boundToOverlay) return;
45
+ if (instance) instance.close();
46
+ else ports.errors.capture(new Error(`[summon] ${surface} boundToOverlay rendered outside an overlay instance`), { surface });
47
+ },
48
+ [boundToOverlay, instance, onClose, ports, surface]
49
+ );
50
+ return { open, onOpenChange };
51
+ }
52
+
53
+ // src/dialog/OverlayDialog.tsx
54
+ var import_jsx_runtime = require("react/jsx-runtime");
55
+ function OverlayDialog({ onClose, boundToOverlay, open, children, ...rest }) {
56
+ const binding = boundToOverlay ? { boundToOverlay, onClose } : { onClose, open };
57
+ const resolved = useOverlaySurfaceClose(binding, "OverlayDialog");
58
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_design_system.Dialog, { ...rest, open: resolved.open, onOpenChange: resolved.onOpenChange, children });
59
+ }
60
+ //# sourceMappingURL=dialog.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dialog.ts","../src/dialog/OverlayDialog.tsx","../src/dialog/closeBinding.ts"],"sourcesContent":["// @ai-matrx/summon/dialog — the design-system Dialog, close-bound: closes through its overlay\n// instance when rendered by SummonHost, or through onClose when page-local. Non-blocking on\n// desktop by default. See SUMMON-PACKAGE-DESIGN.md (common-docs) for its contract.\nexport { OverlayDialog, type OverlayDialogProps } from \"./dialog/OverlayDialog\";\nexport { useOverlaySurfaceClose, type OverlaySurfaceCloseBinding, type ResolvedSurfaceClose } from \"./dialog/closeBinding\";\n","\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { Dialog } from \"@ai-matrx/design-system\";\nimport { useOverlaySurfaceClose, type OverlaySurfaceCloseBinding } from \"./closeBinding\";\n\ntype DialogRootProps = Omit<ComponentProps<typeof Dialog>, \"open\" | \"onOpenChange\" | \"defaultOpen\">;\n\nexport type OverlayDialogProps = DialogRootProps & OverlaySurfaceCloseBinding;\n\n/**\n * The design-system Dialog, close-bound: windowed (non-blocking) on desktop and a blocking sheet on\n * a phone by default — the design-system's own routing; `modal` forces either. Compose its content\n * with `DialogContent`, `DialogTitle`, … from `@ai-matrx/design-system` as usual.\n */\nexport function OverlayDialog({ onClose, boundToOverlay, open, children, ...rest }: OverlayDialogProps) {\n const binding = (boundToOverlay ? { boundToOverlay, onClose } : { onClose, open }) as OverlaySurfaceCloseBinding;\n const resolved = useOverlaySurfaceClose(binding, \"OverlayDialog\");\n return (\n <Dialog {...rest} open={resolved.open} onOpenChange={resolved.onOpenChange}>\n {children}\n </Dialog>\n );\n}\n","\"use client\";\n\n/**\n * The close binding every close-bound surface (dialog, sheet, drawer, bottom sheet) shares.\n *\n * Exactly one of two shapes, enforced by the type system (passing neither is a compile error):\n * - page-local: `onClose` (and usually `open`) — the page owns whether it is shown;\n * - instance-bound: `boundToOverlay` — rendered by an SummonHost; open while the instance is\n * shown (a `keepAlive: \"hidden\"` instance closes it), and closing closes the instance.\n */\nimport { useCallback } from \"react\";\nimport { useOptionalOverlayInstance, useOptionalSummonPorts } from \"@ai-matrx/summon/react\";\n\nexport type OverlaySurfaceCloseBinding =\n | {\n /** Page-local: called when the surface asks to close (Escape, scrim, close button). */\n onClose: () => void;\n boundToOverlay?: never;\n /** Page-local open state. Default true (render it only while open, or pass it). */\n open?: boolean | undefined;\n }\n | {\n /** Close through the enclosing SummonHost instance. */\n boundToOverlay: true;\n /** Optional extra hook run before the instance closes. */\n onClose?: (() => void) | undefined;\n open?: never;\n };\n\nexport interface ResolvedSurfaceClose {\n readonly open: boolean;\n readonly onOpenChange: (open: boolean) => void;\n}\n\nexport function useOverlaySurfaceClose(binding: OverlaySurfaceCloseBinding, surface: string): ResolvedSurfaceClose {\n const instance = useOptionalOverlayInstance();\n const ports = useOptionalSummonPorts();\n const { onClose, boundToOverlay } = binding;\n const open = boundToOverlay ? (instance ? !instance.hidden : false) : (binding.open ?? true);\n const onOpenChange = useCallback(\n (next: boolean) => {\n if (next) return;\n onClose?.();\n if (!boundToOverlay) return;\n if (instance) instance.close();\n else ports.errors.capture(new Error(`[summon] ${surface} boundToOverlay rendered outside an overlay instance`), { surface });\n },\n [boundToOverlay, instance, onClose, ports, surface],\n );\n return { open, onOpenChange };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGA,2BAAuB;;;ACOvB,mBAA4B;AAC5B,IAAAA,gBAAmE;AAuB5D,SAAS,uBAAuB,SAAqC,SAAuC;AACjH,QAAM,eAAW,0CAA2B;AAC5C,QAAM,YAAQ,sCAAuB;AACrC,QAAM,EAAE,SAAS,eAAe,IAAI;AACpC,QAAM,OAAO,iBAAkB,WAAW,CAAC,SAAS,SAAS,QAAU,QAAQ,QAAQ;AACvF,QAAM,mBAAe;AAAA,IACnB,CAAC,SAAkB;AACjB,UAAI,KAAM;AACV,gBAAU;AACV,UAAI,CAAC,eAAgB;AACrB,UAAI,SAAU,UAAS,MAAM;AAAA,UACxB,OAAM,OAAO,QAAQ,IAAI,MAAM,YAAY,OAAO,sDAAsD,GAAG,EAAE,QAAQ,CAAC;AAAA,IAC7H;AAAA,IACA,CAAC,gBAAgB,UAAU,SAAS,OAAO,OAAO;AAAA,EACpD;AACA,SAAO,EAAE,MAAM,aAAa;AAC9B;;;AD/BI;AAJG,SAAS,cAAc,EAAE,SAAS,gBAAgB,MAAM,UAAU,GAAG,KAAK,GAAuB;AACtG,QAAM,UAAW,iBAAiB,EAAE,gBAAgB,QAAQ,IAAI,EAAE,SAAS,KAAK;AAChF,QAAM,WAAW,uBAAuB,SAAS,eAAe;AAChE,SACE,4CAAC,+BAAQ,GAAG,MAAM,MAAM,SAAS,MAAM,cAAc,SAAS,cAC3D,UACH;AAEJ;","names":["import_react"]}
@@ -0,0 +1,16 @@
1
+ import * as react from 'react';
2
+ import { ComponentProps } from 'react';
3
+ import { Dialog } from '@ai-matrx/design-system';
4
+ import { O as OverlaySurfaceCloseBinding } from './closeBinding-C6raC3qJ.cjs';
5
+ export { R as ResolvedSurfaceClose, u as useOverlaySurfaceClose } from './closeBinding-C6raC3qJ.cjs';
6
+
7
+ type DialogRootProps = Omit<ComponentProps<typeof Dialog>, "open" | "onOpenChange" | "defaultOpen">;
8
+ type OverlayDialogProps = DialogRootProps & OverlaySurfaceCloseBinding;
9
+ /**
10
+ * The design-system Dialog, close-bound: windowed (non-blocking) on desktop and a blocking sheet on
11
+ * a phone by default — the design-system's own routing; `modal` forces either. Compose its content
12
+ * with `DialogContent`, `DialogTitle`, … from `@ai-matrx/design-system` as usual.
13
+ */
14
+ declare function OverlayDialog({ onClose, boundToOverlay, open, children, ...rest }: OverlayDialogProps): react.JSX.Element;
15
+
16
+ export { OverlayDialog, type OverlayDialogProps, OverlaySurfaceCloseBinding };
@@ -0,0 +1,16 @@
1
+ import * as react from 'react';
2
+ import { ComponentProps } from 'react';
3
+ import { Dialog } from '@ai-matrx/design-system';
4
+ import { O as OverlaySurfaceCloseBinding } from './closeBinding-C6raC3qJ.js';
5
+ export { R as ResolvedSurfaceClose, u as useOverlaySurfaceClose } from './closeBinding-C6raC3qJ.js';
6
+
7
+ type DialogRootProps = Omit<ComponentProps<typeof Dialog>, "open" | "onOpenChange" | "defaultOpen">;
8
+ type OverlayDialogProps = DialogRootProps & OverlaySurfaceCloseBinding;
9
+ /**
10
+ * The design-system Dialog, close-bound: windowed (non-blocking) on desktop and a blocking sheet on
11
+ * a phone by default — the design-system's own routing; `modal` forces either. Compose its content
12
+ * with `DialogContent`, `DialogTitle`, … from `@ai-matrx/design-system` as usual.
13
+ */
14
+ declare function OverlayDialog({ onClose, boundToOverlay, open, children, ...rest }: OverlayDialogProps): react.JSX.Element;
15
+
16
+ export { OverlayDialog, type OverlayDialogProps, OverlaySurfaceCloseBinding };
package/dist/dialog.js ADDED
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ // src/dialog/OverlayDialog.tsx
4
+ import { Dialog } from "@ai-matrx/design-system";
5
+
6
+ // src/dialog/closeBinding.ts
7
+ import { useCallback } from "react";
8
+ import { useOptionalOverlayInstance, useOptionalSummonPorts } from "@ai-matrx/summon/react";
9
+ function useOverlaySurfaceClose(binding, surface) {
10
+ const instance = useOptionalOverlayInstance();
11
+ const ports = useOptionalSummonPorts();
12
+ const { onClose, boundToOverlay } = binding;
13
+ const open = boundToOverlay ? instance ? !instance.hidden : false : binding.open ?? true;
14
+ const onOpenChange = useCallback(
15
+ (next) => {
16
+ if (next) return;
17
+ onClose?.();
18
+ if (!boundToOverlay) return;
19
+ if (instance) instance.close();
20
+ else ports.errors.capture(new Error(`[summon] ${surface} boundToOverlay rendered outside an overlay instance`), { surface });
21
+ },
22
+ [boundToOverlay, instance, onClose, ports, surface]
23
+ );
24
+ return { open, onOpenChange };
25
+ }
26
+
27
+ // src/dialog/OverlayDialog.tsx
28
+ import { jsx } from "react/jsx-runtime";
29
+ function OverlayDialog({ onClose, boundToOverlay, open, children, ...rest }) {
30
+ const binding = boundToOverlay ? { boundToOverlay, onClose } : { onClose, open };
31
+ const resolved = useOverlaySurfaceClose(binding, "OverlayDialog");
32
+ return /* @__PURE__ */ jsx(Dialog, { ...rest, open: resolved.open, onOpenChange: resolved.onOpenChange, children });
33
+ }
34
+ export {
35
+ OverlayDialog,
36
+ useOverlaySurfaceClose
37
+ };
38
+ //# sourceMappingURL=dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dialog/OverlayDialog.tsx","../src/dialog/closeBinding.ts"],"sourcesContent":["\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { Dialog } from \"@ai-matrx/design-system\";\nimport { useOverlaySurfaceClose, type OverlaySurfaceCloseBinding } from \"./closeBinding\";\n\ntype DialogRootProps = Omit<ComponentProps<typeof Dialog>, \"open\" | \"onOpenChange\" | \"defaultOpen\">;\n\nexport type OverlayDialogProps = DialogRootProps & OverlaySurfaceCloseBinding;\n\n/**\n * The design-system Dialog, close-bound: windowed (non-blocking) on desktop and a blocking sheet on\n * a phone by default — the design-system's own routing; `modal` forces either. Compose its content\n * with `DialogContent`, `DialogTitle`, … from `@ai-matrx/design-system` as usual.\n */\nexport function OverlayDialog({ onClose, boundToOverlay, open, children, ...rest }: OverlayDialogProps) {\n const binding = (boundToOverlay ? { boundToOverlay, onClose } : { onClose, open }) as OverlaySurfaceCloseBinding;\n const resolved = useOverlaySurfaceClose(binding, \"OverlayDialog\");\n return (\n <Dialog {...rest} open={resolved.open} onOpenChange={resolved.onOpenChange}>\n {children}\n </Dialog>\n );\n}\n","\"use client\";\n\n/**\n * The close binding every close-bound surface (dialog, sheet, drawer, bottom sheet) shares.\n *\n * Exactly one of two shapes, enforced by the type system (passing neither is a compile error):\n * - page-local: `onClose` (and usually `open`) — the page owns whether it is shown;\n * - instance-bound: `boundToOverlay` — rendered by an SummonHost; open while the instance is\n * shown (a `keepAlive: \"hidden\"` instance closes it), and closing closes the instance.\n */\nimport { useCallback } from \"react\";\nimport { useOptionalOverlayInstance, useOptionalSummonPorts } from \"@ai-matrx/summon/react\";\n\nexport type OverlaySurfaceCloseBinding =\n | {\n /** Page-local: called when the surface asks to close (Escape, scrim, close button). */\n onClose: () => void;\n boundToOverlay?: never;\n /** Page-local open state. Default true (render it only while open, or pass it). */\n open?: boolean | undefined;\n }\n | {\n /** Close through the enclosing SummonHost instance. */\n boundToOverlay: true;\n /** Optional extra hook run before the instance closes. */\n onClose?: (() => void) | undefined;\n open?: never;\n };\n\nexport interface ResolvedSurfaceClose {\n readonly open: boolean;\n readonly onOpenChange: (open: boolean) => void;\n}\n\nexport function useOverlaySurfaceClose(binding: OverlaySurfaceCloseBinding, surface: string): ResolvedSurfaceClose {\n const instance = useOptionalOverlayInstance();\n const ports = useOptionalSummonPorts();\n const { onClose, boundToOverlay } = binding;\n const open = boundToOverlay ? (instance ? !instance.hidden : false) : (binding.open ?? true);\n const onOpenChange = useCallback(\n (next: boolean) => {\n if (next) return;\n onClose?.();\n if (!boundToOverlay) return;\n if (instance) instance.close();\n else ports.errors.capture(new Error(`[summon] ${surface} boundToOverlay rendered outside an overlay instance`), { surface });\n },\n [boundToOverlay, instance, onClose, ports, surface],\n );\n return { open, onOpenChange };\n}\n"],"mappings":";;;AAGA,SAAS,cAAc;;;ACOvB,SAAS,mBAAmB;AAC5B,SAAS,4BAA4B,8BAA8B;AAuB5D,SAAS,uBAAuB,SAAqC,SAAuC;AACjH,QAAM,WAAW,2BAA2B;AAC5C,QAAM,QAAQ,uBAAuB;AACrC,QAAM,EAAE,SAAS,eAAe,IAAI;AACpC,QAAM,OAAO,iBAAkB,WAAW,CAAC,SAAS,SAAS,QAAU,QAAQ,QAAQ;AACvF,QAAM,eAAe;AAAA,IACnB,CAAC,SAAkB;AACjB,UAAI,KAAM;AACV,gBAAU;AACV,UAAI,CAAC,eAAgB;AACrB,UAAI,SAAU,UAAS,MAAM;AAAA,UACxB,OAAM,OAAO,QAAQ,IAAI,MAAM,YAAY,OAAO,sDAAsD,GAAG,EAAE,QAAQ,CAAC;AAAA,IAC7H;AAAA,IACA,CAAC,gBAAgB,UAAU,SAAS,OAAO,OAAO;AAAA,EACpD;AACA,SAAO,EAAE,MAAM,aAAa;AAC9B;;;AD/BI;AAJG,SAAS,cAAc,EAAE,SAAS,gBAAgB,MAAM,UAAU,GAAG,KAAK,GAAuB;AACtG,QAAM,UAAW,iBAAiB,EAAE,gBAAgB,QAAQ,IAAI,EAAE,SAAS,KAAK;AAChF,QAAM,WAAW,uBAAuB,SAAS,eAAe;AAChE,SACE,oBAAC,UAAQ,GAAG,MAAM,MAAM,SAAS,MAAM,cAAc,SAAS,cAC3D,UACH;AAEJ;","names":[]}