Skip to content
Merged
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]
}
}
}
177 changes: 177 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# AGENTS.md

Guidance for AI agents (and humans) working in **junwen-k/ui-x**. These are
principles to apply with judgment, not boxes to tick — when a case doesn't fit,
reason from the "why" here and say so.

---

## What this project is

ui-x is a **shadcn-style component registry** — a natural extension of
[shadcn/ui](https://ui.shadcn.com), not a fork. We ship the components shadcn
doesn't (yet) have, built and styled so they feel like they came from shadcn
itself: a user should be able to `npx shadcn add` one of ours next to theirs and
not feel a seam.

Everything below follows from that stance.

---

## Principles

- **Compose shadcn, don't reinvent it.** Depend on shadcn's components; never
copy or restyle them. When shadcn ships a first-class version of something we
filled in, that component is **superseded** and removed in favor of the
official one (see `apps/v4/content/docs/changelog.mdx`).
- **Lean on great headless libraries; author behavior only when none exists.**
Reach for a popular, well-maintained headless library before writing
interaction logic yourself — `react-dropzone`, `react-phone-number-input`,
`timescape`, `react-day-picker`, `@dnd-kit`, `frimousse`, `virtua`. We hand-
author behavior (on [Base UI](https://base-ui.com)) only where no good library
fits — e.g. the password visibility toggle.
- **Match shadcn's conventions.** `data-slot` attributes, CVA variants,
`render`-prop composition, file layout, naming, prose tone. Unsure how to do
something? Find the closest shadcn/ui component and mirror it.
- **Model the API deliberately** — see [the wrapping principle](#the-wrapping-principle),
the one rule most worth getting right.

## Hard rules

- **Base UI, never Radix.** Base UI is the headless foundation shadcn moved to;
it's what we build on when we author unstyled behavior. Never add `@radix-ui/*`.
- **One style: `base-nova`** (set in `apps/v4/components.json`). Pull shadcn core
in via the CLI — `npx shadcn@latest add <name>` — never hand-transcribe
classes. Base-nova payloads sometimes run ahead of the released Tailwind CSS
(new `cn-*` utilities); if you must vendor a class, confirm it exists in the
released `tailwind.css` first.
- **Conventional Commits**, enforced by commitlint.
- **Never self-merge a PR**, force-push, or run destructive git without explicit
confirmation. Branch, push, open the PR, leave it for the maintainer.
- **Verify state directly** — read the file, check the branch — rather than
assuming what's frozen or already done.

---

## How components are built

There's no single mold. A component takes the simplest shape that fits:

- **Styled composition** — styling over shadcn core and/or a headless library,
composed directly, no separate primitive (`emoji-picker`, `sortable`,
`wheel-picker`, `confirmer`).
- **Primitive + styled** — an unstyled `<name>-primitive.tsx` layer plus a
styled `<name>.tsx` that composes it with shadcn core. Author a primitive when
the headless behavior is worth publishing on its own — usually a thin
Base UI-flavored adapter over a library (`dropzone` → react-dropzone,
`phone-input` → react-phone-number-input, `date-time-field` → timescape), or
hand-written on Base UI when no library fits (`password-input`).

`apps/v4/src/registry/new-york/ui/date-picker.tsx` is the reference to study
before designing a new component.

**Import boundaries:**

- shadcn core → `@/components/ui/*` (added via CLI; **not** part of our registry).
- ui-x components & primitives → `@/registry/new-york/{ui,components}/*`.
- Demos import the styled component from `@/registry/new-york/ui/<name>`.

(The folder is named `new-york/` for historical reasons — the _style_ is `base-nova`.)

---

## Anatomy & API consistency

A new component's API should feel like one a consumer already knows. Before
naming anything, look at the closest shadcn primitive and the nearest ui-x
sibling, and match them.

- **Parts are `<Component><Part>`**, PascalCase, exported root-first in
composition order — the same tree the docs "Usage" block shows
(`PhoneInput` → `PhoneInputCountrySelect` → `PhoneInputCountrySelectContent`).
- **`data-slot` is the kebab-case of the exported name** — nothing invented
(`PhoneInputCountrySelectValue` → `data-slot="phone-input-country-select-value"`).
- **Reuse the established part vocabulary.** A surface is `*Content`, an opener
`*Trigger`, an option `*Item`, the selected display `*Value`, the field
`*Input`. Don't coin a new noun for a role that already has one — a consumer
should be able to guess a part's name from what it does.
- **Mirror the underlying anatomy so composition transfers.** When a part wraps
a shadcn primitive, keep the same shape and let it compose with that
primitive's own parts — `PhoneInputCountrySelect` sits inside Select's own
`SelectTrigger`, so anyone who knows Select already knows this.

When a new component and an existing one solve the same sub-problem, they should
read the same way. Consistency across the set beats a locally clever API.

---

## The wrapping principle

When you expose a sub-component, choose deliberately between **wrapping and
re-exporting** a part vs. letting the consumer **compose it from the outside**.

> Wrap + re-export only when the wrapper earns it — behavior, styling, or a
> composed default. A wrapper that only renames a `data-slot` on a part that
> belongs to something else (shadcn core, another ui-x component, a library) is
> cosmetic indirection; drop it and let the consumer use the part directly.

**The test:** remove the wrapper and inline the part. Does anything change
besides the `data-slot` string? No → delete it. Yes → keep it.

Worth keeping — earns its place by:

- reading your primitive's context/hook (`useDatePicker`, `usePhoneInput`, drag state);
- binding a primitive part via `render` (`<Primitive.Input render={<Input />} />`);
- adding real styling (`w-auto`, heavy item styling) or a composed default (a
placeholder, a state-swapping icon).

Precedents: `DropzoneUploadIcon` (swaps icon on drag state) and
`PhoneInputCountrySelect*` (wire `usePhoneInput`, add a flag placeholder) stay;
`PasswordInputAdornment` (was `<InputGroupAddon data-slot=… />`) and
`PhoneInputCountrySelectTrigger` (was `<SelectTrigger data-slot=… />`) were
removed — consumers compose `InputGroupAddon` / `SelectTrigger` directly.

When you add or drop a part, update the demos, docs (Usage / API Reference /
Accessibility), and — if a part disappears — the changelog, in lockstep.

---

## Registry & docs conventions

Registry items live in `apps/v4/registry.json`:

- `dependencies` — npm packages; `registryDependencies` — **shadcn core as plain
strings** (`"input-group"`, `"select"`), **ui-x-internal as
`"junwen-k/ui-x/<name>"`**. Validate with `cd apps/v4 && pnpm registry:validate`.

Docs are fumadocs MDX under `apps/v4/content/docs/`:

- Component pages carry Installation, Usage, Examples
(`<ComponentPreview name="…" />` → `src/components/examples/`), Accessibility,
API Reference. Primitive pages document the unstyled layer with full prop
tables; the styled API Reference links back rather than repeating them.
- **Primitive demos stay unstyled** — plain markup plus the "Unstyled" callout.
- **Forms are library-agnostic**: show Base UI `Field`/`Form` markup and link to
shadcn's [forms guides](https://ui.shadcn.com/docs/forms) for RHF/TanStack
wiring. In form demos pass `field.value ?? null` so Base UI's `useControlled`
never flips modes.
- **Tone:** no maintenance promises, no "as-is" laundry lists, clean-cut over
keeping the superseded around; changelog tracks meaningful releases, not
per-part churn.

---

## Workflow & tooling

Understand first (nearest shadcn and in-repo precedent) → design the API against
the wrapping principle → implement (library or primitive as needed, then the
styled layer, `registry.json`, demos, docs in lockstep) → verify → commit and
open a PR.

Verify from `apps/v4/`: `npx tsc --noEmit`, `pnpm lint` (0 errors; a few known
warnings pre-exist), `pnpm registry:validate`, then eyeball the affected pages on
`pnpm dev` (:3000).

The **shadcn MCP** (`.mcp.json`) gives live registry access — prefer it over
guessing a component's shape or classes. The **shadcn CLI** adds core components
(`npx shadcn@latest add <name>`) and fetches docs (`npx shadcn@latest docs <name>`).
5 changes: 3 additions & 2 deletions apps/v4/content/docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ui-x is now built on [Base UI](https://base-ui.com). Following shadcn/ui's adopt
- **Base UI everywhere** — All Radix UI dependencies are gone. Components are built on Base UI primitives and utilities, with React 19 ref handling throughout.
- **Restyled** — Every component, demo and docs page has been refreshed against the latest shadcn/ui styling.
- **Simpler forms** — Demos use Base UI's `Field` and `Form` for form plumbing. For React Hook Form or TanStack Form wiring, see the shadcn/ui [forms guides](https://ui.shadcn.com/docs/forms).
- **Dropzone composes with Empty** — `DropzoneGroup`, `DropzoneTitle` and `DropzoneDescription` are gone. Build the zone's title and description with shadcn/ui's [Empty](https://ui.shadcn.com/docs/components/empty) component instead; `DropzoneUploadIcon` stays for the drag-state icon swap.

### Superseded components

Expand Down Expand Up @@ -69,9 +70,9 @@ We've added a new Virtualizer documentation page.

Learn how to use the `virtua` library to efficiently render large lists and grids by virtualizing your components. This documentation provides practical examples and guidance on integrating `virtua` with your existing components.

<ComponentPreview name="virtualizer-demo" />
<ComponentPreview name="virtualized-demo" />

[Read the docs](/docs/utilities/virtualizer)
[Read the docs](/docs/utilities/virtualized)

## February 2025 - Sortable and Control Group

Expand Down
8 changes: 7 additions & 1 deletion apps/v4/content/docs/components/confirmer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Add the `<AlertDialog />` and `<Button />` component to your project.

The `<Confirmer />` component uses `<AlertDialog />` and `<Button />` components. Make sure you have it installed in your project.

See installation instructions for the [AlertDialog](/docs/components/alert-dialog#installation) and the [Button](/docs/components/button#installation) components.
See installation instructions for the [AlertDialog](https://ui.shadcn.com/docs/components/alert-dialog) and the [Button](https://ui.shadcn.com/docs/components/button) components.

<Step>Copy and paste the following code into your project.</Step>

Expand Down Expand Up @@ -127,6 +127,12 @@ confirm({

<ComponentPreview name="confirmer-demo" />

## Accessibility

`<Confirmer />` renders an `<AlertDialog />`, so it inherits Base UI's alert dialog semantics: the dialog traps focus while open, labels itself from `<AlertDialogTitle>` and `<AlertDialogDescription>` (the `title` and `description` options), and returns focus to the triggering element when it closes.

Pressing <Kbd>Escape</Kbd> or activating the cancel button resolves `confirm()` with `false`, mirroring a dismissed dialog. Always pass a `title` (and a `description` for anything non-obvious) so screen reader users hear what they're confirming, and set `ActionProps={{ variant: "destructive" }}` rather than color alone to distinguish destructive actions.

## API Reference

### Confirmer
Expand Down
2 changes: 1 addition & 1 deletion apps/v4/content/docs/components/date-time-field.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Add `<DateTimeFieldPrimitive />` and `<InputGroup />` component to your project.

The `<DateTimeField />` component uses `<DateTimeFieldPrimitive />` and `<InputGroup />` components. Make sure you have it installed in your project.

See installation instructions for the [DateTimeFieldPrimitive](/docs/components/date-time-field-primitive#installation) and [Input Group](https://ui.shadcn.com/docs/components/input-group) components.
See installation instructions for the [DateTimeFieldPrimitive](/docs/primitives/date-time-field#installation) and [Input Group](https://ui.shadcn.com/docs/components/input-group) components.

<Step>Copy and paste the following code into your project.</Step>

Expand Down
54 changes: 33 additions & 21 deletions apps/v4/content/docs/components/dropzone.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,23 @@ npm install @base-ui/react

<Step>

Add the `<Empty />` component to your project.

</Step>

The `<Dropzone />` component uses the `<Empty />` component for its visual content. Make sure you have it installed in your project.

See installation instructions for the [Empty](https://ui.shadcn.com/docs/components/empty) component.

<Step>

Add the `<DropzonePrimitive />` component to your project.

</Step>

The `<Dropzone />` component uses `<DropzonePrimitive />` component. Make sure you have it installed in your project.

See installation instructions for the [DropzonePrimitive](/docs/components/dropzone-primitive#installation) component.
See installation instructions for the [DropzonePrimitive](/docs/primitives/dropzone#installation) component.

<Step>Copy and paste the following code into your project.</Step>

Expand All @@ -62,12 +72,16 @@ See installation instructions for the [DropzonePrimitive](/docs/components/dropz
## Usage

```tsx
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import {
Dropzone,
DropzoneDescription,
DropzoneGroup,
DropzoneInput,
DropzoneTitle,
DropzoneUploadIcon,
DropzoneZone,
} from "@/components/ui/dropzone";
Expand All @@ -83,16 +97,18 @@ import {
>
<DropzoneZone>
<DropzoneInput />
<DropzoneGroup className="gap-4">
<DropzoneUploadIcon />
<DropzoneGroup>
<DropzoneTitle>Drop files here or click to upload</DropzoneTitle>
<DropzoneDescription>
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<DropzoneUploadIcon />
</EmptyMedia>
<EmptyTitle>Drop files here or click to upload</EmptyTitle>
<EmptyDescription>
You can upload files up to 10MB in size. Supported formats: JPG, PNG,
PDF.
</DropzoneDescription>
</DropzoneGroup>
</DropzoneGroup>
</EmptyDescription>
</EmptyHeader>
</Empty>
</DropzoneZone>
</Dropzone>
```
Expand All @@ -113,11 +129,11 @@ import {

## Accessibility

The zone is focusable and opens the file dialog with <Kbd>Enter</Kbd> or <Kbd>Space</Kbd>, so the drag-and-drop interaction always has a keyboard equivalent. Make sure the zone contains visible text describing what to upload (such as `<DropzoneTitle />` and `<DropzoneDescription />`) — the upload icon alone is not an accessible name. Drag states are conveyed with color and iconography via `<DropzoneUploadIcon />`; keep the title text meaningful so the state change is not communicated by color alone.
The zone is focusable and opens the file dialog with <Kbd>Enter</Kbd> or <Kbd>Space</Kbd>, so the drag-and-drop interaction always has a keyboard equivalent. Make sure the zone contains visible text describing what to upload (such as `<EmptyTitle />` and `<EmptyDescription />`) — the upload icon alone is not an accessible name. Drag states are conveyed with color and iconography via the `<DropzoneUploadIcon />` swap; keep the title text meaningful so the state change is not communicated by color alone.

## API Reference

Styled wrappers around the [Dropzone primitive](/docs/primitives/dropzone#api-reference) — see the primitive page for the full prop tables.
Styled wrappers around the [Dropzone primitive](/docs/primitives/dropzone#api-reference) — see the primitive page for the full prop tables. The zone's title and description are composed with shadcn/ui's [Empty](https://ui.shadcn.com/docs/components/empty) component rather than a Dropzone-specific part.

### Dropzone

Expand All @@ -131,17 +147,13 @@ Wraps [Zone](/docs/primitives/dropzone#zone) with dashed-border styling that rea

Wraps [Input](/docs/primitives/dropzone#input), the visually hidden file input.

### DropzoneTrigger

Wraps [Trigger](/docs/primitives/dropzone#trigger) for opening the file dialog from a button outside the zone.

### DropzoneUploadIcon

A convenience part that renders an upload, accepted or rejected icon based on the current drag state, using the [DragDefault](/docs/primitives/dropzone#dragdefault), [DragAccepted](/docs/primitives/dropzone#dragaccepted) and [DragRejected](/docs/primitives/dropzone#dragrejected) parts. Accepts `lucide-react` icon props.
Swaps the icon based on the drag state — an upload icon by default, a check when the drag is accepted and a ban icon when rejected. Render it inside `<EmptyMedia variant="icon" />`. Accepts the same props as a [Lucide](https://lucide.dev) icon.

### DropzoneGroup, DropzoneTitle, DropzoneDescription
### DropzoneTrigger

Layout and typography parts for the zone's content. Render a `<div>`, `<h3>` and `<p>` respectively, and support the `render` prop.
Wraps [Trigger](/docs/primitives/dropzone#trigger) for opening the file dialog from a button outside the zone.

### DropzoneAccepted, DropzoneRejected

Expand Down
8 changes: 2 additions & 6 deletions apps/v4/content/docs/components/password-input.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Add `<InputGroup />` and `<PasswordInputPrimitive />` component to your project.

The `<PasswordInput />` component uses `<InputGroup />` and `<PasswordInputPrimitive />` components. Make sure you have it installed in your project.

See installation instructions for the [Input Group](https://ui.shadcn.com/docs/components/input-group) and [PasswordInputPrimitive](/docs/components/password-input-primitive#installation) components.
See installation instructions for the [Input Group](https://ui.shadcn.com/docs/components/input-group) and [PasswordInputPrimitive](/docs/primitives/password-input#installation) components.

<Step>Copy and paste the following code into your project.</Step>

Expand Down Expand Up @@ -86,7 +86,7 @@ The visibility toggle is icon-only — give it an accessible name, e.g. `<Passwo

## API Reference

Styled wrappers around the [Password Input primitive](/docs/primitives/password-input#api-reference) — see the primitive page for the full prop tables.
Styled wrappers around the [Password Input primitive](/docs/primitives/password-input#api-reference) — see the primitive page for the full prop tables. Custom adornments, such as a leading lock icon, are composed with shadcn/ui's [Input Group](https://ui.shadcn.com/docs/components/input-group) parts (`InputGroupAddon`, `InputGroupButton`) rather than Password Input-specific parts.

### PasswordInput

Expand All @@ -99,7 +99,3 @@ Wraps [Input](/docs/primitives/password-input#input) with input-group styling. A
### PasswordInputAdornmentToggle

Wraps [Toggle](/docs/primitives/password-input#toggle) as an icon button in the trailing addon, swapping the eye icon based on the visibility state.

### PasswordInputAdornment, PasswordInputAdornmentButton

Input-group addon and icon-button parts for composing custom adornments, e.g. a leading lock icon or a custom toggle.
Loading