Contribution
Contribute to shuip to make it better.
Setup
Clone the repository
git clone https://github.com/plvo/shuip.git
cd shuipInstall dependencies
bun installCreate a branch for your changes
git checkout -b feat/my-componentStart the development server
bun devRepository 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 presetsThe generator emits registry.json, __index__.ts, stubs, MDX symlinks, and public/r/*.json — never 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:
| Category | Prefix | Used for |
|---|---|---|
components | (none) | Standalone UI components |
blocks | (none, registry:block) | Larger composition components |
react-hook-form | rhf- | Fields integrated with react-hook-form |
tanstack-form | tsf- | Fields integrated with @tanstack/react-form |
tanstack-query | tsq- | 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, notui)
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:generateConfirm 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:docsThis chains registry:generate → registry:build → next 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 checkruns Biome with auto-fix. Pre-commit hook runsbiome check --write --unsafevia lint-staged. - TypeScript 6. Use
import typefor type-only imports. - Shared external deps are versioned via Bun catalogs (
workspaces.catalogandworkspaces.catalogs.{fumadocs,radix,forms}in rootpackage.json). Reference as"<pkg>": "catalog:"or"catalog:<name>"— never pin a literal version in a workspace'spackage.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-componentThen open a Pull Request on github.com/plvo/shuip.
Thank you for your contribution to shuip! 🚀