# Changelog (/docs/changelog) # Configuration (/docs/configuration) ## Component Customization [#component-customization] shuip components are designed to be **easily customizable**. Each component accepts all the standard props of its base component, along with some specific ones. ### Method 1: Props and className [#method-1-props-and-classname] The simplest way to customize a component: ```tsx import { SubmitButton } from '@/components/ui/shuip/submit-button' ``` ### Method 2: Editing the Source Code [#method-2-editing-the-source-code] Since the components are copied directly into your project, you can freely modify them: ```tsx // src/components/ui/shuip/submit-button.tsx export function SubmitButton({ onClick, label, disabled, loading, icon = , ...props }: SubmitButtonProps) { return ( ); } ``` # Contribution (/docs/contribution) ## Setup [#setup] Clone the repository ```bash git clone https://github.com/plvo/shuip.git cd shuip ``` Install dependencies ```bash bun install ``` Create a branch for your changes ```bash git checkout -b feat/my-component ``` Start the development server ```bash bun dev ``` ## Repository layout [#repository-layout] shuip is a Bun + turborepo monorepo. Items are published from `packages/registry/items/` through a generator script. ```bash 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/// # 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/*.json` — **never edit those by hand**; they're regenerated from `items/`. ## Add an item [#add-an-item] ### Categories [#categories] Each item lives at `packages/registry/items///`. 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 [#file-structure] ```bash packages/registry/items/// ├── component.tsx # REQUIRED, exact filename. The published source. ├── default.example.tsx # Primary preview (recommended) ├── .example.tsx # Additional previews (recommended: at least one) ├── index.mdx # Doc page (skip for blocks — see below) ├── extras/ # Optional. Files copied alongside on install: │ ├── .action.ts # → installs at ./actions/shuip/.ts │ └── . # → installs at ./components/ui/shuip/. └── meta.shuip.json # Optional. { "dependsOn": [""] } ``` ### Detailed steps [#detailed-steps] Create the folder `packages/registry/items///` (unprefixed name). Write `component.tsx`. Import shadcn primitives via `@/components/ui/` (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`: ```tsx import { MyComponent } from '@/components/ui/shuip//'; ``` The `` segment is: * `components/` items → no subdir: `@/components/ui/shuip/` * `react-hook-form/` items → `@/components/ui/shuip/react-hook-form/` * `tanstack-form/` items → `@/components/ui/shuip/tanstack-form/` * `tanstack-query/` items → `@/components/ui/shuip/tanstack-query/` * `blocks/` items → `@/components/block/shuip/` (note: `block`, not `ui`) Add at least one variant example (`.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. ```mdx --- title: My Component description: One-line description for SEO and the sidebar. registryName: --- Prose explaining what this item does and when to use it. import { TypeTable } from 'fumadocs-ui/components/type-table'; ## Examples '} /> ## Props ``` `` and `` are registered globally (no import needed). `` is from fumadocs-ui and must be imported inline as shown. **Blocks are different** — do **not** put `index.mdx` in `items/blocks//`. Write the doc as a real MDX file at `apps/docs/content/blocks/.mdx` instead. Blocks live in a separate fumadocs collection (`apps/docs/source.config.ts`). Regenerate the registry: ```bash bun registry:generate ``` Confirm the output shows `[generate] N items processed` with N incremented by one. If you see `[generate] skipping /: no component.tsx`, the filename is wrong (must be exactly `component.tsx`). End-to-end check: ```bash bun build:docs ``` This chains `registry:generate` → `registry:build` → `next build`. If it passes, the item is installable via `npx shadcn@latest add "https://shuip.plvo.dev/r/"`. ## Coding conventions [#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 `"": "catalog:"` or `"catalog:"` — never pin a literal version in a workspace's `package.json`. * **Conventional commits.** Format: `(): `. Examples: `feat(registry): add rhf-phone-field`, `fix(theme-button): add 'use client'`, `chore(dx): bump deps`. ## Submit your contribution [#submit-your-contribution] ```bash 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](https://github.com/plvo/shuip). *Thank you for your contribution to shuip!* 🚀 # Introduction (/docs) ## Why shuip? [#why-shuip] * **🏗️ Built on shadcn/ui**: Constructed on the solid foundation of shadcn/ui * **🛡️ Native TypeScript**: Comes with types for a better developer experience * **🎨 Customizable**: Styled with Tailwind CSS, easily adaptable to your design system * **📦 Flexible Installation**: CLI to install only the components you need * **🔧 Ready to Use**: Add, copy-paste, tweak, and use immediately * **🎯 Concise Code**: Clear syntax and less boilerplate ## Who is shuip for? [#who-is-shuip-for] shuip is perfect for: * **React/Next.js developers** looking to speed up their workflow * **Teams** aiming to standardize their UI components * **Freelancers** who need to deliver high-quality projects quickly * **Anyone** already using shadcn/ui and wanting to go further ## Philosophy [#philosophy] shuip is built on a philosophy of **efficiency and optimization**. Eliminate repetitive tasks to focus on what truly matters in your application. As developers, we spend too much time recreating the same UI components project after project — a submit button with loading state, an input field with built-in validation, a confirmation dialog — these elements keep coming back, and implementing them takes valuable time that could be spent on the unique business logic of your app. This is where the symbiotic relationship between **shuip** and **shadcn/ui** comes in. While **shadcn/ui** excels at providing solid, accessible UI primitives (` Settings Manage your preferences and account settings. ``` ### With scrollable content [#with-scrollable-content] Use `ResponsiveDialogBody` when the dialog content may overflow. The header and footer stay anchored while the body scrolls — on both desktop and mobile. ```tsx Activity Log Recent actions across your workspace. {/* long list, form, or any overflowing content */} ``` On mobile, `ResponsiveDialogBody` automatically adds horizontal padding (`px-4 py-3`) to match the drawer's layout. On desktop, padding comes from `SideDialogContent`. > **Wrapper elements (e.g. `
`):** Any element wrapping `Header/Body/Footer` inside `ResponsiveDialogContent` must carry `className="flex flex-col flex-1 min-h-0"` so it participates in the flex layout. Without it the body has no bounded height and cannot scroll. > > ```tsx > > > ... > ... > ... > >
> ``` ### Different positions (desktop only) [#different-positions-desktop-only] Desktop positions are ignored on mobile where the drawer behavior takes precedence: ```tsx // Notification from bottom-right Success! Your changes have been saved. // User menu from top-right Account
``` ### Custom breakpoints [#custom-breakpoints] Control when the dialog switches between desktop and mobile modes: ```tsx // Use predefined breakpoints {/* Switches at 640px */} Mobile at 640px This dialog becomes a drawer on screens smaller than 640px. // Use custom pixel values Custom at 900px Switches to mobile mode at exactly 900px and below. ``` ### Different sizes (desktop only) [#different-sizes-desktop-only] Size props affect the desktop side dialog appearance: ```tsx // Small dialog for quick actions Confirm Are you sure? // Large dialog for complex content Navigation ``` ## Common use cases [#common-use-cases] ### Mobile-first navigation [#mobile-first-navigation] ```tsx Menu ``` ## Examples [#examples] ## Props [#props] ## ResponsiveDialogBody Props [#responsivedialogbody-props] # Title Section (/blocks/title-section) ## Examples [#examples] # Components (/components) # React Hook Form (/components/react-hook-form) These fields integrate [React Hook Form](https://react-hook-form.com) with shuip's shadcn/ui components. Each field connects to your form through a typed lens from [`@hookform/lenses`](https://github.com/react-hook-form/lenses), so field paths are autocompleted and type-checked — no generics repeated at every call site, no stringly-typed names. ## How it works [#how-it-works] A lens is a typed pointer into your form's values. Create the form as usual, derive a lens from its `control`, then focus the lens on a field and hand it to a field component: ```tsx 'use client'; import { useLens } from '@hookform/lenses'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { Form } from '@/components/ui/form'; import { InputField } from '@/components/ui/shuip/react-hook-form/input-field'; import { SubmitButton } from '@/components/ui/shuip/submit-button'; const schema = z.object({ name: z.string().nonempty('Name is required') }); type Values = z.infer; export function NameForm() { const form = useForm({ defaultValues: { name: '' }, resolver: zodResolver(schema) }); const lens = useLens({ control: form.control }); return (
alert(values.name))} className='space-y-4'> Submit ); } ``` `lens.focus('name')` only accepts real keys of `Values`, and the field's value type is inferred from there. Validation is whatever you wire on the form (here, a Zod resolver) — the field reads `fieldState.invalid` and renders the error for you. ## What you get [#what-you-get] * **Typed field paths** — autocomplete from your form's value type, caught at compile time. * **Consistent UI** — every field renders shadcn's `Field`, `FieldLabel`, `FieldDescription` and `FieldError` primitives. * **Less boilerplate** — the field owns its `useController` wiring, error display and accessibility attributes. Browse the individual fields in the sidebar under **React Hook Form fields**. # TanStack Form (/components/tanstack-form) ## What is this? [#what-is-this] TanStack Form is a headless UI library for building forms with strong TypeScript support and framework-agnostic validation. Unlike traditional form libraries that couple form state with UI components, TanStack Form handles only the state management—leaving you free to choose your UI layer. These shuip components bridge TanStack Form with shadcn/ui, providing pre-built field components that reduce boilerplate and speed up development. They handle the repetitive setup work while preserving TanStack Form's type safety and validation capabilities. ## How it works under the hood [#how-it-works-under-the-hood] When you use a field component like [`InputField`](/components/tanstack-form/input-field), it: 1. **Creates a `form.Field`** instance for your field name 2. **Accesses field state** via `field.state.value` and `field.state.meta` 3. **Wires up event handlers** (`field.handleChange`, `field.handleBlur`) 4. **Renders shadcn UI components** (`Field`, `FieldLabel`, `FieldError`, etc.) 5. **Manages validation** through the `formProps.validators` configuration The field's `state.meta` object contains: * `errors`: array of validation error messages * `isTouched`: whether the user has interacted with the field * `isDirty`: whether the value differs from initial * `isValidating`: whether async validation is in progress * `isValid`: whether all validators pass ## Integration with shadcn/ui [#integration-with-shadcnui] All field components are built on shadcn's primitives: * **`Field`**: Wrapper component that manages invalid states * **`FieldLabel`, `FieldDescription`, `FieldError`**: Consistent field UI * **`InputGroup`**: For password toggle buttons, tooltips, and addons * **`Select`, `RadioGroup`, `Checkbox`**: Radix UI form primitives This ensures visual consistency with your shadcn-based application while adding TanStack Form's powerful state management. ## Type safety [#type-safety] Full TypeScript support with automatic type inference: ```tsx type UserForm = { user: { email: string profile: { age: number } } } const form = useForm({ defaultValues: { user: { email: '', profile: { age: 0 } } } }) // TypeScript autocompletes nested paths: 'user.email', 'user.profile.age' ``` The `name` prop uses `DeepKeys` for compile-time validation of field paths, and values are typed as `DeepValue` for correct inference. ## Validation system [#validation-system] TanStack Form supports multiple validation strategies: ```tsx !value.includes('@') ? 'Invalid email' : undefined, // Runs when field loses focus onBlur: ({ value }) => !value ? 'Email is required' : undefined, // Async validation with debounce onChangeAsync: async ({ value }) => { const available = await checkEmailAvailability(value) return available ? undefined : 'Email already taken' }, onChangeAsyncDebounceMs: 500, // Runs on form submission onSubmit: ({ value }) => !value ? 'This field is required' : undefined } }} /> ``` Validators receive `{ value, fieldApi }` and must return `undefined` for valid or an error string for invalid. ## Linked field validation [#linked-field-validation] Use `onChangeListenTo` to validate one field based on another's value: ```tsx { const password = fieldApi.form.getFieldValue('password') return value !== password ? 'Passwords do not match' : undefined } } }} /> ``` When `password` changes, `confirmPassword` is automatically re-validated. ## Form submission [#form-submission] Handle form submission with the `onSubmit` callback: ```tsx const form = useForm({ defaultValues: { email: '', password: '' }, onSubmit: async ({ value }) => { // value is fully typed based on defaultValues const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify(value), }) if (!response.ok) { // Handle server errors } }, // Optional: handle submit errors onSubmitInvalid: ({ formApi }) => { toast.error('Please fix form errors') } }) ``` Use `` to get automatic loading states and disabled handling. # TanStack Query (/components/tanstack-query) These utilities integrate [TanStack Query](https://tanstack.com/query) with shuip. Instead of manually nesting an error boundary and a Suspense boundary around every data-fetching component, you wrap them once with [`QueryBoundary`](/components/tanstack-query/query-boundary). ## How it works [#how-it-works] `QueryBoundary` combines an error boundary with React Suspense, so the children can use suspense-enabled queries and let the boundary handle the loading and error UI: ```tsx import { QueryBoundary } from '@/components/ui/shuip/tanstack-query/query-boundary'; export default function Page() { return ( Loading users…}> ); } function UsersList() { const { data } = useQuery({ queryKey: ['users'], queryFn: fetchUsers }); return
    {data.map((user) =>
  • {user.name}
  • )}
