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

@zibby-run/app-kit

v0.7.5

Published

The client half of the Zibby app contract: the /api/ai streaming client, the host bridge, the dev mock engine, the image helpers and the shared UI layer.

Readme

@zibby-run/app-kit

The client half of the Zibby app contract, as an npm package. Read CONTRACT.md first — it is the authority on what an embedded app must do; this package is the reference implementation of the app side.

Consumers today: the five Zibby apps (Photo Restore, Product Photo Studio, Ad Maker, Resume Tailor, Image Extender), each on @zibby-run/app-kit from the registry, and any app a developer builds for the platform.

Starting from nothing

npx @zibby-run/app-kit init my-app
cd my-app && pnpm install && pnpm dev

init writes a complete, framework-free Zibby app — vanilla TypeScript on Vite, the system font stack, its own CSS — with one AI action on /api/ai, the host bridge, the dev mock, both publish manifests and an end-to-end test that runs on the mock. It takes none of this package's optional halves; the catalogue look is one import line away and a separate decision. The directory's name becomes the app's slug and, title-cased, its display name; a directory that already has something in it is refused.

Install (into an app you already have)

pnpm add @zibby-run/app-kit
# or
npm i @zibby-run/app-kit

The look is yours. The platform requires the CONTRACT and nothing else — there is no house style to adopt. The UI layer below, fonts.css, the Vite preset, /publish and zibby-zip are five separate choices, each takeable alone; the root entry (streamZibbyAi, installHostBridge, navigateHost) is the one part that implements the contract itself.

Two import lines are all an app needs to start:

import "@zibby-run/app-kit/ui.css";
import { AppLayout, Button, useZibbyAi } from "@zibby-run/app-kit/react";

A third line vendors Inter off the serving origin, which CONTRACT.md §1 requires and no app should copy by hand any more:

import "@zibby-run/app-kit/fonts.css";

React is an optional peer dependency (>=18); only the /react and /ui entry points need it. Node >=20.

The package is ESM-only. It ships "type": "module" and an exports map, and there is no CommonJS build: require("@zibby-run/app-kit") is not supported. A consumer's TypeScript needs moduleResolution set to bundler (what Vite and Next projects use, and what all of our apps use), or to node16/nodenext in a project that is itself ESM — a node16 project that is CommonJS gets TS1479 on every entry point, which is the type system telling the truth. moduleResolution: node10 is too old to read an exports map: the root entry still resolves through main/types, but /react, /mock, /images and /ui do not resolve at all.

Our own five apps take the kit from the registry (@zibby-run/app-kit@^0.2.1).

For coding agents

The guidance ships inside the package, so it cannot drift from the code it describes. After installing, point your agent at node_modules/@zibby-run/app-kit/AGENTS.md: what a Zibby app is, the contract in ten lines, the rules that break an upload, the exported surface as a table and the layout contract — written to be read in one go. llms.txt beside it is the map of the package's four documents, and llms-full.txt is all four concatenated for a single read. CHANGELOG.md carries a "For your agent" block per version: grep-this → replace-with-that for applying a bump. CLAUDE.md is a pointer at AGENTS.md.

What's in the box

transcribe (POST /api/transcribe, CONTRACT §8) and appData (/api/app/data, §12, with the app_user_id of §4 behind getAppUserId()) are live on zibby.run since platform v0.4.35 (2026-09-18), the person's Export/Erase settings page and the 90-day removal included; the speaker model came with v0.4.36. A platform older than that answers a transcription unknown with status: 404 and every appData call not_available, and an app nobody switched on for user data answers data_store_disabled — so build each as a feature the app degrades without.

