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

@laird-wt/portal

v1.12.1

Published

Publishable React game-input and virtual-controller component library.

Readme

Portal

Portal is a publishable React game-input and virtual-controller component library. It provides reusable button, input, slider, panel, HUD, and controller components, a decoupled input core, and an optional React Three Fiber presentation surface.

Demo

Portal RadialMenu: an octagonal radial menu of eight machined wedge sections (Attack, Defend, Item, Magic, Talk, Flee, Wait, Scan) around an octagonal center hub carrying a bold green confirm ring, a red cancel cross, and previous/next triangles, all framed in a lit accent rim over dark HUD glass.

The RadialMenu above is the flagship control: true wedge-shaped sections computed as clip-path geometry around a center hub that holds 0, 1, 2, or 4 drawn action symbols, with staggered open/close animation, paging through the center previous/next actions, and an inline collapsible form whose hub persists as the toggle. Its Tier-1 sibling RadialPad shares the same geometry and emits per-section input signals.

Portal game HUD: two octagonal arc gauges (hull integrity, and reactor output carrying a red redline band), a machined rotary throttle dial, a hotbar of keybound ability keys beside a radial cooldown chip mid-sweep, and a scanlined mono mission-log console, all on dark HUD glass.

The Patterns/Game HUD story wires the HUD set together through real state: hotbar presses arm the cooldown chip and append mission-log entries, the rotary dial drives the reactor gauge into its redline, and the cargo key opens an inventory-grid dialog whose moves are logged too.

Live, interactive Storybook (every control, the controller showcase with the R3F orb backdrop, and the Unity-binding example): https://lairdwt.github.io/portal/

Stack

  • React and TypeScript.
  • Vite in library mode.
  • CSS Modules with CSS custom-property theming.
  • Anime.js for UI motion.
  • React Three Fiber and Drei for the optional 3D surface.
  • Storybook, Playwright, and Vitest for review and testing.

Install

pnpm add @laird-wt/portal

React, React DOM, and Anime.js are required peer dependencies (React 18 or newer, Anime.js 4 or newer). Anime.js powers the motion presets exported from the package root, so it is required even if you do not use the 3D surface:

pnpm add react react-dom animejs

The shader core depends on three (it imports Color and the IUniform type), so three is a required peer whenever you import either portal/shaders or portal/r3f. The @react-three/fiber and @react-three/drei peers are needed only for the portal/r3f surface:

pnpm add three
pnpm add @react-three/fiber @react-three/drei

Portal is ESM-only. Use a bundler (Vite, webpack, Rollup, esbuild) or Node with ESM enabled, and a TypeScript moduleResolution of bundler, node16, or nodenext so the exports subpaths (./theme, ./r3f, ./shaders) resolve.

Usage

Import components from the package root and load the stylesheet once at your application entry point. The flagship RadialMenu in a few lines - an octagonal action wheel that collapses to its own themed hub toggle (the default inline form) with a cancel key in the middle:

import { type ReactElement, useState } from 'react';

import { ERadialAction, RadialMenu } from '@laird-wt/portal';
import '@laird-wt/portal/styles.css';

export function Example(): ReactElement {
    const [open, setOpen] = useState(false);
    return (
        <RadialMenu
            open={open}
            onOpen={() => setOpen(true)}
            onClose={() => setOpen(false)}
            label="Battle actions"
            sides={8}
            items={[
                { id: 'attack', label: 'Attack' },
                { id: 'defend', label: 'Defend' },
                { id: 'item', label: 'Item' },
                { id: 'magic', label: 'Magic' },
                { id: 'talk', label: 'Talk' },
                { id: 'flee', label: 'Flee' },
                { id: 'wait', label: 'Wait' },
                { id: 'scan', label: 'Scan' },
            ]}
            centerActions={[ERadialAction.Cancel]}
            onSelect={(id) => console.log('selected', id)}
        />
    );
}

The collapsed hub is the open/close toggle: it wears a drawn plus mark by default, or pass toggleIcon / toggleText for a custom icon or short text label. Set collapsible={false} for the portaled modal-overlay form driven by your own trigger. The radial also pages: pass more items than sides plus the ERadialAction.Previous / ERadialAction.Next center actions and the wheel pages through them. Sections take an optional icon (with iconOnly for pure glyph keys), and every mode honors the user's reduced-motion preference.

The optional 3D surface lives behind a separate entry point. Mount it inside a React Three Fiber <Canvas> (the 3D peers above must be installed):

import { Canvas } from '@react-three/fiber';

import { OrbBackdrop } from '@laird-wt/portal/r3f';

export function Backdrop(): ReactElement {
    return (
        <Canvas>
            <OrbBackdrop />
        </Canvas>
    );
}

Theming

Portal exposes design tokens as CSS custom properties namespaced with the --portal- prefix. Override any token on a containing element or on :root to retheme the components:

:root {
    --portal-color-accent: #3da9fc;
    --portal-radius-md: 0.75rem;
    --portal-touch-target-min: 3rem;
}

The theme entry point exports the token names (as var() references) for programmatic use - for example to feed a React Three Fiber material the same color the DOM uses:

import { PORTAL_TOKENS, type PortalTokens } from '@laird-wt/portal/theme';

Sizing uses clamp, min, max, and intrinsic units rather than fixed pixels or viewport-stretch units. Layout is safe-area aware through the env(safe-area-inset-*) values, touch targets meet the 3rem minimum, and motion respects the user prefers-reduced-motion setting.

Entry points

