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

@tjcages/presentation

v0.2.1

Published

URL-derived push/pop presentation and responsive shell — iOS-style page pushing on mobile, fixed rail and behind-nav on phone, synced rails on desktop. Router-free, zero runtime dependencies.

Readme

@tjcages/presentation

URL-derived push navigation. An iOS-style page push on a phone, synced rails on a desktop, a drawer for a single view — chosen per breakpoint, in CSS.

Router-free: the host supplies the current path, a way to navigate, and a resolver saying where a path sits in the stack. No runtime dependencies.

<Shell> owns the responsive frame around those pages: a fixed desktop rail that stays out of document flow, and mobile behind-navigation that translates the page surface with a sampled spring (static open-state radius and shadow). Hosts still own branding, nav data, docks, and router adapters.

Install

pnpm add @tjcages/presentation

React and React DOM 18 or newer are peer dependencies. Import the stylesheet once in the host application, as shown below.

import { Presentation, createResolver } from "@tjcages/presentation";
import "@tjcages/presentation/presentation.css";

const resolve = createResolver({
  root: "/settings",
  title: "Settings",
  routes: [
    { path: "/settings/appearance", title: "Appearance" },
    { path: "/settings/access", title: "Access & permissions" },
    { path: "/settings/access/roles", title: "Roles" },
  ],
});

export default function Layout({ children }) {
  const pathname = usePathname();
  const router = useRouter();

  return (
    <Presentation
      path={pathname}
      navigate={router.push}
      resolve={resolve}
      present={{ base: "push", md: "rails" }}
      Link={Link}
    >
      {children}
    </Presentation>
  );
}

Why depth comes from the URL

/settings is depth 0, /settings/appearance is 1, /settings/access/roles is 2. Nothing is held in state, so hardware back, the browser's own edge-swipe, and deep links all work without this package being told they happened. A link straight to depth 2 opens at depth 2, with a back affordance that goes up a level rather than out of the section.

Presentation styles

push · rails · drawer · fade · none — or auto.

present takes one style or a breakpoint map:

present={{ base: "push", md: "rails" }}

Breakpoints resolve in CSS, not JavaScript. There is no media-query hook, no server-versus-client snapshot to guess wrong, and no reflow after hydration — the markup is byte-identical at every width, and only which keyframes apply changes. A resize or a rotate never remounts the page.

auto currently resolves to push, so every row in one navigation list opens the same way. A route can opt into another style explicitly:

createResolver({
  root: "/settings",
  routes,
  presentFor: (path) => (path === "/settings/profile" ? "drawer" : undefined),
});

The back gesture

A left-edge drag pops, tracking the finger, and springs to rest on release.

It declares no touch-action anywhere. The obvious build — touch-action: pan-y on the dragged panel — works and silently breaks every horizontally scrollable descendant, because touch-action resolves as the intersection down the ancestor chain. A wide table or a capability matrix nested anywhere inside stops panning sideways, and nothing in the CSS says why.

Instead the gesture hit-tests the touch against the screen edge, runs its own direction lock, and calls preventDefault() only once it is certain the gesture is horizontal and started at the edge. Every other touch is never intercepted, so nested scrollers behave as if this package were not installed.

Motion is CSS. The drag writes a registered custom property; the release transitions that same property with a sampled spring as its timing function, which is what lets the pop resume from wherever the finger stopped instead of restarting from zero.

Chrome the page declares

The resolver's title is the default. A pushed page can override it from inside itself, where the data it wants to show already is:

import { Title, Actions, usePresentation } from "@tjcages/presentation";

function RoleEditor({ role }) {
  const { dismiss } = usePresentation();
  return (
    <>
      <Title>{role.name}</Title>
      <Actions>
        <button onClick={dismiss}>Done</button>
      </Actions>
      …
    </>
  );
}

Theming

Every colour, size, and duration is a --pr-* custom property with a neutral default. Nothing hardcodes a palette. Override from your own theme:

