@ai-matrx/summon

中文文档
0.1.65发布于 2026/10/30MITTypeScriptESM + CJS近 30 天下载 3万

通过发送来渲染任何回放组件,在需要它之前绝不挂载:单个或多例,在Redux旁边处理回调和回调,保持保存的组件状态,与数据绑定的地址栏链接,可选的持久性,和一个完整的wi

中文使用文档
文档目录18 节
  1. 安装
  2. 快开始
  3. 定义选项
  4. 保持活动状态并保存状态
  5. Redux 中没有函数的召回
  6. 大型应用程序: 渲染地圖活在主机上
  7. 挂入已存在的 Redux 商店
  8. 只有在有东西打开时才支付:大门
  9. 港口
  10. 链接
  11. 持久性
  12. 处理
  13. 窗口面板
  14. 对话框、 工作表、 轻型窗口
  15. (漂浮的无铬卡、药丸和码头)
  16. 样式
  17. 测试
  18. 盖茨

@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没有
./openercreateOpener(id, { multi }) → { openAction, useOpen, Controller, useIsOpen, useClose, close }: 一个打开器 ./define 财务报告和审定财务报表 ./react 已经建在, 足够轻的第一载对
./open经典的创造者openOverlay 与 .type编号 :.match),每个选择者,注册,端口, createSummonGate (功能与 createBootGate) (中文(简体) ). 大约是靴子集的两倍字节: 偏好 ./boot + 键 ./boot-kit 在第一个负载中没有
./root./boot-root 加选择器、 调度器和端口钩、 调度器、 例上下文对
./definedefineOverlay (开口者, Controller,打开/关闭的钩子,发射器). 只从定义模块导入对
.核心:切片,中间器件, createSummonStore窗口几何没有
./handles回调、 事件频道、 请求、 裁判没有Redux, 没有反应
./request等待对话框 . requestOverlay(overlayId, request, { cancel, open, close }) 打开一个覆盖并用它的答案解决(useOverlayRequest 打开 ./react 页:1 关闭它的任何方式,或者一个从未显示请求的渲染,答案 cancel没有
./load-onceloadOnce(load): 在装入器成功时运行一次, 重试失败的装入器( 备忘) lazyOpener 使用量)没有
./hook-controllerhookController(useOpen, toOptions?): 为开口者开口的控制器,其开口逻辑是你自己的,没有 createOpener对
./instance-valueregisterInstanceValue 编号 : getInstanceValue:一个开箱的打开器被捕获, 当关闭时处理没有
./reactSummonProvider, (中文(简体) ), SummonHost, (中文(简体) ), useOverlayState, (中文(简体) ), useOverlayInstance对
./windowWindowPanel 和桌面对
./window-host窗口部分可挂起的根 : WindowsProvider, (中文(简体) ), WindowTraySync, (中文(简体) ), registerTrayPreview, (中文(简体) ), usePopout, (中文(简体) ), usePopoutContainer, (中文(简体) ), useOverlaySurfaceRenderAck。从不装入窗口面板对
./dialog, (中文(简体) ), ./sheet, (中文(简体) ), ./lite, (中文(简体) ), ./dock对话框、工作表/绘图、灯窗、浮动码头对
./urlOverlayLinkManager, (中文(简体) ), 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, (中文(简体) ), labelid 是注册密钥; 标签在托盘、 菜单和诊断中显示
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 关于提供者(部分对象).

端口形状默认
identitycurrent(): { key, ready }, (中文(简体) ), subscribe(listener)匿名 随时待命 变化 key 关闭并释放所有东西( 包含 kept- alive 例) 并加载下一个人的保存状态
routerpathname(), (中文(简体) ), search(), (中文(简体) ), replaceSearch(next), (中文(简体) ), subscribe(listener)window.location + 键 history.replaceState (在服务器上插入)
notifyinfo, (中文(简体) ), warn, (中文(简体) ), error(message, options?)选项 dismiss(id)控制台
errorscapture(error, context)控制台错误
debugenabled()选项 subscribe关
persistencea 一个 OverlayPersistencePort,或 nullnull (持久性关闭)

下一个.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。 。 。 .

版本与发布记录

@ai-matrx/summon 共发布 68 个版本,首次发布于 2026/10/17;过去 12 个月发布了 68 个版本。

版本发布日期距今
0.1.65latest2026/10/306 天前
0.1.642026/10/288 天前
0.1.632026/10/2611 天前
0.1.622026/10/2611 天前
0.1.612026/10/2512 天前
0.1.602026/10/2512 天前

其他发布通道:bootstrap → 0.0.0。

关于 @ai-matrx/summon 的常见问题

@ai-matrx/summon 是什么?

通过发送来渲染任何回放组件,在需要它之前绝不挂载:单个或多例,在Redux旁边处理回调和回调,保持保存的组件状态,与数据绑定的地址栏链接,可选的持久性,和一个完整的wi

如何安装 @ai-matrx/summon?

在项目目录运行 npm install @ai-matrx/summon,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@reduxjs/toolkit(>=2.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@ai-matrx/summon 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ai-matrx/summon 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@ai-matrx/summon 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ai-matrx/summon 可以商用吗?

@ai-matrx/summon 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ai-matrx/summon 的下载量有多少?

根据 npm 官方下载统计,@ai-matrx/summon 在 2026/10/05 – 2026/11/03 这 30 天内被下载 30,022 次,日均约 1,001 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ai-matrx/summon 还在维护吗?

最新版本 0.1.65 发布于 2026/10/30(6 天前);至今共发布 68 个版本,其中过去 12 个月发布了 68 个;首次发布于 2026/10/17。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。