Portal ships several ESM subpath exports:

  • @laird-wt/portal - the component library and React hooks.
  • @laird-wt/portal/styles.css - the stylesheet; import once at your entry.
  • @laird-wt/portal/theme - PORTAL_TOKENS and the PortalTokens type.
  • @laird-wt/portal/r3f - the optional React Three Fiber surface (OrbBackdrop, ShaderSurface, isWebGlAvailable); requires the 3D peers.
  • @laird-wt/portal/shaders - the shader core behind the R3F surface (rippleGridShader, createRippleField, and the ShaderDescriptor contract) for consumers wiring their own renderer. It needs only the three peer, not the full @react-three/fiber/@react-three/drei stack.

Input and Unity binding

Portal's input core is decoupled from React and from any game. A control describes itself with an InputDescriptor (an opaque id, a value kind, and a label) and emits an InputSignal carrying the value, the interaction, and a timestamp. Controls share emit hooks so the wiring is uniform:

  • useDigitalPress and useEmitBinding for button-like inputs.
  • useAxis2DControl for absolute (joystick) and relative (thumbpad) pads.
  • useScalarControl for sliders.

Timestamps come from an injectable TimeProvider (default performance.now). Override it through TimeProviderContext to make emission deterministic in tests or to align the clock with a host application:

import { TimeProviderContext } from '@laird-wt/portal';

<TimeProviderContext.Provider value={() => engineClock.nowMs()}>
    <Controller />
</TimeProviderContext.Provider>;

Inputs are mapped to actions by a data-driven registry rather than hard-coded in the controls. The registry is immutable; each mutator returns a new registry, and a binding can be scoped to a named context (a profile such as menu or gameplay) with a global fallback:

import { createRegistry } from '@laird-wt/portal';

const registry = createRegistry([
    { inputId: 'fire', actionId: 'weapon.primary' },
    { inputId: 'fire', actionId: 'ui.confirm', context: 'menu' },
]);

registry.resolve('fire').actionId; // 'weapon.primary'
registry.resolve('fire', 'menu').actionId; // 'ui.confirm'
const rebound = registry.rebind('fire', 'weapon.special');

At the Unity boundary, toWireInput narrows a signal to a serializable FInputWirePayload (the descriptor collapsed to its id, plus the interaction, value, and timestamp) and throws if a value does not match the descriptor's declared kind. The Patterns/Unity Binding Storybook story wires a binding profile, a custom TimeProvider, and the wire codec together.

Generic UI components

A domain-agnostic UI layer, separate from the game-input controllers above. It emits plain value and selection callbacks and never the input-signal contract, so it suits any React UI, not only game input. It spans, among others:

  • Inputs and forms: CTA, TextField, TextArea, SecretField, SearchBox, Select, Combobox, TagInput, Checkbox, RadioGroup, SegmentedControl, NumberStepper, RangeSlider, Rating, ColorPicker, Toggle, OtpField, Field (form scaffolding), Calendar, DatePicker, TimePicker, FileUpload.
  • Data and navigation: List / SearchableList, DataTable, TreeView, Accordion, Tabs, Breadcrumb, Pagination, NavRail, Toolbar, Link, Menu / MenuBar / ContextMenu, CommandPalette, Wizard, RadialMenu (the flagship radial action wheel; RadialPad is its game-input sibling).
  • Surfaces and overlays: Panel, ReadoutPanel, Section, Dialog, Drawer (with resize snap points), Popover, Tooltip, Toast, Window, SplitPane, Carousel, Lightbox, DockLayout (the docking manager: a serializable split/tab/floating layout over SplitPane, Tabs, and Window).
  • Display and feedback: Text, Badge, Chip, StatPill, StatTile, KeyValue, the Chart family (bars, Sparkline, LineChart), Progress, Spinner, Banner, Avatar, Skeleton, EmptyState, Marquee, Scanlines, StepTrack, Timeline, TitleBar, StatusFooter, SelectableTile.
  • Game HUD: Gauge (radial arc meter with hub content, amount labelling, and shoulder bounds), Cooldown (unwinding ability scrim), LogConsole (virtualized scrollback with opt-in wrap, resizable time column, and a dockable fill mode), InventoryGrid (slot grid with keyboard grab/move, drag-reorder, and rectangular multi-cell item spans), Minimap (radar/map instrument with an optional sweep), Compass (heading strip), Reticle (decorative aiming glyph), FloatingText (combat-text layer), Odometer (rolling digit counter), DialogueBox (typewriter narrative line), ObjectiveTracker (quest list), plus the signal-emitting Dial, Hotbar, and VirtualKeyboard on the game-input side.
  • Layout: Stack, Grid, Divider.

See the live Storybook (linked above) for the full, current catalogue with interactive examples.

Colour comes from one opaque tone prop rather than a fixed palette. A component sets --portal-tone from it and derives its accent, border, glow, and fill, so a consumer maps any domain palette without per-component CSS:

import { SelectableTile, StatPill } from '@laird-wt/portal';

<SelectableTile id="ship-1" tone="var(--faction-crimson)" onSelect={select}>
    Frigate
</SelectableTile>;
<StatPill label="Energy" value={7} tone="oklch(0.7 0.16 150)" />;

With no tone, components use the neutral portal accent. An optional SelectionProvider supplies an ambient selection sink so tiles are wired once rather than per control.

Scripts

  • pnpm verify runs typecheck, lint, CSS lint, format check, tests, and build.
  • pnpm storybook starts Storybook for component review.
  • pnpm build produces the library bundle in dist.
  • pnpm probe:hud and pnpm probe:rtl drive the built Storybook in headless Chromium (the visual-verification probes; run pnpm build-storybook first - see scripts/probes/README.md).

Example app

A runnable Vite demo lives in example/. Run pnpm -C example dev to start it locally (it renders ControlSurface with behavior fixtures selectable via a ?fixture= query), and pnpm example:typecheck to typecheck the demo against the workspace package.