Ravenwood Software
Blog

Shipping a Personal ravenwood Registry Without Becoming a Design-System Company

I keep rebuilding the same buttons. Same warm palette, same type pairing, same small decisions about borders and focus rings, copied from one project to the next and drifting a little each time. So I did the boring, useful thing: I put those components in one place and made them installable with the shadcn CLI. That place is `ravenwood-ui`, a personal shadcn registry. It isn't a product, and it isn't a launch. It's how I stop copy-pasting my own work.

What this is (and isn't)

`ravenwood-ui` lives on GitHub at `raythurman2386/ravenwood-ui`. The repo is public, so anyone can read it or pull from it. The `package.json` is set to `"private": true`, though, and that's on purpose. I don't publish it to npm, there's no versioned design-system package, and there's no support promise attached to it.

It's a shadcn registry: a set of JSON files the shadcn CLI knows how to read. When a project needs a component, the CLI copies the source into that project, and from then on the project owns the code. That fits how I already work. I want the code to sit in the app, where I can read it and change it, not behind a dependency I have to work around.

If you want to see it before reading the rest, there's a live preview at ui.ravenwoodsoftware.dev.

Naming: cabin vs radix-nova vs @ravenwood

The project has three names in it, and each one means something different:

  • radix-nova is the shadcn CLI style. I kept Radix primitives and the `radix-nova` style rather than inventing a style of my own. Consuming projects set `"style": "radix-nova"` in `components.json`, so whatever the CLI generates matches what the registry expects.
  • Cabin is the look: the tokens on top of that style. Fraunces handles display type and Figtree handles body text. The colors are named for what they look like: bark, canopy, lantern, moss, and rust.
  • `@ravenwood` is the registry namespace, the prefix you type when you install something, as in `@ravenwood/<name>`.

Splitting them up this way keeps each layer honest. Radix and radix-nova cover structure and behavior. Cabin covers how things look. `@ravenwood` is where they come from.

The cabin colors weren't invented for this registry. Several of my internal apps already use the Ravenwood colors, and so do my `ravenwood-vscode` theme and my omarchy themes. The registry is built on that existing palette. As I take on more client work, I'm gradually bringing the registry into those projects too.

What's in the catalog

The registry has 61 catalog items under `@ravenwood` right now. Each one is a JSON file at `public/r/{name}.json`, served raw from the repo. Nothing more is needed. There's no registry server and no API, just static JSON the CLI fetches.

Two things are on the way but not shipped yet:

  • Carousel: an Embla-based carousel is in progress and will go into the catalog in time.
  • Maps: map components are also in progress.

Neither one is in the catalog today. If you go looking for them, you won't find them yet.

How publish and consume actually work

Both sides are short.

Publishing. In the registry repo:

pnpm registry:build

That script runs `shadcn build`, which reads `registry.json` and the component sources and writes the built item files to `public/r/`. Then I commit and push three things together:

  • `registry.json`
  • the component sources
  • `public/r/`

Pushing is the release. Once the JSON is on GitHub, it's live.

Consuming. A project can be set up from the registry's index, or point at the registry and add items one at a time. In either case, `components.json` gets a `@ravenwood` registry entry that points at the raw `public/r/{name}.json` URLs:

{
  "style": "radix-nova",
  "registries": {
    "@ravenwood": "https://raw.githubusercontent.com/raythurman2386/ravenwood-ui/main/public/r/{name}.json"
  }
}

Then adding a component looks like this:

npx shadcn@latest add @ravenwood/<name>

When I change something in the registry and want the update in an app, I pull it again:

npx shadcn@latest add @ravenwood/<name> --overwrite

That covers the whole workflow.

Proof: two consumers, not a launch

I didn't want this to stay a nice idea with nothing using it, so two of my own projects use it now:

  • `ravenwood-software`, the agency site, adopted it in PR #13 (around September 30, 2026).
  • `ravenwood`, my portfolio, adopted it in PR #207 (around October 2, 2026). That PR also moved the portfolio off Bootstrap.

Both projects have `components.json` set to `radix-nova` with the `@ravenwood` registry URL. Each one has a small set of UI components installed, only what it needs right now. Neither project has signature `components/ravenwood` pieces checked in yet.

That's all the proof there is: two real projects, both mine, using the registry the way it was built to be used. It's enough to show the publish → consume → update loop works outside the repo it lives in.

Where apps intentionally diverge

The main design decision across these projects is about light and dark mode, and my rule is simple: follow the user's system preference.

Every app that uses the registry follows `prefers-color-scheme`. There's no theme switcher. If your OS is in dark mode, you get dark. If it's in light mode, you get light. I'm not forcing a dark brand experience on anyone, and I'm not adding a toggle that a few people will click once and then forget about.

Earlier notes of mine described the registry as "dark by default" and the portfolio as "light-first." That made it look like the projects disagreed. The real policy is the one above: the OS decides, everywhere. Every app owns both modes deliberately. Neither one is a fallback.

That's the line between what's shared and what's local. Tokens, components, and the light/dark policy are shared. Layout, content, and how much of the catalog an app pulls in are up to each app.

The update loop (why this stays small)

The registry stays manageable because of how updates work:

  1. Change a component in `ravenwood-ui`.
  2. Run `pnpm registry:build`.
  3. Push.
  4. In any app that wants the change, run `shadcn add @ravenwood/<name> --overwrite` and review the diff.

There's no version bump, changelog ritual, or release pipeline. Each app gets the change when I decide to pull it, and I see exactly what changed in a normal code review.

That's also why this isn't turning into a design-system company. A packaged design system needs versioning, deprecation policies, and a migration story, because the people consuming it aren't the people maintaining it. Here, those are mostly the same person: me.

What we deliberately skip

Some things I'm choosing not to do, at least for now:

  • No npm package. The repo is `private: true` and stays that way. Distribution happens through the shadcn CLI and raw JSON.
  • No big-bang install. Signature components and the fuller catalog arrive in each project gradually, as the work calls for them. That's why neither consumer has `components/ravenwood` signatures checked in yet. It's the same pace at which I'm bringing the registry into client work.
  • No theme switcher. The OS preference decides, as covered above.
  • No early shipping. The Embla carousel and the map components are works in progress. They'll go into the catalog when they're ready, not before.
  • No version pinning (for now). I could pin consumers to a tag or commit, and nothing stops me. But I expect to be the main consumer, and with the shadcn CLI, `add --overwrite` plus a diff review is easy enough that tracking `main` is the honest setup. If someone other than me starts depending on this, I'll revisit that.

Closing

It's a small project. It's a folder of JSON files, a build script, and a few CLI commands that let me reuse my own work without pretending it's a product. If it ever needs to grow into something more formal, the code is all there and easy to read. Until then, it does what I need: my projects look like they belong together, and I'm not rebuilding the same button anymore.

Need a map or app built? Book a free consultation.