New in 0.6.0 and live on zibby.run since platform v0.5.0 (2026-10-02), the platform release after v0.4.36: files (the person's files in this app, with the shell's file picker, CONTRACT §14), transcribeJob and jobs (a stored recording transcribed in the background, charged once on success, §15) and knownSpeakers on both transcribe routes (names on the diarize model's segments, §8). On an older platform every files and jobs call answers not_available and knownSpeakers is ignored; the recipe is in AGENTS.md.

New in 0.7.0: comped (CONTRACT §16) — a person whose runs are on Zibby, an admin or an account an admin comped, is told so with one key, comped: true, on the token answer, the final event, a transcription and a job, with charged_cr: 0. The kit reads it into getComped() (root), useComped() (/react) and ZIBBY_COMPED_EVENT; comped?: true sits on ZibbyTerminal, TranscribeResult, TranscribeJob and useZibbyAi's capPrompt; ZIBBY_COMPED_LINE is Zibby's own sentence, "Comped: your runs are on Zibby"; the mocks play such a person on window.__zibbyAiMock = { comped: true }. The platform half is live on zibby.run since platform v0.7.0 (2026-10-03); on any older platform, comped is never sent and everybody reads as a regular person, exactly as before.

src/
  config.ts        contract constants (endpoint, credits path, CSRF meta names)
  client.ts        streamZibbyAi — the /api/ai lifecycle, SSE parsing, typed refusals
  transcribe.ts    transcribe — /api/transcribe: base64, the refusal table, the charge
  appData.ts       appData — /api/app/data: the name grammar, versions, quotas, refusals
  files.ts         files — /api/app/files: reserve, signed PUT, complete; list/get/delete; the shell's picker
  transcribeJobs.ts transcribeJob / jobs — /api/transcribe/jobs: start, poll + zibby:job push, cancel
  knownSpeakers.ts the known-speakers rules and wire shape, shared by both transcribe routes
  hostBridge.ts    hello/token postMessage bridge, trust model, avg_action_cr, app_user_id
  balance.ts       the zibby:balance in-page event
  appUser.ts       the zibby:appuser in-page event and getAppUserId()
  comped.ts        the zibby:comped in-page event and getComped() — runs on Zibby
  mock.ts          createMockStream / createTranscribeMock / createAppDataMock / createFilesMock /
                   createTranscribeJobsMock — the dev mocks (separate entry)
  hostNav.ts       the six-path shell allow-list, hostNavigate, fallbackHref
  vite.ts          zibbyViteConfig — base, assetsInlineLimit, the vitest keys
  react/
    useZibbyAi.ts        one hook per AI action button
    useCreditBalance.ts  the listening half of a balance display
    useAppUser.ts        this app's own id for the signed-in person, or null
    useComped.ts         true for a person whose runs are on Zibby
  ui/
    fonts.css            the two Inter @font-face rules (separate entry)
  fonts/           inter-var-latin(.ext).woff2 — the vendored faces
  publish/         NODE ONLY: the manifest bounds, validators and writers
  bin/zibby-zip.ts NODE ONLY: the `zibby-zip` command
  bin/app-kit.ts   NODE ONLY: the `app-kit` command; bin/init.ts is its `init`
template/          the app `app-kit init` writes — shipped, never compiled here

The package ships built: dist/ carries ESM with explicit .js specifiers and its own .d.ts files, so Node's ESM loader, Vite and Next all resolve it as published — no transpilePackages entry, no bundler special-casing. The sources under src/ are what you read; dist/ is what you import.

The build-time half

An app's src/ imports @zibby-run/app-kit, /react and /ui. Its scripts/, its vite.config.ts and its package.json reach for these instead. /publish and the bin are Node code that runs BESIDE the bundle and must never end up inside it; /vite is a plain object that imports nothing and is simply of no use anywhere but the Vite config.

| Entry | What it is | |---|---| | @zibby-run/app-kit/publish | Node only. The two publish manifests: the page bounds (SLUG_RE, TITLE_MIN/MAX, DESCRIPTION_MIN/MAX, PAGE_CAP, ENTRY_FRAGMENT_MAX, PAGES_MAX_BYTES), validatePages(pages, extra?), pagesManifest, serializePages, writePagesManifest(outDir, pages, extra?), and the sample's SAMPLE_ACTION_RE, SAMPLE_REQUEST_CAP, SAMPLE_MAX_BYTES, SAMPLE_PROVIDERS, validateSample, serializeSample, writeSampleManifest(outDir, sample), plus bundleProblems / zipBundle behind the bin. The bounds are the PLATFORM's, quoted from x-mart/lib/zibby/apps/ and lib/zibby/ai/provider.ex; the writers fail the build with a list of problems, because the platform refuses the whole publish for the same reasons and leaves the previous bundle live. extra keeps an app's OWN page rule (a required slug prefix, say) running beside them. Never import it from src/. | | zibby-zip (bin) | "zip": "zibby-zip". Reads name from the package.json in the current directory, refuses a dist/ missing index.html, zibby-pages.json or zibby-sample.json, and writes <name>.zip with dist's root as the archive root. | | app-kit (bin) | npx @zibby-run/app-kit init my-app — copies template/ into a new directory, fills the name and the display name, pins this package's own version, and prints the three next commands. Refuses a non-empty directory, a missing name, a name that cannot become a slug, and an unknown subcommand. The bin's name is the package's unscoped name on purpose: that is the one npx <package> <args> runs. | | @zibby-run/app-kit/vite | zibbyViteConfig — a plain object, no vite import and no peer dependency: base: "./", build.assetsInlineLimit: 0, test.environment: "happy-dom", test.include. Spread it: defineConfig({ ...zibbyViteConfig, plugins: [react()] }). The test key needs the app's own /// <reference types="vitest" />, which it already has. | | @zibby-run/app-kit/fonts.css | The two Inter @font-face rules and the two .woff2 files behind them, emitted as bundle assets off the serving origin. ui.css still carries no face — it only names the family — so an app with its own typeface imports ui.css alone. |

Adopting an app (the short version)

  1. Depend on the kit. pnpm add @zibby-run/app-kit — or, while working against an unpublished change, pnpm add @zibby-run/app-kit@file:/path/to/zibby-app-kit (a file: dependency resolves through dist/, so run pnpm build in the kit first).
  2. Define your actions in one config module of your own: an action union, a model per action ({provider, model} with PLATFORM model ids), quoted costs, token/context budgets.
  3. Mount the bridge once at app startup: React.useEffect(() => installHostBridge(), []).
  4. Wrap the hook so buttons stay one-liners:
    export default (action: MyAction) =>
      useZibbyAi(action, MY_MODELS[action], { onCharged: t => myChargeToast(t) });
  5. Wire the mock (optional but recommended) in a lazily-loaded module built with createMockStream, registered via setMockLoader. An image app adds cannedImages: () => [MOCK_IMAGE_DATA_URL] and develops its gallery offline.
  6. Take the build-time half rather than writing it again: spread zibbyViteConfig, import @zibby-run/app-kit/fonts.css, call writePagesManifest / writeSampleManifest from your two postbuild scripts, and set "zip": "zibby-zip". See The build-time half above.
  7. Link out through the allow-list. hostNavigate("/billing") — not an href — for every link out of the app, with fallbackHref on the standalone anchor and shellPathLabel for its text.
  8. Build a static export that satisfies CONTRACT.md §1 (relative URLs, nothing leaves the origin). Publishing is by invitation. A creator's route is a GitHub repository, which Zibby builds itself — live on zibby.run since platform v0.5.0 (2026-10-02), by invitation (CONTRACT.md §13; AGENTS.md → Making this repository publishable on Zibby). When the Zibby team publishes an app for you instead, pnpm zip it and hand over the zip, the app's name, an icon and a cover (PNG/JPG/WEBP, at most 2 MB each). AGENTS.md → What the platform checks at publish tables every limit the archive must meet (50 MB, 400 MB unpacked, 20,000 entries, no absolute or .. paths, no symlinks, index.html at the archive root).

Images

An image action is an ordinary action whose payload carries modalities: ["image","text"] (CONTRACT.md §2, "Images"). The generated pictures come back on ZibbyResult.images / the hook's images as data URLs. The platform stores none of them — offer a download while they are on screen.

The request body is capped at 24 MB (a 12 MP photo is ~16 MB once base64), and over it the proxy answers 413 payload_too_large, which the kit raises as a ZibbyAiError with that code and details.max_bytes. Downscale before sending. result.requestId is on a SUCCESSFUL call too — show it when an answer arrives charged but unusable.

UI layer

src/ui/ is the shared look of the five apps, built from the measured Figma frames (x-mart/docs/design/apps-2026-09-15/README.md). It is presentational only — no network, no storage, no host bridge — so it composes with the hooks above rather than replacing them. The one page-wide fact it reads (0.7.0) is whether the person's runs are on Zibby (useComped()), so that CreditsChip never quotes such a person a price. Two import lines:

import "@zibby-run/app-kit/ui.css";
import { AppLayout, Button, Dropzone } from "@zibby-run/app-kit/react";

(@zibby-run/app-kit/ui is the same components without the hooks.) The stylesheet does NOT load a font: it only NAMES the family (--zk-font: "Inter var", Inter, system-ui, …), as CONTRACT.md §1 requires. The faces are one import away — import "@zibby-run/app-kit/fonts.css" — and an app with its own typeface simply leaves that line out. Every class is zk- and every custom property --zk-, so an app stylesheet cannot collide; the tokens live on the .zk root class, which AppLayout already carries.

| Component | What it is | |---|---| | AppLayout | The two panes — controls (white, right divider) and output (page grey) — with an optional tabs slot above the output. controlsWidth is a CSS grid track, default minmax(360px, 30%). Stacks at 900 px. Owns the height and the scrolling — see The layout contract below. | | TitleBlock | H6 app name + Body lede. | | Section, Stack | A BodyBold label over its block (gap 10); blocks stacked (gap 32). Section takes as. | | PaneHeader | A pane heading with an action opposite it — "Your scenes" + Download all, "Restored from this browser" + Clear. Titles in a column on the left, the action held against the right edge by space-between and never stretched, both topped out so a two-line note does not drag the button down; it wraps under the titles in a narrow pane. as picks the element (default "h2"), look the size ("h6" default, "body-bold"), id lands on the heading so a landmark can be labelled by it. note takes the WORDS, not a <Helper> — the kit wraps it in one, and a <p> in a <p> is markup the browser unnests. A falsy note or actions (undefined, null, false, "") renders no element at all, so a conditional note costs no empty line. | | RestoredSection | "Restored from this browser" — the landmark, its three sentences, its confirm and its Clear button, as one component, so the five apps stop wording it five ways. children is the restored list (or the "shown above" helper, or nothing); onClear runs only when the confirm is accepted. It brings its own top margin when something sits above it — see The layout contract. | | Button, ButtonRow | primary \| secondary \| tertiary, disabled, icon, credits, fullWidth; a real <button> unless as="a"/href. A button with no text child must pass aria-label — the type refuses to compile without one. | | CreditsChip | Coin + "~N". The coin paints with currentColor, so it is white on primary, brand on secondary/tertiary and disabled grey on a disabled button, with no per-variant asset. Renders nothing for a person whose runs are on Zibby (useComped(), 0.7.0), so no Button, RadioRow or Segmented option quotes them a price. | | Field, Input, Textarea, Select, FieldRow | Label row (+ counter) over a control; Textarea takes a fixed height, Select carries the 16 px caret, FieldRow puts fields side by side with optional fixed widths. | | Dropzone, FileCard | idle \| uploaded \| busy, a real file input inside the label (keyboard reachable), drag-and-drop wired; the Studio file card with an 88 px thumb. disabled is the whole-run lock: label, hint and glyph at the disabled colour, cursor: not-allowed as on a disabled radio row, no focus highlight, the box keeping its ground and border. | | InfoBox | SmallBold lead + Small body, with the credits sentence as note one blank line below. | | Notice | The error / warn / info surface. Tone is the FACE and role is the ANNOUNCEMENT, and they are separate: error (negative tokens) and warn (amber) default to role="alert", info (tertiary tokens) to role="status", and role overrides any of them — "none" renders no role, for a panel that defers to the alert below it. | | RadioGroup, RadioRow | Real radios in 56 px rows: label + price, selected and running states. description puts a Small ink-2 blurb inside the row, wired to the input with aria-describedby; the row grows only when one is given. | | Segmented | The brand-5 container of equal buttons: selected primary, the rest tertiary. | | Tabs | The 55 px role="tablist" strip, with the WAI-ARIA keyboard map (Arrow Left/Right wrap, Home/End) behind its roving tabindex. | | Helper, ChargedLine | The Small ink-2 lines under a button and under a result. ChargedLine is a polite live region (role="status") — it is the money surface, and it replaced a toast that announced itself. Helper is not. Pass the run's own comped (terminal.comped, result.comped, a pickup's outcome.terminal.comped; 0.7.0) and it says "Comped: your runs are on Zibby" in place of its children. It never reads the page-wide value, so a run the person paid for stays paid. | | EmptyState | Centred (or align="start") H6 + Body; tone="brand" is the working state. | | ResultCard, SceneGrid | The scene shell — picture area, title, actions row — in a 2-column grid. The picture is shown WHOLE, centred, symmetrically letterboxed onto the card's own white: the card and the Download button beside it must be the same photograph. Square by default; mediaRatio takes any CSS aspect-ratio value ("auto" included) and lands on the picture area itself, not on the padded box. title takes as. | | SliderTags | The "Original"/"Result" tags; the divider and handle STYLES ship in ui.css (zk-slider__divider, zk-slider__handle) and the mechanics stay in the app. | | Chip, ChipRow | Keyword chips, positive and negative. | | ScoreCard, CriteriaTable, GapCard, FooterBar | The Tailor analysis blocks. GapCard and FooterBar take as. | | Icon | One exported Figma glyph, inlined from src/ui/icons/glyphs.ts. |

