Agent Skills

Install shuip know-how into your AI coding agent — skills that teach it when and how to use shuip

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, so the @shuip namespace from the installation guide must be configured first. Each lands in your project at .claude/skills/<name>/SKILL.md, versioned alongside your code.

The skills work with both bases. They tell your agent to read the style in components.json and to compose accordingly: render in base-* (Base UI) projects, asChild in Radix projects. The installation guide lists which styles map to which base.

Available skills

SkillUse it for
shuip-overviewOrientation — what shuip is, how to install items, the item categories, and which skill to reach for next.
shuip-componentsChoosing and composing components — the full item catalog plus install and import conventions.
shuip-formsBuilding production-grade forms with the react-hook-form (rhf-) and tanstack-form (tsf-) field items: validation, submission, and accessible error and loading states.

Install

Add a single skill:

npx shadcn@latest add @shuip/shuip-forms

Or install all of them at once with the bundle:

npx shadcn@latest add @shuip/shuip-skills

Where they go

Skills install into your project's .claude/ directory:

.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

The exact Markdown each skill ships. This is what your agent reads.

shuip-overview

Routes the agent: what shuip is, how items install, and which skill to use next.

---
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 reference (@shuip/<name>).
---

# 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

Declare the namespace once in the project's `components.json`:

```json
{ "registries": { "@shuip": "https://shuip.plvo.dev/r/{style}/{name}.json" } }
```

Then install by name:

```bash
npx shadcn@latest add @shuip/<item-name>
```

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.

## Base UI or Radix

The project's `components.json` `style` decides which variant of an item installs; the install command is the same for both.

| `style` | Variant |
|---------|---------|
| `base-*` (for example `base-vega`) | Base UI |
| `radix-*`, `new-york`, `new-york-v4`, `default` | Radix |
| `aria-*` | Not supported: the registry answers 404 |

Read the `style` before writing any trigger or slot, and compose accordingly:

- In `base-*` projects, compose with `render`: `<DialogTrigger render={<Button />}>Open</DialogTrigger>`. Add `nativeButton={false}` when the rendered element is not a button, except for a link: Base UI discourages `nativeButton={false}` on one, so style a plain anchor with `buttonVariants` instead.
- In Radix projects, compose with `asChild`: `<DialogTrigger asChild><Button /></DialogTrigger>`.

Never mix the two in one project.

## 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-components

The full item catalog plus install and import conventions; the agent uses this to choose the right item before building UI from scratch.

---
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 @shuip/<item-name>` (requires the `@shuip` namespace in `components.json`, see **shuip-overview**).
- Import paths by category:
  - components → `@/components/ui/shuip/<name>`
  - blocks → `@/components/block/shuip/<name>`
  - react-hook-form → `@/components/ui/shuip/react-hook-form/<name>`
  - tanstack-form → `@/components/ui/shuip/tanstack-form/<name>`
  - tanstack-query → `@/components/ui/shuip/tanstack-query/<name>`
- **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.
- **Composition follows the project's base** (`style` in `components.json`): in `base-*` projects, wrap a trigger with `render` (`<SideDialogTrigger render={<Button />}>Open</SideDialogTrigger>`); in Radix projects, with `asChild` (`<SideDialogTrigger asChild><Button>Open</Button></SideDialogTrigger>`). See **shuip-overview**.

## Catalog

<!-- shuip:catalog:start -->
**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`
<!-- shuip:catalog:end -->

## 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/<name>`. |

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.

---
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 `<FormField render={...}>` 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 @shuip/rhf-input-field
npx shadcn@latest add @shuip/rhf-password-field
npx shadcn@latest add @shuip/submit-button

# tanstack-form: each field pulls in tsf-form-context automatically
npx shadcn@latest add @shuip/tsf-input-field
npx shadcn@latest add @shuip/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')`. Fields bind through the lens, so no provider is needed. The exception is `InlineEditField`, which validates on commit through `useFormContext()` and must render under `<FormProvider {...form}>`. Wrap the form in it as well when your own components call `useFormContext()`.

```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 { 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<typeof schema>;

export function SignupForm() {
  const form = useForm<Values>({
    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 (
    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
      <InputField lens={lens.focus('email')} label="Email" type="email" placeholder="you@example.com" />
      <PasswordField lens={lens.focus('password')} label="Password" />
      <PasswordField lens={lens.focus('confirmPassword')} label="Confirm password" />
      <SubmitButton loading={form.formState.isSubmitting}>Create account</SubmitButton>
    </form>
  );
}
```

- 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 `<form.AppField>`; 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 (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        form.handleSubmit();
      }}
      className="space-y-4"
    >
      <form.AppField
        name="email"
        validators={{ onChange: ({ value }) => (!value.includes('@') ? 'Invalid email' : undefined) }}
        children={(field) => <field.InputField label="Email" props={{ type: 'email' }} />}
      />
      <form.AppField
        name="password"
        validators={{ onChange: ({ value }) => (value.length < 8 ? 'At least 8 characters' : undefined) }}
        children={(field) => <field.PasswordField label="Password" />}
      />
      <form.AppForm>
        <form.SubmitButton>Create account</form.SubmitButton>
      </form.AppForm>
    </form>
  );
}
```

- tsf fields take split props: `props` for the native input, `fieldProps` for the field wrapper. There is no lens and no `<FormProvider>` wrapper.
- The tsf `SubmitButton` auto-disables via `form.Subscribe` — render it inside `<form.AppForm>`, no `loading` prop needed.

## Composition follows the project's base

The field components work the same on Base UI and on Radix. The triggers around a form do not: when you open a form from a dialog or popover trigger, read `style` in `components.json` and compose accordingly (see **shuip-overview**).

- In `base-*` projects, compose with `render`: `<SideDialogTrigger render={<Button />}>Open</SideDialogTrigger>`.
- In Radix projects, compose with `asChild`: `<SideDialogTrigger asChild><Button>Open</Button></SideDialogTrigger>`.

## 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 |
|---------|---------|
| `<InputField control={form.control} name="email" />` | 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 `<FormField render={...}>` | That's the boilerplate shuip fields replace. Use the field component. |
| Raw `<Button disabled={isSubmitting}>` | Use `SubmitButton`. rhf: `loading={form.formState.isSubmitting}`; tsf: auto via `form.Subscribe`. |
| Using tsf fields without `tsf-form-context` | Every tsf field reads `useFieldContext`; install and wire `tsf-form-context` first via `createFormHook`. |
| Mixing rhf and tsf fields in one form | Pick one family per form. |

## Red flags — STOP

- About to type `control={...}` or `name="..."` on an rhf shuip field → use the lens.
- About to import a `Rhf*`/`Tsf*`-prefixed symbol → the export is unprefixed.
- About to write `FormField`/`FormItem`/`FormControl` by hand → a shuip field already does this.
- Looking for an `rhf-form` install command → it doesn't exist.

Skills use the Claude Code SKILL.md format. Their content does not depend on the agent.

On this page