@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +380 -2
- package/dist/closeBinding-C6raC3qJ.d.cts +20 -0
- package/dist/closeBinding-C6raC3qJ.d.ts +20 -0
- package/dist/dialog.cjs +60 -0
- package/dist/dialog.cjs.map +1 -0
- package/dist/dialog.d.cts +16 -0
- package/dist/dialog.d.ts +16 -0
- package/dist/dialog.js +38 -0
- package/dist/dialog.js.map +1 -0
- package/dist/errors-eJrKXq33.d.cts +30 -0
- package/dist/errors-eJrKXq33.d.ts +30 -0
- package/dist/handles.cjs +411 -0
- package/dist/handles.cjs.map +1 -0
- package/dist/handles.d.cts +166 -0
- package/dist/handles.d.ts +166 -0
- package/dist/handles.js +388 -0
- package/dist/handles.js.map +1 -0
- package/dist/index.cjs +2383 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1259 -0
- package/dist/index.d.ts +1259 -0
- package/dist/index.js +2363 -0
- package/dist/index.js.map +1 -0
- package/dist/lite.cjs +460 -0
- package/dist/lite.cjs.map +1 -0
- package/dist/lite.d.cts +56 -0
- package/dist/lite.d.ts +56 -0
- package/dist/lite.js +444 -0
- package/dist/lite.js.map +1 -0
- package/dist/next.cjs +61 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +5 -0
- package/dist/next.d.ts +5 -0
- package/dist/next.js +39 -0
- package/dist/next.js.map +1 -0
- package/dist/persistence-data.cjs +143 -0
- package/dist/persistence-data.cjs.map +1 -0
- package/dist/persistence-data.d.cts +72 -0
- package/dist/persistence-data.d.ts +72 -0
- package/dist/persistence-data.js +120 -0
- package/dist/persistence-data.js.map +1 -0
- package/dist/persistence.cjs +1073 -0
- package/dist/persistence.cjs.map +1 -0
- package/dist/persistence.d.cts +236 -0
- package/dist/persistence.d.ts +236 -0
- package/dist/persistence.js +1077 -0
- package/dist/persistence.js.map +1 -0
- package/dist/react.cjs +733 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +261 -0
- package/dist/react.d.ts +261 -0
- package/dist/react.js +718 -0
- package/dist/react.js.map +1 -0
- package/dist/sheet.cjs +71 -0
- package/dist/sheet.cjs.map +1 -0
- package/dist/sheet.d.cts +17 -0
- package/dist/sheet.d.ts +17 -0
- package/dist/sheet.js +49 -0
- package/dist/sheet.js.map +1 -0
- package/dist/styles.css +78 -0
- package/dist/testing.cjs +132 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +44 -0
- package/dist/testing.d.ts +44 -0
- package/dist/testing.js +115 -0
- package/dist/testing.js.map +1 -0
- package/dist/url.cjs +362 -0
- package/dist/url.cjs.map +1 -0
- package/dist/url.d.cts +153 -0
- package/dist/url.d.ts +153 -0
- package/dist/url.js +352 -0
- package/dist/url.js.map +1 -0
- package/dist/window-host.cjs +123 -0
- package/dist/window-host.cjs.map +1 -0
- package/dist/window-host.d.cts +72 -0
- package/dist/window-host.d.ts +72 -0
- package/dist/window-host.js +104 -0
- package/dist/window-host.js.map +1 -0
- package/dist/window.cjs +3978 -0
- package/dist/window.cjs.map +1 -0
- package/dist/window.d.cts +796 -0
- package/dist/window.d.ts +796 -0
- package/dist/window.js +4003 -0
- package/dist/window.js.map +1 -0
- package/package.json +237 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-10-16
|
|
4
|
+
|
|
5
|
+
First release (C38): the Redux-driven render system carved out of matrx-frontend.
|
|
6
|
+
|
|
7
|
+
- Core: store, slices (`overlays`, `windowManager`, `urlSync`, `overlayState`), close lifecycle, render watchdog, ports with working defaults, definition registry.
|
|
8
|
+
- 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.
|
|
9
|
+
- 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.
|
|
10
|
+
- 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).
|
|
11
|
+
- window-host (root-mountable config, tray sync, tray previews), lite window, dialog and sheet wrappers.
|
|
12
|
+
- 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).
|
|
13
|
+
- 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.
|
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
|
-
|
|
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 };
|
package/dist/dialog.cjs
ADDED
|
@@ -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 };
|
package/dist/dialog.d.ts
ADDED
|
@@ -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":[]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { StoredValue } from '@ai-matrx/summon';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Errors every persistence store throws in the same shape. Matched by `name`, never `instanceof`:
|
|
5
|
+
* each section entry may carry its own copy of this module in a built consumer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare const VERSION_CONFLICT_ERROR = "OverlayPersistenceVersionConflict";
|
|
9
|
+
declare const LOAD_TIMEOUT_ERROR = "OverlayPersistenceLoadTimeout";
|
|
10
|
+
/** A save whose `expectedVersion` no longer matches the stored version. */
|
|
11
|
+
declare class PersistenceVersionConflictError extends Error {
|
|
12
|
+
readonly key: string;
|
|
13
|
+
readonly expectedVersion: number;
|
|
14
|
+
readonly currentVersion: number;
|
|
15
|
+
constructor(key: string, expectedVersion: number, currentVersion: number);
|
|
16
|
+
}
|
|
17
|
+
declare function isVersionConflict(error: unknown): error is PersistenceVersionConflictError;
|
|
18
|
+
/**
|
|
19
|
+
* The fast tier missed and the slow tier did not answer within the read budget. `pending` still
|
|
20
|
+
* resolves the authoritative answer: a caller keeps every automatic writer gated until it does,
|
|
21
|
+
* so an unknown stored value is never overwritten.
|
|
22
|
+
*/
|
|
23
|
+
declare class PersistenceLoadTimeoutError extends Error {
|
|
24
|
+
readonly key: string;
|
|
25
|
+
readonly pending: Promise<StoredValue | null>;
|
|
26
|
+
constructor(key: string, budgetMs: number, pending: Promise<StoredValue | null>);
|
|
27
|
+
}
|
|
28
|
+
declare function isLoadTimeout(error: unknown): error is PersistenceLoadTimeoutError;
|
|
29
|
+
|
|
30
|
+
export { LOAD_TIMEOUT_ERROR as L, PersistenceVersionConflictError as P, VERSION_CONFLICT_ERROR as V, PersistenceLoadTimeoutError as a, isLoadTimeout as b, isVersionConflict as i };
|