The glyphs are the exported SVGs, copied into src/ui/icons/ and turned into one TypeScript module by pnpm icons — no bundler plugin, so the same import works in the kit's tsc build and in every app's Vite build.

Headings

Section (its label), ResultCard, GapCard and FooterBar (their titles) each take the same prop:

as?: "p" | "h2" | "h3" | "h4"   // exported as LabelAs; default "p"

The classes and the styling never change with it — only the element does, so a heading costs nothing. A kit component cannot know where it sits in a page's outline; the app can, and this is how it says so.

PaneHeader takes the same prop and the same values, but defaults to "h2": a pane header IS the top of a section, so there the heading is the common case and a <p> the exception. Its look prop is the separate question of size — "h6" (default) or "body-bold" — so an app can have a BodyBold <h2> (the restored section) or an H6 one ("Your scenes") without the element and the type size being the same decision.

The layout contract

AppLayout owns the height and the scrolling, so the apps do not have to.

At 900 px and up the shell fills the height of whatever it is mounted in and each pane scrolls on its own. A control pane longer than the window scrolls inside itself; the output pane's heading stays where it is. Below 900 px the panes stack, both scrollers are released, and the page scrolls as it always did.

What the app owes the shell — a height, and nothing else:

html, body, #root { height: 100%; }
body { margin: 0; }

