Contribution

Contribute to shuip to make it better.

Setup

Clone the repository

git clone https://github.com/plvo/shuip.git
cd shuip

Install dependencies

bun install

Create a branch for your changes

git checkout -b feat/my-component

Start the development server

bun dev

Repository layout

shuip is a Bun + turborepo monorepo. Items are published from packages/registry/items/ through a generator script.

apps/docs/                        # Next.js docs site (registry endpoints, MDX content)
packages/ui/                      # shadcn primitives + base.css
packages/registry/                # SOURCE OF TRUTH for published items
  items/<category>/<name>/        # One folder per item — see "Add an item" below
  scripts/generate.ts             # Scans items/, emits all downstream artifacts
packages/config/                  # Shared tsconfig presets

The generator emits registry.json, __index__.ts, stubs, MDX symlinks, and public/r/*.jsonnever edit those by hand; they're regenerated from items/.

Add an item

Categories

Each item lives at packages/registry/items/<category>/<name>/. Pick the category that matches what you're building:

CategoryPrefixUsed for
components(none)Standalone UI components
blocks(none, registry:block)Larger composition components
react-hook-formrhf-Fields integrated with react-hook-form
tanstack-formtsf-Fields integrated with @tanstack/react-form
tanstack-querytsq-Components integrated with @tanstack/react-query

The folder name is unprefixed — the generator adds the prefix from the CATEGORIES map in packages/registry/scripts/generate.ts. Naming a folder rhf-my-field publishes as rhf-rhf-my-field.

File structure

packages/registry/items/<category>/<name>/
├── component.tsx                # REQUIRED, exact filename. The published source.
├── default.example.tsx          # Primary preview (recommended)
├── <variant>.example.tsx        # Additional previews (recommended: at least one)
├── index.mdx                    # Doc page (skip for blocks — see below)
├── extras/                      # Optional. Files copied alongside on install:
   ├── <file>.action.ts         # → installs at ./actions/shuip/<file>.ts
   └── <file>.<ext>             # → installs at ./components/ui/shuip/<file>.<ext>
└── meta.shuip.json              # Optional. { "dependsOn": ["<other-shuip-item>"] }

Detailed steps

Create the folder packages/registry/items/<category>/<name>/ (unprefixed name).

Write component.tsx. Import shadcn primitives via @/components/ui/<name> (these become registryDependencies automatically). Don't import @/components/ui/shuip/* unless your item composes another shuip item — in that case, also declare it in meta.shuip.json with dependsOn.

Add 'use client' at the top only when the component uses client-only React features (state, refs, effects, browser APIs).

Add default.example.tsx. Import your component via the stub alias, not via relative ./component:

import { MyComponent } from '@/components/ui/shuip/<category-subdir>/<name>';

The <category-subdir> segment is:

  • components/ items → no subdir: @/components/ui/shuip/<name>
  • react-hook-form/ items → @/components/ui/shuip/react-hook-form/<name>
  • tanstack-form/ items → @/components/ui/shuip/tanstack-form/<name>
  • tanstack-query/ items → @/components/ui/shuip/tanstack-query/<name>
  • blocks/ items → @/components/block/shuip/<name> (note: block, not ui)

Add at least one variant example (<variant>.example.tsx) showing an alternate use-case. Doc pages with a single preview look thin.

Write index.mdx — for components, react-hook-form, tanstack-form, tanstack-query items.

---
title: My Component
description: One-line description for SEO and the sidebar.
registryName: <prefixed-name>
---

Prose explaining what this item does and when to use it.

import { TypeTable } from 'fumadocs-ui/components/type-table';

## Examples

<ItemExamples registryName={'<prefixed-name>'} />

## Props

<TypeTable
  type={{
    label: {
      description: 'The label of the component',
      type: 'string?',
    },
  }}
/>

<ItemExamples> and <ItemHeader> are registered globally (no import needed). <TypeTable> is from fumadocs-ui and must be imported inline as shown.

Blocks are different — do not put index.mdx in items/blocks/<name>/. Write the doc as a real MDX file at apps/docs/content/blocks/<name>.mdx instead. Blocks live in a separate fumadocs collection (apps/docs/source.config.ts).

Regenerate the registry:

bun registry:generate

Confirm the output shows [generate] N items processed with N incremented by one. If you see [generate] skipping <cat>/<name>: no component.tsx, the filename is wrong (must be exactly component.tsx).

End-to-end check:

bun build:docs

This chains registry:generateregistry:buildnext build. If it passes, the item is installable via npx shadcn@latest add "https://shuip.plvo.dev/r/<prefixed-name>".

Coding conventions

  • Biome is the only linter/formatter. Single quotes, 2-space indent, 120-col, trailing commas. bun check runs Biome with auto-fix. Pre-commit hook runs biome check --write --unsafe via lint-staged.
  • TypeScript 6. Use import type for type-only imports.
  • Shared external deps are versioned via Bun catalogs (workspaces.catalog and workspaces.catalogs.{fumadocs,radix,forms} in root package.json). Reference as "<pkg>": "catalog:" or "catalog:<name>" — never pin a literal version in a workspace's package.json.
  • Conventional commits. Format: <type>(<scope>): <subject>. Examples: feat(registry): add rhf-phone-field, fix(theme-button): add 'use client', chore(dx): bump deps.

Submit your contribution

git add .
git commit -m "feat(registry): add my-component"
git push origin feat/my-component

Then open a Pull Request on github.com/plvo/shuip.

Thank you for your contribution to shuip! 🚀

On this page