.pr-stack {
  --pr-surface: var(--background);
  --pr-backdrop: rgb(0 0 0 / 0.12);
  --pr-shadow: -12px 0 28px -8px rgb(0 0 0 / 0.25);
}

What it handles

  • Scroll position restored per level on the way back up, and reset on the way in.
  • The stack's height pinned while two levels overlap, so pushing from a long page to a short one does not collapse the document mid-animation.
  • A departing level kept mounted for its animation, inert, and wrapped in an error boundary — route data it reads may already be gone, and without the boundary that throw escapes to the app.
  • prefers-reduced-motion: the transition goes, the navigation and the gesture stay.

Known limits

  • A back-swipe from a deep link reveals the backdrop, not the parent page. The level being swiped toward is normally the one that was on screen a moment ago, and is remembered. Arriving straight at depth 2 leaves nothing to remember. The alternative is speculatively fetching the parent route on touch-down, at a request per aborted swipe.
  • Scroll memory tracks the window scroller. A level that scrolls inside its own container is not restored.
  • drawer supplies the motion style, but not sheet policy such as detents, a scrim, or a drag-to-dismiss gesture. Hosts that opt into it own those details.

Development

pnpm install
pnpm check
pnpm demo

pnpm check runs the behavior suite, typecheck, production build, and a dry run of the exact npm tarball. The tarball check fails if demo or nested source output leaks into the package.

Shell

import { Shell, Presentation, createResolver } from "@tjcages/presentation";

<Shell
  path={pathname}
  rail={<Nav />}
  mobileDock={<Dock />}   // mounted only on mobile
  edgeOpen={depth === 0}  // root opens nav; deeper pages keep swipe-back
>
  <Presentation path={pathname} navigate={navigate} resolve={resolve}>
    {children}
  </Presentation>
</Shell>

| Prop | | | ------------- | --------------------------------------------------------------- | | path | Route key — snaps behind-nav closed on change. | | rail | Navigation content for the fixed desktop / behind mobile rail. | | mobileDock | Optional dock; mounted only when the viewport is mobile. | | edgeOpen | Left-edge open gesture when closed. Default true. | | open | Controlled behind-nav open state. |

ShellMenuButton is the discoverable mobile open control (hidden on desktop). Edge-swipe still works; hosts that already have chrome can omit the button.

Theme via --pr-shell-* custom properties (--pr-shell-rail-width, --pr-shell-card-radius, --pr-shell-card-shadow, …).

API

| Prop | | | --------------- | ------------------------------------------------------- | | path | Current path, from the host router. | | navigate | (path) => void. Used by back and by a released swipe. | | resolve | (path) => StackEntry \| null. See createResolver. | | children | The level's content, from the host router. | | present | Style or breakpoint map. Default "auto". | | Link | Host link component, so back is a real anchor. | | renderBar | Replace the default bar. Return null for none. | | bar | false suppresses the built-in bar. | | swipe | Default true. | | restoreScroll | Default true. | | onSound | Optional push / pop / change sound adapter. |

Optional sounds

Presentation has no audio dependency. Supply onSound to connect the sound system your app already uses:

<Presentation
  {...props}
  onSound={(cue) => {
    sounds.play(cue === "pop" ? "dismiss" : "navigation");
  }}
/>

An audio library remains an app-level choice. For example, a host that already uses cuelume can pass play through the same callback; apps that omit onSound ship no sound code from this package.

Demo

https://presentation-demo.ty-944.workers.dev

A reproduction of Totem admin's sidebar and settings area, driven by this package. Deploy it with:

pnpm demo:deploy

Static assets on Cloudflare Workers. not_found_handling: "single-page-application" is load-bearing — the demo routes with history.pushState, so paths like /crm and /settings/access exist only in the client. Without it the edge 404s anyone who opens a link instead of clicking their way in, which is exactly what testing on a phone does.