That is the whole contract. AppLayout sets height: 100% and flex: 1 1 auto on itself, so it fills its container whether it is mounted as a block in a full-height #root or as the growing child of a full-height flex column — pick either; if you pick the flex column, give that column min-height: 0 as flex containers require, and flex-shrink: 0 to any header or footer sibling in it, because the shell's flex basis is its full height and the shrink weights are no longer what they were.

What the app must NOT do any more:

| Stop doing | Why | |---|---| | min-height: 100vh (or any min-height) on a wrapper around the shell | it makes the shell taller than the window, which is what put the output heading off screen | | overflow-y: auto on .zk-app__controls / .zk-app__output, scoped or otherwise | the kit sets it, and a scoped override cannot release it again below 900 px | | any app-side rule that reaches a zk-app* class to set a height, an overflow or a min-height | all three are the kit's now |

In a container that has no height of its own — a docs page, a storybook cell — the shell behaves exactly as it did before, so nothing breaks where the contract is not met. The two declarations get there by different routes, and the difference matters: height: 100% against an indefinite containing block computes to auto, while flex: 1 1 auto does not resolve to anything — it simply has no free space to grow into when the container is content-sized. The stacked band below 900 px therefore has to release the flex growth explicitly (flex: 0 1 auto) alongside height: auto; releasing only the height would leave a flex-column mount still filling its column.

