> ## Documentation Index
> Fetch the complete documentation index at: https://docs.9am.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Maintaining the kit

> Drift checks, adding components, and releasing changes to the 9AM UI registry.

## Drift policy

Copy-in distribution means every script owns its files and can edit them. That flexibility is the point — but drift you did not choose is how a set of scripts stops looking related.

```bash theme={null}
bun src/scripts/9am-ui.mjs check
```

| What changed                       | Result                                                                                                                                    |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `9am-theme.css` or `9am-fonts.css` | **Fails, exit 1.** Restore with `bunx shadcn@latest add @9am/theme @9am/fonts --overwrite`, or make the change in the kit and release it. |
| Any component or icon              | **Warns, exit 0.** Allowed, but a local fix helps nobody else — consider upstreaming.                                                     |
| `src/nui.config.ts`                | Ignored. It is supposed to differ per script.                                                                                             |
| Anything from `@9am/scaffold`      | Ignored. A starting point, not a managed file.                                                                                            |

The asymmetry is deliberate. Identical tokens are what make scripts read as siblings; a one-off button variant in one script is a legitimate escape hatch.

Line endings are normalised before comparison, so a Windows checkout will not report false drift.

Wire `check` into CI. It only fetches the items you actually have installed, not all 76.

## Doctor

```bash theme={null}
bun src/scripts/9am-ui.mjs doctor
```

Verifies the things that are easy to get wrong and hard to notice:

* The `@9am` registry is configured and the token is set and working
* `index.css` imports both a theme and a font stylesheet
* `postcss.config.js` still has the [oklch conversion](/ui-kit/cef#oklch-is-not-parsed)
* Your built CSS contains no surviving `oklch()`

<Note>
  The checker is not an installable package. Bun resolves `github:` and `git+https:` dependencies through the GitHub tarball API, which returns 404 for a private repository even with a token — so it ships through the registry as `@9am/tools` and reuses the credentials already in `components.json`.
</Note>

## Repository layout

```
registry/
  theme/      9am-theme.css (locked), 9am-fonts.css (generated), fonts/*.woff2
  lib/        cn(), smooth-scroll
  nui/        fetchNui, useNuiEvent, misc, debugData, useTheme,
              VisibilityProvider, nui.config.ts, vite-env.d.ts
  i18n/       runtime dictionary from Lua
  ui/         19 primitives
  icons/      47 animated icons
  tools/      the check/doctor script
  scaffold/   web/ + lua/
r/            BUILT registry JSON — committed, served by raw.githubusercontent
apps/preview/ gallery, imports straight out of registry/
cli/          setup commands (lua, fonts)
scripts/      build-fonts.ts, build-registry.ts
```

<Warning>
  `registry.json` is **generated**. Never hand-edit it. `scripts/build-registry.ts` derives every item's npm dependencies and registry dependencies from the real import graph, so the manifest cannot disagree with the code.
</Warning>

## The preview gallery

```bash theme={null}
bun run preview
```

Renders every primitive, icon, and the viewport in both themes, importing straight out of `registry/` rather than from an installed copy. If a component would break on `shadcn add`, it breaks here first.

Use it to check both palettes before releasing a theme change.

## Adding a component

<Steps>
  <Step title="Add the file">
    Drop it in `registry/ui/`, or `registry/icons/` if it exposes the `startAnimation()` handle.
  </Step>

  <Step title="Use consumer-facing import paths">
    `@/lib/utils`, `@/components/ui/button`, `@/hooks/useNuiEvent`, `@/utils/fetchNui`, `@/i18n`.

    Never use a relative path across directories. That is what the file resolves to once installed — a relative path that works in the kit will break in a script.
  </Step>

  <Step title="Declare new packages">
    Add any new npm dependency to the root `package.json` first. The generator errors out on an import it cannot find a version for, rather than shipping an item whose package never installs.
  </Step>

  <Step title="Show it in the gallery">
    Add it to `apps/preview/src/Gallery.tsx`. Icons appear automatically — they are globbed.
  </Step>

  <Step title="Build and verify">
    ```bash theme={null}
    bun run build
    bun run typecheck
    bun run preview
    ```
  </Step>
</Steps>

## Adding a user-facing string

Kit components must not hardcode English. Use `t("ui.common.…")` and add the English fallback to `KIT_DEFAULTS` in `registry/i18n/index.ts` **and** to `registry/scaffold/lua/locales/en.json`.

If the string is domain-specific rather than generic, take it as a prop instead. `DataTable`'s `countLabel` exists for exactly this reason — so a coupons table can say "12 coupons" while the shared default stays "12 rows".

## Changing the theme

Token changes land in every 9AM script, so this is the highest-blast-radius edit in the repo.

* Check **both** palettes in the preview. Light mode is not a tint of dark mode.
* Never author a colour outside the token set. A raw hex in a component is drift `check` cannot see.
* Fonts: replace the woff2 in `registry/theme/fonts/`, then run `bun run fonts`. Never edit `9am-fonts.css` — it is generated.

## Releasing

```bash theme={null}
bun run build       # regenerates fonts, registry.json and r/
bun run typecheck
bun run preview     # eyeball both themes
git commit -am "feat(button): …" && git push
```

`r/` is committed on purpose. `raw.githubusercontent.com` serves committed files, which is what lets a private repository act as a registry with no hosting at all.

CI re-runs the build and fails if the committed `r/` differs from what the sources generate, so a stale registry cannot reach consumers.

Nothing auto-updates. Consumers pick a change up on their next `shadcn add … --overwrite`, which is deliberate — a script should adopt a token change when someone is watching it, not silently on a Tuesday.
