# Agent Skills (/docs/agent-skills) shuip ships **agent skills** alongside its components. A skill is a Markdown guide your AI coding agent (Claude Code) reads to learn what shuip is, when to reach for which item, and how to build production-grade forms — so the code it writes follows shuip's conventions instead of guessing at item names and APIs. Skills install through the same registry and CLI as components. Each lands in your project at `.claude/skills//SKILL.md`, versioned alongside your code. ## Available skills [#available-skills] | Skill | Use it for | | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`shuip-overview`](#shuip-overview) | Orientation — what shuip is, how to install items, the item categories, and which skill to reach for next. | | [`shuip-components`](#shuip-components) | Choosing and composing components — the full item catalog plus install and import conventions. | | [`shuip-forms`](#shuip-forms) | Building production-grade forms with the react-hook-form (`rhf-`) and tanstack-form (`tsf-`) field items: validation, submission, and accessible error and loading states. | ## Install [#install] Add a single skill: npm pnpm yarn bun ```bash npx shadcn@latest add https://shuip.plvo.dev/r/shuip-forms ``` ```bash pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/shuip-forms ``` ```bash yarn dlx shadcn@latest add https://shuip.plvo.dev/r/shuip-forms ``` ```bash bun x shadcn@latest add https://shuip.plvo.dev/r/shuip-forms ``` Or install all of them at once with the bundle: npm pnpm yarn bun ```bash npx shadcn@latest add https://shuip.plvo.dev/r/shuip-skills ``` ```bash pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/shuip-skills ``` ```bash yarn dlx shadcn@latest add https://shuip.plvo.dev/r/shuip-skills ``` ```bash bun x shadcn@latest add https://shuip.plvo.dev/r/shuip-skills ``` ## Where they go [#where-they-go] Skills install into your project's `.claude/` directory: ```bash .claude/ └── skills/ ├── shuip-overview/ │ └── SKILL.md ├── shuip-components/ │ └── SKILL.md └── shuip-forms/ └── SKILL.md ``` Once installed, your agent picks them up automatically — ask it to build a form or add a component and it will follow shuip's patterns. ## What each skill contains [#what-each-skill-contains] The exact Markdown each skill ships. This is what your agent reads. ### shuip-overview [#shuip-overview] Routes the agent: what shuip is, how items install, and which skill to use next. {/* shuip:skill:start name="shuip-overview" */} ````md --- name: shuip-overview description: Use when working in a project that uses shuip and you need to know what shuip provides, how to install items from its registry, or which shuip skill/category to reach for. Triggers on shuip imports (@/components/ui/shuip/...) or a shuip registry URL (shuip.plvo.dev/r/...). --- # shuip ## What it is shuip is a component library built on shadcn/ui for Next.js + React. You install individual **items** through the shadcn CLI from shuip's registry; the source is copied into the project (same model as shadcn) and you own it afterwards. ## Install any item ```bash npx shadcn@latest add "https://shuip.plvo.dev/r/" ``` Items install under `@/components/ui/shuip/...` (or `@/components/block/shuip/...` for blocks) and pull their shadcn primitives and npm dependencies automatically. **Exported symbols are unprefixed** — the item `rhf-input-field` exports `InputField`; the `rhf-`/`tsf-`/`tsq-` prefix is only the registry name. ## Categories - **components** — standalone UI: dialogs, buttons, theme toggle (e.g. `side-dialog`, `confirmation-dialog`, `copy-button`, `theme-button`). - **blocks** — composed multi-part UI (e.g. `kanban`, `responsive-dialog`, `title-section`). - **react-hook-form** (`rhf-`) — form fields wired to react-hook-form via typed lenses. - **tanstack-form** (`tsf-`) — form fields wired to tanstack-form via context. - **tanstack-query** (`tsq-`) — data-fetching helpers (e.g. `query-boundary`). ## Which skill next - Building or editing a **form** → use the **shuip-forms** skill (rhf/tsf field patterns in depth). - Choosing or composing **components/blocks**, or you need the full item catalog → use the **shuip-components** skill. Before building UI from scratch, check whether shuip already ships it. ```` {/* shuip:skill:end */} ### shuip-components [#shuip-components] The full item catalog plus install and import conventions; the agent uses this to choose the right item before building UI from scratch. {/* shuip:skill:start name="shuip-components" */} ```md --- name: shuip-components description: Use when choosing which shuip item fits a need, or composing shuip components and blocks — provides the full item catalog, install/import conventions, and composition notes. Use before building UI that shuip may already provide. --- # Choosing & composing shuip components ## Conventions - Install: `npx shadcn@latest add "https://shuip.plvo.dev/r/"`. - Import paths by category: - components → `@/components/ui/shuip/` - blocks → `@/components/block/shuip/` - react-hook-form → `@/components/ui/shuip/react-hook-form/` - tanstack-form → `@/components/ui/shuip/tanstack-form/` - tanstack-query → `@/components/ui/shuip/tanstack-query/` - **Exports are unprefixed**: `rhf-input-field` → `InputField`, `kanban` → `Kanban`. - For anything form-related, use the **shuip-forms** skill — it covers the rhf/tsf field patterns in depth. ## Catalog **components** - `confirmation-dialog` - `copy-button` - `hover-reveal` - `side-dialog` - `submit-button` - `theme-button` **blocks** - `kanban` - `responsive-dialog` - `title-section` **react-hook-form** - `rhf-address-field` - `rhf-autocomplete-field` - `rhf-checkbox-field` - `rhf-date-field` - `rhf-date-range-field` - `rhf-datetime-field` - `rhf-inline-edit` - `rhf-input-field` - `rhf-month-field` - `rhf-number-field` - `rhf-password-field` - `rhf-radio-field` - `rhf-select-field` - `rhf-textarea-field` - `rhf-time-field` - `rhf-time-range` **tanstack-form** - `tsf-autocomplete-field` - `tsf-checkbox-field` - `tsf-date-field` - `tsf-date-range-field` - `tsf-datetime-field` - `tsf-form-context` - `tsf-inline-edit` - `tsf-input-field` - `tsf-month-field` - `tsf-password-field` - `tsf-radio-field` - `tsf-select-field` - `tsf-submit-button` - `tsf-textarea-field` - `tsf-time-field` - `tsf-time-range` **tanstack-query** - `tsq-query-boundary` ## Composition notes - `responsive-dialog` composes `side-dialog`; installing it resolves the dependency. - `confirmation-dialog` for destructive-action confirmation; `side-dialog` for sheet-style panels. - `query-boundary` wraps data-fetching subtrees so you don't hand-roll loading/error UI. - Form fields are never used alone — they compose inside a form. Use **shuip-forms**. ## Common mistakes | Mistake | Reality | |---------|---------| | Building a dialog / kanban / clipboard button from scratch | Check the catalog first — shuip likely ships it. | | Importing a prefixed symbol (`RhfInputField`, `TsfInputField`) | Exports are unprefixed (`InputField`). | | Flat path `@/components/ui/shuip/rhf-input-field` | Form categories use a sub-folder: `@/components/ui/shuip/react-hook-form/input-field`. | | Treating blocks as `@/components/ui/shuip/...` | Blocks import from `@/components/block/shuip/`. | ``` {/* shuip:skill:end */} ### shuip-forms [#shuip-forms] The flagship: rhf vs tsf decision, the lens binding for react-hook-form, `createFormHook` for tanstack-form, full annotated examples, validation, submission, and accessibility. {/* shuip:skill:start name="shuip-forms" */} ````md --- name: shuip-forms description: Use when building or editing a form in a project that uses shuip — covers shuip's react-hook-form (rhf-) and tanstack-form (tsf-) field components, their install commands, validation, submission, and accessible error/loading state. Use before hand-writing shadcn FormField boilerplate. --- # Building forms with shuip ## Overview shuip ships pre-wired form **field components** for two libraries: **react-hook-form** (registry prefix `rhf-`) and **tanstack-form** (prefix `tsf-`). Each field collapses the label / control / error-message / accessibility wiring into one component. **Core principle:** compose shuip field components — do NOT hand-write shadcn's `` boilerplate. If you're reaching for `FormField`/`FormItem`/`FormControl`/`FormMessage`, you're rebuilding what these items already give you. There is **no composite "form" item** in the registry — you assemble a form from individual field items plus your own schema and submit handler. ## Choose the library first Match whatever the project already uses. If it's greenfield: - **react-hook-form** (`rhf-*`) — mature ecosystem, uncontrolled/minimal re-renders, validates with a zod resolver. Fields bind through a typed **lens** (`@hookform/lenses`). Pick this unless you have a reason not to. - **tanstack-form** (`tsf-*`) — end-to-end type-safe field names, validators co-located on the field, good async validation. Natural fit if the project is already on the TanStack stack (Query/Router). Requires the `tsf-form-context` item as its foundation. Do not mix the two families in one form. ## Install ```bash # react-hook-form fields (install the ones you need) npx shadcn@latest add "https://shuip.plvo.dev/r/rhf-input-field" npx shadcn@latest add "https://shuip.plvo.dev/r/rhf-password-field" npx shadcn@latest add "https://shuip.plvo.dev/r/submit-button" # tanstack-form: install the form context FIRST, then fields npx shadcn@latest add "https://shuip.plvo.dev/r/tsf-form-context" npx shadcn@latest add "https://shuip.plvo.dev/r/tsf-input-field" npx shadcn@latest add "https://shuip.plvo.dev/r/tsf-submit-button" ``` Field naming follows the input type: `input-field`, `password-field`, `number-field`, `select-field`, `checkbox-field`, `radio-field`, `textarea-field`, `date-field`, `date-range-field`, `datetime-field`, `time-field`, `month-field`, `autocomplete-field`, plus `address-field` (rhf only). For the full catalog use the **shuip-components** skill. The shadcn CLI pulls each item's shadcn primitives and npm deps (react-hook-form / zod / @hookform/lenses, or @tanstack/react-form) automatically. **Exports are unprefixed.** The item `rhf-input-field` exports `InputField`. The `rhf-`/`tsf-` prefix is only the registry name, never the import symbol. ## react-hook-form pattern Create one lens per form with `useLens({ control })`, then give each field `lens.focus('fieldName')`. Wrap everything in `
`. ```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 { PasswordField } from '@/components/ui/shuip/react-hook-form/password-field'; import { SubmitButton } from '@/components/ui/shuip/submit-button'; const schema = z .object({ email: z.string().email('Enter a valid email'), password: z.string().min(8, 'At least 8 characters'), confirmPassword: z.string(), }) .refine((v) => v.password === v.confirmPassword, { message: 'Passwords do not match', path: ['confirmPassword'], }); type Values = z.infer; export function SignupForm() { const form = useForm({ resolver: zodResolver(schema), defaultValues: { email: '', password: '', confirmPassword: '' }, }); const lens = useLens({ control: form.control }); async function onSubmit(values: Values) { const result = await signup(values); // a server action if (result?.fieldErrors) { for (const [name, message] of Object.entries(result.fieldErrors)) { form.setError(name as keyof Values, { message }); } } } return ( Create account ); } ``` - Field-specific props (`type`, `placeholder`, …) pass straight through to the underlying input. - `SubmitButton` does **not** auto-disable — bind `loading={form.formState.isSubmitting}` (it disables and shows a spinner while loading). - Server-side validation failures map back onto fields with `form.setError`. ## tanstack-form pattern Build the typed form hook once with `createFormHook`, passing the contexts from `tsf-form-context` and your field/form components. Bind fields with ``; validators live on the field. ```tsx 'use client'; import { createFormHook } from '@tanstack/react-form'; import { fieldContext, formContext } from '@/components/ui/shuip/tanstack-form/form-context'; import { InputField } from '@/components/ui/shuip/tanstack-form/input-field'; import { PasswordField } from '@/components/ui/shuip/tanstack-form/password-field'; import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button'; const { useAppForm } = createFormHook({ fieldContext, formContext, fieldComponents: { InputField, PasswordField }, formComponents: { SubmitButton }, }); export function SignupForm() { const form = useAppForm({ defaultValues: { email: '', password: '' }, onSubmit: async ({ value }) => { await signup(value); // a server action }, }); return (
{ e.preventDefault(); form.handleSubmit(); }} className="space-y-4" > (!value.includes('@') ? 'Invalid email' : undefined) }} children={(field) => } /> (value.length < 8 ? 'At least 8 characters' : undefined) }} children={(field) => } /> Create account ); } ``` - tsf fields take split props: `props` for the native input, `fieldProps` for the field wrapper. There is no lens and no `
` wrapper. - The tsf `SubmitButton` auto-disables via `form.Subscribe` — render it inside ``, no `loading` prop needed. ## Accessibility & states (both libraries) The field components already render `aria-invalid`, associate the error message, and show validation errors — you get accessible errors for free. For a form-level server error, render a `role="alert"` region yourself. Loading/disabled on submit is handled by `SubmitButton` as shown above. ## Common mistakes | Mistake | Reality | |---------|---------| | `` | Wrong API. rhf fields bind via a lens: `lens={lens.focus('email')}` after `const lens = useLens({ control: form.control })`. | | Importing `RhfInputField` / `TsfInputField` | Export is `InputField`. The `rhf-`/`tsf-` prefix is the registry name only. | | `@/components/ui/shuip/rhf-input-field` (flat) | Real path is `@/components/ui/shuip/react-hook-form/input-field` (category sub-folder). | | Installing a `rhf-form` / `tsf-form` item | No composite form item exists. Assemble from field items. | | Hand-writing `` | That's the boilerplate shuip fields replace. Use the field component. | | Raw ` ); } ``` # 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 (`} /> // Loading // skeleton rows (default) // keep rows, dim + spinner (best for refetch) // animated sweep (reduced-motion aware) ``` ## Advanced filtering, sorting & saved views [#advanced-filtering-sorting--saved-views] Three composable controls — drop them in any row, no toolbar assumed. Each is driven by the `table` instance alone; the filterable fields and their operators are derived from each column's existing `meta`, so there is no separate filter config to keep in sync. ```tsx
{/* or 'dialog' */}
``` * **`DataTableFilterMenu`** — a condition builder (`Where [field] [operator] [value]`); rows reorder by dragging the handle. Operators are inferred from `meta.variant`: `text` → contains / is / is empty…, `number` → `= ≠ > < ≥ ≤`, `date` → before / after / on or before…, `select` / `multiSelect` → is / is any of / is none of. The `variant` prop renders the same builder in a popover or a dialog. * **`DataTableSortMenu`** — multi-column sort; order is priority (first row = primary), and rows reorder by dragging the handle. Secondary columns show a priority badge in their header. * **`DataTableViews`** — save the current `{ filters, sorting }` as a named view in `localStorage` (`shuip:dt-views:`), then apply or delete it. Conditions live in TanStack's `columnFilters` as `{ id, value: { operator, value } }`. In client mode `useDataTable` registers an operator-aware `filterFn` (`dataTableFilterFn`), so filtering works with no per-column wiring; in server mode, read `table.getState().columnFilters` and translate it to your query. One condition per column (the `columnFilters` model); conditions combine with AND. ## The `meta` convention [#the-meta-convention] Per-entity configuration rides on TanStack's `ColumnMeta`, augmented by this block: ```ts interface ColumnMeta { label?: string; placeholder?: string; variant?: 'text' | 'number' | 'date' | 'select' | 'multiSelect'; options?: { label: string; value: string; icon?: React.ComponentType; count?: number }[]; icon?: React.ComponentType; } ``` A column with `meta.variant: 'multiSelect'` and `meta.options` automatically gets a faceted filter in the toolbar. For client-side multi-select filtering, set `filterFn: 'arrIncludesSome'` on that column. ## Client vs server mode [#client-vs-server-mode] Client mode is the default — pass `data` and `columns` and the table computes everything: ```tsx const { table } = useDataTable({ data, columns, getRowId: (row) => row.id }); ``` Server mode activates when you pass `pageCount`. Control the state yourself and react to changes by refetching: ```tsx const { table } = useDataTable({ data: result.rows, columns, pageCount: Math.ceil(result.total / pagination.pageSize), state: { pagination, sorting, columnFilters, globalFilter }, onPaginationChange: setPagination, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onGlobalFilterChange: setGlobalFilter, }); ``` ## Recipe: sync state to the URL with nuqs [#recipe-sync-state-to-the-url-with-nuqs] Server mode pairs naturally with shareable, bookmarkable URLs. This is opt-in and not bundled with the block (it would force a router dependency on every consumer). Install nuqs and wrap your app once: ```bash bun add nuqs ``` ```tsx // app/layout.tsx import { NuqsAdapter } from 'nuqs/adapters/next/app'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` Then drop in this hook and feed its output straight into `useDataTable`: ```tsx // hooks/use-data-table-url-state.ts 'use client'; import type { ColumnFiltersState, PaginationState, SortingState } from '@tanstack/react-table'; import { parseAsInteger, parseAsJson, parseAsString, useQueryStates } from 'nuqs'; export function useDataTableUrlState() { const [state, setState] = useQueryStates( { pageIndex: parseAsInteger.withDefault(0), pageSize: parseAsInteger.withDefault(10), sorting: parseAsJson((value) => value as SortingState).withDefault([]), columnFilters: parseAsJson((value) => value as ColumnFiltersState).withDefault([]), globalFilter: parseAsString.withDefault(''), }, { history: 'push', shallow: false, throttleMs: 50, clearOnDefault: true }, ); const pagination: PaginationState = { pageIndex: state.pageIndex, pageSize: state.pageSize }; return { pagination, sorting: state.sorting, columnFilters: state.columnFilters, globalFilter: state.globalFilter, onPaginationChange: (updater: PaginationState | ((old: PaginationState) => PaginationState)) => { const next = typeof updater === 'function' ? updater(pagination) : updater; void setState({ pageIndex: next.pageIndex, pageSize: next.pageSize }); }, onSortingChange: (updater: SortingState | ((old: SortingState) => SortingState)) => { const next = typeof updater === 'function' ? updater(state.sorting) : updater; void setState({ sorting: next, pageIndex: 0 }); }, onColumnFiltersChange: (updater: ColumnFiltersState | ((old: ColumnFiltersState) => ColumnFiltersState)) => { const next = typeof updater === 'function' ? updater(state.columnFilters) : updater; void setState({ columnFilters: next, pageIndex: 0 }); }, onGlobalFilterChange: (updater: string | ((old: string) => string)) => { const next = typeof updater === 'function' ? updater(state.globalFilter) : updater; void setState({ globalFilter: next, pageIndex: 0 }); }, }; } ``` ```tsx const url = useDataTableUrlState(); const { table } = useDataTable({ data: result.rows, columns, pageCount: Math.ceil(result.total / url.pagination.pageSize), state: url, onPaginationChange: url.onPaginationChange, onSortingChange: url.onSortingChange, onColumnFiltersChange: url.onColumnFiltersChange, onGlobalFilterChange: url.onGlobalFilterChange, }); ``` ## Props [#props] # Overview (/blocks) # Kanban (/blocks/kanban) `Kanban` is a generic board (`Kanban`) that renders any array of typed items into draggable columns. Columns, the property fields shown on each card, and the searchable fields are all configured against your own data shape — nothing is hardcoded to a domain. ### Built-in features [#built-in-features] * **Generic data model**: works with any item type `T` via `idField` / `columnField`. * **Structured cards**: the title is rendered from `title(item)` (as a `CardTitle`, so it can be a string, a link, or any element); the content area is driven by a `fields` config or fully customized with `cardContent`. * **Inter and intra-column drag-and-drop**: move cards between columns and reorder within a column. * **Hybrid state**: works standalone from `defaultData`, or controlled via `data` + `onDataChange`. * **Global search**: filter cards across `searchableFields`. * **Per-column extras**: count, a `renderColumnSummary` slot, color accent, and an add button. > The card order is implicit in the `data` array order. On any move, the board emits the reordered array via `onDataChange` (and a semantic `onCardMove` event); map that to your own persistence. > Define your item type with a `type` alias, not an `interface` — `Kanban` requires `T extends Record`, which a TypeScript `interface` does not satisfy. ## Examples [#examples] ## Props [#props] # Responsive Dialog (/blocks/responsive-dialog) ## About [#about] `ResponsiveDialog` is a smart dialog component that automatically adapts to the user's screen size: * **Desktop (≥1024px)**: Renders as a `SideDialog` with flexible positioning * **Mobile (\<1024px)**: Renders as a `Drawer` for better mobile UX Unlike traditional dialogs that force the same experience across all devices, `ResponsiveDialog` provides the optimal interface for each screen size while maintaining a consistent API. ### Built-in features [#built-in-features] * **Automatic responsive behavior**: Switches between side dialog and drawer based on screen size * **6 positions (desktop)**: Choose from `top-left`, `top-right`, `bottom-left`, `bottom-right`, `left`, `right` * **Flexible sizes (desktop)**: Predefined sizes (`xs`, `sm`, `md`, `lg`, `xl`, `2xl`) * **Scrollable body**: `ResponsiveDialogBody` keeps the header and footer anchored while the content scrolls on both desktop and mobile * **Mobile-optimized**: Uses native drawer with swipe gestures on mobile * **Lightweight**: Combines existing components without additional overhead * **Accessible**: Inherits accessibility features from both SideDialog and Drawer ## Usage [#usage] ### Basic responsive dialog [#basic-responsive-dialog] ```tsx 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