khanhanh@sydney: ~/projects/kaui-shadcn-registry · cat kaui-shadcn-registry.md
$ cd .. back to ~/projects (esc)
project: KaUI: installable shadcn patterns delivered as editable sourcerole: solo engineer: component APIs, registry pipeline and documentationstack: Astro · React 19 · TypeScript · Tailwind · shadcn/ui · Vitesttimeline: active development · 2026status: ● live at kaui-shadcn-registry.vercel.app · source on github

KaUI ships source, not another dependency.

The same async button and searchable select kept appearing across projects. KaUI turns those repeated solutions into shadcn registry items: one CLI command installs readable source inside the consuming application, with no KaUI runtime left behind.

The product constraint

A registry component has two customers. It must behave correctly for the person using it, and its generated source must arrive with valid imports and dependencies for the engineer installing it.

  • Ownership is the feature. Consumers receive source files they can inspect and change.
  • The manifest is the source of truth. Generated files under public/r are build artifacts, never hand-edited content.
  • The useful edge cases stay visible. Controlled state, server filtering and stale async responses are part of the API rather than undocumented exceptions.

Decision 1: generate shadcn endpoints from one manifest

registry.json declares each item, target path and dependency. Before development and production builds, a Node script reads the declared source, rewrites internal imports and emits one shadcn-compatible endpoint per item plus the discovery catalog.

for (const item of registry.items) {
  const files = item.files.map((file) => {
    const raw = readFileSync(resolve(root, file.path), "utf8");
    const { path: _path, ...rest } = file;
    return {
      ...rest,
      path: file.path,
      content: transformImports(raw),
    };
  });

  writeFileSync(`public/r/${item.name}.json`, JSON.stringify({
    name: item.name,
    type: item.type,
    files,
  }, null, 2));
}
WHY IT MATTERS

Documentation, CLI installation and manual source listings all derive from the same catalog. A component cannot quietly drift away from the endpoint that installs it.

Decision 2: make stale async completion impossible

The registry began with useAsync. A simple loading boolean was not enough: two overlapping requests can finish out of order, and an old result can overwrite a newer user choice.

Every execution receives an incrementing request ID. Only the latest request may commit state or run callbacks. Resetting increments the same ID, invalidating work already in flight.

const requestId = ++requestIdRef.current;
const data = await action(...args);

if (requestId === requestIdRef.current && mountedRef.current) {
  setState({ status: "success", data, error: undefined });
  onSuccess?.(data, args);
}

The hook exposes a discriminated state union, a fire-and-forget execute function and a rejecting executeAsync function. Four Vitest cases cover success, rejection, stale-response suppression and reset during an in-flight request.

Decision 3: defer expensive password analysis

The password input can score strength with zxcvbn, but its dictionaries do not belong in the initial interaction path. The component defers rapid input updates and imports language graphs only when the checker mounts.

const deferredPassword = useDeferredValue(password);

Promise.all([
  import("@zxcvbn-ts/language-common"),
  import("@zxcvbn-ts/language-en"),
]).then(([common, english]) => {
  setZxcvbn(
    new ZxcvbnFactory({
      translations: english.translations,
      graphs: common.adjacencyGraphs,
      dictionary: {
        ...common.dictionary,
        ...english.dictionary,
      },
    }),
  );
});

If the optional language data fails, the field still works. Strength feedback disappears rather than taking password entry down with it.

What shipped

10
installable registry items
11
generated JSON endpoints
4
race-condition tests

The catalog includes async controls, confirmation flows, searchable selection and password input. Astro Starlight provides the documentation shell, while React islands run interactive examples only where a component needs them.

Tradeoffs I accepted

  • KaUI is a focused registry, not a comprehensive component library. New items enter when repeated project work proves their value.
  • Components target the Radix-based shadcn primitives used by the registry. Supporting every shadcn base would weaken the source contract.
  • Generated endpoints duplicate source by design. The build step owns that duplication so contributors edit only the manifest and implementation.

KaUI saves more than setup time. It preserves the decisions that make async UI reliable while leaving the final code under the consumer's control.

-- EOF · written by khanhanh, edited by nobody● open live ↗