npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ai-matrx/summon

v0.1.3

Published

Render any React component by dispatch and never mount it until it is needed: single or multi instance, handles for callbacks and refs beside Redux, keep-alive and saved component state, address-bar links bound to data, optional persistence, and a full wi

Readme

@ai-matrx/summon

Summon any React component from anywhere — with its handlers, refs and state — and never mount it until it is called.

  • Dispatch to open. Open a window, dialog, sheet or any component from anywhere with a hook or an action. A closed overlay puts nothing in the React tree and fetches no code.
  • Single or multi instance per overlay.
  • Handles beside Redux. Callbacks, event channels, one-shot requests and imperative refs live in a registry next to the store, owned by the open instance and disposed when it closes.
  • Keep-alive. On close an overlay unmounts (none), stays alive hidden (hidden), or unmounts and remembers its saved state (restore). useOverlayState is the saved-state hook.
  • Address-bar links. ?panels=notes:42 names an open overlay, loads its data and survives reload.
  • Persistence, off or on: browser-local, your own database through a small port, or @ai-matrx/data.
  • Surfaces. WindowPanel is a desktop in the browser (drag, resize, snap, arrange, tray with thumbnails, minimize, pop-out to a real browser window, three phone presentations). dialog, sheet and lite (a light floating window) share the same close binding.

The package owns its Redux slices. It runs on a private store, or mounts into the store you already have.

Install

pnpm add @ai-matrx/summon react react-dom react-redux @reduxjs/toolkit

| Peer | Needed for | |---|---| | react ≥ 19, react-dom, react-redux ≥ 9, @reduxjs/toolkit ≥ 2 | everything except the framework-free . and ./handles entries | | next ≥ 16 (optional) | only ./next | | html-to-image (optional) | tray thumbnails of minimized windows (without it the tile shows no preview) | | @supabase/supabase-js | only ./persistence/data, through @ai-matrx/data |

Entries: . (core, no React), ./handles (no React, no Redux), ./react, ./window, ./window-host (the root-mountable half of the window section: WindowsProvider, WindowTraySync, registerTrayPreview — never loads WindowPanel), ./dialog, ./sheet, ./lite, ./url, ./persistence, ./persistence/data, ./next, ./testing, ./styles.css.

Quick start

1. Provider and host, once, near the root.

"use client";
import { SummonProvider, SummonHost } from "@ai-matrx/summon/react";
import { APP_OVERLAYS } from "./overlays";

export function Root({ children }: { children: React.ReactNode }) {
  return (
    <SummonProvider>
      {children}
      <SummonHost definitions={APP_OVERLAYS} />
    </SummonProvider>
  );
}

2. Declare an overlay once: a light definition file (safe to import anywhere) and a render module (the lazy chunk).

// notes.overlay.ts
import { defineOverlay } from "@ai-matrx/summon/react";

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 arrives as untrusted Readonly<Record<string, unknown>> | null (it crossed Redux, a URL or storage). Narrow it in the render module, or give the definition a parse(raw) that returns the typed value (or null to refuse and close with a warning).

// overlays.ts
import { notesWindow } from "./notes.overlay";
export const APP_OVERLAYS = [notesWindow];

3. Open it.

import { notesWindow } from "./notes.overlay";

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() returns { overlayId, instanceId, close() }. The definition also gives you useIsOpen(instanceId?), useClose() and, inside the render tree, useEmit().

Definition options

| Field | Meaning | |---|---| | id, label | id is the registry key; label shows in the tray, menus and diagnostics | | instance | "single" (default, instance id "default") or "multi" (a fresh id per open, or pass instanceId) | | keepAlive | "none" (default), "hidden" or "restore" | | closesOnNavigation | close when the route changes (the overlay shows a piece of the page that opened it) | | defaults | merged under every open's data | | window | { mobile, mobileSidebarAs, ephemeral, preservation, restoreDefaults, windowId, deprecated, unbiddenHome } for window overlays | | link | { key, aliases?, hydrate }, see Links | | parse | narrow raw data once; null refuses | | render | () => import("./x.render"); optional when the host supplies it (see below), and wins over the host map when present |

Minimizing a window always keeps its component mounted, whatever keepAlive says.

Keep-alive and saved state

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": closing hides the live component (<Activity mode="hidden">); reopening shows the same instance. At most keepAliveLimit (default 8) closed instances stay mounted; the oldest is evicted and released.
  • keepAlive: "restore": closing unmounts; useOverlayState values come back on reopen (and persist when persistence is on).
  • useOverlayState values must be serialisable. Any descendant of the overlay may call it.

Callbacks without functions in Redux

Declare an event map and pass plain functions when opening. They travel through an event channel owned by the instance; only its id rides in the data.

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 });

Closing the instance disposes the channel. Emitting after that is a silent no-op.

Large apps: the render map lives on the host