; } ``` On error the boundary shows a retry fallback that resets the failing query; you can replace it with your own `errorFallback`. This keeps data components focused on the happy path while loading and failure states stay consistent across the app. Browse the component in the sidebar under **TanStack Query**. # Confirmation Dialog (/components/confirmation-dialog) ## Simple integration [#simple-integration] ```tsx Are you sure? This action cannot be undone. This will permanently delete the item. ``` With `ConfirmationDialog`, the same result in a clean, reusable component: ```tsx Delete} title="Are you sure?" description="This action cannot be undone. This will permanently delete the item." labelConfirmButton="Delete" onConfirm={handleDelete} /> ``` ## Common use cases [#common-use-cases] ### Delete confirmation [#delete-confirmation] ```tsx Delete Account} title="Delete Account" description="This will permanently delete your account and all associated data. This action cannot be undone." labelConfirmButton="Delete Account" onConfirm={() => deleteAccount()} /> ``` ### Save confirmation [#save-confirmation] ```tsx Save Changes} title="Save Changes" description="Are you sure you want to save these changes?" labelConfirmButton="Save" onConfirm={() => saveChanges()} /> ``` ### Custom trigger with icon [#custom-trigger-with-icon] ```tsx } title="Delete Item" description="This item will be permanently removed." labelConfirmButton="Delete" onConfirm={() => deleteItem(id)} /> ``` ## Examples [#examples] ## Props [#props] # Copy Button (/components/copy-button) ## Examples [#examples] ## Props [#props] # Hover Reveal (/components/hover-reveal) ## Common use cases [#common-use-cases] ```tsx // Help text on hover ``` ### User profile preview [#user-profile-preview] ```tsx // User profile preview {user.initials}

