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

uidex

v0.5.2

Published

Convention-driven UI element registry and devtools surface for React apps.

Readme

uidex

uidex surfaces structured context about a running UI — pages, features, widgets, regions, elements, primitives, flows — to humans (devtools overlay, command palette, detail panels), agents (typed uidex.gen.ts registry), and test runners (Playwright fixture + reporter).

This is uidex 0.1 — a greenfield rewrite. The published surface is a single npm package with framework-neutral core, an optional React adapter, and a built-in cloud client, each reachable through its own subpath export.

Install

pnpm add uidex

Quick start (React)

npx uidex init            # write .uidex.json and .gitignore entry
npx uidex scan            # generate src/uidex.gen.ts
// any React root
import { UidexProvider, UidexMount } from "uidex/react"

export default function Root({ children }: { children: React.ReactNode }) {
  return (
    <UidexProvider projectKey="pk_your_project">
      {children}
      {process.env.NODE_ENV !== "production" && <UidexMount />}
    </UidexProvider>
  )
}

That is the full integration. Shadow DOM mounts under the <UidexMount> node, the Inspector is always on while the surface is mounted (hover highlights any uidex-annotated element; plain click opens the element menu anchored to its bounds), and Cmd+K opens the palette. Passing projectKey auto-wires the built-in cloud client; pass cloud={null} to opt out, or cloud={customAdapter} to supply your own.

Dev-only mount

Because the Inspector intercepts clicks on every uidex-annotated element while mounted, uidex is a dev-only surface. Consumers MUST gate the mount on an environment check — the SDK does not enforce this itself.

// React — gate <UidexMount> on NODE_ENV
{
  process.env.NODE_ENV !== "production" && <UidexMount />
}
// Vanilla — gate createUidex().mount() on NODE_ENV
if (process.env.NODE_ENV !== "production") {
  createUidex({ cloud: cloud({ projectKey: "pk_..." }) }).mount()
}

Quick start (vanilla)

import { createUidex } from "uidex"
import { cloud } from "uidex/cloud"

const uidex = createUidex({
  cloud: cloud({ projectKey: "pk_your_project" }),
})
uidex.mount() // defaults to document.body

Package layout

| Subpath | Purpose | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | uidex | Vanilla core. createUidex, session store (XState v5 surface + highlight + form machines), entity registry, injected surface, panel registry, Zag machines. No React, no network. | | uidex/react | React boot wrapper. UidexProvider, useUidex, UidexMount, createReactPanel. No network. | | uidex/cloud | Fetch client for the cloud ingest API. cloud({ projectKey, endpoint }), FeedbackPayload, etc. No React. | | uidex/headless | Narrower vanilla entry — Registry + Session + Shadow DOM + Overlay + Inspector + MenuBar. No panels. | | uidex/scan | Scanner pipeline (discover → walk → extract → resolve → audit → emit) and CLI. | | uidex/playwright | Test fixture (uidex(id)). Optional peer: @playwright/test. | | uidex/playwright/reporter | Coverage reporter. |

The build enforces subpath isolation: uidex contains no React or network code, uidex/react contains no network code, uidex/cloud contains no React.

Entity model

Eight kinds, one registry, one ref shape:

type EntityKind =
  | "route"
  | "page"
  | "feature"
  | "widget"
  | "region"
  | "element"
  | "primitive"
  | "flow"

