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

ui-detox

v0.1.3

Published

HTML-first Web Components framework with Markdown SFCs. Not a React, nor Vue or Angular.

Readme

UIDetox

UIDetox

An HTML-first UI framework where the component tree is real DOM. Built on native Web Components, signal reactivity, and a section-based authoring language — no virtual DOM, no <div id="root">, no class soup.

<!-- View Source shows your components, not a black box -->
<app-shell>
  <ingredient-list>
    <ingredient-row>…</ingredient-row>
  </ingredient-list>
</app-shell>

Why

  • Visible tree. Every component is a real custom element. View Source and DevTools show <ingredient-row>, not an opaque render root. You style by tag, inspect by tag, test by tag.
  • Signals, not dependency arrays. state() is a reactive Proxy; derived() computes; effect() re-runs when what it read changes. No useMemo, no deps lists, no compiler tracking magic.
  • Frame-batched. Reactive re-runs and DOM writes coalesce into one paint per frame via a 4-phase scheduler; heavy work is deferred off the tick.
  • Two ways to author, one runtime. A Markdown single-file component or the .dtx DSL — both compile to the same custom elements.
  • Batteries included. Forms, typed HTTP + OpenAPI codegen, a file-based router, i18n, drag & drop, windowed lists, a Semantic-UI kit, and a Vite plugin.

Reactivity in 5 lines

import { state, derived, effect } from 'ui-detox';

const s = state({ count: 0, items: [1, 2] });
const doubled = derived(() => s.count * 2);
effect(() => console.log(s.count, doubled.value));  // re-runs on change
s.count++;            // ▶ 1 2  → effect fires
s.items.push(3);      // arrays are reactive too

batch(fn) coalesces writes; shallow() skips deep tracking for big payloads; untrack(fn) reads without subscribing.


A component

Markdown SFC (Counter.md):

---
name: Counter
tag: app-counter
---

```html template
<div class="counter">
  <button @click=${dec}>–</button>
  <span>${s.count}</span>
  <button @click=${inc}>+</button>
</div>
```

```ts script
const s = state({ count: props.start ?? 0 });
function inc() { s.count++; }
function dec() { s.count--; }
```

.dtx DSL — the same component:

component Counter export tag app-counter

props
number start
end props

script
const s = state({ count: props.start ?? 0 });
end script

actions
function inc() { s.count++; }
function dec() { s.count--; }
end actions

template
<div class="counter">
  <button #dec @click=${dec}>–</button>
  <span>${s.count}</span>
  <button #inc @click=${inc}>+</button>
</div>
end template

end component

Bindings by prefix: ${x} text, @click=${fn} event, .prop=${v} property, ?disabled=${v} boolean. #name / name="…" auto-bind element refs into ctx.refs.


Virtual directives — no wrapper nodes

<if>, <for>, <case> compile away; in production they anchor on text nodes, leaving no <div> scaffolding.

<case on=${list.status}>
  <when is="loading"><spinner-el/></when>
  <when is="error"><p>Failed</p></when>
  <else>
    <for each=${list.data.items} item="ing" key="ing.id">
      <ingredient-row data=${ing}/>
    </for>
  </else>
</case>

Windowed lists — 10 000 rows, ~15 DOM nodes

Rendering a long list kills scrolling. UIDetox virtualizes it by flipping one attribute on <for> — same loop, same key, now it renders only the visible slice plus a small overscan and recycles rows as you scroll. DOM node count stays bounded no matter how long the array grows.

<for each=${ingredients}          <!-- 8 000 items -->
     item="ing" key="ing.id"
     viewport="virtual"           <!-- ← turns on windowing -->
     row-height="48"
     overscan="6">
  <ingredient-row data=${ing}/>
</for>
plain <for>          →  8 000 <ingredient-row> in the DOM, janky scroll
<for viewport=…>     →  ~15 in the DOM, smooth at any length
  • Keyed reconcile — a row with the same key keeps its DOM node across scroll, so focus, inputs and selection survive.
  • Bounded: (visible + 2·overscan) rows, whatever each.length is; total scroll height is preserved via a spacer.
  • Scroll API: grab the element with a #refel.scrollToKey('ing-42'), el.scrollToIndex(1000).
  • Works when the element itself scrolls or an ancestor does (scroll-parent), and inside <select> / <table>. <virtual-for> is an alias for the same thing.

