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

editor-shell

v0.36.0

Published

Shared editor-chrome primitives for the EFFICIENT editors: EditorRail (a Next-16-safe left icon-rail leaf), the shell token layer, and GoldTextInput — type in place and watch the markup formatting appear as you type.

Readme

editor-shell

Shared editor-chrome primitives for the EFFICIENT editors — the owner's own editor shell, so every editor draws the same chrome from one source instead of hand-rolling and drifting.

Status: foundation. Ships one brick — the shared left icon-rail. Not published to npm; no product repo consumes it yet.

Why

The storefront editor (efficient-shop, Next 16 + Puck) and the campaign designer (efficient-admin-portal, Vite/React) each hand-rolled their own narrow left icon-rail. They had already drifted — button 34px vs 36px, icon 18px vs 20px, radius 9 vs 8, a CSS-var accent vs a Tailwind one. EditorRail is the one rail they converge on.

The leaf

The storefront's editor chrome renders in Next-16 server components, so the rail is a self-contained leaf at editor-shell/rail: it imports nothing heavy and touches no document / window / localStorage at import or render, so a server component can pull it in safely. tests/rail-leaf-safety.test.tsx proves it.

// A client component in any editor:
'use client';
import {
  EditorRail,
  PagesIcon, LayersIcon, StylesIcon, MediaIcon, SitemapIcon, AddIcon,
} from 'editor-shell/rail';
import 'editor-shell/rail.css'; // once, from a client/global entry

export function LeftRail({ view, onView, onAdd }) {
  return (
    <EditorRail
      ariaLabel="Left panel views"
      items={[
        { id: 'pages',  label: 'Pages',  icon: <PagesIcon />,  active: view === 'pages',  onSelect: () => onView('pages') },
        { id: 'layers', label: 'Layers', icon: <LayersIcon />, active: view === 'layers', onSelect: () => onView('layers') },
        { id: 'styles', label: 'Styles', icon: <StylesIcon />, active: view === 'styles', onSelect: () => onView('styles') },
        { id: 'media',  label: 'Media',  icon: <MediaIcon />,  onSelect: onOpenMedia },
        { id: 'add',    label: 'Add page', icon: <AddIcon />,  onSelect: onAdd },
      ]}
    />
  );
}

API

<EditorRail items ariaLabel? className? orientation? />

A pure presentational toolbar. It owns the look, never the app state — you decide which item is active and what each onSelect does.

| prop | type | default | notes | |---|---|---|---| | items | EditorRailItem[] | — | buttons, in render order | | ariaLabel | string | "Editor" | names the toolbar for assistive tech | | className | string | — | merged onto .es-rail | | orientation | 'column' \| 'row' | 'column' | which way the buttons run |

orientation

'column' is the rail every editor draws today: a fixed 46px strip wearing its own card. It is the default, and it renders exactly what it rendered before this prop existed.

'row' lays the same buttons left to right for a top bar — no fixed width (as wide as its buttons) and no card (no background, radius or shadow), because the bar already has its own surface. The button size, icon size, gap and every hover/active colour are identical in both directions; only the container changes. A row is 34px tall, so a 52px bar centres it with 9px to spare.

// The same rail, in the top bar's empty middle.
<EditorRail orientation="row" ariaLabel="Editor views" items={items} />

aria-orientation follows the layout (vertical / horizontal); the toolbar role does not change, because a rail that mixes view toggles with plain actions is a toolbar whichever way it runs.

EditorRailItem

interface EditorRailItem {
  id: string;            // React key + selection correlation
  label: string;         // aria-label + hover title
  icon: React.ReactNode; // an 18px currentColor glyph — use the shipped icons
  active?: boolean;      // present ⇒ view toggle (renders aria-pressed); omit for actions
  disabled?: boolean;
  onSelect?: () => void;
}

A view item passes active: true | false and renders as a toggle (aria-pressed, .is-active highlight). An action item (the quick-add "+") omits active and renders as a plain button.

EditorRailButton — the single-button building block EditorRail maps to — is exported too, for bespoke layouts.

Tokens

The same values, two ways — railTokens (JS) and rail.css (CSS custom properties on .es-rail). Taken verbatim from the storefront editor's .sf-rail (the visual reference).

| token | value | CSS var | |---|---|---| | rail width | 46px | --es-rail-w | | rail padding (y) | 8px | --es-rail-pad-y | | button gap | 4px | --es-rail-gap | | rail radius | 12px | --es-rail-radius | | button size | 34px | --es-rail-btn-size | | button radius | 9px | --es-rail-btn-radius | | icon size | 18px | --es-rail-icon-size | | background | #ffffff | --es-rail-bg | | idle icon | #6b7280 | --es-rail-fg | | hover bg / fg | #f3f4f6 / #111827 | --es-rail-hover-bg / --es-rail-hover-fg | | accent (active) | #2563eb | --es-rail-accent | | accent tint (active bg) | #eff6ff | --es-rail-accent-tint |