interface EntityRef {
  kind: EntityKind
  id: string
}
  • route — a URL pattern. Auto-detected from the framework router.
  • page — the module that renders a route. export const uidex = { page: "id", ... } as const satisfies Uidex.Page overrides the derived id and declares acceptance.
  • feature — cross-cutting behaviour under src/features/*. export const uidex = { feature: "id", ... } overrides.
  • widget — composite interactive unit (video player, date picker). Declared with data-uidex-widget="id" on the DOM root AND export const uidex = { widget: "id", acceptance: [...] } on the component module. The scanner cross-validates the two.
  • region — HTML5 landmark or role="region". data-uidex-region overrides id.
  • element — explicit interactive element. Always annotated via data-uidex.
  • primitive — reusable presentational component (button, input) under src/ui/**.
  • flow — top-level test.describe in e2e/**; @uidex:flow tag adds richer metadata. Opt out with export const uidex = { notFlow: true }.

Runtime API

import { createUidex } from "uidex"

const uidex = createUidex({
  cloud: null, // or a CloudAdapter from uidex/cloud
  theme: "auto", // "light" | "dark" | "auto"
  defaultPanels: true, // register bundled detail + palette panels
  panels: [myCustomPanel], // additional panels (last registered wins on conflict)
})

uidex.mount() // defaults to document.body
uidex.unmount()

createUidex returns:

| Field | Shape | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | registry | Registry.add, .get(kind, id), .list(kind), .query(fn). | | session | Zustand store projecting the XState surface machine. hover, selection, stack, inspectorActive, theme, ingestActive. session.send(event) for direct machine events. | | panels | { register, unregister, list } — panel registrar. | | cloud | The configured CloudAdapter or null. | | ingest | Console + network capture (auto-enabled when cloud is configured). | | shadowRoot | The Shadow DOM root after mount(), else null. |

React wiring

import { UidexProvider, UidexMount, useUidex } from "uidex/react"
  • <UidexProvider> accepts projectKey, cloud, and config props. It creates the core instance on mount and disposes on unmount. If projectKey is provided and cloud is not explicitly set, it auto-wires cloud({ projectKey }) from uidex/cloud.
  • useUidex() returns the core instance and throws UidexContextError outside a provider.
  • <UidexMount> renders the host DOM node the surface attaches to; the surface detaches on unmount.

Headless

import { createHeadless } from "uidex/headless"

const h = createHeadless({ theme: "auto" })
h.mount()
h.overlay.show(el, { label: "target" })
h.inspector.start()

No panels, no ViewStack. Same Shadow DOM, Overlay, MenuBar, CursorTooltip, CommandPalette (empty). Intended for agents, screencasts, and non-panel consumers.

Panel system

Panels are the pluggable UI surface. Both detail views and palette commands are Panels. Panels are framework-agnostic — render is imperative:

interface Panel {
  id: string
  matches?: (ref: EntityRef) => boolean // detail panel
  command?: { label: string; group?: string; shortcut?: string } // palette entry
  render: (ctx: PanelContext, root: HTMLElement) => () => void
}
  • render receives a mount root and returns a cleanup function.
  • Last registered wins on matches() overlap. id collision replaces the prior registration.
  • ctx.navigate(ref) pushes the matching panel for ref onto the view stack; the current view stays mounted underneath and is revealed on pop.
  • ctx.push({ id, ref? }) and ctx.pop() drive the stack directly; ctx.close() clears every entry at once.
  • ctx.cloud is the configured CloudAdapter or null; cloud-backed affordances MUST degrade gracefully when it is null.

Authoring a panel with React:

import { createReactPanel } from "uidex/react"

export const myPanel = createReactPanel({
  id: "my-panel",
  matches: (ref) => ref.kind === "widget",
  component: ({ ctx }) => <div>Widget: {ctx.ref?.id}</div>,
})

createReactPanel wraps createRoot / unmount and produces a conforming Panel.

Element menu

Clicking any uidex-annotated element opens a popover menu anchored to the element's bounds. The menu is chrome owned by the Surface (not a Panel), composed from the shared menu + popover Zag machines. All rows close the menu on activation; Escape and outside-click dismiss and return focus to the prior focus holder.

| Row | When shown | Action | | ---------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | Copy path | Always | navigator.clipboard.writeText("<kind>:<id>"). | | Open details | A registered panel matches the entity | session.actions.select(ref) + session.actions.openPanel(panel.id, ref) where panel = panels.findMatch(ref). | | Submit feedback | A registered panel matches the entity | Routes to the matched detail panel (feedback currently lives as a sub-view). Suppressed if there is no detail panel for the entity. | | View acceptance criteria | ref.kind === "widget" and a detail panel matches | Opens the widget detail panel. | | Find usages | ref.kind === "primitive" | Queries the registry for entities composing the primitive and opens the primitive detail panel. |

The ⌘K palette provides the keyboard-driven counterpart: its "On this page" section enumerates every uidex entity present in the current DOM, and selecting a row calls ctx.navigate(ref).

Bundled panels

createUidex({ defaultPanels: true }) (the default) registers:

commandPalettePanel, componentDetailPanel (element), pageDetailPanel, featureDetailPanel, widgetDetailPanel, flowDetailPanel, primitiveDetailPanel, regionDetailPanel.

All eight are vanilla TypeScript built on Zag machines and Tailwind classes.

Report uidex issue

Every view's footer Actions popup (⌘K from depth 1, or the footer affordance) includes a Report uidex issue entry. It opens the SDK's built-in feedback form and submits to a uidex-maintained project — separate from your host cloud adapter, so SDK bugs surface to the uidex team without contaminating your own ticketing. The report is stamped with the SDK version and the originating view id (e.g. uidex-sdk:command-palette); when the SDK cannot reach its endpoint the form falls back to copying a Markdown report to the clipboard.

Scanner

One pipeline, six stages, each stage in its own file and independently callable:

discover(cwd)        // locate .uidex.json files across the monorepo
  -> walk(sources)   // enumerate files per include/exclude
  -> extract(files)  // data-uidex* attrs + export const uidex AST literals
  -> resolve(ann)    // apply conventions, compute scopes, back-references
  -> audit(registry) // diagnostics: missing, scope-leak, acceptance coverage
  -> emit(registry)  // deterministic uidex.gen.ts

CLI:

npx uidex init                      # scaffold .uidex.json
npx uidex scan                      # run full pipeline
npx uidex scan --check              # fail on registry drift (CI gate)
npx uidex scan --lint               # annotation lint diagnostics (includes legacy-jsdoc)
npx uidex scan --audit              # --check + --lint + coverage
npx uidex scan --json               # machine-readable output
npx uidex scaffold widget <id>      # emit Playwright spec from declared acceptance
npx uidex claude install            # Claude Code rules + skill + hooks

See the docs site for the convention table, annotation surfaces, and the acceptance workflow.

Bundler plugins

Optional opt-in watch integrations regenerate uidex.gen.ts on file save:

  • @uidex/vite-pluginplugins: [uidex()] in vite.config.ts.
  • @uidex/webpack-pluginnew UidexPlugin() in webpack.config.js.
  • @uidex/next-pluginwithUidex(config) wrapper for next.config.*. Webpack transport today; Turbopack support tracked as a follow-up.

All three wrap @uidex/plugin-core so behaviour and diagnostic shapes are identical across bundlers.

Config (.uidex.json)

Flat schema. Legacy nested scanner.*, defaults, colors are rejected with descriptive errors.

{
  "$schema": "./node_modules/uidex/uidex.schema.json",
  "sources": [
    {
      "rootDir": "src",
      "include": ["**/*.{ts,tsx}"],
      "exclude": ["**/*.test.*"]
    }
  ],
  "output": "src/uidex.gen.ts",
  "flows": ["e2e/**/*.spec.ts"],
  "typeMode": "strict",
  "audit": { "scopeLeak": true, "coverage": true, "acceptance": true },
  "conventions": {
    "primitives": ["src/ui/**", "src/components/ui/**"],
    "features": "src/features/*",
    "pages": "auto",
    "flows": ["e2e/**/*.spec.ts"],
    "regions": "landmarks"
  }
}

