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

@riteofstring/onirigiri

v0.3.0

Published

A reusable React window system with tiled panes, overview navigation, resizing, and keyboard layout commands.

Readme

Onirigiri is a React window system for spatially arranged panes. You supply pane content; Onirigiri supplies tiled columns and rows, keyboard navigation and rearrangement, a zoomable overview, resizing, maximization, a compact mobile pager and serializable layouts.

Onirigiri is in beta. The API is settling; see Known issues.

Browser support

| Capability | Requirement | | ------------------------------------------------------ | ----------------------------------------------------------------------------- | | Workspace, navigation, overview, native content | Current Chrome and Edge; Firefox and Safari pass smoke checks only | | Retained pane pictures and HTML-in-Canvas presentation | Chrome with WebGPU and HTML-in-Canvas (chrome://flags/#canvas-draw-element) |

Without WebGPU or HTML-in-Canvas nothing is blocked: panes present their native content, requested canvas or texture presentation falls back to native, and panes without a picture show a placeholder while moving or in overview.

Install

pnpm add @riteofstring/onirigiri

React 18.2 or 19 is required as a peer dependency.

Quick start

import {
  OnirigiriWorkspace,
  type OnirigiriPaneDefinition,
} from "@riteofstring/onirigiri";
import "@riteofstring/onirigiri/styles.css";

const panes: OnirigiriPaneDefinition[] = [
  { paneId: "docs", surfaceKind: "document", title: "Documentation" },
  { paneId: "preview", surfaceKind: "preview", title: "Preview" },
];

export function App() {
  return (
    <div style={{ height: "100vh" }}>
      <OnirigiriWorkspace
        initialPanes={panes}
        renderPane={(pane) => <YourPane id={pane.paneId} />}
      />
    </div>
  );
}

The workspace fills its container, so give the container a height. Press Alt + arrow keys (Option on macOS) to move between panes, and Alt + O to toggle the overview.

Core ideas

Panes are data. Each pane has a paneId, a title and a surfaceKind (your own content type, such as "editor" or "video"). Put anything your content needs in data. Layouts are plain JSON, so they can be saved, restored and synchronized.

You render the content. renderPane(pane, state) returns ordinary React. Onirigiri keeps that subtree mounted and moves it; it never clones or serializes it. state tells content how it is being shown:

| Field | Meaning | | ------------------ | ------------------------------------------------------------------------- | | runtimeState | "live", "frozen" (moving or overview) or "hidden" (offscreen) | | visible | The pane intersects the viewport | | focused | The pane has workspace focus | | maximized | The pane fills the workspace | | presentationMode | "normal" or "overview" | | content | Content-fit preferences from pane defaults |

Pause expensive work when a pane is not live:

renderPane={(pane, state) => (
  <Chart series={pane.data as Series} paused={state.runtimeState !== "live"} />
)}

Everything is configured with props. Every option is a plain value or callback on <OnirigiriWorkspace>; there are no providers or global setup.

Recipes

Save and restore the layout

import {
  assertOnirigiriLayout,
  type OnirigiriLayout,
} from "@riteofstring/onirigiri";

function savedLayout(): OnirigiriLayout | null {
  try {
    const layout = JSON.parse(localStorage.getItem("workspace") ?? "null");
    if (layout) assertOnirigiriLayout(layout);
    return layout;
  } catch {
    return null;
  }
}

<OnirigiriWorkspace
  initialLayout={savedLayout()}
  initialPanes={panes}
  onLayoutChange={(layout) =>
    localStorage.setItem("workspace", JSON.stringify(layout))
  }
  renderPane={renderPane}
/>;

onLayoutChange reports initial, user and restore changes with a mutation ID so one resize gesture can become one undo entry. Camera movement and presentation changes do not emit layout changes. initialLayout and restoreLayout throw on an invalid document, so validate stored layouts with assertOnirigiriLayout first, as above.

Control the workspace from code

const workspace = useRef<OnirigiriWorkspaceHandle>(null);

<OnirigiriWorkspace
  ref={workspace}
  initialPanes={panes}
  renderPane={renderPane}
/>;

workspace.current?.openPane({
  title: "Report",
  surfaceKind: "document",
  data: report,
});
workspace.current?.focusPane("docs");
workspace.current?.toggleOverview();

See Imperative handle for every method.

Size panes by content type

<OnirigiriWorkspace
  paneDefaults={{ width: 480, height: "viewport" }}
  paneTypeDefaults={{
    video: { width: "auto", aspectRatio: 16 / 9, content: { fit: "contain" } },
    editor: { width: 900, minWidth: 360 },
  }}
  {...props}
/>

Workspace defaults are overridden by type defaults, then by a pane's own defaults. Details: Pane defaults.

Choose resize handles

Panes get right and bottom resize handles by default. resizeEdges picks any combination of "left", "right", "top" and "bottom" for the workspace, a content type or a single pane:

import { defaultPaneResizeEdges } from "@riteofstring/onirigiri";

<OnirigiriWorkspace
  paneDefaults={{ resizeEdges: [...defaultPaneResizeEdges, "left", "top"] }}
  initialPanes={[
    { paneId: "home", surfaceKind: "page", title: "Home" },
    {
      paneId: "hover",
      surfaceKind: "game",
      title: "Hover!",
      defaults: { resizeEdges: ["right"] },
    },
  ]}
  {...props}
/>;

Dragging a left or top edge keeps the opposite edge still. Details: Pane defaults.

Link to a pane

paneLink keeps the focused pane in the URL, so https://example.com/?pane=hover opens the workspace on the pane whose paneId is hover, wherever it sits:

import { onirigiriPaneHref } from "@riteofstring/onirigiri";

<OnirigiriWorkspace paneLink initialPanes={panes} renderPane={renderPane} />;

const href = onirigiriPaneHref("hover"); // current URL with ?pane=hover

Pass { param: "window", history: "push" } to rename the parameter or record each focus change as a history entry. Details: Navigation and layout.

Theme it

import { defineOnirigiriTheme } from "@riteofstring/onirigiri";

const theme = defineOnirigiriTheme({
  "--onirigiri-pane-radius": "16px",
  "--onirigiri-focus": "oklch(0.72 0.17 250)",
});

<OnirigiriWorkspace tokens={theme} {...props} />;

classNames and styles target individual slots, and chromeComponents swaps in your design system's buttons. Details: Styling.

Use your own components (for example shadcn/ui)

Onirigiri renders plain accessible buttons and no tooltips. Overrides receive the button props to forward plus a typed descriptor with the label, availability and platform-formatted shortcuts (primary binding first). A pane action override may return null to omit that action.

const chromeComponents = defineOnirigiriChromeComponents({
  WorkspaceControlButton: ({ control, ...props }) => (
    <Tooltip>
      <TooltipTrigger asChild>
        <Button variant="ghost" size="icon" {...props} />
      </TooltipTrigger>
      <TooltipContent>
        {control.label}
        {control.shortcuts[0]?.keys.map((key) => (
          <kbd key={key}>{key}</kbd>
        ))}
      </TooltipContent>
    </Tooltip>
  ),
  PaneActionButton: ({ action, ...props }) =>
    action.id === "maximize" ? null : <Button size="icon-sm" {...props} />,
});

<OnirigiriWorkspace chromeComponents={chromeComponents} {...props} />;

With showControls={false}, getOnirigiriShortcutBindings and formatOnirigiriShortcut give your own control bar the same shortcut data. Details: Styling.

Keep content animating while navigating

<OnirigiriWorkspace liveContent {...props} />

liveContent keeps every pane mounted and live through navigation and overview. It costs memory and CPU proportional to the number of panes, so prefer it for small to medium workspaces. Without it, only nearby panes stay mounted and applications receive runtimeState: "frozen" while the camera moves.

Choose how panes are presented

<OnirigiriWorkspace
  panePresentation="auto"
  getPanePresentation={(pane, context) =>
    pane.surfaceKind === "map" && context.phase === "overview"
      ? { kind: "texture", detail: "preview" }
      : undefined
  }
  {...props}
/>

"auto" (the default) keeps content native, which is the fastest option on every machine we measured. Per pane and phase you can request "dom", "canvas" (HTML-in-Canvas), { kind: "texture" } (a frozen picture) or { kind: "placeholder" }. Details: Presentation.

Customize shortcuts

import { defaultOnirigiriShortcuts } from "@riteofstring/onirigiri";

<OnirigiriWorkspace
  shortcuts={{
    toggleOverview: [
      ...defaultOnirigiriShortcuts.toggleOverview,
      {
        key: "F3",
        altKey: false,
        ctrlKey: false,
        metaKey: false,
        shiftKey: false,
      },
    ],
    splitDown: [],
  }}
  shortcutScope="application"
  {...props}
/>;

Actions you omit keep their defaults, and an empty array disables an action.

shortcutScope="workspace" (the default) handles keys only while focus is inside the workspace; "application" handles them page-wide. Pass shortcuts={false} to disable all of them. Details: Navigation and keyboard.

Props

Only renderPane is required.

Content and layout

| Prop | Default | Purpose | | ------------------------------------- | ----------------------- | ------------------------------------------------------- | | renderPane | — | (pane, state) => ReactNode for each pane's content | | initialPanes | [] | Panes for a new workspace | | initialLayout | null | A saved layout; takes precedence over initialPanes | | onLayoutChange | — | Receives every committed layout with change metadata | | onPaneClose | — | Return false (or a promise of it) to keep a pane open | | paneDefaults | — | Size, content fit and resize edges for every pane | | paneTypeDefaults | — | Defaults keyed by surfaceKind | | paneLimits | — | Maximum number of panes per surfaceKind | | allowResizedPanesToOverflowViewport | false | Let resized panes grow wider than the viewport | | keepHeightWhenSplitting | false | Split a pane up or down within its current height | | workspaceId | "onirigiri-workspace" | Identity stored in layouts |

Navigation and camera

| Prop | Default | Purpose | | -------------------------------------------------- | ------------ | ---------------------------------------------------------------- | | focusAnchor | "center" | Center the focused pane, or keep it at the "start" edge | | gridAxes | "spatial" | "horizontal" restricts navigation to one row of columns | | cameraModes | follow both | "fixed" or "follow" camera per presentation mode | | cameraMotion | follow | Curves for navigation, overview moves and the overview zoom | | focusHighlight | true | false hides the highlight; { motion } sets how it moves | | showMinimap | false | Show a minimap of pane boxes that focuses the pane you click | | minimapPlacement, onMinimapPlacementChange | bottom-right | Minimap corner and size, and a callback to persist changes | | minimapAdjustable | true | Let people move the minimap between corners and resize it | | cursorRunway | — | Limit empty-cell navigation to N cells around the panes | | paneLink | — | Keep the focused pane in the URL query; true or options | | directionControlMode | "focus" | What the on-screen arrows do: focus, move a pane or move a group | | onPresentationModeChange | — | Called when entering or leaving overview | | onPaneRearrangementSelectionChange | — | Called when pane or group rearrangement selection changes | | overviewCardMinWidthPx, overviewCardMaxWidthPx | — | Bounds for pane size in overview |

Controls, keyboard and compact layout

| Prop | Default | Purpose | | -------------------------- | ----------------------- | --------------------------------------------------------- | | showControls | true | Show the on-screen navigation controls | | showOverviewControl | true | Show the overview button | | desktopControlsContainer | — | Render the desktop controls into your own element | | shortcuts | defaults | Key bindings, or false to disable | | shortcutScope | "workspace" | "workspace" or page-wide "application" | | compactBreakpoint | 640 | Width in pixels below which the workspace becomes a pager | | compactPanePeek | 0 | Pixels of neighboring panes visible in the compact pager | | ariaLabel | "Onirigiri workspace" | Accessible name of the workspace region |

Styling

| Prop | Purpose | | ---------------------- | ----------------------------------------------------------------- | | tokens | Theme tokens (defineOnirigiriTheme) | | className, style | Class and style for the workspace root | | classNames, styles | Per-slot classes and styles | | chromeComponents | Replace buttons and controls with your design system's components |

Presentation and pictures

| Prop | Default | Purpose | | -------------------------- | -------- | ----------------------------------------------------------------- | | liveContent | false | Keep every pane mounted and live through motion and overview | | panePresentation | "auto" | Workspace-wide presentation choice | | getPanePresentation | — | Per-pane, per-phase presentation choice | | renderPanePlaceholder | icon | Custom placeholder content | | getPanePicture | — | Supply your own pictures for panes that are not mounted | | onPanePresentationChange | — | Observe each pane's effective presentation | | onCaptureStatusChange | — | Observe picture capture, including "unavailable" and its reason |

Advanced tuning, rarely needed: retainedAreaBudgetViewports (8), preloadMarginPanes (1), preloadAllPanePictures (false), captureMissingOverviewPictures (false), pictureBudgetBytes (256 MiB), pictureRasterBudgetBytes (256 MiB), pictureMinLongEdgePx (128). See Presentation.

Imperative handle

Attach a ref to <OnirigiriWorkspace> to receive an OnirigiriWorkspaceHandle.

| Area | Methods | | ------------- | ------------------------------------------------------------------------------------------------------------ | | Panes | openPane, openPaneNear, configurePane, renamePane, closePane | | Splits | splitPane, splitPaneToPlane, insertPaneAsSplit, createReservedBlankSplit, removeReservedBlankSplit | | Focus | focus, focusPane, focusColumn, returnHome, setFocusAnchor | | Rearrangement | movePane, movePaneGroup, selectPaneGroup, clearPaneRearrangementGroup | | View | toggleOverview, resizePanes("default" \| "fit-content" \| "minimum" \| "full") | | Layout | getLayout, restoreLayout, getScene, getSnapshot | | Pictures | getCaptureStatus, getPanePresentationState, refreshPanePictures |

restoreLayout validates the whole document first and throws without changing the workspace if it is invalid.

Guides

Development

The interactive 1D and 2D demo playground lives in playground/, and its lab panes come from browser-surface-lab. Run pnpm dev:1d or pnpm dev:2d; see Development.

Known issues

  • Retained pictures and HTML-in-Canvas presentation require Chrome with WebGPU and the HTML-in-Canvas flag. Elsewhere, panes that are not mounted show placeholders during motion and overview.
  • Chrome copies element images with a synchronous GPU readback. Explicit "canvas" presentation of content that changes every frame therefore cannot hold high refresh rates on discrete GPUs; the default "auto" presentation avoids it.
  • Occasional single dropped frames remain when navigation starts while applications initialize or pictures are captured. In branded Chrome, moving pane content that contains form controls schedules a password-manager scan about 100 ms later, which can drop a frame if motion has started.
  • Captured text can differ from native text at glyph edges on Linux.

License

Licensed under the Apache License 2.0.