orientation="row" adds no new token. It re-points three of the ones above — --es-rail-bg to transparent, --es-rail-shadow to none, --es-rail-radius to 0 — so the card look stays declared in one place, and it reuses --es-rail-pad-y rotated onto the row's ends and --es-rail-gap as-is.

Re-theme without new CSS by overriding a var on the element: style={{ ['--es-rail-accent']: brand }}.

The gold layer — type in place

GoldTextInput (subpath editor-shell/gold, added in 0.3.0) is the surface a merchant types INTO on the page, with the markup formatting appearing as they type. A textarea whose own text is transparent sits on top of a layer holding the same characters, painted; what you read is the layer, what the caret walks is the box. Both inherit their type from whatever the host renders them inside, so the glyphs land on top of each other.

It is controlled and writes nothing — no document, no sections, no Puck. The host owns the string and decides what a finished edit means.

'use client';
import { GoldTextInput } from 'editor-shell/gold';
import { STOREFRONT_MARKUP } from 'react-os-shell/markup';
import 'editor-shell/gold.css'; // once, from a client/global entry

<GoldTextInput
  value={draft}
  rules={STOREFRONT_MARKUP}
  placeholder="Add a heading"
  ariaLabel="Section heading"
  onInput={setDraft}
  onCommit={(value) => save(value)}   // blur, or Enter on a single-line field
  onCancel={(restored) => setDraft(restored)}
/>;

| prop | type | default | notes | |---|---|---|---| | value | string | — | the authored string, delimiters and all | | onInput | (value: string) => void | — | every keystroke; the layer repaints from what you send back | | onCommit | (value: string) => void | — | blur, or Enter when multiline is false. Once per session, never if nothing was typed | | onCancel | (restored: string) => void | — | Escape. The argument is the value held at focus time; the host puts it back | | multiline | boolean | false | what Enter means, not how the text wraps — both modes wrap | | placeholder | string | — | visible until the first keystroke | | className | string | — | merged onto .es-gold | | rules | readonly InlineRule[] | STANDARD_MARKUP | pass your product's set — STOREFRONT_MARKUP / CAMPAIGN_MARKUP | | ariaLabel | string | — | in-place editing has no visible label |

The grammar is not ours. Runs are parsed by react-os-shell/markup, the same module the published page and the campaign email render from, so a toolbar button, a page and this box can never disagree about what a delimiter means. That subpath is framework-free and imports nothing, which is why the leaf can use it; react-os-shell is declared as an optional peer dependency, needed only if you import editor-shell/gold.

Two deliberate limits. The asterisks are FADED, never removed. They go to nothing while the box is unfocused, so a merchant who is not typing does not read ** in their own page text, and they come back dimmed while the caret is in the box — but the characters stay in the layer at their full width in both states. Actually removing them needs a second text engine, and is out of scope. And the layer only paints what cannot move a glyph: colour, opacity, decoration, a faux-bold shadow. A real font-weight: 600 is wider, so a marked line would wrap earlier than the transparent box on top of it and every following line would be drawn over the wrong text. The real weight and slant appear on the page the moment the merchant clicks away.

The edit lock — two operators, one document

editor-shell/edit-lock (added in 0.32.0) is the answer to "is the save path allowed to run", shared by every editor in the house. It is pure TypeScript: no React, no browser global, no dependency at all, so a server component may import it.

It exists because the storefront editor and the campaign designer had written the same rules twice, in two repos, and the two copies had already drifted — the take-over half only existed on one side and the claim driver only on the other. Both are here now, and neither editor keeps a copy.

import { createEditLock, canWriteDocument, documentPermissions } from 'editor-shell/edit-lock';

// The only two things that differ between one editor and the next.
export const pageEditLock = createEditLock({
  readFailure: (err) =>
    err instanceof ApiError ? { status: err.status, data: err.data } : null,
  conflictCode: 'page_changed_elsewhere',
});

The plain half needs no binding: canWriteDocument, holderOf, readLockHolder, accessForNewDocument, documentPermissions, describeAge, nameOrSomeone, newEditorClientId, shouldReleaseHeldWrite. The bound half — accessAfterClaimFailure, accessAfterTakeoverFailure, accessAfterHeartbeatFailure, isAuthRejection, readSaveConflict, makeClaimBeat — comes back from createEditLock.

Two things this module will not do. It never takes the ability to edit away because a courtesy call failed: every claim failure that is not a 409 naming a holder falls back to editing, because the save-time version check is what actually protects the work. And a failed TAKE-OVER is a different question from a failed first claim — it keeps the state it was pressed from, so a take-over that did not land never puts two people in one document with the holder untold.

Develop

No Node on the dev Mac — everything runs in Docker:

docker run --rm -v "$PWD":/w -w /w node:26-slim \
  sh -lc "npm install && npm run typecheck && npm test && npm run build"
  • npm run typechecktsc over src and the specs.
  • npm test — the leaf-safety, gold-layer and rail-shape gates. The gold-layer spec is the one that needs a DOM (focus, keys, blur); it stands up a single jsdom window in tests/_dom-env.ts and runs in its own process.
  • npm run buildtsupdist/ (root + the rail, shell and gold subpaths) + the copied CSS.