EmptyState is height: 100%, so "centred, fills the pane" follows from the same contract: with a height on the shell it fills and centres in the output pane; without one it is as tall as its own two lines.

RestoredSection brings its own top margin. It carries margin-top: 32px, which inside a gap-32 column — the kit's Stack, or an app's own results column — puts 64 px between it and the block above: twice today's gap, so the browser's leftovers read as a different thing from this run's output (the designer, 2026-09-16). The margin is on the section rather than on a parent so it holds in either column. An app must not add another — no margin-top on a wrapper around it, no bigger gap on the column that holds it.

The margin is there only when something sits above the section. .zk-restored:first-child zeroes it, so a section that is the first child of its column starts flush with the top of the pane. That case is real: in an output pane whose other blocks are all conditional — Extender's is — a return visit with saved results and nothing uploaded renders the section alone, and a 32 px margin there would push it out of line with the control pane's first block instead of setting it apart from anything. An app needs to do nothing for this; it needs only to not wrap the section in a <div> that would make the wrapper, rather than the section, the column's first child.

pnpm demo renders every component in every state to ui-demo.html at the repo root (CSS and icons inlined, no external request) for a human to open.

Development

pnpm install
pnpm typecheck
pnpm test
pnpm build    # -> dist/ (+ llms-full.txt), what the package actually ships
              #    two tsc passes: the browser half (tsconfig.build.json) and
              #    the node half, src/publish + src/bin (tsconfig.node.json)
              #    and what a `file:` consumer resolves through; run it after
              #    any src change
pnpm demo     # -> ui-demo.html, the UI layer in every state
pnpm icons    # regenerate src/ui/icons/glyphs.ts after re-exporting from Figma

License

MIT — see LICENSE.