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/canvas

v0.10.81

Published

The AI Matrx canvas: a docked right-hand column with split panes, tabs, full screen, a kind registry, Redux-compatible state and persistence.

Readme

@ai-matrx/canvas

A docked right-hand canvas column with split panes, tabs, expand, identity-based deduplication, persisted layout and ONE pane chrome. Core state is framework-independent (it runs in an Electron main process); React components ship through ./react.

import "@ai-matrx/canvas/styles.css";
import "@ai-matrx/canvas/tokens.css"; // follows a design-system theme; else built-in light/dark values
import { CanvasProvider, CanvasFrame, CanvasToggle, registerCanvasKind, defineCanvasKind } from "@ai-matrx/canvas/react";

registerCanvasKind(defineCanvasKind({ id: "note", label: "Note", icon: NoteIcon, load: () => import("./NoteView") }));

<CanvasProvider>
  <CanvasFrame><App header={<CanvasToggle />} /></CanvasFrame>
</CanvasProvider>

NoteIcon, NoteView and App are your host components. A Redux host passes store={bindCanvasToReduxStore(store, (s) => s.canvasHost)} after mounting canvasReducer. Hosts own identity, navigation, placement and token values; the package owns layout, pane/tab behavior, chrome and restoration. Item data must be plain JSON; failures are reported through onError.

Entries: core (.), React (./react), structural CSS (./styles.css) and default theme values (./tokens.css).

A kind

defineCanvasKind<{ sessionKey: string }>({
  id: "terminal",
  label: "Terminal",
  icon: TerminalIcon,
  load: () => import("./TerminalView"),
  restore: "session",          // closing detaches; a restore or a move reattaches
  onAttach: ({ sessionKey, reason }) => attach(sessionKey, reason),
  onDetach: ({ sessionKey }) => detach(sessionKey), // never kill here
  HeaderLeading: NewTerminalButton, // "+ new terminal", a browser bar…
  HeaderAction: ClearButton,
  unavailable: () => (backendUp() ? null : "Terminal · next release"),
});

body: "native" makes the body a placeholder whose frame the host receives (below).

Bodies never dismount

Every open item's body is rendered once, in the column's body layer, and placed over its pane by measured rect. Switching tabs hides it; splitting, moving a tab between panes, dragging the width, expanding and putting the canvas away only re-place or hide it. React state, focus and scroll survive everything but closing the tab. A body mounts the first time it is shown. isVisible tells it whether it is on screen (pause polling while false); unmountWhenHidden: true opts a genuinely heavy body out.

Content that knows its shape

function GraphView({ presentation }: CanvasKindProps) {   // or useCanvasPresentation() anywhere inside the body
  return <Graph direction={preferredFlowDirection(presentation) === "vertical" ? "TB" : "LR"} />;
}

presentation = { host: "canvas", width, height, orientation: "portrait" | "landscape" | "square", isNarrow, isFullscreen, paneCount }, live. useCanvasPresentation() returns it to any nested component and null outside the canvas. When you have a lot of vertical space and little horizontal space, adapt: lay graphs, timelines and card rows out top to bottom, stack side-by-side columns, shorten toolbars.

Tabs, New tab and Back

Every pane header has "+" (a launcher tab). Choosing in the launcher NAVIGATES that tab in place, so the header offers Back. Do the same from a kind:

canvas.navigate(item.id, { kind: "doc", key: docId, data: { docId } }); // records history; Back returns
canvas.back(item.id);
canvas.forward(item.id); // after Back: the same live body Back left

Navigating to something already open elsewhere focuses it there (a launcher tab closes; any other tab keeps what it shows). History is plain JSON and persists. A lone tab shown as a title keeps its own close.

Back never throws a body away. The newest CANVAS_KEEP_ALIVE_DEPTH (5) steps Back can reach, and the 5 most recent contents Back stepped away from (item.kept), keep their bodies mounted and hidden. Back returns the SAME live body; choosing a content Back left (Quick Chat again from the launcher) revives its body — a typed draft is still there. Older steps still work but mount fresh. Closing the tab disposes every kept body.

Forward. After Back the header also shows Forward (selectCanvasCanGoForward); it returns the same live body Back left. A navigate somewhere new ends the forward trail. Shortcut data canvas.back / canvas.forward (⌘[ / ⌘]) is for a desktop host; browsers own those keys.