Full guide: docs/patterns/virtual-for.md · example: examples/virtual/IngredientList.dtx.


Forms — reactive, schema-validated, two-way bound

const fm = form({
  schema: f.object({
    name: f.string().min(2),
    nutrients: f.array(f.object({ code: f.string().min(1) })).min(1),
  }),
  initial: { name: '', nutrients: [] },
  onSubmit: async (v) => api.ingredients.create(v),
});
<form @submit=${fm.submit}>
  <input bind=${fm.field('name')} placeholder="Назва"/>
  <field-error .of=${fm.field('name')}></field-error>
  <button ?disabled=${!fm.valid || fm.submitting}>Зберегти</button>
</form>

Validation runs off-tick and coalesced; server errors flow in with fm.applyServerErrors(err).


Typed HTTP + OpenAPI

ui-detox openapi --input ./openapi.json --output ./src/api.ts
const list = resource(
  (signal) => api.ingredients.list({ query, signal }),
  { key: () => JSON.stringify(query) },       // re-fetches on change, auto-aborts on unmount
);
// list.status · list.data · list.error · list.reload()

Auth with single-flight 401 refresh, mutation() with optimistic rollback, and command() for CQRS writes — all in ui-detox/http.


File-based router

routes.dtx compiles to a RouteEntry[]:

import Login from "pages.Login"
import Dashboard from "pages.Dashboard"
import requireAuth from "lib.auth-guard"

router AppRoutes export
routes
"/login" -> Login
group layout=AppShell guard=requireAuth
"/"              -> Dashboard
"/recipes/:id"   -> RecipeCard  { id: string }
end group
"**" -> NotFound status=404
end routes
end router

navigate('/x') and <a data-nav href="/x"> for SPA transitions; routeState() exposes the reactive current match (params, layout).


More in the box

| Import | What | |---|---| | ui-detox | reactivity, components, directives, registry (DI), router, traits, filters, dnd, animations, SSR/islands, DevTools | | ui-detox/forms | schema DSL, form(), bind=, <field-error> | | ui-detox/http | createHttpClient, resource, mutation, command, OpenAPI codegen | | ui-detox/i18n | setLocale, fmt.{number,percent,qty,date,relative}, unit conversion | | ui-detox/ui | Semantic-UI primitive kit (ui-button, ui-card, ui-modal, …) | | ui-detox/vite | Vite plugin — load .dtx/.md, dotted-module resolve, HMR, TS shims |

Also: reusable traits (use="draggable"), filters (${v | qty:'g'}), hierarchical registry DI, islands (render: 'never' + hydrate), FLIP animations, and <lazy-load>.


Install & run

pnpm install
pnpm test        # 540+ tests
pnpm build       # tsc → dist/
// vite.config.ts
import uidetox from 'ui-detox/vite';
export default { plugins: [uidetox()] };

Documentation

  • REFERENCE.md — the full manual, every feature with examples.

Deep-dive guides in docs/patterns/:

| Guide | Topic | |---|---| | forms.md | schema DSL, form(), bind=, validation | | http.md | HTTP client, resource, mutation, command, OpenAPI | | navigation.md | router, navigate(), <a data-nav>, routeState() | | virtual-for.md | windowed list rendering | | drag-and-drop.md | draggable / droppable / sortable traits | | i18n.md | locale-aware number / date / unit formatting | | ui-kit.md | Semantic-UI primitive components | | shallow-batch.md | shallow(), batch(), untrack() | | island-wrapper.md | wrapping imperative libraries (canvas, maps) | | vite-plugin.md | ui-detox/vite — load .dtx/.md, HMR, TS shims |

Examples

Runnable examples in examples/:

| Example | Shows | |---|---| | hello/ | minimal Markdown SFC (App.md) | | todo/ | SFC with colocated test blocks (Todo.md) | | dsl/ | .dtx components, traits, filters | | forms/ | IngredientForm.dtx — schema + bind= + arrays | | virtual/ | IngredientList.dtxviewport="virtual" | | dnd/ | Palette.dtx — draggable → droppable canvas | | island/ | CanvasClock — imperative canvas island | | island-cytoscape/ | GraphEditor — canvas-graph island (props↔lib, emits, drop) | | routing/ | file-based routes | | include/ | <include> partials | | showcase/ | traits + filters + docs page | | culinary-lite/ | full Vite app: router, layout, pages, detox.toml | | vitest-setup/ | Vitest recipe with uidetoxEsbuild |