{user.name}

{user.role}

} > @{user.username}
``` ### Feature explanation [#feature-explanation] ```tsx

Premium Feature

This feature is available for premium subscribers. Upgrade your plan to access advanced analytics.

} >
``` ### Quick preview [#quick-preview] ```tsx } > View image preview ``` ## Examples [#examples] ## Props [#props] # Side Dialog (/components/side-dialog) ## Contextual positioning [#contextual-positioning] Unlike traditional centered dialogs, `SideDialog` provides a more contextual user experience by opening from the edges of the screen, perfect for notifications, user menus, and contextual actions. ### Built-in features [#built-in-features] * **6 positions**: Choose from `top-left`, `top-right`, `bottom-left`, `bottom-right`, `left`, `right` * **Flexible sizes**: Predefined sizes (`xs`, `sm`, `md`, `lg`, `xl`, `2xl`) * **Scrollable body**: `SideDialogBody` keeps the header and footer anchored while the content scrolls * **Lightweight**: Custom implementation without depending on shadcn dialog * **Accessible**: Keyboard support (Escape), focus management, and ARIA compliance * **Portal rendering**: Renders in `document.body` to avoid z-index issues * **Smooth animations**: Entry/exit animations with Tailwind CSS * **Controlled & Uncontrolled**: Works with or without state management ## Position options [#position-options] The `position` prop controls where the dialog appears: ```tsx // Corner positions {/* Top left corner */} {/* Top right corner */} {/* Bottom left corner */} {/* Bottom right corner (default) */} // Side positions (vertically centered) {/* Left side, centered */} {/* Right side, centered */} ``` ## Size options [#size-options] The `size` prop controls the dialog size with predefined responsive values: ```tsx // Predefined sizes (all automatically responsive) {/* 320px max-width, adapts to small screens */} {/* 384px max-width (default) */} {/* 448px max-width */} {/* 512px max-width */} {/* 576px max-width */} {/* 672px max-width */} // For custom sizes, use className with responsive constraints {/* Custom size with responsive protection */} ``` ### Responsive behavior [#responsive-behavior] All predefined sizes automatically include responsive constraints using CSS `min()` function: * **Desktop**: Uses the specified max-width (e.g. `lg` = 32rem/512px) * **Small screens**: Automatically reduces to `calc(100vw - 2rem)` when viewport is smaller * **Never overflows**: The dialog will always fit within the viewport with 1rem margin on each side * **CSS native**: Uses `min(targetSize, calc(100vw - 2rem))` for optimal performance ## Scrollable content [#scrollable-content] Use `SideDialogBody` when the dialog content may overflow. The header and footer stay anchored while the body scrolls independently. ```tsx Activity Log Recent actions across your workspace. {/* long list or form content */} ``` Without `SideDialogBody`, content simply stacks inside the dialog — fine for short dialogs. Add it as soon as the content might exceed the viewport height. ## Common use cases [#common-use-cases] ### Notifications and alerts [#notifications-and-alerts] ```tsx // Success notification - top right Success! Your changes have been saved. // Error notification - top left Error Something went wrong. Please try again. ``` ### Different sizes for different content [#different-sizes-for-different-content] ```tsx // Small notification - xs size Saved! // User profile - medium size User Profile
JD

John Doe

john@example.com

// Wide content panel - custom size with responsive constraints Content Editor