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

@vitavision/workbench

v0.3.0

Published

Studio-app building blocks on @vitavision/ui: app shell, split panes, tree view, playback bar and playhead store, file drop (toasts re-exported from @vitavision/ui).

Readme

@vitavision/workbench

The building blocks of a vitavision studio app — a tool whose surfaces are all permanent: a navigator, a viewport, an inspector, a timeline. Built on @vitavision/ui and its tokens.

Scope: the application shell and its navigation parts, shared by the vitavision studio apps.

bun add @vitavision/workbench @vitavision/ui
@import "tailwindcss";
@import "@vitavision/ui/styles.css";
@import "@vitavision/workbench/styles.css";

| Export | What | |---|---| | AppShell | Full-viewport frame: header, a fixed-width rail (an icon nav, <nav>), left | main | right (side panels resizable and optionally remembered), bottom. Landmarks included. | | NavRail, NavRailItem | The workspaces of an app as an icon column for AppShell's rail slot, one current (aria-current="page"): labels under the icons or in tooltips, count badges, disabled items, and asChild to render each item as a router's link. Controlled or not. | | StatusBar | One 24 px status line for AppShell's bottom slot: ReadoutItem facts at the start and end, anything (a ProgressBar) between. A named group; a polite role="status" with live. | | SplitPane | Two panes, horizontal or vertical, with a draggable, keyboard-operable divider (role="separator", WAI-ARIA window splitter). Sizes in px or %, min/max, collapsible, controlled or not, storageKey. Nests. | | TreeView | Data-driven tree (TreeNode[] with icon/meta), WAI-ARIA tree keyboard, controlled selection, controlled or uncontrolled expansion; reveals a selection made elsewhere. | | PlaybackBar | Transport for a sampled timeline: step, play/pause, markers, scrubber, t = k · dt, speed, loop. | | createPlayhead, usePlayhead, usePlaybackClock | The playback position as an external store, and the real-time clock that drives it. | | FileDrop | Drop zone (or full-window overlay) plus an "Open files…" button; accept applied to both routes; dropped folders walked. In a desktop shell, a pathSource yields paths to onPaths instead of Files (see below). | | SequenceNavigator | The current item of an ordered set (a frame of a capture) as a lazy thumbnail strip with previous/next, the position, and [ / ] from anywhere outside a text field. A per-item status ({ tone, label }) draws a dot in the thumbnail's corner and is said in the item's name. | | Stepper | The steps of a gated sequence (Teach, then Find, then Verify): numbered, the current one aria-current="step", done ones checked. A step with a blockedBy reason stays focusable but aria-disabled, described by its reason (also its tooltip), and cannot be chosen. Row or column; controlled or not. | | Toaster, toast() | Notifications from anywhere; one <Toaster /> near the root. Now lives in @vitavision/ui; re-exported here (same bindings, one default store) so existing imports keep working. Prefer import { toast } from "@vitavision/ui". |

The pure logic behind them is exported too and tested without a DOM: splitSize.ts (resolveSize, resolveLimits, clampSize), treeModel.ts (visibleRows, treeKeyAction, ancestorIds, parentIds), playhead.ts (advance, clampIndex, formatSeconds), dropFiles.ts (acceptsFile, collectDroppedFiles), stepperModel.ts (stepStates).

Playback without re-rendering the app

The current frame changes every animation frame, so it is not React state. It is a Playhead:

const [playhead] = useState(() => createPlayhead(trajectory.count, trajectory.dt));
const [playing, setPlaying] = useState(false);
const [speed, setSpeed] = useState(1);
const [loop, setLoop] = useState(false);
usePlaybackClock({ playhead, playing, speed, loop, onEnd: () => setPlaying(false) });

<PlaybackBar
  playhead={playhead}
  playing={playing} onPlayingChange={setPlaying}
  speed={speed} onSpeedChange={setSpeed}
  loop={loop} onLoopChange={setLoop}
  markers={captures.map((c) => ({ index: c.sample, label: c.name }))}
/>

// In the 3D view's own frame loop — no React involved:
renderer.setAnimationLoop(() => {
  robot.setJoints(trajectory.joints(playhead.get()));
  renderer.render(scene, camera);
});

The contract, kept deliberately small:

  • get() — the current sample index, an integer in 0 … count − 1. Cheap; call it every frame.
  • set(k) — rounds and clamps; notifies only on a change.
  • subscribe(listener) — returns the unsubscribe function (the useSyncExternalStore shape).
  • count, dt — the timeline; sample k is at t = k · dt.
  • setTimeline(count, dt) — a new scenario in the same store; the index is clamped, not reset.

Every member may be called detached. Only the bar (and anything using usePlayhead) re-renders during playback. The fractional position real-time playback needs lives in the clock, not the store, so a reader never sees a fractional frame.

Desktop shells: paths, not files

A Tauri or Electron webview gives paths, not Files: its native drops and dialogs report file system paths, and an app that opens images by path never wants their bytes in the webview. Give FileDrop a pathSource built on the shell's APIs, and it delivers paths to onPaths, filtered by accept on their extensions. The package stays shell-agnostic. For Tauri 2:

import { getCurrentWebview } from "@tauri-apps/api/webview";
import { open } from "@tauri-apps/plugin-dialog";
import type { PathSource } from "@vitavision/workbench";

const tauriPaths: PathSource = {
  pick: async ({ multiple, directory }) => {
    const chosen = await open({ multiple, directory });
    return chosen === null ? [] : Array.isArray(chosen) ? chosen : [chosen];
  },
  subscribe: ({ onEnter, onLeave, onDrop }) => {
    let stop = () => {};
    void getCurrentWebview()
      .onDragDropEvent(({ payload }) => {
        if (payload.type === "enter") onEnter();
        else if (payload.type === "leave") onLeave();
        else if (payload.type === "drop") onDrop(payload.paths);
      })
      .then((unlisten) => (stop = unlisten));
    return () => stop();
  },
};

<FileDrop overlay accept="image/*" pathSource={tauriPaths} onPaths={openByPath} />

Scope

No routing, no menus, no app state beyond what each component is asked to remember. A page that scrolls — a document, a settings form — does not want AppShell.

License

Licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this package by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.