Set any conventions.* entry to false to disable that auto-discovery stage.

typeMode controls whether emitted id types are literal unions ("strict", default) or string ("loose", a temporary migration knob).

Acceptance workflow

Declare criteria in the module-scoped export const uidex:

import type { Uidex } from "@/uidex.gen"

export const uidex = {
  widget: "video-player",
  acceptance: [
    "Plays on space",
    "Mutes with m",
    "Scrubs with arrow keys",
  ],
} as const satisfies Uidex.Widget

export function VideoPlayer() {
  return <div data-uidex-widget="video-player">...</div>
}
  1. Scanner extracts acceptanceentity.meta.acceptance (in source order).
  2. Flows touching the widget (directly or via descendants) are collected into meta.flows.
  3. scan --audit flags any criterion not covered by a flow; the hint suggests uidex scaffold widget <id>.
  4. uidex scaffold widget video-player emits a Playwright spec with one test() per criterion, tagged @uidex:flow. Re-running is idempotent unless --force is passed.

Cloud + ingest

Configure cloud by passing a CloudAdapter into createUidex({ cloud }) or, from React, by passing projectKey / cloud to <UidexProvider>. The built-in client lives at uidex/cloud:

import { cloud } from "uidex/cloud"

const adapter = cloud({ projectKey: "pk_..." })
await adapter.feedback.submit(payload)
await adapter.integrations.getConfig()

cloud() takes { projectKey, endpoint?, fetch? }. endpoint defaults to the production cloud URL; fetch allows tests or self-hosted deployments to inject a custom transport. Errors surface as CloudError with status, retryAfter, and details.

When cloud is configured, uidex/ingest auto-enables:

  • Console capture: warn/error, ring buffer ≤ 50. Originals always invoked.
  • Network capture: failed fetches only, ring buffer ≤ 20. Native fetch reference is captured at module load, so cloud.feedback.submit always uses an un-intercepted fetch.

Override per-channel via createUidex({ ingest: { console: {...}, network: null } }), or pass ingest: null to disable entirely.

Playwright

import { test, expect } from "uidex/playwright"

test.describe("Add and complete a todo", { tag: "@uidex:flow" }, () => {
  test("adds a todo", async ({ uidex }) => {
    await uidex("todo-text-field").fill("Buy milk")
    await uidex("todo-add-button").click()
    await expect(uidex("todo-item")).toHaveCount(1)
  })
})

Resolves any data-uidex, data-uidex-region, data-uidex-widget, or data-uidex-primitive. Register the coverage reporter to emit flow → entity coverage data — it is additive, compatible with any other reporter.

// playwright.config.ts
reporter: [["list"], ["uidex/playwright/reporter"]]

Scripts

pnpm build           # build:css + tsup + check:bundles
pnpm test            # vitest (watch)
pnpm test:run        # vitest (run)
pnpm typecheck       # tsc --noEmit
pnpm lint            # eslint src/