Every file that imports a definition to open it also imports that definition's render: () => import(…). To a bundler, each such file gains an edge to the overlay's whole window subtree. With a few overlays that is free. With hundreds of overlays and hundreds of opener files it multiplies the build graph: in AI Matrx, about 450 opener files each reached about 220 window subtrees, and a sub-app build went from about 10 s to an out-of-memory kill. This is the host-build law: never fan out import() edges.

The fix keeps "declare once" at two files plus one line. Leave render off the definitions, and give the host one id → import map, generated from the file names, that only the host imports:

// 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 erases each module's data type (OverlayRenderProps<never>), so differently typed render modules share one map without any. An overlay that is opened with no render source at all (no render, no map entry) shows its error card (OverlayRenderMissingError, exported as MISSING_RENDER_ERROR) and logs in development. It is never a silent blank.

Mounting into an existing Redux store

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>

Pass the same ports object to both. The overlays slice keys are fixed. Without store, the provider makes a private store (createSummonStore({ ports })).

Provider knobs: fallbacks (loading, error components), errorReportExtras (extra controls in the error card), renderWrapper (wrap every rendered overlay), keepAliveLimit (8), importTimeoutMs (12 s; a render module that has not loaded becomes a ChunkLoadError with Try again), stallWarnMs (10 s).

Ports

Every port has a working default, so a bare host needs none. Override only what you have; pass them as ports on the provider (a partial object).

| Port | Shape | Default | |---|---|---| | identity | current(): { key, ready }, subscribe(listener) | anonymous, always ready. A changing key swaps all saved state | | router | pathname(), search(), replaceSearch(next), subscribe(listener) | window.location + history.replaceState (inert on the server) | | notify | info, warn, error(message, options?), optional dismiss(id) | console | | errors | capture(error, context) | console.error | | debug | enabled(), optional subscribe | off | | persistence | an OverlayPersistencePort, or null | null (persistence off) |

Next.js: import { useNextRouterPort } from "@ai-matrx/summon/next" returns a stable RouterPort (call it inside a client component under Suspense; it reads useSearchParams). Testing fakes: see Testing.

Links

OverlayLinkManager keeps ?panels= and open overlays in sync through ports.router. Mount it once inside the provider.

import { OverlayLinkManager, useOverlayLink } from "@ai-matrx/summon/url";

<SummonProvider><OverlayLinkManager /><SummonHost definitions={APP_OVERLAYS} /></SummonProvider>

Give the definition a link:

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
  },
});
  • Opening /page?panels=notes:42 runs hydrate({ id: "42", args, open, dispatch }). hydrate may be async (load a record, then open).
  • A mounted overlay publishes its own address with useOverlayLink("notes", noteId, { tab: "history" }). WindowPanel does it for you from its urlSyncKey, urlSyncId and urlSyncArgs props.
  • Unknown keys are announced at once; tokens whose overlay never registers are announced at a deadline and kept verbatim.
  • Grammar v2: panels = token ("," token)*, token = key[:id[:k-v(_k-v)*]]. Ids are percent-encoded for % , :, arg keys and values for % , : _ -, so any value survives; v1 links still parse. Helpers: parseLinkToken, parseLinkParam, formatLinkToken, readPanelsParam, withPanelsParam.
  • One door: openFromLink(searchOrUrl, { dispatch, requireAll?, onError? }) returns { opened, refused, unknown, failed } and serves the address bar, notifications and any bridge. requireAll: true opens nothing if any key is unknown. openPanelRequest(payload, { dispatch }) validates and opens a { panelId, instanceId?, data? } request from a bridge (returns { ok: true, … } or { ok: false, error: "invalid_payload" | "unknown_panel" }).

Persistence

Off by default. Turn it on with a persistence port plus one mounted component:

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>

What is saved: the person's open windows and layout (the workspace), and the component state of instances that are keepAlive: "restore" or preserved windows. Window data is saved only for the keys a window lists in window.preservation.dataKeys (default-deny); window.ephemeral: true never saves. Caps: 256 KiB workspace, 64 sessions, 32 KiB per window's data and per state entry; overruns report through ports.errors.capture (source: "overlay-persistence").

<OverlayPersistence> options: scope ("tab" one workspace per browser tab, "person" one per person; default "tab" for the local store, "person" for per-person stores), seedFromPerson (a new tab starts from the person's last layout; default true), saveDebounceMs (250), viewport. useOverlayPersistence(options) returns { hydrated }.

Browser-local store. createLocalPersistence({ namespace?, backend?, storage?, session?, locks?, idbReadBudgetMs?, isReload?, now? }): a localStorage mirror plus IndexedDB, per identity, per tab workspace with leases. It also has purgeIdentity(identityKey) for sign-out cleanup.

Your own database. Implement the port:

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
};

