@ai-matrx/ summon (韩语)
召集任何来自任何地方的反动元件——用它的处理器,校对和状态——,在调用之前从不挂载.
- 准备开机. 打开窗口、 对话框、 工作表或任何部件, 用钩子或动作从任何地方打开 。 关闭的上覆在回放树上不放任何内容,并获取任何代码 .
- 单一或多个实例 每一页.
- 在Redux旁边处理. 召回,事件频道,一发请求和必须裁判在商店旁边的注册处进行直播,由开口实例拥有,关闭后处理.
- 持住活计. 关闭一个叠叠的卸载(
none),以隐居为生hidden),或卸载并记住所保存的状态(restore) (中文(简体) ).useOverlayState是保存状态的钩子. - 地址栏链接 .
?panels=notes:42命名一个打开的覆盖,加载其数据并活下来重新加载. - 持久性,关闭或打开:浏览器本地端口,您自己的数据库通过一个小端口,或者
@ai-matrx/data。 。 。 . - 地平线.
WindowPanel是浏览器中的桌面(drag, 调整大小, snap, 安排, 托盘有缩略图, 最小化, 弹出到一个真实的浏览器窗口, 3个电话演示) .dialog, (中文(简体) ),sheet财务报告和审定财务报表lite(一个轻浮的窗口)共享相同的相近绑定.
包裹拥有其Redux切片. 它运行在私人商店上, 或者挂入你已有的商店.
安装
pnpm add @ai-matrx/summon react react-dom react-redux @reduxjs/toolkit
| 端点 | 需要时间 |
|---|---|
react ≥19, react-dom, (中文(简体) ), react-redux 第9条, @reduxjs/toolkit 页:1 | 除了无框架外的一切 . 财务报告和审定财务报表 ./handles 条目 |
next 第16个(可选) | 仅限 ./next |
@supabase/supabase-js | 仅限 ./persistence/data,通过 @ai-matrx/data |
@ai-matrx/data 财务报告和审定财务报表 @ai-matrx/design-system 它们是普通的依附者(平台的同父异母规则),所以它们用召唤安装,而它们的:Supabase, @ai-matrx/auth 和Radix软件包也到达了(新鲜的Vite应用程序安装时只安装了大约310 MB的传呼) node_modules) (中文(简体) ). 他们加 安装 重量,没有 捆绑 重量:每个条目都是自己的模块,未使用条目是树震去取用,只通过 ./persistence/data,以及 src/importCost.test.ts + 键 src/firstLoadBudget.test.ts 保持首发装入的靴子(./boot, (中文(简体) ), ./boot-root, (中文(简体) ), ./boot-kit以字节预算免去它们。 每一页都要付出的代价是样式表 样式。 。 。 .
条目 :
| 条目 | 这是什么 | 反应? |
|---|---|---|
./boot | 第一载. 大门createBootGate),平地开口者summon, (中文(简体) ), dismiss, (中文(简体) ), openOverlay, (中文(简体) ), closeOverlay,(......), isOverlayOpen, (中文(简体) ), mintInstanceId, (中文(简体) ), DEFAULT_INSTANCE_ID | 没有 |
./boot-root | 第一载. <SummonRoot>它的背景, useSummonStore, (中文(简体) ), useOptionalSummonStore, (中文(简体) ), useOverlayLink, (中文(简体) ), usePopoutContainer | 对 |
./boot-kit | 第一个负载,可选. 处理没有注册的条目, lazyOpener,则 ?panels= 编剧( e)formatLinkToken) 三光读出, expectNavigation, (中文(简体) ), locationRouter | 没有 |
./opener | createOpener(id, { multi }) → { openAction, useOpen, Controller, useIsOpen, useClose, close }: 一个打开器 ./define 财务报告和审定财务报表 ./react 已经建在, 足够轻的第一载 | 对 |
./open | 经典的创造者openOverlay 与 .type编号 :.match),每个选择者,注册,端口, createSummonGate (功能与 createBootGate) (中文(简体) ). 大约是靴子集的两倍字节: 偏好 ./boot + 键 ./boot-kit 在第一个负载中 | 没有 |
./root | ./boot-root 加选择器、 调度器和端口钩、 调度器、 例上下文 | 对 |
./define | defineOverlay (开口者, Controller,打开/关闭的钩子,发射器). 只从定义模块导入 | 对 |
. | 核心:切片,中间器件, createSummonStore窗口几何 | 没有 |
./handles | 回调、 事件频道、 请求、 裁判 | 没有Redux, 没有反应 |
./request | 等待对话框 . requestOverlay(overlayId, request, { cancel, open, close }) 打开一个覆盖并用它的答案解决(useOverlayRequest 打开 ./react 页:1 关闭它的任何方式,或者一个从未显示请求的渲染,答案 cancel | 没有 |
./load-once | loadOnce(load): 在装入器成功时运行一次, 重试失败的装入器( 备忘) lazyOpener 使用量) | 没有 |
./hook-controller | hookController(useOpen, toOptions?): 为开口者开口的控制器,其开口逻辑是你自己的,没有 createOpener | 对 |
./instance-value | registerInstanceValue 编号 : getInstanceValue:一个开箱的打开器被捕获, 当关闭时处理 | 没有 |
./react | SummonProvider, (中文(简体) ), SummonHost, (中文(简体) ), useOverlayState, (中文(简体) ), useOverlayInstance | 对 |
./window | WindowPanel 和桌面 | 对 |
./window-host | 窗口部分可挂起的根 : WindowsProvider, (中文(简体) ), WindowTraySync, (中文(简体) ), registerTrayPreview, (中文(简体) ), usePopout, (中文(简体) ), usePopoutContainer, (中文(简体) ), useOverlaySurfaceRenderAck。从不装入窗口面板 | 对 |
./dialog, (中文(简体) ), ./sheet, (中文(简体) ), ./lite, (中文(简体) ), ./dock | 对话框、工作表/绘图、灯窗、浮动码头 | 对 |
./url | OverlayLinkManager, (中文(简体) ), useOverlayLink,则 ?panels= 读取侧面 | 对 |
./persistence, (中文(简体) ), ./persistence/data | 保存窗口和组件状态 @ai-matrx/data 适配器 | 是/没有 |
./next, (中文(简体) ), ./testing, (中文(简体) ), ./styles.css | 下一个路由器端口, 测试假冒, 结构 CSS |
快开始
建议的设置持续召唤出每条路线的第一个负载: 根只挂载 <SummonRoot> (从) ./boot-root商店的大门从 ./boot,定义使用 ./define,而召唤者从 ./boot (或 createOpener 从 ./opener) (中文(简体) ). 召唤核心,主机和窗口代码负载在第一个打开.
1. 一库可出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出入出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出出. 闸门队列第一个召唤动作,装入核心一次,将它的切片注入到您的减压器并重放队列.
"use client";
import { combineReducers, configureStore, type Reducer, type ReducersMapObject } from "@reduxjs/toolkit";
import { createBootGate } from "@ai-matrx/summon/boot";
import { SummonRoot } from "@ai-matrx/summon/boot-root";
let reducers: ReducersMapObject = { app: (state: number = 0) => state };
let root: Reducer = combineReducers(reducers);
const gate = createBootGate();
gate.attachReducer((injected) => {
reducers = { ...reducers, ...injected };
root = combineReducers(reducers);
});
export const store = configureStore({
reducer: ((state, action) => root(state, action)) as Reducer,
middleware: (getDefault) => getDefault().concat(gate.middleware),
});
export function Root({ children }: { children: React.ReactNode }) {
return (
<SummonRoot store={store} runtime={() => import("./SummonRuntime")}>
{children}
</SummonRoot>
);
}
2. 运行时间: 由 <SummonRoot> 一旦核心装入。 它使任何东西是开放的.
// SummonRuntime.tsx
import { SummonHost } from "@ai-matrx/summon/react";
import { APP_OVERLAYS } from "./overlays";
export default function SummonRuntime() {
return <SummonHost definitions={APP_OVERLAYS} />;
}
3. 宣布重叠一次:轻定义文件和渲染模块(懒散块).
// notes.overlay.ts
import { defineOverlay } from "@ai-matrx/summon/define";
export interface NotesWindowData {
noteId?: string;
}
export const notesWindow = defineOverlay<NotesWindowData>()({
id: "notesWindow",
label: "Notes",
render: () => import("./notes.render"),
});
// One line per overlay: opens are type-checked and a mistyped id is a compile error.
declare module "@ai-matrx/summon" {
interface OverlayRegistry {
notesWindow: NotesWindowData;
}
}
// notes.render.tsx — loaded only when the overlay is first opened
import type { OverlayRenderProps } from "@ai-matrx/summon/react";
import { WindowPanel } from "@ai-matrx/summon/window";
export default function render({ data }: OverlayRenderProps) {
const noteId = typeof data?.noteId === "string" ? data.noteId : null;
return (
<WindowPanel id="notes" title="Notes" boundToOverlay width={480} height={420}>
<NoteEditor noteId={noteId} />
</WindowPanel>
);
}
data 来到了无人信任的地方 Readonly<Record<string, unknown>> | null (它跨越了Redux,一个URL或存储). 在渲染模块中缩小,或给定义 parse(raw) 返回输入值(或) null 以警告抵赖.
// overlays.ts — imported only by the runtime
import { notesWindow } from "./notes.overlay";
export const APP_OVERLAYS = [notesWindow];
4. 打开它 从任何地方:从 ./boot (不需要反应),一个从 ./opener,或者定义的钩.
import { summon } from "@ai-matrx/summon/boot";
import { createOpener } from "@ai-matrx/summon/opener";
import { notesWindow } from "./notes.overlay";
import { store } from "./Root";
// Anywhere, even outside React:
store.dispatch(summon("notesWindow", { noteId: "42" }));
// A caller that must stay light (first load) opens through createOpener, never the definition.
const notes = createOpener<{ noteId?: string }>("notesWindow");
function NotesButton() {
const open = notes.useOpen();
return <button onClick={() => open({ data: { noteId: "42" } })}>Notes</button>;
}
function OpenNotes() {
const open = notesWindow.useSummon();
return <button onClick={() => open({ data: { noteId: "42" } })}>Notes</button>;
}
// Declarative: opens on mount, closes on unmount, re-opens when the (serialisable) props change.
const Always = () => <notesWindow.Controller noteId="42" />;
open() 返回时 { overlayId, instanceId, close() }。定义也给予你 useIsOpen(instanceId?), (中文(简体) ), useClose() 和在树上, useEmit()保持调用者导入用于打开的文件不包含定义(./define: 导入定义的模块支付.
5. 问及等. 等待的对话框是覆盖的,其解答请求。 呼叫者等待答案; 关闭对话框 cancel,而从不制作的对话框(通过错误端口报告,从不挂起).
import { useCallback, useEffect, useState } from "react";
import { summonAction } from "@ai-matrx/summon/boot";
import { defineOverlay } from "@ai-matrx/summon/define";
import { hookController } from "@ai-matrx/summon/hook-controller";
import { loadOnce } from "@ai-matrx/summon/load-once";
import { useOverlayRequest, type OverlayRenderProps } from "@ai-matrx/summon/react";
import { requestOverlay } from "@ai-matrx/summon/request";
import { store } from "./Root";
// The dialog's render answers the request its open data names (and claims it, so it never times out).
function ConfirmBody({ data, close }: OverlayRenderProps) {
const { request, settle } = useOverlayRequest<{ question: string }, boolean>(data, close);
if (!request) return null;
return (
<div role="dialog">
<p>{request.question}</p>
<button onClick={() => settle(true)}>Yes</button>
<button onClick={() => settle(false)}>No</button>
</div>
);
}
export const confirmDialog = defineOverlay<{ requestId: string }>()({
id: "confirmDialog",
label: "Confirm",
render: () => Promise.resolve({ default: ConfirmBody }),
});
declare module "@ai-matrx/summon" {
interface OverlayRegistry {
confirmDialog: { requestId: string };
}
}
// The caller awaits a boolean; any close answers `cancel` (false).
export function confirm(question: string): Promise<boolean> {
return requestOverlay<{ question: string }, boolean>("confirmDialog", { question }, {
cancel: false,
open: (target) => void store.dispatch(summonAction("overlays/openOverlay", target)),
close: (target) => void store.dispatch(summonAction("overlays/closeOverlay", target)),
});
}
// One loader shared by many callers: it runs once while it succeeds; a failed load is retried next call.
const loadEditor = loadOnce(() => import("./notes.overlay"));
// Your own open logic still gets the one open-on-mount Controller (no createOpener in this route).
// Return a stable function (useCallback): a new identity re-opens.
function useOpenConfirm() {
return useCallback((props: { question: string }) => {
void confirm(props.question);
return { close: () => void store.dispatch(summonAction("overlays/closeOverlay", { overlayId: "confirmDialog" })) };
}, []);
}
const ConfirmOnMount = hookController(useOpenConfirm);
function DeleteButton() {
const [ready, setReady] = useState(false);
useEffect(() => void loadEditor().then(() => setReady(true)), []);
return <button disabled={!ready} onClick={async () => (await confirm("Delete this note?")) && console.log("deleted")}>Delete</button>;
}
样式 需要正确查看 : 样式。 。 。 .
在一个供应商中的一切(小型应用程序,测试). <SummonProvider> 从 ./react 创建一个安装了核心的私人商店; 不推迟任何内容 :
import { SummonProvider, SummonHost } from "@ai-matrx/summon/react";
import { APP_OVERLAYS } from "./overlays";
export function SmallApp({ children }: { children: React.ReactNode }) {
return (
<SummonProvider>
{children}
<SummonHost definitions={APP_OVERLAYS} />
</SummonProvider>
);
}
定义选项
| 外地 | 含义 |
|---|---|
id, (中文(简体) ), label | id 是注册密钥; 标签在托盘、 菜单和诊断中显示 |
instance | "single" (默认,例编号) "default")或(或) "multi" (每开口一次新鲜id,或通过) instanceId(中文(简体) ) |
keepAlive | "none" (违约), "hidden" 或者说 "restore" |
closesOnNavigation | 当路由改变时关闭( 覆盖显示打开它的页面的一块); 重新加载只恢复它打开的页面 |
defaults | 在每个开放数据下合并 |
window | { mobile, mobileSidebarAs, ephemeral, preservation, restoreDefaults, windowId, deprecated, unbiddenHome } 对于窗口覆盖( 每个窗口都被记住, 除非 preservation: false; 见"持久性" |
link | { key, aliases?, hydrate }链接 |
parse | 一次狭窄的原始数据; null 拒绝 |
render | () => import("./x.render"); 当主机供应时可选( 见下文) , 当当前时胜过主机地图 |
尽量减少窗口总是保持它的组件挂起,不管什么 keepAlive 说.
保持活动状态并保存状态
import { useOverlayState, useOverlayInstance } from "@ai-matrx/summon/react";
function Draft() {
const [text, setText] = useOverlayState("draft", ""); // like useState; scoped to this open instance
const { close, hidden } = useOverlayInstance();
return <textarea value={text} onChange={(e) => setText(e.target.value)} />;
}
keepAlive: "hidden": 关闭隐藏活性组件(<Activity mode="hidden">); 重开同一实例再次显示,状态完好无损. 单次重叠重开其一次。 A 类multi覆盖未打开instanceId创建新实例( 第二个窗口, 一个新的聊天); 关闭的窗口只有在重新打开时才会回来instanceId最多keepAliveLimit(default 8) 关闭的例仍被挂载;最老的被赶走并释放. 身份变更(签出、账户互换) 发布每个保留实例(discardAllOverlays),所以下一个人从来没有看到过前一个人的活体成分.keepAlive: "restore": 关闭卸载;useOverlayState价值观在重开时会重新出现(并在坚持下去时继续存在).useOverlayState值必须是可序列的。 任何封面的后人都可以称之.
Redux 中没有函数的召回
打开时宣布事件映射并传递平版函数 。 他们通过实例拥有的事件频道旅行;数据中只有其身份证明.
export const uploader = defineOverlay<{ folder?: string }, { uploaded: { url: string }; cleared: void }>()({
id: "uploader", label: "Uploader", render: () => import("./uploader.render"),
});
const open = uploader.useSummon();
open({ data: { folder: "logos" }, handlers: { uploaded: (r) => setUrl(r.url) } });
// inside the overlay's render tree
const emit = uploader.useEmit();
emit("uploaded", { url });
关闭实例处理频道 。 在那之后就默默无声.
大型应用程序: 渲染地圖活在主机上
每一个导入定义的文件都会打开它 也导入定义的 render: () => import(…)。对于一个捆绑者来说,每个这样的文件都会得到覆盖整个窗口子树的边缘。 有一些免费的封面。 由上百个覆盖和上百个开口文件,它将构建图相乘:在AI Mattrx中,大约450个开口文件每个到达了大约220个窗口子树上,一个子应用程序的构建从大约10 s到一个出门杀. 这是主机建造法:永远不要迷倒 import() 边缘.
固定保持"一次声明"在两个文件外加一行. 离开 render 关闭定义,并给主机一个由文件名生成的 id = 导入地图,只给主机导入:
// definitions/notes.overlay.ts — no render: importing it costs nothing
export const notes = defineOverlay<{ noteId?: string }>()({ id: "notes", label: "Notes" });
// definitions/render-map.ts — generated, imported ONLY by the host (behind its own lazy boundary)
import type { OverlayRenderMap } from "@ai-matrx/summon/react";
export const RENDER_MAP: OverlayRenderMap = { notes: () => import("./notes.render") };
// the host
<SummonHost definitions={APP_OVERLAYS} renders={RENDER_MAP} />
OverlayRenderMap 清除每个模块的数据类型( n)OverlayRenderProps<never>),如此不同键入的渲染模块共享一个映射,没有 any。打开的、完全没有渲染源的覆盖(无) render,没有地图条目)显示其出错卡(OverlayRenderMissingError,导出为 MISSING_RENDER_ERROR)和正在开发的日志。 它从来不是沉默的空白.
挂入已存在的 Redux 商店
import { configureStore } from "@reduxjs/toolkit";
import { summonReducers, createSummonMiddleware } from "@ai-matrx/summon";
const ports = { /* see Ports */ };
export const store = configureStore({
reducer: { ...appReducers, ...summonReducers }, // keys: overlays, windowManager, urlSync, overlayState
middleware: (getDefault) => getDefault().concat(createSummonMiddleware(ports)),
});
<SummonProvider store={store} ports={ports}>…</SummonProvider>
传出同样的 ports 两者均反对。 上方的切片键是固定的. 无 store,提供商制造私人商店(createSummonStore({ ports })) (中文(简体) ).
提供者接头 : fallbacks (单位:千美元)loading, (中文(简体) ), error 构成部分, errorReportExtras (错误卡中的外部控制), renderWrapper (覆盖每个完成的叠加), keepAliveLimit (8), (中文), importTimeoutMs (12 s; 未装入的渲染模块成为 ChunkLoadError 再试一次, stallWarnMs (10 s) (中文(简体) ).
只有在有东西打开时才支付:大门
快速启动就是这个设置: 店里没有传呼切片或中间软件,直到第一个
召唤动作, 闸门队列所在; 它开始一个 import() 输入
切片穿过 gate.attachReducer,添加中间软件并重放队列。 传出大门
与根相同端口( r)createSummonGate({ ports }), (中文(简体) ), <SummonRoot ports={ports}>) (中文(简体) ). 模块
以静态导入核心(一页 WindowPanel)在制造之前安装.
runtime (主机,托盘同步,路由器端口) 安装核心后挂载.
prefetchSummonRuntime() 提前启动负载(idle,指向意图)。 打开者和选择者来了
从 ./open, (中文(简体) ), defineOverlay 从 ./define, (中文(简体) ), <SummonRoot> 从 ./root。 。 。 .
港口
每个端口都有工作默认值,所以一个赤裸的主机不需要. 你只负你的责任,你当传扬他们 ports 关于提供者(部分对象).
| 端口 | 形状 | 默认 |
|---|---|---|
identity | current(): { key, ready }, (中文(简体) ), subscribe(listener) | 匿名 随时待命 变化 key 关闭并释放所有东西( 包含 kept- alive 例) 并加载下一个人的保存状态 |
router | pathname(), (中文(简体) ), search(), (中文(简体) ), replaceSearch(next), (中文(简体) ), subscribe(listener) | window.location + 键 history.replaceState (在服务器上插入) |
notify | info, (中文(简体) ), warn, (中文(简体) ), error(message, options?)选项 dismiss(id) | 控制台 |
errors | capture(error, context) | 控制台错误 |
debug | enabled()选项 subscribe | 关 |
persistence | a 一个 OverlayPersistencePort,或 null | null (持久性关闭) |
下一个.js: import { useNextRouterPort } from "@ai-matrx/summon/next" 返回稳定 RouterPort (在"悬疑"下的客户部分内调用;其内容为: useSearchParams) (中文(简体) ). 测试假货:参见测试.
链接
OverlayLinkManager 保留 ?panels= 并同步打开覆盖 ports.router把它挂在供应商内部.
import { OverlayLinkManager, useOverlayLink } from "@ai-matrx/summon/url";
<SummonProvider><OverlayLinkManager /><SummonHost definitions={APP_OVERLAYS} /></SummonProvider>
给定义一个链接 :
defineOverlay<NotesWindowData>()({
id: "notesWindow", label: "Notes", render: () => import("./notes.render"),
link: {
key: "notes",
hydrate: ({ id, args, open }) => open({ data: { noteId: id } }), // return "refused" to drop the token
},
});
- 开幕
/page?panels=notes:42运行hydrate({ id: "42", args, open, dispatch })。 。 。 .hydrate可能是aync(装入记录,然后open) (中文(简体) ). - 挂载式上加公布自己的地址
useOverlayLink("notes", noteId, { tab: "history" })(也从./react,因此一个渲染模块可以在不装入链接部分的情况下发布.WindowPanel做它为你从它的urlSyncKey, (中文(简体) ),urlSyncId财务报告和审定财务报表urlSyncArgs道具来. - 未知的密钥一并宣布;其封面从未注册的符号在截止日期前被宣布并保持逐字记录.
- 语法 v2数字 :
panels = token ("," token)*, (中文(简体) ),token = key[:id[:k-v(_k-v)*]]编码为%% , :,参数键和值% , : _ -,所以任何值都存在;v1链接仍然分析。 帮助者 :parseLinkToken, (中文(简体) ),parseLinkParam, (中文(简体) ),formatLinkToken, (中文(简体) ),readPanelsParam, (中文(简体) ),withPanelsParam。 。 。 . - 一扇门数字 :
openFromLink(searchOrUrl, { dispatch, requireAll?, onError? })返回时{ opened, refused, unknown, failed }并且为地址栏、通知和任何桥梁服务.requireAll: true如果未知任何密钥,则不打开.openPanelRequest(payload, { dispatch })验证并打开{ panelId, instanceId?, data? }桥上的请求(返回){ ok: true, … }或者说{ ok: false, error: "invalid_payload" | "unknown_panel" }) (中文(简体) ).
持久性
默认关闭。 打开它 persistence 端口加一个挂载组件 :
import { createLocalPersistence, OverlayPersistence } from "@ai-matrx/summon/persistence";
const persistence = createLocalPersistence(); // create once, module level
<SummonProvider ports={{ persistence }}>
<OverlayPersistence /> {/* mount once */}
<SummonHost definitions={APP_OVERLAYS} />
</SummonProvider>
被记住的是什么,在哪里:
| 什麽 | 何处 | 回来啦 |
|---|---|---|
| 打开的窗口 工作空间(中文(简体) ) | 此标签 sessionStorage | 重装同一标签。 一个新的标签开头没有窗口;另一个设备从不看到它们. 从未写到端口 |
| 每个窗口最后的位置及其大小 | 这个设备 localStorage (一张地图,最多200个窗口) | 下一次打开此设备时 |
组件状态( O)useOverlayState(单位:千美元) keepAlive: "restore" 实例和记忆窗口 | 港口,每人(state:<overlayId>:<instanceId>(中文(简体) ) | 任何标签或设备 |
每个窗口默认都会被记住:其位置、大小、状态和JSON开放数据(处理诸如 callbackGroupId 永无拯救) useOverlayState 数值. window.preservation: { dataKeys, requiredDataKeys?, allowedDataValues?, maxDataBytes? } 将保存的数据缩小为允许列表,并在恢复时予以验证; window.preservation: false (或 window.ephemeral: true)从来就无法保存窗口. 封面: 256 KiB 工作空间, 64个开放窗口, 每个窗口的数据和每个状态条目32 KiB; 超支报告通过 ports.errors.capture (单位:千美元)source: "overlay-persistence") (中文(简体) ).
<OverlayPersistence> 选项: tabStorage (默认) window.sessionStorage· ; null = 打开的窗口没有被记住), deviceStorage (默认) window.localStorage· ; null = 窗口总是按其宣布的位置打开, saveDebounceMs (250个), viewport。 。 。 . useOverlayPersistence(options) 返回时 { hydrated }。Boot使端口不读:保存的组件状态每个身份读取一次(loadAll,则 list + 键 load(s) 首次使用时:a useOverlayState 挂载( 它暂停直到数值在商店中), 第一个保存的值, 或保存“ 恢复” 实例打开 。 端口写入等待读取; 在组件状态值变化之前, 端口没有写入 。 没有代码读取保存状态 useOverlayState 电话 savedStateGateFor(store)?.ensure() 先说 签出清除标签打开的窗口; 身份更改会丢弃每个保留保存的实例 .
浏览器- 本地商店 . createLocalPersistence({ namespace?, backend?, storage?, locks?, idbReadBudgetMs? }):一个本地Storage镜像外加索引DB,每个身份的密钥. 还有.. purgeIdentity(identityKey) 用于注册清理.
你自己的数据库. 执行端口 :
import type { OverlayPersistencePort } from "@ai-matrx/summon";
export const myPersistence: OverlayPersistencePort = {
perPerson: true, // true: rows are already scoped to the signed-in person (plain keys). Omit/false: keys arrive prefixed "<identityKey>/"
load: async (key) => /* { value, version } | null */ null,
save: async (key, value, options) => {
/* if options?.expectedVersion is set and differs from the stored version, throw an error named "PersistenceVersionConflictError" */
return { version: 1 };
},
remove: async (key) => {},
list: async (prefix) => [], // keys starting with prefix
};
写入的密钥 : state:<overlayId>:<instanceId> (只有组件状态). 价值为JSON. 建议的表格:
| 栏 | 类型 | |
|---|---|---|
person_id | 乌伊德 | 所有人; 仅为签名人读取并写入行 |
key | 文本 | 上面的密钥之一; 独有 person_id |
value | 乔森b | 保存的值 |
version | 单位 | 以 1 + 1 开始( 乐观的货币) |
冲突时,控制器会重新读取版本并再写出,因此组件状态的最新写法获胜. 版本冲突按错误名称识别( Q)isVersionConflict(error)) (中文(简体) ).
@ai-matrx/data 师曰. createDataPersistence({ db, userId?, table?, feature? }) 从 @ai-matrx/summon/persistence/data 将每个密钥映射到一行 users.user_surface_state (单位:千美元)feature = "summon", (中文(简体) ), surface_key = key, (中文(简体) ), state = value, (中文(简体) ), version),通过数据包的门. 该组织来自 createDb({ client, organization }) 如果没有,则拒绝插入.
import { createDb } from "@ai-matrx/data/db";
import { createDataPersistence } from "@ai-matrx/summon/persistence/data";
const persistence = createDataPersistence({ db: createDb({ client: supabase, organization: () => activeOrgId }) });
处理
@ai-matrx/summon/handles 无框架。 国家生活在一个全球位置,因此重复的捆绑共用一个登记册.
callbackManager(动词):register, (中文(简体) ),registerWithContext(callback, { context, groupId, owner }), (中文(简体) ),subscribe, (中文(简体) ),unsubscribe, (中文(简体) ),trigger, (中文(简体) ),triggerWithContext, (中文(简体) ),triggerGroup, (中文(简体) ),triggerGroupCommand, (中文(简体) ),updateProgress, (中文(简体) ),createGroup, (中文(简体) ),removeGroup, (中文(简体) ),remove, (中文(简体) ),get, (中文(简体) ),has, (中文(简体) ),registerWidgetHandle, (中文(简体) ),ownedBy, (中文(简体) ),groupMembers, (中文(简体) ),disposeOwner, (中文(简体) ),size, (中文(简体) ),subscribeToRegistry。 。 。 .- 输入的助手, 每个接受
{ owner }数字 :
import { createEventChannel, getEventChannel, createRequest, getRequest, registerHandle, getHandle } from "@ai-matrx/summon/handles";
const owner = "notesWindow:default"; // overlayId:instanceId
const channel = createEventChannel<{ saved: string }>({ owner }); // many-shot; pass channel.id in open data
channel.subscribe((e) => console.log(e)); channel.emit({ saved: "42" });
getEventChannel<{ saved: string }>(channel.id)?.emit({ saved: "43" }); // overlay side
const request = createRequest<{ prompt: string }, boolean>({ prompt: "Discard?" }, { owner }); // one-shot
getRequest<{ prompt: string }, boolean>(request.id)?.resolve(true); // overlay answers
const answer = await request.promise; // rejects with HandleDisposedError if disposed unanswered
const dispose = registerHandle("editor:42", { focus() {} }, { owner }); // imperative ref
getHandle<{ focus(): void }>("editor:42")?.focus();
dispose();
业主处置. 当一个实例结束, 所有记录与它的 overlayId:instanceId 作为 owner 被处理:待处理请求被拒绝,频道放弃订阅者,手柄被移除。 无开口者以手取之. 处置一个团体成员可以将其从团体中去除.
窗口面板
import { WindowsProvider, WindowPanel } from "@ai-matrx/summon/window";
槽口合同: 身体只满足窗外的铬都插入了槽里,从不插入体内.
| 槽 | 道具 |
|---|---|
| 标题 | title, (中文(简体) ), titleNode |
| 页眉动作 | actionsLeft, (中文(简体) ), actionsRight |
| 页脚 | footer区域 footerLeft 编号 : footerCenter 编号 : footerRight· ; footerVariant: "bar" | "rich" |
| 左侧栏 | sidebar, (中文(简体) ), sidebarDefaultSize, (中文(简体) ), sidebarMinSize, (中文(简体) ), defaultSidebarOpen, (中文(简体) ), sidebarExpandsWindow |
| 右侧面板 | secondaryPanel, (中文(简体) ), secondaryPanelOpen, (中文(简体) ), secondaryPanelDefaultSize, (中文(简体) ), secondaryPanelMinSize |
关闭绑定 需要并检查类型系统: overlayId (关闭该覆盖), boundToOverlay (关闭附件) SummonHost (例如)或 onClose (中文(简体) ). 没有一个窗口不编译 .
其他道具: id, (中文(简体) ), width编号 :height (数目或 "60vw", (中文(简体) ), minWidth编号 :minHeight, (中文(简体) ), position, (中文(简体) ), fitContent, (中文(简体) ), closeOnEscape, (中文(简体) ), hidePopOutButton, (中文(简体) ), urlSyncKey编号 :urlSyncId编号 :urlSyncArgs, (中文(简体) ), onCollectData, (中文(简体) ), captureTraySnapshot。 。 。 .
对于截图缩略图,通过 ports.elementCapture 改为 SummonRoot 或者说 SummonProvider。 。 render(element, options) 返回画布; Alchemy 上方的主机可以懒散地委托给 renderElement 从 @ai-matrx/alchemy/operate/capture。没有这种能力的宿主仍然得到标签的平板。 明确 captureTraySnapshot 和已注册的快照/审查组件保留优先权。 独立 captureElementThumbnail 帮助工具使用已配置的主机端口, 或它的明文 elementCapture 选项.
行为可以依赖: 最大化,最小化,弹出和停靠永远不重载机体(状态,活性能和效果;最小化使机体挂起并捕获托盘缩略图). Pop-out使用真实的浏览器窗口(Document-in-Picture). 在电话中,窗口使 fullscreen, (中文(简体) ), drawer 或者说 card 按定义 window.mobile。 。 。 .
WindowsProvider 可选; 选项 : escapeYieldSelector (与它匹配的可见元素拥有 Escape 密钥; 默认) [data-overlay-escape-yield], (中文(简体) ), mobileTopOffset (默认) var(--header-height, 0px), (中文(简体) ), selectionSurface (一个包着每个窗口正文的组件,用于主机的选择或右键菜单).
也从 ./window数字 : WindowTraySync 财务报告和审定财务报表 registerTrayPreview (最小的平板的语义预览;同时在光线上) ./window-host 条目 —— 将供应商装入应用程序根, LayoutIcon, (中文(简体) ), usePopoutControl, (中文(简体) ), MobileDrawerSurface, (中文(简体) ), MobileCardSurface, (中文(简体) ), useWindowPanel。 。 。 .
对话框、 工作表、 轻型窗口
import { OverlayDialog } from "@ai-matrx/summon/dialog";
import { OverlaySheet, OverlayDrawer, OverlayBottomSheet } from "@ai-matrx/summon/sheet";
import { LiteWindow } from "@ai-matrx/summon/lite";
// inside a render module: closes through the overlay instance
<OverlayDialog boundToOverlay>{/* DialogContent, DialogTitle … from @ai-matrx/design-system */}</OverlayDialog>
// page-local: you own the state
<OverlayDialog open={open} onClose={() => setOpen(false)}>…</OverlayDialog>
<LiteWindow id="inspector" title="Inspector">…</LiteWindow> {/* inside an SummonHost overlay, onClose can be omitted */}
OverlayDialog, (中文(简体) ), OverlaySheet, (中文(简体) ), OverlayDrawer 财务报告和审定财务报表 OverlayBottomSheet 取出设计系统组件的道具外加相近绑定: boundToOverlay (在显示实例时打开)或 onClose (+ 数) open) (中文(简体) ). LiteWindow 有拖放、边缘大小调整、在最上方时逃出、对焦、电话上有一个底部抽屉;没有最小值、托盘、持久性或链接.
(漂浮的无铬卡、药丸和码头)
import { Dock, DockHandle, DockProvider, useDock, useDockContext } from "@ai-matrx/summon/dock";
<Dock id="attention" corner="bottom-right" defaultPosition={{ x: 16, y: 16 }} zIndex={50} className="w-80">
<DockHandle>⋮⋮ Alerts <button onClick={…}>Close</button></DockHandle>
…
</Dock>
// Your own markup: spread the bindings. Destructure them: the React compiler treats an object
// carrying a `ref` as a ref, so reading `dock.style` during render counts as a ref access.
const { ref, style, handleProps } = useDock({ id: "assists", corner: "bottom-right", position, onPositionChange: save, touch: false, clamp: { margin: 0, minVisible: { width: 120, height: 32 } } });
<div ref={ref} style={style}><button {...handleProps}>pill</button></div>
// Hand every dock the host's storage (declared keys, per-user sync).
<DockProvider store={{ read: (id) => …, write: (id, position) => … }}>…</DockProvider>
指针拖曳在移动时永不退让:每个移动会绘制 CSS translate 属性在一个动画框中,位置承诺一次反应状态,在发布时(dragging 每个手势翻出两次,从不动起,所以一个有流出答案的码头内保持廉价. 职位从一角抵消(corner,默认从下到下右),在每个渲染物上被夹在取景口上(clamp: { margin = 8, minVisible }一个比房间大一点的码头 左上方可以到达; snapToCorner 释放后重新锁定到最近的角落; avoid: { selector, gap } 保持它的固定控制。 没有职位,它就靠 defaultPosition,或者在CSS上 anchor (单位:千美元)top/bottom/left/right/centerX) (中文(简体) ). 记住每个编号:码头 store,否则最接近 DockProvider,其他本地Storage (matrx:summon:dock:<id>(三); persist={false} 每个时刻的码头; 受控主机通行证 position + 键 onPositionChange (每拖一次,和 null 打开 reset()) (中文(简体) ). 媒体通过4px threshold 在它被拖走之前,一个被拖走的,吞下它所显示的线条;在握柄内有各种控制ignore,默认按钮/链接/字段/[data-no-drag]永远不要开始; touch={false} 将触摸到页卷; draggable={false} 钉住它(手机)。 按下时,它会加入窗口管理器的 z- 顺序道 (raiseTransientWindow,窗口上方,模式下方) SummonProvider 已经存在,拖曳则停留 grabbing 通过设计系统 holdDragCursor。在活跃的媒体之外没有听众.
样式
窗口、对话框和工作表画有设计系统主题工具(bg-card, (中文(简体) ), text-foreground, (中文(简体) ), border-border,并需要设计系统的结构CSS. 一个主机需求, 在其 Tailwind v4 CSS 条目中 :
@layer matrx-design-system-tokens, matrx-design-system; /* first line: your own tokens always win */
@import "tailwindcss";
@import "@ai-matrx/design-system/tokens.css"; /* default token values (skip if your theme declares them all) */
@import "@ai-matrx/design-system/styles.css"; /* design-system structural CSS: required */
@import "@ai-matrx/design-system/theme.css"; /* maps bg-card, text-foreground, … onto the tokens and scans design-system */
@import "@ai-matrx/summon/styles.css"; /* summon's structural CSS: required */
@source "../node_modules/@ai-matrx/summon/dist"; /* path relative to this CSS file */
无 styles.css 和主题映射窗口 使透明和无边框。 一个已经映射了语义色彩词汇( AI Matrx does) 跳过的主机 theme.css 并保持自己的,加 @source "../node_modules/@ai-matrx/design-system/dist"。 Tailwind v4(软件包CSS使用 v4 at-rules) 。 颜色只来自设计-系统符号,所以主机的主题适用;交通-光线颜色是 --matrx-window-light-* 自定义属性可以覆盖 .
测试
import { createTestPorts, createTestSummonStore, createMemoryPersistence } from "@ai-matrx/summon/testing";
const ports = createTestPorts({ persistence: createMemoryPersistence() });
const store = createTestSummonStore(ports);
render(<SummonProvider store={store} ports={ports}><SummonHost definitions={[notesWindow]} /></SummonProvider>);
createTestPorts 给出可控 identity (单位:千美元)set(key, ready?)),一个可控 router (单位:千美元)navigate(path, search?))和录音 notify 财务报告和审定财务报表 errors (单位:千美元)messages, (中文(简体) ), captured) (中文(简体) ). createMemoryPersistence 是一个内存、版本端口。 没有测试者依赖.
盖茨
pnpm typecheck (亦以主机登录来编译包),, npx vitest run, (中文(简体) ), pnpm check:package。 。 。 .