@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/dist/url.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/url/useOverlayLink.ts
|
|
4
|
+
import { useEffect, useMemo } from "react";
|
|
5
|
+
import { registerSyncEntry, unregisterSyncEntry } from "@ai-matrx/summon";
|
|
6
|
+
import { useSummonDispatch } from "@ai-matrx/summon/react";
|
|
7
|
+
function useOverlayLink(linkKey, id, args) {
|
|
8
|
+
const dispatch = useSummonDispatch();
|
|
9
|
+
const argsKey = args ? JSON.stringify(
|
|
10
|
+
Object.keys(args).sort().map((k) => [k, args[k]])
|
|
11
|
+
) : "";
|
|
12
|
+
const stableArgs = useMemo(() => args ? { ...args } : void 0, [argsKey]);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!linkKey || !id) return void 0;
|
|
15
|
+
dispatch(registerSyncEntry(stableArgs ? { typeKey: linkKey, instanceId: id, args: stableArgs } : { typeKey: linkKey, instanceId: id }));
|
|
16
|
+
return () => {
|
|
17
|
+
dispatch(unregisterSyncEntry({ typeKey: linkKey, instanceId: id }));
|
|
18
|
+
};
|
|
19
|
+
}, [dispatch, linkKey, id, stableArgs]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/url/grammar.ts
|
|
23
|
+
var hex = (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`;
|
|
24
|
+
function encodeLinkId(id) {
|
|
25
|
+
return id.replace(/[%,:]/g, hex);
|
|
26
|
+
}
|
|
27
|
+
function encodeLinkArg(value) {
|
|
28
|
+
return value.replace(/[%,:_-]/g, hex);
|
|
29
|
+
}
|
|
30
|
+
function decodeLinkPart(value) {
|
|
31
|
+
if (!value.includes("%")) return value;
|
|
32
|
+
return value.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {
|
|
33
|
+
try {
|
|
34
|
+
return decodeURIComponent(run);
|
|
35
|
+
} catch {
|
|
36
|
+
return run;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function parseLinkToken(raw) {
|
|
41
|
+
if (!raw) return null;
|
|
42
|
+
const first = raw.indexOf(":");
|
|
43
|
+
const key = first === -1 ? raw : raw.slice(0, first);
|
|
44
|
+
if (!key) return null;
|
|
45
|
+
if (first === -1) return { raw, key, id: void 0, args: void 0 };
|
|
46
|
+
const rest = raw.slice(first + 1);
|
|
47
|
+
const second = rest.indexOf(":");
|
|
48
|
+
const idPart = second === -1 ? rest : rest.slice(0, second);
|
|
49
|
+
const argsPart = second === -1 ? void 0 : rest.slice(second + 1);
|
|
50
|
+
let args;
|
|
51
|
+
if (argsPart) {
|
|
52
|
+
args = {};
|
|
53
|
+
for (const pair of argsPart.split("_")) {
|
|
54
|
+
const at = pair.indexOf("-");
|
|
55
|
+
if (at <= 0 || at === pair.length - 1) continue;
|
|
56
|
+
args[decodeLinkPart(pair.slice(0, at))] = decodeLinkPart(pair.slice(at + 1));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { raw, key: decodeLinkPart(key), id: decodeLinkPart(idPart), args };
|
|
60
|
+
}
|
|
61
|
+
function parseLinkParam(param) {
|
|
62
|
+
if (!param) return [];
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const raw of param.split(",")) {
|
|
65
|
+
const token = parseLinkToken(raw);
|
|
66
|
+
if (token) out.push(token);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function formatLinkToken(entry) {
|
|
71
|
+
let token = `${encodeLinkId(entry.key)}:${encodeLinkId(entry.id)}`;
|
|
72
|
+
const args = entry.args;
|
|
73
|
+
if (args) {
|
|
74
|
+
const keys = Object.keys(args).sort();
|
|
75
|
+
if (keys.length > 0) {
|
|
76
|
+
token += `:${keys.map((k) => `${encodeLinkArg(k)}-${encodeLinkArg(args[k] ?? "")}`).join("_")}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return token;
|
|
80
|
+
}
|
|
81
|
+
function readPanelsParam(searchOrUrl) {
|
|
82
|
+
const q = searchOrUrl.indexOf("?");
|
|
83
|
+
const hash = searchOrUrl.indexOf("#");
|
|
84
|
+
let search;
|
|
85
|
+
if (q === -1) {
|
|
86
|
+
search = searchOrUrl.includes("=") && !searchOrUrl.includes("/") ? searchOrUrl : "";
|
|
87
|
+
} else {
|
|
88
|
+
search = searchOrUrl.slice(q + 1, hash > q ? hash : void 0);
|
|
89
|
+
}
|
|
90
|
+
return new URLSearchParams(search).get("panels");
|
|
91
|
+
}
|
|
92
|
+
function withPanelsParam(search, param) {
|
|
93
|
+
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
94
|
+
if (param) params.set("panels", param);
|
|
95
|
+
else params.delete("panels");
|
|
96
|
+
const qs = params.toString();
|
|
97
|
+
return qs ? `?${qs}` : "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/url/OverlayLinkManager.tsx
|
|
101
|
+
import { useEffect as useEffect2, useRef, useState, useSyncExternalStore } from "react";
|
|
102
|
+
import {
|
|
103
|
+
LAZY_WINDOW_MOUNT_DEADLINE_MS,
|
|
104
|
+
getOverlayMetaByLinkKey as getOverlayMetaByLinkKey2,
|
|
105
|
+
selectIsUrlHydrated,
|
|
106
|
+
selectUrlSyncEntries,
|
|
107
|
+
setHydrated
|
|
108
|
+
} from "@ai-matrx/summon";
|
|
109
|
+
import { useSummonDispatch as useSummonDispatch2, useSummonPorts, useSummonSelector } from "@ai-matrx/summon/react";
|
|
110
|
+
|
|
111
|
+
// src/url/door.ts
|
|
112
|
+
import {
|
|
113
|
+
DEFAULT_INSTANCE_ID,
|
|
114
|
+
getOverlayMeta,
|
|
115
|
+
getOverlayMetaByLinkKey,
|
|
116
|
+
isRegisteredOverlay,
|
|
117
|
+
overlaysRawActions
|
|
118
|
+
} from "@ai-matrx/summon";
|
|
119
|
+
function openRegisteredOverlay(dispatch, overlayId, instanceId = DEFAULT_INSTANCE_ID, data) {
|
|
120
|
+
const defaults = getOverlayMeta(overlayId)?.defaults;
|
|
121
|
+
dispatch(
|
|
122
|
+
overlaysRawActions.openOverlay({
|
|
123
|
+
overlayId,
|
|
124
|
+
instanceId,
|
|
125
|
+
...data === void 0 ? {} : { data },
|
|
126
|
+
...isRecord(defaults) ? { defaults } : {}
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
function isRecord(value) {
|
|
131
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
function canonicalLinkKey(key) {
|
|
134
|
+
return getOverlayMetaByLinkKey(key)?.link?.key ?? key;
|
|
135
|
+
}
|
|
136
|
+
function canonicalizeToken(raw) {
|
|
137
|
+
const at = raw.indexOf(":");
|
|
138
|
+
const key = at === -1 ? raw : raw.slice(0, at);
|
|
139
|
+
const canonical = canonicalLinkKey(key);
|
|
140
|
+
if (canonical === key) return raw;
|
|
141
|
+
return at === -1 ? canonical : `${canonical}${raw.slice(at)}`;
|
|
142
|
+
}
|
|
143
|
+
function hydrateLinkToken(token, dispatch, onError) {
|
|
144
|
+
const meta = getOverlayMetaByLinkKey(token.key);
|
|
145
|
+
const link = meta?.link;
|
|
146
|
+
if (!meta || !link) return "unknown";
|
|
147
|
+
const ownerId = meta.id;
|
|
148
|
+
const settle = (result) => result === "refused" ? "refused" : "opened";
|
|
149
|
+
const fail = (error) => {
|
|
150
|
+
onError?.(error, token);
|
|
151
|
+
return "failed";
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
const result = link.hydrate({
|
|
155
|
+
id: token.id ?? "",
|
|
156
|
+
args: token.args ?? {},
|
|
157
|
+
open: (options) => openRegisteredOverlay(dispatch, ownerId, options?.instanceId ?? DEFAULT_INSTANCE_ID, options?.data),
|
|
158
|
+
// The host's store dispatch IS the host dispatch at runtime; its thunk-aware type is the
|
|
159
|
+
// host's augmentation (`OverlayHostTypes`), which the package cannot see.
|
|
160
|
+
dispatch
|
|
161
|
+
});
|
|
162
|
+
if (result && typeof result.then === "function") {
|
|
163
|
+
return Promise.resolve(result).then(settle, fail);
|
|
164
|
+
}
|
|
165
|
+
return settle(result);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return fail(error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function openFromLink(searchOrUrl, options) {
|
|
171
|
+
const tokens = parseLinkParam(readPanelsParam(searchOrUrl));
|
|
172
|
+
const result = { opened: [], refused: [], unknown: [], failed: [] };
|
|
173
|
+
const unknown = tokens.filter((token) => !getOverlayMetaByLinkKey(token.key)?.link);
|
|
174
|
+
if (options.requireAll && unknown.length > 0) {
|
|
175
|
+
result.unknown.push(...unknown.map((token) => token.raw));
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
const outcomes = tokens.map((token) => hydrateLinkToken(token, options.dispatch, options.onError));
|
|
179
|
+
const settled = await Promise.all(outcomes.map((outcome) => Promise.resolve(outcome)));
|
|
180
|
+
settled.forEach((outcome, index) => {
|
|
181
|
+
const raw = tokens[index]?.raw ?? "";
|
|
182
|
+
result[outcome === "opened" ? "opened" : outcome].push(raw);
|
|
183
|
+
});
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
function readOpenPanelRequest(raw) {
|
|
187
|
+
if (!isRecord(raw)) return "payload must be an object";
|
|
188
|
+
const { panelId, instanceId, data } = raw;
|
|
189
|
+
if (typeof panelId !== "string" || panelId.length === 0) return "panelId must be a non-empty string";
|
|
190
|
+
if (instanceId !== void 0 && instanceId !== null && (typeof instanceId !== "string" || instanceId.length === 0)) {
|
|
191
|
+
return "instanceId must be a non-empty string when given";
|
|
192
|
+
}
|
|
193
|
+
if (data !== void 0 && data !== null && !isRecord(data)) return "data must be an object when given";
|
|
194
|
+
return {
|
|
195
|
+
panelId,
|
|
196
|
+
...typeof instanceId === "string" ? { instanceId } : {},
|
|
197
|
+
...isRecord(data) ? { data } : {}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function openPanelRequest(rawPayload, options) {
|
|
201
|
+
const request = readOpenPanelRequest(rawPayload);
|
|
202
|
+
if (typeof request === "string") return { ok: false, error: "invalid_payload", details: request };
|
|
203
|
+
if (!isRegisteredOverlay(request.panelId)) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
error: "unknown_panel",
|
|
207
|
+
details: { panelId: request.panelId, hint: "panelId must be the id of a registered overlay definition." }
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
openRegisteredOverlay(options.dispatch, request.panelId, request.instanceId ?? DEFAULT_INSTANCE_ID, request.data);
|
|
211
|
+
return { ok: true, opened: true, panelId: request.panelId, instanceId: request.instanceId ?? null };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/url/OverlayLinkManager.tsx
|
|
215
|
+
function serializeLinkEntries(entries, managedKeys) {
|
|
216
|
+
const managed = managedKeys ? new Set(managedKeys) : null;
|
|
217
|
+
return Object.values(entries).filter((entry) => !managed || managed.has(entry.typeKey)).map((entry) => formatLinkToken({ key: entry.typeKey, id: entry.instanceId, args: entry.args })).join(",");
|
|
218
|
+
}
|
|
219
|
+
function mergeManagedLinkParam(currentParam, nextManaged, managedKeys) {
|
|
220
|
+
if (!managedKeys) return nextManaged;
|
|
221
|
+
const managed = new Set(managedKeys);
|
|
222
|
+
const unmanaged = currentParam.split(",").filter(Boolean).filter((token) => !managed.has(token.split(":")[0] ?? ""));
|
|
223
|
+
return [...unmanaged, ...nextManaged.split(",").filter(Boolean)].join(",");
|
|
224
|
+
}
|
|
225
|
+
function withUnresolvedTokens(managedParam, unresolvedTokens) {
|
|
226
|
+
if (unresolvedTokens.length === 0) return managedParam;
|
|
227
|
+
const managed = managedParam.split(",").filter(Boolean);
|
|
228
|
+
const represented = new Set(managed.map((token) => canonicalLinkKey(token.split(":")[0] ?? "")));
|
|
229
|
+
const kept = unresolvedTokens.filter((token) => Boolean(token) && !represented.has(canonicalLinkKey(token.split(":")[0] ?? "")));
|
|
230
|
+
return [...managed, ...kept].join(",");
|
|
231
|
+
}
|
|
232
|
+
function unopenedWindowNotice(keys) {
|
|
233
|
+
return `This link names a window this build could not open: ${keys.join(", ")}. The link is unchanged in your address bar \u2014 reload to try again, or report it if it keeps failing.`;
|
|
234
|
+
}
|
|
235
|
+
var noopSubscribe = () => () => {
|
|
236
|
+
};
|
|
237
|
+
function OverlayLinkManager({ managedKeys, noticeDeadlineMs }) {
|
|
238
|
+
const ports = useSummonPorts();
|
|
239
|
+
const dispatch = useSummonDispatch2();
|
|
240
|
+
const entries = useSummonSelector(selectUrlSyncEntries);
|
|
241
|
+
const isHydrated = useSummonSelector(selectIsUrlHydrated);
|
|
242
|
+
const router = ports.router;
|
|
243
|
+
const search = useSyncExternalStore(router.subscribe ?? noopSubscribe, router.search, () => "");
|
|
244
|
+
const initialLoadDone = useRef(false);
|
|
245
|
+
const unresolved = useRef(/* @__PURE__ */ new Map());
|
|
246
|
+
const announced = useRef(/* @__PURE__ */ new Set());
|
|
247
|
+
const [noticeDue, setNoticeDue] = useState(false);
|
|
248
|
+
const [asyncTick, setAsyncTick] = useState(0);
|
|
249
|
+
const deadline = noticeDeadlineMs ?? LAZY_WINDOW_MOUNT_DEADLINE_MS;
|
|
250
|
+
const announce = (keys, reason) => {
|
|
251
|
+
const fresh = keys.filter((key) => !announced.current.has(key));
|
|
252
|
+
if (fresh.length === 0) return;
|
|
253
|
+
for (const key of fresh) announced.current.add(key);
|
|
254
|
+
const joined = fresh.join(", ");
|
|
255
|
+
const why = reason === "no-hydrator" ? `no registered definition owns ${fresh.length > 1 ? "these keys" : "this key"}` : reason === "hydrator-threw" ? "its hydrator threw" : `hydrated, but no window registered its link within ${deadline}ms`;
|
|
256
|
+
const message = `[overlays/url] ?panels= token(s) [${joined}]: ${why}. The token is KEPT in the address. Give the overlay definition a matching \`link\` (key or alias), or stop publishing the link.`;
|
|
257
|
+
ports.errors.capture(new Error(message), {
|
|
258
|
+
source: "url-panel-unopened",
|
|
259
|
+
relation: `?panels=${joined}`,
|
|
260
|
+
code: reason,
|
|
261
|
+
typeKeys: fresh,
|
|
262
|
+
deadlineMs: deadline,
|
|
263
|
+
userMessage: unopenedWindowNotice(fresh)
|
|
264
|
+
});
|
|
265
|
+
ports.notify.error(unopenedWindowNotice(fresh), { duration: 12e3 });
|
|
266
|
+
};
|
|
267
|
+
const announceRef = useRef(announce);
|
|
268
|
+
announceRef.current = announce;
|
|
269
|
+
useEffect2(() => {
|
|
270
|
+
if (initialLoadDone.current) return;
|
|
271
|
+
initialLoadDone.current = true;
|
|
272
|
+
const managed = managedKeys ? new Set(managedKeys) : null;
|
|
273
|
+
const tokens = parseLinkParam(readPanelsParam(router.search()));
|
|
274
|
+
const noHydrator = [];
|
|
275
|
+
for (const token of tokens) {
|
|
276
|
+
if (managed && !managed.has(token.key)) continue;
|
|
277
|
+
const canonicalKey = canonicalLinkKey(token.key);
|
|
278
|
+
const pendingKey = `${canonicalKey}\0${token.id ?? ""}`;
|
|
279
|
+
const pending = { token: canonicalizeToken(token.raw), tokenKey: token.key, canonicalKey };
|
|
280
|
+
if (!getOverlayMetaByLinkKey2(token.key)?.link) {
|
|
281
|
+
unresolved.current.set(pendingKey, { ...pending, token: token.raw });
|
|
282
|
+
noHydrator.push(token.key);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
unresolved.current.set(pendingKey, pending);
|
|
286
|
+
const outcome = hydrateLinkToken(token, dispatch, (error) => {
|
|
287
|
+
ports.errors.capture(error, { source: "url-panel-hydrator", token: token.raw });
|
|
288
|
+
});
|
|
289
|
+
const apply = (result) => {
|
|
290
|
+
if (result === "refused" && unresolved.current.get(pendingKey) === pending) {
|
|
291
|
+
unresolved.current.delete(pendingKey);
|
|
292
|
+
setAsyncTick((n) => n + 1);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
if (typeof outcome === "string") apply(outcome);
|
|
296
|
+
else void outcome.then(apply);
|
|
297
|
+
}
|
|
298
|
+
if (noHydrator.length > 0) announceRef.current(noHydrator, "no-hydrator");
|
|
299
|
+
dispatch(setHydrated());
|
|
300
|
+
}, [dispatch, managedKeys, router, ports]);
|
|
301
|
+
useEffect2(() => {
|
|
302
|
+
if (!isHydrated || unresolved.current.size === 0) return void 0;
|
|
303
|
+
const timer = setTimeout(() => setNoticeDue(true), deadline);
|
|
304
|
+
return () => clearTimeout(timer);
|
|
305
|
+
}, [isHydrated, deadline]);
|
|
306
|
+
useEffect2(() => {
|
|
307
|
+
if (!isHydrated) return;
|
|
308
|
+
const observed = new Set(Object.values(entries).map((entry) => canonicalLinkKey(entry.typeKey)));
|
|
309
|
+
for (const [key, pending] of [...unresolved.current.entries()]) {
|
|
310
|
+
if (observed.has(pending.canonicalKey)) unresolved.current.delete(key);
|
|
311
|
+
}
|
|
312
|
+
const stillPending = [...unresolved.current.values()];
|
|
313
|
+
if (noticeDue && stillPending.length > 0) {
|
|
314
|
+
announceRef.current(
|
|
315
|
+
stillPending.map((pending) => pending.tokenKey),
|
|
316
|
+
"never-registered"
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const liveSearch = router.search();
|
|
320
|
+
const currentParam = readPanelsParam(liveSearch) ?? "";
|
|
321
|
+
const nextManaged = withUnresolvedTokens(
|
|
322
|
+
serializeLinkEntries(entries, managedKeys),
|
|
323
|
+
stillPending.map((pending) => pending.token)
|
|
324
|
+
);
|
|
325
|
+
const nextParam = mergeManagedLinkParam(currentParam, nextManaged, managedKeys);
|
|
326
|
+
if (currentParam !== nextParam) router.replaceSearch(withPanelsParam(liveSearch, nextParam));
|
|
327
|
+
}, [entries, isHydrated, managedKeys, router, search, noticeDue, asyncTick]);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
export {
|
|
331
|
+
OverlayLinkManager,
|
|
332
|
+
canonicalLinkKey,
|
|
333
|
+
canonicalizeToken,
|
|
334
|
+
decodeLinkPart,
|
|
335
|
+
encodeLinkArg,
|
|
336
|
+
encodeLinkId,
|
|
337
|
+
formatLinkToken,
|
|
338
|
+
hydrateLinkToken,
|
|
339
|
+
mergeManagedLinkParam,
|
|
340
|
+
openFromLink,
|
|
341
|
+
openPanelRequest,
|
|
342
|
+
openRegisteredOverlay,
|
|
343
|
+
parseLinkParam,
|
|
344
|
+
parseLinkToken,
|
|
345
|
+
readPanelsParam,
|
|
346
|
+
serializeLinkEntries,
|
|
347
|
+
unopenedWindowNotice,
|
|
348
|
+
useOverlayLink,
|
|
349
|
+
withPanelsParam,
|
|
350
|
+
withUnresolvedTokens
|
|
351
|
+
};
|
|
352
|
+
//# sourceMappingURL=url.js.map
|
package/dist/url.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/url/useOverlayLink.ts","../src/url/grammar.ts","../src/url/OverlayLinkManager.tsx","../src/url/door.ts"],"sourcesContent":["import { useEffect, useMemo } from \"react\";\nimport { registerSyncEntry, unregisterSyncEntry } from \"@ai-matrx/summon\";\nimport { useSummonDispatch } from \"@ai-matrx/summon/react\";\n\n/**\n * Publish an open instance's address into `?panels=` while it is mounted. `linkKey` undefined →\n * nothing is published (a window with no link); so is an undefined or empty `id`. The token is `linkKey:id[:args]`; args are\n * compared by value, so a new object with equal contents never re-registers.\n */\nexport function useOverlayLink(linkKey: string | undefined, id: string | undefined, args?: Readonly<Record<string, string>>): void {\n const dispatch = useSummonDispatch();\n const argsKey = args\n ? JSON.stringify(\n Object.keys(args)\n .sort()\n .map((k) => [k, args[k]]),\n )\n : \"\";\n // Rebuilt only when the VALUES change (argsKey), never on a fresh object with equal contents.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const stableArgs = useMemo(() => (args ? { ...args } : undefined), [argsKey]);\n\n useEffect(() => {\n if (!linkKey || !id) return undefined;\n dispatch(registerSyncEntry(stableArgs ? { typeKey: linkKey, instanceId: id, args: stableArgs } : { typeKey: linkKey, instanceId: id }));\n return () => {\n dispatch(unregisterSyncEntry({ typeKey: linkKey, instanceId: id }));\n };\n }, [dispatch, linkKey, id, stableArgs]);\n}\n","/**\n * The `?panels=` link grammar.\n *\n * panels = token (\",\" token)*\n * token = key [\":\" id [\":\" args]]\n * args = k \"-\" v (\"_\" k \"-\" v)*\n *\n * GRAMMAR v2 (this file): on WRITE, ids are percent-encoded for `% , :` and arg keys and values\n * for `% , : _ -`, so any value — a uuid, a date, a path, a list — survives the round trip.\n * Keys and ids keep `-` and `_` unencoded: they are not separators inside an id, and leaving them means a\n * uuid id writes byte-identically to v1. On READ, v1 tokens (unencoded) parse unchanged: an arg\n * pair splits on its FIRST hyphen, and decoding only touches well-formed `%XX` escapes.\n *\n * Framework-free; this is the one grammar every door (address bar, notifications, extension)\n * shares.\n */\n\nexport interface LinkToken {\n /** The token exactly as it appeared (the verbatim form written back while unresolved). */\n readonly raw: string;\n readonly key: string;\n /** `undefined` for a bare-key token (`keyword_research`). */\n readonly id: string | undefined;\n /** `undefined` when the token carries no args segment. */\n readonly args: Readonly<Record<string, string>> | undefined;\n}\n\nexport interface LinkEntry {\n readonly key: string;\n readonly id: string;\n readonly args?: Readonly<Record<string, string>> | undefined;\n}\n\nconst hex = (ch: string): string => `%${ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, \"0\")}`;\n\n/** Encode a token id (`% , :`). */\nexport function encodeLinkId(id: string): string {\n return id.replace(/[%,:]/g, hex);\n}\n\n/** Encode an arg key or value (`% , : _ -`). */\nexport function encodeLinkArg(value: string): string {\n return value.replace(/[%,:_-]/g, hex);\n}\n\n/** Decode well-formed `%XX` escapes; anything else (a v1 literal `%`) is kept as written. */\nexport function decodeLinkPart(value: string): string {\n if (!value.includes(\"%\")) return value;\n return value.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {\n try {\n return decodeURIComponent(run);\n } catch {\n return run;\n }\n });\n}\n\n/** Parse one token (v1 or v2). Returns `null` for an empty token. */\nexport function parseLinkToken(raw: string): LinkToken | null {\n if (!raw) return null;\n const first = raw.indexOf(\":\");\n const key = first === -1 ? raw : raw.slice(0, first);\n if (!key) return null;\n if (first === -1) return { raw, key, id: undefined, args: undefined };\n const rest = raw.slice(first + 1);\n const second = rest.indexOf(\":\");\n const idPart = second === -1 ? rest : rest.slice(0, second);\n const argsPart = second === -1 ? undefined : rest.slice(second + 1);\n let args: Record<string, string> | undefined;\n if (argsPart) {\n args = {};\n for (const pair of argsPart.split(\"_\")) {\n // Split on the FIRST hyphen only: a v1 value may hold hyphens (uuid, date).\n const at = pair.indexOf(\"-\");\n if (at <= 0 || at === pair.length - 1) continue;\n args[decodeLinkPart(pair.slice(0, at))] = decodeLinkPart(pair.slice(at + 1));\n }\n }\n return { raw, key: decodeLinkPart(key), id: decodeLinkPart(idPart), args };\n}\n\n/** Parse a whole `panels` value. Empty segments are skipped. */\nexport function parseLinkParam(param: string | null | undefined): LinkToken[] {\n if (!param) return [];\n const out: LinkToken[] = [];\n for (const raw of param.split(\",\")) {\n const token = parseLinkToken(raw);\n if (token) out.push(token);\n }\n return out;\n}\n\n/** Write one entry as a v2 token. Args are written in key order so equal entries write equal tokens. */\nexport function formatLinkToken(entry: LinkEntry): string {\n let token = `${encodeLinkId(entry.key)}:${encodeLinkId(entry.id)}`;\n const args = entry.args;\n if (args) {\n const keys = Object.keys(args).sort();\n if (keys.length > 0) {\n token += `:${keys.map((k) => `${encodeLinkArg(k)}-${encodeLinkArg(args[k] ?? \"\")}`).join(\"_\")}`;\n }\n }\n return token;\n}\n\n/** The `panels` value from a search string or a full URL (`null` when absent). */\nexport function readPanelsParam(searchOrUrl: string): string | null {\n const q = searchOrUrl.indexOf(\"?\");\n const hash = searchOrUrl.indexOf(\"#\");\n let search: string;\n if (q === -1) {\n // A bare query string without \"?\" (`panels=…`) or a URL with no query.\n search = searchOrUrl.includes(\"=\") && !searchOrUrl.includes(\"/\") ? searchOrUrl : \"\";\n } else {\n search = searchOrUrl.slice(q + 1, hash > q ? hash : undefined);\n }\n return new URLSearchParams(search).get(\"panels\");\n}\n\n/** `search` with `panels` set to `param` (removed when empty). Returns \"\" or \"?…\". */\nexport function withPanelsParam(search: string, param: string): string {\n const params = new URLSearchParams(search.startsWith(\"?\") ? search.slice(1) : search);\n if (param) params.set(\"panels\", param);\n else params.delete(\"panels\");\n const qs = params.toString();\n return qs ? `?${qs}` : \"\";\n}\n","/**\n * OverlayLinkManager — the address bar's `?panels=` channel (successor of matrx-frontend's\n * UrlPanelManager). Mount ONCE inside <SummonProvider>. Driven entirely by `ports.router`.\n *\n * 1. On mount: read `?panels=`, hydrate each token through the one door (`hydrateLinkToken`).\n * 2. After that: write back the union of registered link entries (`useOverlayLink`) plus every\n * pasted token that has not been settled yet, via `router.replaceSearch` — never navigation.\n *\n * THE ADDRESS IS NEVER DESTROYED BY A CLOCK (matrx-frontend V-28 NEW-5). A token whose window has\n * not registered yet is kept verbatim in the address however long its lazy chunk takes. The\n * deadline (12 s prod / 45 s dev) decides one thing only: when to stop waiting silently and tell\n * the person, through `ports.notify` and `ports.errors`.\n *\n * A token is SETTLED when ANY registered entry carries its canonical key — the window a hydrator\n * opens may publish a different id (a vault link names an item, the vault window publishes its\n * singleton id) and may even be a different overlay than the one owning the key (key `agent`\n * opens display-mode shells). An alias resolves to its definition's own `link.key`.\n */\nimport { useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n LAZY_WINDOW_MOUNT_DEADLINE_MS,\n getOverlayMetaByLinkKey,\n selectIsUrlHydrated,\n selectUrlSyncEntries,\n setHydrated,\n type UrlSyncEntry,\n} from \"@ai-matrx/summon\";\nimport { useSummonDispatch, useSummonPorts, useSummonSelector } from \"@ai-matrx/summon/react\";\nimport { canonicalLinkKey, canonicalizeToken, hydrateLinkToken } from \"./door\";\nimport { formatLinkToken, parseLinkParam, readPanelsParam, withPanelsParam } from \"./grammar\";\n\nexport interface OverlayLinkManagerProps {\n /**\n * Restrict hydration and write-back to these link keys (a route-scoped manager). Tokens of\n * other keys are preserved verbatim and never hydrated. Omit for the app-wide manager.\n */\n readonly managedKeys?: readonly string[];\n /** Override the notice deadline (ms). Default: 12 s in production, 45 s in development. */\n readonly noticeDeadlineMs?: number;\n}\n\n/** Serialize registered entries as v2 tokens (only managed keys when given). */\nexport function serializeLinkEntries(entries: Readonly<Record<string, UrlSyncEntry>>, managedKeys?: readonly string[]): string {\n const managed = managedKeys ? new Set(managedKeys) : null;\n return Object.values(entries)\n .filter((entry) => !managed || managed.has(entry.typeKey))\n .map((entry) => formatLinkToken({ key: entry.typeKey, id: entry.instanceId, args: entry.args }))\n .join(\",\");\n}\n\n/** Keep the current param's unmanaged tokens; replace the managed ones with `nextManaged`. */\nexport function mergeManagedLinkParam(currentParam: string, nextManaged: string, managedKeys?: readonly string[]): string {\n if (!managedKeys) return nextManaged;\n const managed = new Set(managedKeys);\n const unmanaged = currentParam\n .split(\",\")\n .filter(Boolean)\n .filter((token) => !managed.has(token.split(\":\")[0] ?? \"\"));\n return [...unmanaged, ...nextManaged.split(\",\").filter(Boolean)].join(\",\");\n}\n\n/**\n * Re-attach pasted tokens no window has settled. A token is skipped when its CANONICAL key is\n * already represented by a live entry, so a late registration replaces the placeholder.\n */\nexport function withUnresolvedTokens(managedParam: string, unresolvedTokens: readonly string[]): string {\n if (unresolvedTokens.length === 0) return managedParam;\n const managed = managedParam.split(\",\").filter(Boolean);\n const represented = new Set(managed.map((token) => canonicalLinkKey(token.split(\":\")[0] ?? \"\")));\n const kept = unresolvedTokens.filter((token) => Boolean(token) && !represented.has(canonicalLinkKey(token.split(\":\")[0] ?? \"\")));\n return [...managed, ...kept].join(\",\");\n}\n\n/** The honest sentence for a link naming a window that did not open. */\nexport function unopenedWindowNotice(keys: readonly string[]): string {\n return (\n `This link names a window this build could not open: ${keys.join(\", \")}. ` +\n `The link is unchanged in your address bar — reload to try again, or report it if it keeps failing.`\n );\n}\n\ninterface PendingToken {\n /** What is written back while unsettled (canonical key, pasted id and args). */\n readonly token: string;\n /** The key the person pasted (named in the notice). */\n readonly tokenKey: string;\n readonly canonicalKey: string;\n}\n\nconst noopSubscribe = () => () => {};\n\nexport function OverlayLinkManager({ managedKeys, noticeDeadlineMs }: OverlayLinkManagerProps): null {\n const ports = useSummonPorts();\n const dispatch = useSummonDispatch();\n const entries = useSummonSelector(selectUrlSyncEntries);\n const isHydrated = useSummonSelector(selectIsUrlHydrated);\n const router = ports.router;\n // Re-run the write-back after ANY address change the router reports.\n const search = useSyncExternalStore(router.subscribe ?? noopSubscribe, router.search, () => \"\");\n\n const initialLoadDone = useRef(false);\n // Keyed by canonical key + id so two tokens that canonicalise together never overwrite each\n // other (V-30 NEW-5); the settle sweep still runs on canonical key.\n const unresolved = useRef(new Map<string, PendingToken>());\n const announced = useRef(new Set<string>());\n const [noticeDue, setNoticeDue] = useState(false);\n const [asyncTick, setAsyncTick] = useState(0);\n const deadline = noticeDeadlineMs ?? LAZY_WINDOW_MOUNT_DEADLINE_MS;\n\n const announce = (keys: readonly string[], reason: \"no-hydrator\" | \"never-registered\" | \"hydrator-threw\"): void => {\n const fresh = keys.filter((key) => !announced.current.has(key));\n if (fresh.length === 0) return;\n for (const key of fresh) announced.current.add(key);\n const joined = fresh.join(\", \");\n const why =\n reason === \"no-hydrator\"\n ? `no registered definition owns ${fresh.length > 1 ? \"these keys\" : \"this key\"}`\n : reason === \"hydrator-threw\"\n ? \"its hydrator threw\"\n : `hydrated, but no window registered its link within ${deadline}ms`;\n const message =\n `[overlays/url] ?panels= token(s) [${joined}]: ${why}. The token is KEPT in the address. ` +\n `Give the overlay definition a matching \\`link\\` (key or alias), or stop publishing the link.`;\n ports.errors.capture(new Error(message), {\n source: \"url-panel-unopened\",\n relation: `?panels=${joined}`,\n code: reason,\n typeKeys: fresh,\n deadlineMs: deadline,\n userMessage: unopenedWindowNotice(fresh),\n });\n ports.notify.error(unopenedWindowNotice(fresh), { duration: 12_000 });\n };\n const announceRef = useRef(announce);\n announceRef.current = announce;\n\n // 1. HYDRATION (address → store), once.\n useEffect(() => {\n if (initialLoadDone.current) return;\n initialLoadDone.current = true;\n const managed = managedKeys ? new Set(managedKeys) : null;\n const tokens = parseLinkParam(readPanelsParam(router.search()));\n const noHydrator: string[] = [];\n for (const token of tokens) {\n if (managed && !managed.has(token.key)) continue; // preserved verbatim by the write-back\n const canonicalKey = canonicalLinkKey(token.key);\n const pendingKey = `${canonicalKey}\\u0000${token.id ?? \"\"}`;\n const pending: PendingToken = { token: canonicalizeToken(token.raw), tokenKey: token.key, canonicalKey };\n if (!getOverlayMetaByLinkKey(token.key)?.link) {\n unresolved.current.set(pendingKey, { ...pending, token: token.raw });\n noHydrator.push(token.key);\n continue;\n }\n unresolved.current.set(pendingKey, pending);\n const outcome = hydrateLinkToken(token, dispatch, (error) => {\n ports.errors.capture(error, { source: \"url-panel-hydrator\", token: token.raw });\n });\n const apply = (result: string): void => {\n // A refused link is dropped from the address; it is not \"still to come\".\n if (result === \"refused\" && unresolved.current.get(pendingKey) === pending) {\n unresolved.current.delete(pendingKey);\n setAsyncTick((n) => n + 1);\n }\n };\n if (typeof outcome === \"string\") apply(outcome);\n else void outcome.then(apply);\n }\n if (noHydrator.length > 0) announceRef.current(noHydrator, \"no-hydrator\");\n dispatch(setHydrated());\n }, [dispatch, managedKeys, router, ports]);\n\n // The notice deadline: never gates a write; only turns a silent wait into a visible notice.\n useEffect(() => {\n if (!isHydrated || unresolved.current.size === 0) return undefined;\n const timer = setTimeout(() => setNoticeDue(true), deadline);\n return () => clearTimeout(timer);\n }, [isHydrated, deadline]);\n\n // 2. WRITE-BACK (store → address).\n useEffect(() => {\n if (!isHydrated) return;\n const observed = new Set(Object.values(entries).map((entry) => canonicalLinkKey(entry.typeKey)));\n for (const [key, pending] of [...unresolved.current.entries()]) {\n if (observed.has(pending.canonicalKey)) unresolved.current.delete(key);\n }\n const stillPending = [...unresolved.current.values()];\n if (noticeDue && stillPending.length > 0) {\n announceRef.current(\n stillPending.map((pending) => pending.tokenKey),\n \"never-registered\",\n );\n }\n // The LIVE address, never a snapshot: the page may have written `?view=` a moment ago.\n const liveSearch = router.search();\n const currentParam = readPanelsParam(liveSearch) ?? \"\";\n const nextManaged = withUnresolvedTokens(\n serializeLinkEntries(entries, managedKeys),\n stillPending.map((pending) => pending.token),\n );\n const nextParam = mergeManagedLinkParam(currentParam, nextManaged, managedKeys);\n if (currentParam !== nextParam) router.replaceSearch(withPanelsParam(liveSearch, nextParam));\n }, [entries, isHydrated, managedKeys, router, search, noticeDue, asyncTick]);\n\n return null;\n}\n","/**\n * The ONE door every link source goes through: the address-bar manager on load, a notification\n * that opens its link in place, the extension bridge. A token is resolved through the registry —\n * `getOverlayMetaByLinkKey(key).link.hydrate({ id, args, open, dispatch })` — never a side table.\n */\nimport type { Dispatch, UnknownAction } from \"@reduxjs/toolkit\";\nimport {\n DEFAULT_INSTANCE_ID,\n getOverlayMeta,\n getOverlayMetaByLinkKey,\n isRegisteredOverlay,\n overlaysRawActions,\n type AnyOverlayMeta,\n type OverlayHostDispatch,\n} from \"@ai-matrx/summon\";\nimport { parseLinkParam, readPanelsParam, type LinkToken } from \"./grammar\";\n\n/** Any Redux dispatch that takes plain actions (a host's thunk-aware dispatch qualifies). */\nexport type LinkDispatch = Dispatch<UnknownAction>;\n\n/**\n * Open an overlay by its registry id from inside the package. Uses the raw action so internal\n * code passes plain strings (a host's augmented `OverlayId` union never needs a cast here); the\n * definition's `defaults` are merged underneath exactly as the typed creator does.\n */\nexport function openRegisteredOverlay(\n dispatch: LinkDispatch,\n overlayId: string,\n instanceId: string = DEFAULT_INSTANCE_ID,\n data?: unknown,\n): void {\n const defaults = getOverlayMeta(overlayId)?.defaults;\n dispatch(\n overlaysRawActions.openOverlay({\n overlayId,\n instanceId,\n ...(data === undefined ? {} : { data }),\n ...(isRecord(defaults) ? { defaults } : {}),\n }),\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** The key a token is judged by: the owning definition's own `link.key` (aliases resolve to it). */\nexport function canonicalLinkKey(key: string): string {\n return getOverlayMetaByLinkKey(key)?.link?.key ?? key;\n}\n\n/** The token with its key replaced by the canonical key; id and args carry across untouched. */\nexport function canonicalizeToken(raw: string): string {\n const at = raw.indexOf(\":\");\n const key = at === -1 ? raw : raw.slice(0, at);\n const canonical = canonicalLinkKey(key);\n if (canonical === key) return raw;\n return at === -1 ? canonical : `${canonical}${raw.slice(at)}`;\n}\n\nexport type TokenOutcome = \"opened\" | \"refused\" | \"unknown\" | \"failed\";\n\n/**\n * Run one token's hydrator. Synchronous hydrators dispatch before this returns; an async one\n * yields a promise. A throw (sync or async) is `\"failed\"` and is reported to `onError`.\n */\nexport function hydrateLinkToken(\n token: LinkToken,\n dispatch: LinkDispatch,\n onError?: (error: unknown, token: LinkToken) => void,\n): TokenOutcome | Promise<TokenOutcome> {\n const meta: AnyOverlayMeta | undefined = getOverlayMetaByLinkKey(token.key);\n const link = meta?.link;\n if (!meta || !link) return \"unknown\";\n const ownerId = meta.id;\n const settle = (result: void | \"refused\"): TokenOutcome => (result === \"refused\" ? \"refused\" : \"opened\");\n const fail = (error: unknown): TokenOutcome => {\n onError?.(error, token);\n return \"failed\";\n };\n try {\n const result = link.hydrate({\n id: token.id ?? \"\",\n args: token.args ?? {},\n open: (options) => openRegisteredOverlay(dispatch, ownerId, options?.instanceId ?? DEFAULT_INSTANCE_ID, options?.data),\n // The host's store dispatch IS the host dispatch at runtime; its thunk-aware type is the\n // host's augmentation (`OverlayHostTypes`), which the package cannot see.\n dispatch: dispatch as unknown as OverlayHostDispatch,\n });\n if (result && typeof (result as PromiseLike<unknown>).then === \"function\") {\n return Promise.resolve(result).then(settle, fail);\n }\n return settle(result as void | \"refused\");\n } catch (error) {\n return fail(error);\n }\n}\n\nexport interface OpenFromLinkResult {\n /** Tokens (verbatim) whose hydrator ran and did not refuse. */\n readonly opened: string[];\n /** Tokens whose hydrator declined the link on purpose. */\n readonly refused: string[];\n /** Tokens whose key no registered definition owns. */\n readonly unknown: string[];\n /** Tokens whose hydrator threw. */\n readonly failed: string[];\n}\n\nexport interface OpenFromLinkOptions {\n readonly dispatch: LinkDispatch;\n /**\n * All or nothing: when any token's key is unknown, open NOTHING and report the unknown tokens\n * (a notification then opens the link in a new tab instead of half of it in place).\n */\n readonly requireAll?: boolean;\n readonly onError?: (error: unknown, token: LinkToken) => void;\n}\n\n/**\n * Open every `?panels=` token of a search string or URL through its definition's hydrator.\n * Synchronous hydrators have dispatched by the time this returns; the promise settles once\n * async hydrators finish.\n */\nexport async function openFromLink(searchOrUrl: string, options: OpenFromLinkOptions): Promise<OpenFromLinkResult> {\n const tokens = parseLinkParam(readPanelsParam(searchOrUrl));\n const result: OpenFromLinkResult = { opened: [], refused: [], unknown: [], failed: [] };\n const unknown = tokens.filter((token) => !getOverlayMetaByLinkKey(token.key)?.link);\n if (options.requireAll && unknown.length > 0) {\n result.unknown.push(...unknown.map((token) => token.raw));\n return result;\n }\n const outcomes = tokens.map((token) => hydrateLinkToken(token, options.dispatch, options.onError));\n const settled = await Promise.all(outcomes.map((outcome) => Promise.resolve(outcome)));\n settled.forEach((outcome, index) => {\n const raw = tokens[index]?.raw ?? \"\";\n result[outcome === \"opened\" ? \"opened\" : outcome].push(raw);\n });\n return result;\n}\n\n// ── The extension door ──────────────────────────────────────────────────────────────────────\n\nexport interface OpenPanelRequest {\n readonly panelId: string;\n readonly instanceId?: string;\n readonly data?: Readonly<Record<string, unknown>>;\n}\n\nexport type OpenPanelResult =\n | { readonly ok: true; readonly opened: true; readonly panelId: string; readonly instanceId: string | null }\n | { readonly ok: false; readonly error: \"invalid_payload\" | \"unknown_panel\"; readonly details?: unknown };\n\nfunction readOpenPanelRequest(raw: unknown): OpenPanelRequest | string {\n if (!isRecord(raw)) return \"payload must be an object\";\n const { panelId, instanceId, data } = raw;\n if (typeof panelId !== \"string\" || panelId.length === 0) return \"panelId must be a non-empty string\";\n if (instanceId !== undefined && instanceId !== null && (typeof instanceId !== \"string\" || instanceId.length === 0)) {\n return \"instanceId must be a non-empty string when given\";\n }\n if (data !== undefined && data !== null && !isRecord(data)) return \"data must be an object when given\";\n return {\n panelId,\n ...(typeof instanceId === \"string\" ? { instanceId } : {}),\n ...(isRecord(data) ? { data } : {}),\n };\n}\n\n/**\n * Open a registered overlay by id for an outside caller (the browser extension's `openPanel`\n * envelope). Validates the payload and the id; failures come back as values, never throws, so\n * the bridge can reply verbatim. `instanceId: null` in the reply = the caller used the singleton.\n */\nexport function openPanelRequest(rawPayload: unknown, options: { readonly dispatch: LinkDispatch }): OpenPanelResult {\n const request = readOpenPanelRequest(rawPayload);\n if (typeof request === \"string\") return { ok: false, error: \"invalid_payload\", details: request };\n if (!isRegisteredOverlay(request.panelId)) {\n return {\n ok: false,\n error: \"unknown_panel\",\n details: { panelId: request.panelId, hint: \"panelId must be the id of a registered overlay definition.\" },\n };\n }\n openRegisteredOverlay(options.dispatch, request.panelId, request.instanceId ?? DEFAULT_INSTANCE_ID, request.data);\n return { ok: true, opened: true, panelId: request.panelId, instanceId: request.instanceId ?? null };\n}\n"],"mappings":"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","names":["useEffect","getOverlayMetaByLinkKey","useSummonDispatch","useSummonDispatch","useEffect","getOverlayMetaByLinkKey"]}
|
|
@@ -0,0 +1,123 @@
|
|
|
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/window-host.ts
|
|
22
|
+
var window_host_exports = {};
|
|
23
|
+
__export(window_host_exports, {
|
|
24
|
+
DEFAULT_ESCAPE_YIELD_SELECTOR: () => DEFAULT_ESCAPE_YIELD_SELECTOR,
|
|
25
|
+
DEFAULT_MOBILE_TOP_OFFSET: () => DEFAULT_MOBILE_TOP_OFFSET,
|
|
26
|
+
DEFAULT_WINDOW_CONFIG: () => DEFAULT_WINDOW_CONFIG,
|
|
27
|
+
WindowSelectionSurface: () => WindowSelectionSurface,
|
|
28
|
+
WindowTraySync: () => WindowTraySync,
|
|
29
|
+
WindowsProvider: () => WindowsProvider,
|
|
30
|
+
getTrayPreviewEntry: () => getTrayPreviewEntry,
|
|
31
|
+
registerTrayPreview: () => registerTrayPreview,
|
|
32
|
+
resetTrayPreviews: () => resetTrayPreviews,
|
|
33
|
+
useWindowConfig: () => useWindowConfig
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(window_host_exports);
|
|
36
|
+
|
|
37
|
+
// src/window/config.tsx
|
|
38
|
+
var import_react = require("react");
|
|
39
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
40
|
+
function PassthroughSurface({ children }) {
|
|
41
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
|
|
42
|
+
}
|
|
43
|
+
var DEFAULT_ESCAPE_YIELD_SELECTOR = "[data-overlay-escape-yield]";
|
|
44
|
+
var DEFAULT_MOBILE_TOP_OFFSET = "var(--header-height, 0px)";
|
|
45
|
+
var DEFAULT_WINDOW_CONFIG = {
|
|
46
|
+
escapeYieldSelector: DEFAULT_ESCAPE_YIELD_SELECTOR,
|
|
47
|
+
mobileTopOffset: DEFAULT_MOBILE_TOP_OFFSET,
|
|
48
|
+
selectionSurface: PassthroughSurface
|
|
49
|
+
};
|
|
50
|
+
var WindowConfigContext = (0, import_react.createContext)(DEFAULT_WINDOW_CONFIG);
|
|
51
|
+
WindowConfigContext.displayName = "WindowConfigContext";
|
|
52
|
+
function WindowsProvider({ escapeYieldSelector, mobileTopOffset, selectionSurface, children }) {
|
|
53
|
+
const value = (0, import_react.useMemo)(
|
|
54
|
+
() => ({
|
|
55
|
+
escapeYieldSelector: escapeYieldSelector ?? DEFAULT_WINDOW_CONFIG.escapeYieldSelector,
|
|
56
|
+
mobileTopOffset: mobileTopOffset ?? DEFAULT_WINDOW_CONFIG.mobileTopOffset,
|
|
57
|
+
selectionSurface: selectionSurface ?? DEFAULT_WINDOW_CONFIG.selectionSurface
|
|
58
|
+
}),
|
|
59
|
+
[escapeYieldSelector, mobileTopOffset, selectionSurface]
|
|
60
|
+
);
|
|
61
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WindowConfigContext.Provider, { value, children });
|
|
62
|
+
}
|
|
63
|
+
function useWindowConfig() {
|
|
64
|
+
return (0, import_react.useContext)(WindowConfigContext);
|
|
65
|
+
}
|
|
66
|
+
function WindowSelectionSurface({ children }) {
|
|
67
|
+
const Surface = useWindowConfig().selectionSurface;
|
|
68
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Surface, { children });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/window/WindowTraySync.tsx
|
|
72
|
+
var import_react2 = require("react");
|
|
73
|
+
var import_react3 = require("@ai-matrx/summon/react");
|
|
74
|
+
var import_summon = require("@ai-matrx/summon");
|
|
75
|
+
var import_summon2 = require("@ai-matrx/summon");
|
|
76
|
+
var DEBOUNCE_MS = 500;
|
|
77
|
+
function WindowTraySync() {
|
|
78
|
+
const dispatch = (0, import_react3.useSummonDispatch)();
|
|
79
|
+
(0, import_react2.useEffect)(() => {
|
|
80
|
+
let timer = null;
|
|
81
|
+
const syncWindowGeometry = () => {
|
|
82
|
+
const { vw, vh, degenerate } = (0, import_summon2.safeViewportDims)();
|
|
83
|
+
if (degenerate) return;
|
|
84
|
+
dispatch(
|
|
85
|
+
(0, import_summon.recomputeTrayPositions)({ viewportWidth: vw, viewportHeight: vh })
|
|
86
|
+
);
|
|
87
|
+
dispatch((0, import_summon.clampAllWindowRects)({ viewportWidth: vw, viewportHeight: vh }));
|
|
88
|
+
};
|
|
89
|
+
syncWindowGeometry();
|
|
90
|
+
const handleResize = () => {
|
|
91
|
+
if (timer !== null) clearTimeout(timer);
|
|
92
|
+
timer = setTimeout(() => {
|
|
93
|
+
timer = null;
|
|
94
|
+
syncWindowGeometry();
|
|
95
|
+
}, DEBOUNCE_MS);
|
|
96
|
+
};
|
|
97
|
+
window.addEventListener("resize", handleResize, { passive: true });
|
|
98
|
+
return () => {
|
|
99
|
+
window.removeEventListener("resize", handleResize);
|
|
100
|
+
if (timer !== null) clearTimeout(timer);
|
|
101
|
+
};
|
|
102
|
+
}, [dispatch]);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/window/tray/trayPreviewRegistry.ts
|
|
107
|
+
var import_global_slot = require("@ai-matrx/kit/global-slot");
|
|
108
|
+
var EMPTY = {};
|
|
109
|
+
var registry = () => (0, import_global_slot.globalSlot)("ai-matrx.summon.window.trayPreviews", () => /* @__PURE__ */ new Map());
|
|
110
|
+
function registerTrayPreview(overlayId, preview) {
|
|
111
|
+
const entry = typeof preview === "function" ? { Preview: preview } : preview;
|
|
112
|
+
registry().set(overlayId, entry);
|
|
113
|
+
return () => {
|
|
114
|
+
if (registry().get(overlayId) === entry) registry().delete(overlayId);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function getTrayPreviewEntry(overlayId) {
|
|
118
|
+
return registry().get(overlayId) ?? EMPTY;
|
|
119
|
+
}
|
|
120
|
+
function resetTrayPreviews() {
|
|
121
|
+
registry().clear();
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=window-host.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/window-host.ts","../src/window/config.tsx","../src/window/WindowTraySync.tsx","../src/window/tray/trayPreviewRegistry.ts"],"sourcesContent":["// @ai-matrx/summon/window-host — the light half of the window section, safe to mount at the app\n// root: window configuration (WindowsProvider), the viewport/tray sync, and tray-preview\n// registration. Importing it never loads WindowPanel; the window section reads this section's\n// context through the package name, so both share one context.\nexport {\n WindowsProvider,\n useWindowConfig,\n WindowSelectionSurface,\n DEFAULT_WINDOW_CONFIG,\n DEFAULT_ESCAPE_YIELD_SELECTOR,\n DEFAULT_MOBILE_TOP_OFFSET,\n type WindowConfig,\n type WindowsProviderProps,\n type WindowSelectionSurfaceProps,\n} from \"./window/config\";\nexport { WindowTraySync } from \"./window/WindowTraySync\";\nexport {\n registerTrayPreview,\n getTrayPreviewEntry,\n resetTrayPreviews,\n type TrayPreviewContext,\n type TrayPreviewEntry,\n type TraySnapshotCapture,\n} from \"./window/tray/trayPreviewRegistry\";\n","\"use client\";\n\n/**\n * WindowsProvider — the window section's host options, read by every WindowPanel through context.\n * Everything has a working default, so no provider is needed at all; a host passes only what it\n * has (AI Matrx: its right-click selection menu and its header offset).\n */\nimport { createContext, useContext, useMemo, type ComponentType, type ReactNode } from \"react\";\n\nexport interface WindowSelectionSurfaceProps {\n readonly children: ReactNode;\n}\n\nexport interface WindowConfig {\n /**\n * Visible elements matching this selector own the Escape key: a window with `closeOnEscape`\n * yields to them (menus, popovers, listboxes and open dialogs always do). Default\n * `[data-overlay-escape-yield]` — mark any host surface that must take Escape first.\n */\n readonly escapeYieldSelector: string;\n /** Top offset of a phone fullscreen window (below the host's header). Default `var(--header-height, 0px)`. */\n readonly mobileTopOffset: string;\n /**\n * Wraps every window body (desktop, popped out, and all three phone presentations). Default:\n * passthrough. AI Matrx injects its selection toolbar + right-click menu here.\n */\n readonly selectionSurface: ComponentType<WindowSelectionSurfaceProps>;\n}\n\nfunction PassthroughSurface({ children }: WindowSelectionSurfaceProps) {\n return <>{children}</>;\n}\n\nexport const DEFAULT_ESCAPE_YIELD_SELECTOR = \"[data-overlay-escape-yield]\";\nexport const DEFAULT_MOBILE_TOP_OFFSET = \"var(--header-height, 0px)\";\n\nexport const DEFAULT_WINDOW_CONFIG: WindowConfig = {\n escapeYieldSelector: DEFAULT_ESCAPE_YIELD_SELECTOR,\n mobileTopOffset: DEFAULT_MOBILE_TOP_OFFSET,\n selectionSurface: PassthroughSurface,\n};\n\nconst WindowConfigContext = createContext<WindowConfig>(DEFAULT_WINDOW_CONFIG);\nWindowConfigContext.displayName = \"WindowConfigContext\";\n\nexport interface WindowsProviderProps extends Partial<WindowConfig> {\n readonly children?: ReactNode;\n}\n\nexport function WindowsProvider({ escapeYieldSelector, mobileTopOffset, selectionSurface, children }: WindowsProviderProps) {\n const value = useMemo<WindowConfig>(\n () => ({\n escapeYieldSelector: escapeYieldSelector ?? DEFAULT_WINDOW_CONFIG.escapeYieldSelector,\n mobileTopOffset: mobileTopOffset ?? DEFAULT_WINDOW_CONFIG.mobileTopOffset,\n selectionSurface: selectionSurface ?? DEFAULT_WINDOW_CONFIG.selectionSurface,\n }),\n [escapeYieldSelector, mobileTopOffset, selectionSurface],\n );\n return <WindowConfigContext.Provider value={value}>{children}</WindowConfigContext.Provider>;\n}\n\nexport function useWindowConfig(): WindowConfig {\n return useContext(WindowConfigContext);\n}\n\n/** The configured selection surface, as a component. */\nexport function WindowSelectionSurface({ children }: WindowSelectionSurfaceProps) {\n const Surface = useWindowConfig().selectionSurface;\n return <Surface>{children}</Surface>;\n}\n","\"use client\";\n\n/**\n * WindowTraySync\n *\n * Mounts ONCE in the root layout. Attaches a single debounced resize listener\n * that:\n * 1. Recomputes the positions of all minimized windows (tray slots can change\n * when chips-per-row shifts on viewport resize).\n * 2. Clamps every docked windowed window into the new viewport so windows\n * positioned for a larger screen stay reachable after a downsize.\n *\n * Zero re-renders — fires and forgets.\n *\n * Debounce: 500ms — a continuous window-drag generates exactly one dispatch\n * when the user stops moving. Short bursts (e.g. snapping) are ignored.\n *\n * Usage (in both authenticated and SSR root layouts):\n * <WindowTraySync />\n */\n\nimport { useEffect } from \"react\";\nimport { useSummonDispatch } from \"@ai-matrx/summon/react\";\nimport {\n recomputeTrayPositions,\n clampAllWindowRects,\n} from \"@ai-matrx/summon\";\nimport { safeViewportDims } from \"@ai-matrx/summon\";\n\nconst DEBOUNCE_MS = 500;\n\nexport function WindowTraySync() {\n const dispatch = useSummonDispatch();\n\n useEffect(() => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const syncWindowGeometry = () => {\n // A resize that measures 0×0 is a hidden/undisplayed tab or a\n // display:none ancestor, never a real screen. Bail WITHOUT dispatching:\n // reacting to it would rewrite every window rect and every tray slot\n // against invented dimensions, and the next real measurement would\n // then have to undo it. No measurement, no write.\n const { vw, vh, degenerate } = safeViewportDims();\n if (degenerate) return;\n dispatch(\n recomputeTrayPositions({ viewportWidth: vw, viewportHeight: vh }),\n );\n dispatch(clampAllWindowRects({ viewportWidth: vw, viewportHeight: vh }));\n };\n\n // This island may load after the viewport already changed. Synchronising\n // once on mount closes that event-listener gap for persisted windows.\n syncWindowGeometry();\n\n const handleResize = () => {\n if (timer !== null) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n syncWindowGeometry();\n }, DEBOUNCE_MS);\n };\n\n window.addEventListener(\"resize\", handleResize, { passive: true });\n\n return () => {\n window.removeEventListener(\"resize\", handleResize);\n if (timer !== null) clearTimeout(timer);\n };\n }, [dispatch]);\n\n return null;\n}\n\nexport default WindowTraySync;\n","/**\n * Tray previews — overlay id → a semantic minimized preview and/or a custom snapshot capture.\n *\n * The host registers its previews once (AI Matrx keeps its own preview components in the app):\n *\n * registerTrayPreview(\"notesWindow\", NotesTrayPreview);\n * registerTrayPreview(\"canvasWindow\", { captureTraySnapshot: captureCanvas });\n *\n * A window with a preview component skips snapshot capture entirely (cheaper, higher fidelity).\n * A window with neither gets the default capture (`defaultTraySnapshotCapture`, html-to-image when\n * the host installed it). State lives in a global slot (kit globalSlot, D9).\n */\nimport type { ComponentType } from \"react\";\nimport { globalSlot } from \"@ai-matrx/kit/global-slot\";\n\nexport interface TrayPreviewContext {\n /** The window's allowlisted semantic data (open data merged with what `onCollectData` returned). */\n readonly data: Readonly<Record<string, unknown>>;\n /** Stable overlay id (the registry key). */\n readonly overlayId: string;\n /** Instance id — `\"default\"` for singleton windows. */\n readonly instanceId: string;\n /** The display title. */\n readonly title: string;\n}\n\nexport type TraySnapshotCapture = (bodyEl: HTMLElement) => Promise<Blob | null>;\n\nexport interface TrayPreviewEntry {\n readonly Preview?: ComponentType<TrayPreviewContext>;\n readonly captureTraySnapshot?: TraySnapshotCapture;\n}\n\nconst EMPTY: TrayPreviewEntry = {};\n\nconst registry = (): Map<string, TrayPreviewEntry> =>\n globalSlot<Map<string, TrayPreviewEntry>>(\"ai-matrx.summon.window.trayPreviews\", () => new Map());\n\n/**\n * Register the minimized preview for an overlay (or any window id). Pass a component, or an entry\n * with `Preview` and/or `captureTraySnapshot`. Re-registering replaces. Returns an unregister.\n */\nexport function registerTrayPreview(\n overlayId: string,\n preview: ComponentType<TrayPreviewContext> | TrayPreviewEntry,\n): () => void {\n const entry: TrayPreviewEntry = typeof preview === \"function\" ? { Preview: preview } : preview;\n registry().set(overlayId, entry);\n return () => {\n if (registry().get(overlayId) === entry) registry().delete(overlayId);\n };\n}\n\nexport function getTrayPreviewEntry(overlayId: string): TrayPreviewEntry {\n return registry().get(overlayId) ?? EMPTY;\n}\n\n/** Test-only. */\nexport function resetTrayPreviews(): void {\n registry().clear();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACOA,mBAAuF;AAuB9E;AADT,SAAS,mBAAmB,EAAE,SAAS,GAAgC;AACrE,SAAO,2EAAG,UAAS;AACrB;AAEO,IAAM,gCAAgC;AACtC,IAAM,4BAA4B;AAElC,IAAM,wBAAsC;AAAA,EACjD,qBAAqB;AAAA,EACrB,iBAAiB;AAAA,EACjB,kBAAkB;AACpB;AAEA,IAAM,0BAAsB,4BAA4B,qBAAqB;AAC7E,oBAAoB,cAAc;AAM3B,SAAS,gBAAgB,EAAE,qBAAqB,iBAAiB,kBAAkB,SAAS,GAAyB;AAC1H,QAAM,YAAQ;AAAA,IACZ,OAAO;AAAA,MACL,qBAAqB,uBAAuB,sBAAsB;AAAA,MAClE,iBAAiB,mBAAmB,sBAAsB;AAAA,MAC1D,kBAAkB,oBAAoB,sBAAsB;AAAA,IAC9D;AAAA,IACA,CAAC,qBAAqB,iBAAiB,gBAAgB;AAAA,EACzD;AACA,SAAO,4CAAC,oBAAoB,UAApB,EAA6B,OAAe,UAAS;AAC/D;AAEO,SAAS,kBAAgC;AAC9C,aAAO,yBAAW,mBAAmB;AACvC;AAGO,SAAS,uBAAuB,EAAE,SAAS,GAAgC;AAChF,QAAM,UAAU,gBAAgB,EAAE;AAClC,SAAO,4CAAC,WAAS,UAAS;AAC5B;;;AChDA,IAAAA,gBAA0B;AAC1B,IAAAA,gBAAkC;AAClC,oBAGO;AACP,IAAAC,iBAAiC;AAEjC,IAAM,cAAc;AAEb,SAAS,iBAAiB;AAC/B,QAAM,eAAW,iCAAkB;AAEnC,+BAAU,MAAM;AACd,QAAI,QAA8C;AAElD,UAAM,qBAAqB,MAAM;AAM/B,YAAM,EAAE,IAAI,IAAI,WAAW,QAAI,iCAAiB;AAChD,UAAI,WAAY;AAChB;AAAA,YACE,sCAAuB,EAAE,eAAe,IAAI,gBAAgB,GAAG,CAAC;AAAA,MAClE;AACA,mBAAS,mCAAoB,EAAE,eAAe,IAAI,gBAAgB,GAAG,CAAC,CAAC;AAAA,IACzE;AAIA,uBAAmB;AAEnB,UAAM,eAAe,MAAM;AACzB,UAAI,UAAU,KAAM,cAAa,KAAK;AACtC,cAAQ,WAAW,MAAM;AACvB,gBAAQ;AACR,2BAAmB;AAAA,MACrB,GAAG,WAAW;AAAA,IAChB;AAEA,WAAO,iBAAiB,UAAU,cAAc,EAAE,SAAS,KAAK,CAAC;AAEjE,WAAO,MAAM;AACX,aAAO,oBAAoB,UAAU,YAAY;AACjD,UAAI,UAAU,KAAM,cAAa,KAAK;AAAA,IACxC;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAEb,SAAO;AACT;;;AC3DA,yBAA2B;AAoB3B,IAAM,QAA0B,CAAC;AAEjC,IAAM,WAAW,UACf,+BAA0C,uCAAuC,MAAM,oBAAI,IAAI,CAAC;AAM3F,SAAS,oBACd,WACA,SACY;AACZ,QAAM,QAA0B,OAAO,YAAY,aAAa,EAAE,SAAS,QAAQ,IAAI;AACvF,WAAS,EAAE,IAAI,WAAW,KAAK;AAC/B,SAAO,MAAM;AACX,QAAI,SAAS,EAAE,IAAI,SAAS,MAAM,MAAO,UAAS,EAAE,OAAO,SAAS;AAAA,EACtE;AACF;AAEO,SAAS,oBAAoB,WAAqC;AACvE,SAAO,SAAS,EAAE,IAAI,SAAS,KAAK;AACtC;AAGO,SAAS,oBAA0B;AACxC,WAAS,EAAE,MAAM;AACnB;","names":["import_react","import_summon"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentType, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface WindowSelectionSurfaceProps {
|
|
5
|
+
readonly children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
interface WindowConfig {
|
|
8
|
+
/**
|
|
9
|
+
* Visible elements matching this selector own the Escape key: a window with `closeOnEscape`
|
|
10
|
+
* yields to them (menus, popovers, listboxes and open dialogs always do). Default
|
|
11
|
+
* `[data-overlay-escape-yield]` — mark any host surface that must take Escape first.
|
|
12
|
+
*/
|
|
13
|
+
readonly escapeYieldSelector: string;
|
|
14
|
+
/** Top offset of a phone fullscreen window (below the host's header). Default `var(--header-height, 0px)`. */
|
|
15
|
+
readonly mobileTopOffset: string;
|
|
16
|
+
/**
|
|
17
|
+
* Wraps every window body (desktop, popped out, and all three phone presentations). Default:
|
|
18
|
+
* passthrough. AI Matrx injects its selection toolbar + right-click menu here.
|
|
19
|
+
*/
|
|
20
|
+
readonly selectionSurface: ComponentType<WindowSelectionSurfaceProps>;
|
|
21
|
+
}
|
|
22
|
+
declare const DEFAULT_ESCAPE_YIELD_SELECTOR = "[data-overlay-escape-yield]";
|
|
23
|
+
declare const DEFAULT_MOBILE_TOP_OFFSET = "var(--header-height, 0px)";
|
|
24
|
+
declare const DEFAULT_WINDOW_CONFIG: WindowConfig;
|
|
25
|
+
interface WindowsProviderProps extends Partial<WindowConfig> {
|
|
26
|
+
readonly children?: ReactNode;
|
|
27
|
+
}
|
|
28
|
+
declare function WindowsProvider({ escapeYieldSelector, mobileTopOffset, selectionSurface, children }: WindowsProviderProps): react.JSX.Element;
|
|
29
|
+
declare function useWindowConfig(): WindowConfig;
|
|
30
|
+
/** The configured selection surface, as a component. */
|
|
31
|
+
declare function WindowSelectionSurface({ children }: WindowSelectionSurfaceProps): react.JSX.Element;
|
|
32
|
+
|
|
33
|
+
declare function WindowTraySync(): null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tray previews — overlay id → a semantic minimized preview and/or a custom snapshot capture.
|
|
37
|
+
*
|
|
38
|
+
* The host registers its previews once (AI Matrx keeps its own preview components in the app):
|
|
39
|
+
*
|
|
40
|
+
* registerTrayPreview("notesWindow", NotesTrayPreview);
|
|
41
|
+
* registerTrayPreview("canvasWindow", { captureTraySnapshot: captureCanvas });
|
|
42
|
+
*
|
|
43
|
+
* A window with a preview component skips snapshot capture entirely (cheaper, higher fidelity).
|
|
44
|
+
* A window with neither gets the default capture (`defaultTraySnapshotCapture`, html-to-image when
|
|
45
|
+
* the host installed it). State lives in a global slot (kit globalSlot, D9).
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
interface TrayPreviewContext {
|
|
49
|
+
/** The window's allowlisted semantic data (open data merged with what `onCollectData` returned). */
|
|
50
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
51
|
+
/** Stable overlay id (the registry key). */
|
|
52
|
+
readonly overlayId: string;
|
|
53
|
+
/** Instance id — `"default"` for singleton windows. */
|
|
54
|
+
readonly instanceId: string;
|
|
55
|
+
/** The display title. */
|
|
56
|
+
readonly title: string;
|
|
57
|
+
}
|
|
58
|
+
type TraySnapshotCapture = (bodyEl: HTMLElement) => Promise<Blob | null>;
|
|
59
|
+
interface TrayPreviewEntry {
|
|
60
|
+
readonly Preview?: ComponentType<TrayPreviewContext>;
|
|
61
|
+
readonly captureTraySnapshot?: TraySnapshotCapture;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Register the minimized preview for an overlay (or any window id). Pass a component, or an entry
|
|
65
|
+
* with `Preview` and/or `captureTraySnapshot`. Re-registering replaces. Returns an unregister.
|
|
66
|
+
*/
|
|
67
|
+
declare function registerTrayPreview(overlayId: string, preview: ComponentType<TrayPreviewContext> | TrayPreviewEntry): () => void;
|
|
68
|
+
declare function getTrayPreviewEntry(overlayId: string): TrayPreviewEntry;
|
|
69
|
+
/** Test-only. */
|
|
70
|
+
declare function resetTrayPreviews(): void;
|
|
71
|
+
|
|
72
|
+
export { DEFAULT_ESCAPE_YIELD_SELECTOR, DEFAULT_MOBILE_TOP_OFFSET, DEFAULT_WINDOW_CONFIG, type TrayPreviewContext, type TrayPreviewEntry, type TraySnapshotCapture, type WindowConfig, WindowSelectionSurface, type WindowSelectionSurfaceProps, WindowTraySync, WindowsProvider, type WindowsProviderProps, getTrayPreviewEntry, registerTrayPreview, resetTrayPreviews, useWindowConfig };
|