Keys written: workspace, workspace:<tabWorkspaceId> (tab scope), state:<overlayId>:<instanceId>. Values are JSON. Suggested table:

| Column | Type | | |---|---|---| | person_id | uuid | owner; rows are read and written only for the signed-in person | | key | text | one of the keys above; unique with person_id | | value | jsonb | the saved value | | version | int | starts at 1, +1 per save (optimistic concurrency) |

On a conflict the controller re-reads the version and writes once more, so the latest device wins. Version conflicts are recognised by error name (isVersionConflict(error)).

@ai-matrx/data hosts. createDataPersistence({ db, userId?, table?, feature? }) from @ai-matrx/summon/persistence/data maps each key to a row of users.user_surface_state (feature = "summon", surface_key = key, state = value, version), through the data package's doors. The organization comes from createDb({ client, organization }) and the insert is refused when there is none.

import { createDb } from "@ai-matrx/data/db";
import { createDataPersistence } from "@ai-matrx/summon/persistence/data";

const persistence = createDataPersistence({ db: createDb({ client: supabase, organization: () => activeOrgId }) });

Handles

@ai-matrx/summon/handles is framework-free. State lives in one global slot, so duplicated bundles share one registry.

  • callbackManager (verbs): register, registerWithContext(callback, { context, groupId, owner }), subscribe, unsubscribe, trigger, triggerWithContext, triggerGroup, triggerGroupCommand, updateProgress, createGroup, removeGroup, remove / unregister, get, has, registerWidgetHandle, ownedBy, groupMembers, disposeOwner, size, subscribeToRegistry.
  • Typed helpers, each accepting { 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();

Owner disposal. When an instance closes, everything registered with its overlayId:instanceId as owner is disposed: pending requests reject, channels drop subscribers, handles are removed. No opener disposes by hand. Disposing a group member removes it from the group.

WindowPanel

import { WindowsProvider, WindowPanel } from "@ai-matrx/summon/window";

Slots contract: the body is content only. Everything that is window chrome goes in a slot, never in the body.

| Slot | Props | |---|---| | Title | title, titleNode | | Header actions | actionsLeft, actionsRight | | Footer | footer, or zoned footerLeft / footerCenter / footerRight; footerVariant: "bar" \| "rich" | | Left sidebar | sidebar, sidebarDefaultSize, sidebarMinSize, defaultSidebarOpen, sidebarExpandsWindow | | Right panel | secondaryPanel, secondaryPanelOpen, secondaryPanelDefaultSize, secondaryPanelMinSize |

Close binding is required and checked by the type system: exactly one of overlayId (close that overlay), boundToOverlay (close the enclosing SummonHost instance) or onClose (page-local). A window with none of them does not compile.

Other props: id, width/height (numbers or "60vw"), minWidth/minHeight, position, fitContent, closeOnEscape, hidePopOutButton, urlSyncKey/urlSyncId/urlSyncArgs, onCollectData, captureTraySnapshot.

Behaviour you can rely on: maximize, minimize, pop-out and dock never remount the body (state, refs and effects survive; minimize keeps the body mounted and captures the tray thumbnail). Pop-out uses a real browser window (Document Picture-in-Picture where available). On phones a window renders as fullscreen, drawer or card per the definition's window.mobile.

WindowsProvider is optional; options: escapeYieldSelector (visible elements matching it own the Escape key; default [data-overlay-escape-yield]), mobileTopOffset (default var(--header-height, 0px)), selectionSurface (a component wrapping every window body, for a host's selection or right-click menu).

Also exported from ./window: WindowTraySync and registerTrayPreview (a semantic preview for a minimized tile; both also on the light ./window-host entry — mount providers from there at the app root), LayoutIcon, usePopoutControl, MobileDrawerSurface, MobileCardSurface, useWindowPanel.

Dialog, sheet, light window

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 and OverlayBottomSheet take the design-system component's props plus the close binding: boundToOverlay (open while the instance is shown) or onClose (+ open). LiteWindow has drag, edge resize, Escape when topmost, focus in and out, and a bottom drawer on phones; no minimize, tray, persistence or links.

Styling

Import the structural CSS once, and make Tailwind v4 scan the package, since the components use utility classes:

@import "tailwindcss";
@import "@ai-matrx/summon/styles.css";
@source "../node_modules/@ai-matrx/summon/dist"; /* path relative to this CSS file */

Colors come only from @ai-matrx/design-system tokens, so the host's theme applies. The traffic-light colors are --matrx-window-light-* custom properties you can override.

Testing

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 gives a controllable identity (set(key, ready?)), a controllable router (navigate(path, search?)) and recording notify and errors (messages, captured). createMemoryPersistence is an in-memory, versioned port. No test-runner dependency.

Gates

pnpm typecheck (also compiles the package with a host-augmented registry), npx vitest run, pnpm check:package.