Contributing
How an item is added or changed, and the rules apps follow when they need UI.
Rules for apps
When a screen needs UI, go in this order:
- Reuse what the app already has in
src/components/ui. - Install it from
@nalarxif the registry has it. - Ask before writing a new primitive. A new pattern that more than one screen needs becomes an item here.
A change to an installed file goes upstream: change it in this repo, release a new version, and reinstall. A local edit that cannot wait is listed under overrides in the app's nalarx-ui.lock.json with a reason and removed once the upstream version lands.
Adding an item
Each item lives in its own folder, so two people adding items never edit the same file.
registry/
items/button/
item.json the item definition
CHANGELOG.md one "## <version> (<date>)" entry per release
nx/Button.tsx source, laid out as it installs (src/components/ui/nx/Button.tsx)
nx/lib/cn.ts
styles/tokens.css installs to src/styles/nalarx-ui/
styles/nx.css- Add the source under
registry/nx/and createregistry/items/<item>/item.json. Paths infilesare repo-relative; the install target comes from the path, so never writetarget.namemust equal the folder name. - Set
meta.versionto0.1.0and write the first changelog entry. - Run
bun run registry:build. It assemblesregistry.jsonand rebuildspublic/r. - Add a docs page under
content/docs/components/. - Run
bun run checkandbun run build, thenbun run clean-consumerif the item changes how installation works.
Commit the generated files (registry.json, public/r). CI fails when they are stale.
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "button",
"type": "registry:ui",
"title": "Button",
"description": "The NalarX button: four sizes, seven variants, press feedback.",
"meta": { "version": "0.1.0" },
"dependencies": ["@base-ui/react@1.9.0", "class-variance-authority@0.7.1"],
"registryDependencies": ["@nalarx/base"],
"files": [{ "path": "registry/nx/Button.tsx", "type": "registry:ui" }]
}The assembler checks that:
- every dependency on another item is written
@nalarx/<item>and that item exists; - every npm dependency is pinned as
name@x.y.z; - every file exists, sits under
registry/nx/,registry/styles/orregistry/scripts/, and belongs to one item; - the changelog has an entry for the current version.
Writing item source
- Name exports without a prefix (
Button,buttonVariants), one PascalCase file per item, named exports only. - Import relatively inside
nx/:./lib/cn,./Icon. Installed files then never depend on the app's own paths. - Put
"use client"only on files that use hooks or Base UI parts. - Text the user reads defaults to English and can be replaced by a prop or by the optional
UIStringsProvider. - Build on
@base-ui/reactandclass-variance-authority. No Radix. - Style with Tailwind utilities and the theme names (
bg-bg-elev,text-body,rounded-nx,h-control-md). Inlinestylemay only pass CSS variables; lint enforces it. - Keep product meaning out: no API calls, no app routes, no i18n hooks. Text that the user reads comes in through props.
- Shared CSS that several items need is an
@utilityor--animate-*key inregistry/styles/nx.css, with abaseversion bump. Item-specific class blocks fromglobals.css(.nx-btn,.nx-chip,.nx-seg-*) become cva variants inside the item.
Global classes still in nalarx-frontend
base carries the shared classes. These rules stay in the frontend's globals.css until the item that owns them is ported:
| Classes | Owner item |
|---|---|
.nx-btn, .nx-btn.*, .nx-btn-touch (and the .nx-btn press and transition rules) | button |
.nx-chip, .nx-chip-dot | chip |
.nx-seg-list, .nx-seg-tab, .nx-seg-tab-underline, .nx-seg-ind | segmented-tabs |
.nx-select-panel, .nx-select-open | select |
.nx-dots-loader, @keyframes nx-pixel-on | dots-loader |
.nx-modal-close, .nx-icon-close | modal-close-button |
.nx-field | input |
.nx-page and screen entrance classes | app (not an item) |
Releasing
- Bump
meta.versionin each changed item and add its changelog entry. - Bump
versioninpackage.jsonand add an entry to the rootCHANGELOG.md. - Run
bun run registry:release. It rebuildspublic/rand freezes it intopublic/r/v<version>/, which the docs site serves as an immutable snapshot. Commit, and tagv<version>plus<item>@<version>for each changed item.
Pushing tags and deploying the docs need the repo owner's go-ahead.