The strip (Chrome's model). Tabs share the strip evenly — 12rem each with room — and shrink together to one readable floor, icon + ~10 letters + "…" (CANVAS_TAB_MIN_WIDTH, 120px); a cut name always ends in an ellipsis and every tab's tooltip is its full name. Past the floor the strip draws only the tabs that fit WHOLE, a run around the current tab that moves as little as possible (fitCanvasTabWindow), and "All tabs (N)" lists every tab and shows how many are out of the strip — a tab is never sliced at an edge (JetBrains' single-row tabs; a scrolling strip, as in VS Code or Firefox, leaves half-tabs at both edges). The arrow keys walk every tab. A tab never shrinks to an icon sliver and nothing in the header overlaps another control.

Splits. A split canvas keeps ONE window-wide tab strip above its panes (CanvasWindowTabs), never one per pane: every pane's tabs are in it, the panes' visible tabs are drawn as ONE joined tab (the focused pane's half strongest; planCanvasTabStrip), and its "+" opens in the focused pane. Each pane header keeps its own Back/Forward, title, kind controls and menu. A side-by-side split whose panes would be narrower than paneMinWidth (web: CANVAS_PANE_MIN_WIDTH, 280px) renders stacked under the same one strip; the tree is unchanged. Every pane keeps paneMinHeight (CANVAS_PANE_MIN_HEIGHT, 200px): a split draws no child shorter (fitCanvasSplitSizes), and a new split that cannot give both panes their minimum goes the other axis, or the new tab joins the focused pane (fitCanvasSplit; the pane menu offers only splits that fit).

Phones (CANVAS_PHONE_MAX_WIDTH, 767px): the column presents ONE pane — the focused one when it holds a tab, else the first that does (selectCanvasPresentedPaneId) — whose strip carries every pane's tabs; choosing a tab from another pane presents that pane. The pane menu offers no Split. The layout tree is untouched, so a wider screen gets its split back.

One pane menu

A pane has ONE menu: the kind's menuItems, then what the body offers right now, then the host's itemActions, then Split / Pop out / Close. A body adds its own actions — never a second overflow button in the header:

useCanvasMenuItems(item.id, [{ id: "next", label: "Next record", onSelect: goNext }]);

A kind's HeaderAction sits on the solid pane header: draw tap-target buttons with TapTargetButtonTransparent (glass only floats).

A page's own launcher entries

useCanvasLauncherEntry({ kind: "documents", key: conversationId, data: { conversationId }, title: "This chat's documents" });

Shown first ("On this page") while the calling component is mounted.

Where a new item opens

defineCanvasKind({ id: "notifications", preferredTarget: "split-down", /* … */ });

While the canvas already shows something, a NEW item of the kind opens below (the layout rules' split share, 70/30) or beside; a pane already showing the kind takes it. An explicit target wins. open and toggleKind honor it.

Controls inside the pane header, and "no canvas here"

A kind's HeaderAction can be KindHeaderSlot; its body then draws controls into the header with <KindHeaderPortal itemId={item.id}> (context is kept; useKindPaneHeaderWidth reports the header width). Wrap an immersive viewer in <CanvasUnavailableBoundary>: inside it useCanvasIsPresented() is false.

A header's tool icons

<CanvasKindIcons kinds={["terminal", "review", { kind: "browser", label: "Browser" }]} variant="glass" />

Each icon toggles or focuses its kind and is pressed while its item is in front.

Motion

The column animates its width on --mxc-motion-duration / --mxc-motion-ease (tokens.css: 600ms, cubic-bezier(0.4, 0, 0.2, 1)), with its content keeping its width so it slides. A host that reflows beside it moves in lockstep on the same pair:

.app-shell { right: var(--canvas-w); transition: right var(--mxc-motion-duration) var(--mxc-motion-ease); }
:root[data-mxc-resizing] .app-shell { transition: none; }               /* the edge is being dragged */
@media (prefers-reduced-motion: reduce) { .app-shell { transition: none; } }

<CanvasFrame> needs nothing: its main area is a flex sibling and follows the column every frame.

A toolbar row under the header

A kind's Toolbar renders between the pane header and its body for the active item (a browser bar). It is DOM, and a native body's frame is measured below it.

Toolbar icons: toggle or focus a kind

const terminal = useCanvasKindToggle({ kind: "terminal" });
<button aria-pressed={terminal.isVisible} onClick={terminal.toggle}>…</button>
// outside React (Electron main, a menu handler):
toggleKind(controller, { kind: "terminal" }); // absent → open · behind → focus · in front → close

whenVisible: "hide" puts the canvas away instead of closing the tab.

Chrome for panels a host frames itself

CanvasPanel (title or header, leading, actions, renderMenu, onToggleExpand, onClose), CanvasPanelEmpty, CanvasHeaderButton, CanvasTab, BrowserTabs, BrowserBar — the same components every canvas pane draws with. They render outside a provider too.

The macOS look

<CanvasProvider look="mac"> draws every pane as a section of a Mac window instead of a card: the column flush to the window's edge behind one hairline, panes split by hairlines, and each pane's header the unified toolbar's band (--mxc-toolbar-h, default 52px) — the active tab's title centered in bold with its kind icon as the proxy icon (Terminal, Finder), or the kind's Toolbar in its place (Safari's address bar). "+" sits with the trailing controls; two or more tabs get macOS's tab bar under the band. A host that puts the header under its own window buttons sets --mxc-header-inset-start on the column. Default look="card" keeps the Claude desktop / Codex cards.

Layout rules

<CanvasProvider layoutRules={CANVAS_DESKTOP_LAYOUT_RULES} regionWidth={windowWidth - railAndSidebar}>

| Rule | Web (default) | Desktop | |---|---|---| | defaultWidth | 640 | { fraction: 0.36 } | | minWidth | 360 | 360 | | centreMinWidth (the canvas shrinks first) | 600 | 520 | | stackSplit (split down: first pane's share) | 0.7 | 0.7 | | sideSplit | 0.5 | 0.5 | | paneMinWidth (narrower side-by-side panes stack) | 280 | 360 | | paneMinHeight (no pane shorter; a split that cannot fit goes the other way or opens as a tab) | 200 | 200 |

Expand is one flag per window; it fills whatever the host placed the column in (useCanvasColumnWidth() returns null).

Electron

One store in the main process; every window's renderer is a client.

// main
import { createCanvasStore, createCanvasController, createKeyValueCanvasPersistence, isCanvasAction,
  CANVAS_DESKTOP_LAYOUT_RULES, CANVAS_SHORTCUTS, runCanvasCommand } from "@ai-matrx/canvas";

const store = createCanvasStore();
const canvas = createCanvasController({
  store,
  persistence: createKeyValueCanvasPersistence({ get: (k) => kv.get(k), set: (k, v) => kv.set(k, v) }),
  layoutRules: CANVAS_DESKTOP_LAYOUT_RULES,
  isRestorable: (kind) => kind !== "browser", // main has no kind registry: name the kinds that never come back
});
canvas.start(); // hydrates, then autosaves

ipcMain.handle("canvas:get", () => store.getState());
ipcMain.handle("canvas:dispatch", (_e, action: unknown) => { if (isCanvasAction(action)) store.dispatch(action); });
store.subscribe(() => { for (const w of BrowserWindow.getAllWindows()) w.webContents.send("canvas:snapshot", store.getState()); });

// Shortcuts as data: bind them where keys inside native views still arrive.
for (const s of CANVAS_SHORTCUTS) { /* menu accelerator or before-input-event → */ runCanvasCommand(canvas.forWindow(windowIdOf(win)), s.id); }
// A BrowserWindow that closes: its tabs go home.
win.on("closed", () => canvas.closeWindow(windowIdOf(win)));
// renderer (one per window)
import { createRemoteCanvasStore } from "@ai-matrx/canvas";

const store = createRemoteCanvasStore({
  initialSnapshot: await ipcRenderer.invoke("canvas:get"),
  dispatch: (action) => ipcRenderer.invoke("canvas:dispatch", action),
  subscribe: (onSnapshot) => {
    const on = (_e: unknown, s: CanvasState) => onSnapshot(s);
    ipcRenderer.on("canvas:snapshot", on);
    return () => ipcRenderer.off("canvas:snapshot", on);
  },
});

<CanvasProvider
  store={store}                    // remote: the main process hydrates and saves
  windowId={myWindowId}            // "main" for the first window
  layoutRules={CANVAS_DESKTOP_LAYOUT_RULES}
  regionWidth={rightOfSidebar}
  hotkeys={false}                  // main binds CANVAS_SHORTCUTS
  onNativeBodyFrame={(itemId, rect, { visible }) => ipc.send("view:place", itemId, visible ? rect : null)}
  showMenu={(entries, anchor) => ipc.invoke("menu:popup", entries, anchor)}   // Menu.popup → chosen id
  overlay={{ show: (el) => { ipc.send("overlay:show", el); return () => ipc.send("overlay:hide", el); } }}
  popOut={(item) => ipc.invoke("canvas:pop-out", item.id)}                    // → { windowId } of a new BrowserWindow
>
  <CanvasColumn />  {/* a pop-out window renders <CanvasColumn placement="fill" /> */}
</CanvasProvider>

Rects are window CSS pixels with devicePixelRatio, reported in the frame the layout changed (layout effects + ResizeObserver). Call useCanvasNativeBodies().remeasure() per animation frame while the host animates its own layout. visible: false means package DOM floats over the view (a DOM menu or a tab drag): hide it so the DOM is seen and receives the drop.

Web pop-out

popOut={async (item) => { const id = `pop-${item.id}`; openFloatingPanel(id); return { windowId: id }; }}
// the floating panel:
<CanvasWindowScope windowId={id}><CanvasColumn placement="fill" /></CanvasWindowScope>

When a pop-out window disappears from state.windows (docked back, last tab closed), close its panel.

MIT.