Skip to content

Registry catalog, Field variants, form generator, and Radix → Base UI migration - #106

Merged
hasanharman merged 35 commits into
mainfrom
refactor/registry-catalog-base-ui
Sep 28, 2026
Merged

hasanharman merged 35 commits into
mainfrom
refactor/registry-catalog-base-ui

Conversation

@hasanharman

Copy link
Copy Markdown
Owner

Summary

This PR does four things: it makes the shadcn registry complete, deepens the form builder, runs the official Radix → Base UI migration, and adds CI. It is 35 commits, one per concern and one per migrated components/ui wrapper, so it can be reviewed or reverted commit by commit.

+registry.json                     # Registry catalog: 33 items, built by `shadcn build` → public/r
-registry/  scripts/build-registry.ts  public/registry/   # hand-rolled schema, 16 byte-copied twins
 lib/
+├── registry-catalog.ts           # only reader of registry.json (docs, sidebars, install commands)
+├── form-code/                    # one generator + adapters: react-hook-form, tanstack-form
 components/
+├── field-variants/               # one module per Field variant (22)
 ├── ui/                           # Primitive seam: now @base-ui/react, the only place allowed to import it
-screens/render-form-field  generate-code-field  generate-code-parts/   # 18 switch statements on `variant: string`

Registry. registry.json is now the single source for each registry item, and docs pages read it instead of repeating metadata. Adding a component used to mean editing 7 places; it now means editing 3 (source file, catalog entry, docs page).

  • Published: all 16 existing components, the 8 playground components that previously couldn't be installed, and every Template as a registry:block.
  • Items are primitive-agnostic: no asChild/render, no state-attribute classes. So one published item installs into Radix and Base UI projects alike.
  • The old /registry/*.json URLs redirect to /r/*.json.

Field variants. Adding a field type is now one module.

FieldVariant = defaults, settings, schema(field) → zx expr, defaultValue, Control (preview), control(field, binding) (source)
generateFormCode(fields, library)
  adapter.field(name, binding → fieldShellSource(field, binding, variant.control(field, binding)))

Base UI. This follows the official migrate-radix-to-base skill: wrappers come from the shadcn CLI as base-vega (shadcn's "classic look" style), with the site's customizations ported onto them. Consumers moved from asChild to render, and the Radix packages are removed. Details are in .migration/project.md.

Evidence

Bugs fixed along the way:

Before After
Generated schemas lost .min/.max/.email under zod v4 (zodSchemaToString read check.kind). form schema › keeps zod checks in generated source passes.
React Hook Form output rendered 15 of 22 variants as plain <Input>, and used register() on Radix Switch/Select, which never fire onChange. renders a bound control for every field and binds … through the library, not register() pass.
TanStack output used the removed zod-form-adapter and never validated. validates TanStack forms with the schema through Standard Schema passes.
location-input installed without its countries/states data, and credit-card installed a 294-line older API than the site renders. Installed code is the rendered code.
6 of 7 templates showed shadcn add <slug> for items that didn't exist. The blocks exist and install.

Checks:

  • pnpm vitest run: 7 files, 344 tests pass. This includes typechecking a generated form that uses every variant, for both libraries.
  • pnpm typecheck, pnpm registry:validate (33 items) and pnpm build all pass.
  • Manual, not in CI: all 33 items shadcn add and typecheck in fresh radix-nova, base-nova and new-york-v4 projects. Generated forms typecheck against stock radix-nova and base-nova components.
  • Checked in a browser: the playground renders all 22 variants and shows validation messages on submit, and the theme menu, selects, popovers, the Signature Pad dialog, tabs, sidebars and template flow links all work.

Merge Danger

Door: two-way. Nothing is destructive and each commit reverts cleanly. The Base UI switch is spread across 25 small commits, one per wrapper plus setup and finishing steps.

Blast Radius:

  • Look: wrappers now use base-vega classes instead of the 2024 new-york ones, so padding, rings and hover shades change slightly across the site.
  • Behavior:
    • Tabs activate manually.
    • Select lists align over the trigger.
    • The accordion allows one open item at a time.
    • The full list is in .migration/project.md.
  • Removed from the playground: the Server Actions (disabled "Coming Soon") and Bring-Your-Own (unreachable) generators. Saved library choices fall back to React Hook Form.
  • Registry consumers: item URLs move to /r/<name>.json, and /registry/* 308-redirects there. credit-card now ships the API the site renders (cvvLabel is a prop, not a value field).
  • Build: pnpm dev/build run shadcn build first. public/r is gitignored, and shadcn is now a runtime dependency because globals.css imports shadcn/tailwind.css.
  • Not done here: listing the registry in the shadcn directory (a PR to shadcn-ui/ui) and a {style} per-base split (not needed while items stay primitive-agnostic).

🤖 Generated with Claude Code

hasanharman and others added 30 commits September 27, 2026 19:17
Remove files no route reaches through static imports: legacy
*-form docs demos, unused templates (login, register, code-viewer),
components/blocks, DropdownContext, the Radix toast stack, and unused
ui primitives (context-menu, progress, sonner wrapper). Drop the three
Radix packages only those files used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tailwind v4 never loaded tailwind.config.js (globals.css has no @config),
so the col-span safelist for runtime form layouts was dead. Move it to an
@source inline() rule, delete the config file, point components.json at
no config as shadcn expects for v4, drop the nonexistent "form" alias,
and declare the lucide icon library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the hand-rolled registry (registry/ui twins, registry/schema.ts,
scripts/build-registry.ts, committed public/registry/*.json) with the
official shadcn workflow: a root registry.json built by `shadcn build`
into public/r on dev/build.

- Items point at the files the app renders (components/ui/*), so what
  users install is what the site shows. This fixes credit-card, whose
  published copy had drifted to an older API.
- location-input now ships its countries/states data as registry:lib
  files instead of asking users to copy them by hand.
- Drop signature-pad's stale "progress" dependency; declare lucide-react.
- lib/registry-catalog.ts is the one reader of registry.json. The docs
  shell, sidebar and [slug] route take title, description and install
  command from it; constants/components.ts is gone.
- /components/[slug] is statically generated with per-item metadata and
  now appears in the sitemap.
- /registry/* redirects to /r/* so existing install commands keep working.
- __tests__/registry-catalog.spec.ts checks every item's files, npm deps
  and registryDependencies against its real imports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shadcn projects now start on Base UI (base-*), Radix (radix-*) or the
legacy new-york style. A registry item only installs cleanly into all of
them if it uses the props every stock wrapper shares, so:

- Popover triggers are styled with buttonVariants instead of
  `asChild` + <Button> (Base UI spells that `render`).
- Select triggers render an explicit label in <SelectValue>; Base UI
  otherwise shows the raw value ("hex" instead of "Hex").
- onValueChange handlers tolerate Base UI's nullable value.

The catalog test now fails if an item imports a primitive library,
uses asChild/render, styles data-state attributes, or relies on types
the stock wrappers don't export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated forms import eight components the registry did not ship, and
the playground sent users to third-party sites (or nowhere, for Rating)
to find them. Add phone-input, datetime-picker, smart-datetime-input,
file-upload, tags-input, multi-select, password-input and rating to
registry.json, each with a docs page.

Make them installable into any shadcn project:
- drop InputProps/CalendarProps imports the stock wrappers don't export
- replace the relative './scroll-area' import and react-icons with
  alias imports and lucide
- trigger popovers via buttonVariants instead of asChild
- use a plain <style> in color-picker instead of Next-only styled-jsx

The playground notice now lists the components a form uses, links their
docs pages, and shows one install command for all of them, including
fields inside multi-column rows (previously skipped).

Verified: all 24 items install with `shadcn add` and typecheck in fresh
radix-nova, base-nova and new-york-v4 projects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Six of the seven templates showed `shadcn add <slug>` for items that
existed nowhere, and shadcn-auth pointed at the unrelated official
login-05 block. Copied code also failed to compile without
lib/validation-schemas and the site's password-input.

- registry.json gains a block per template plus the shared auth-shell
  and validation-schemas items they depend on (by URL), with features in
  meta and install notes in docs.
- Templates link between flows via real routes (/sign-in, /sign-up, ...)
  and next/link; the preview intercepts those links to switch flows.
- contact, newsletter and the Supabase/Firebase/Better Auth templates
  move from the deprecated <Form> (an empty item in new shadcn styles)
  to <Field> + form.register.
- constants/templates.ts keeps only grouping and branding; titles,
  descriptions and features come from the catalog. The flow lists that
  were duplicated between the sidebar and the page are one AUTH_FLOWS.
- The block viewer reads file contents from the built /r/<name>.json,
  so /api/file is gone. Open in v0 now opens the real registry item.

Verified: all 33 items install with `shadcn add` and typecheck in fresh
radix-nova, base-nova and new-york-v4 projects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FormBuilder imported components/editor but never rendered it, so the
editor, its stale command list (constants/menu.tsx) and the BlockNote,
react-grid-layout and lodash packages it pulled in were dead weight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What a field variant is used to be spread over ~18 switch statements
keyed by an untyped string: the preview renderer (711 lines), zod schema,
two default-value functions, four import lists, the legacy snippet
generator and a JSON-schema generator, which had already drifted apart.

components/field-variants/ now holds one module per variant with its
defaults, builder settings, validation, default value, preview control
and generated control. Adding a field type means adding one module.

- Validation is written once with a small recorder (schema-expr.ts)
  that yields both the runtime zod schema and its source. This replaces
  zodSchemaToString, which read zod v3 internals and silently dropped
  .min/.max/.email under zod v4.
- Preview and generated code share one default value per variant (the
  preview used to check checkboxes and switches the code left false).
- The preview renders through Controller + Field; FieldShell arranges
  label, control, description and error.
- Optional fields accept their empty defaults; credit card and location
  report one field-level message instead of empty nested errors.
- The edit dialog shows settings each variant declares.
- FormFieldType drops the unused runtime callbacks and value props.
- Removes renderFormField, generate-code-field, json-schema-generator
  and the now unused components/ui/form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck adapters

The four generators (RHF, TanStack, Server Actions, Bring Your Own)
duplicated import collection and row layout verbatim but handled only
7 of 22 variants; everything else became a plain <Input>, sometimes
without importing it. RHF spread form.register() onto Radix Checkbox,
Select, Switch and Slider, which never fire onChange, and TanStack used
the removed zod-form-adapter and never validated.

lib/form-code/ now generates every variant through its control()
template and a small adapter per library:
- RHF: Controller + Field, zodResolver with input/output types.
- TanStack: form.Field + Field, the zod schema as a Standard Schema
  onSubmit validator.

Generated files start with the one shadcn install command they need
(official items plus this registry's), import exactly what they render,
and are formatted with prettier instead of js-beautify. The playground
notice shows the same command. Server Actions (disabled in the UI) and
Bring Your Own (never reachable) are dropped.

__tests__/form-code.spec.ts generates a form with every variant for
each library and typechecks it against the app's components. Verified
separately against stock radix-nova and base-nova components.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two app modules imported Radix directly: features-vertical defined an
Accordion it never rendered, and the readme page used the raw Separator
primitive. Delete the former, route the latter through ui/separator, and
add a test that fails when app code imports a primitive library outside
components/ui. The Base UI migration then only has to change the
wrappers and the props consumers pass them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Following the official migrate-radix-to-base skill: add @base-ui/react
next to the Radix packages (they coexist until the last wrapper moves),
apply Base UI's setup (an isolated app root and position: relative on
body for iOS Safari), and alias --color-sidebar for the base sidebar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the Radix Slot-based button with the base-vega button built on
@base-ui/react/button, keeping the site's extra variants (arrow, smile,
gooey*, linkHover*, shine, ringHover, expandIcon). The unused
Icon/iconPlacement props are dropped. Consumers that rendered a link
through `asChild` now pass `render={<Link />}` with nativeButton={false}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega label from the shadcn CLI (), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega separator from the shadcn CLI (@base-ui/react/separator ), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega checkbox from the shadcn CLI (@base-ui/react/checkbox ), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega switch from the shadcn CLI (@base-ui/react/switch ), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega slider from the shadcn CLI (@base-ui/react/slider ), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega radio-group from the shadcn CLI (@base-ui/react/radio @base-ui/react/radio-group ), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega scroll-area from the shadcn CLI (@base-ui/react/scroll-area ), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega avatar from the shadcn CLI (@base-ui/react/avatar ), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega tabs from the shadcn CLI (@base-ui/react/tabs ), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega popover (@base-ui/react/popover: Portal > Positioner >
Popup). The sponsors radar hover trigger moves from asChild to
render={<div />} with nativeButton={false}. Triggers styled with
buttonVariants (combobox, date picker, registry items) need no change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega accordion (@base-ui/react/accordion, Content -> Panel).
The FAQ drops type="single"/collapsible: Base UI accordions open one
item at a time and collapse by default. The accordion keyframes now read
--accordion-panel-height instead of the Radix variable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega dialog from the shadcn CLI (@base-ui/react/dialog ), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega sheet from the shadcn CLI (@base-ui/react/dialog ), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cmdk stays (it is not Radix); the wrapper's dialog now comes from the
Base UI dialog and its search field from the new input-group component,
which the CLI adds alongside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega select from the shadcn CLI (@base-ui/react/select ), replacing the Radix
new-york wrapper. No consumer changes needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega dropdown-menu (@base-ui/react/menu). The theme switch
and the column picker pass their Button through `render` instead of
`asChild`, and the picker's label now sits in a DropdownMenuGroup, which
Base UI's GroupLabel requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega breadcrumb: BreadcrumbLink drops the Radix Slot for
useRender + mergeProps from @base-ui/react. Existing href usage is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take the base-vega sidebar and tooltip together (the tooltip's only
consumer is the sidebar). Keep this site's sidebar layout: 12rem wide,
positioned inside the page (absolute, auto height) rather than fixed to
the viewport, and no left border. Sidebar links move from asChild to
render={<Link />}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hasanharman and others added 5 commits September 27, 2026 20:02
These wrappers sit on react-day-picker and input-otp, which the
migration leaves alone; their only Radix dependency was
@radix-ui/react-icons. Use the lucide equivalents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- field: take the base-vega wrapper, whose choice-card styles key off
  data-checked instead of Radix's data-[state=checked].
- edit dialog: give the input-type SelectValue its label; Base UI selects
  render the raw value ("text") otherwise.

Left alone on purpose: the header's vaul Drawer (asChild is vaul's API,
not Radix), cmdk, sonner, input-otp and react-day-picker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Remove every @radix-ui/* package; nothing imports them any more.
- Flip components.json to base-vega (the classic look on Base UI), so
  `shadcn add` now delivers Base UI components; `shadcn info` reports
  base: "base".
- Import shadcn/tailwind.css for the data-open/data-horizontal/…
  variants the base wrappers rely on, and replace tailwindcss-animate
  with tw-animate-css as current shadcn projects do. The accordion
  keyframes now come from shadcn/tailwind.css. shadcn moves to
  dependencies since the CSS is imported at build time.
- .migration/project.md records what changed, what was left alone
  (vaul, cmdk, sonner, input-otp, react-day-picker), behavior changes
  and a manual check list, per the official skill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add CONTEXT.md with the domain glossary (Field variant, Form library,
Schema expression, Registry item/catalog, Template, Primitive seam), and
update the README: pnpm commands, how to install registry items, what
the tests check, and where each concern lives. The old Components
section described modules that never existed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repo had no CI, so nothing ran the new catalog, field-variant and
generated-code tests. Add a workflow for pull requests and main, and a
typecheck script that generates Next's types first (next-env.d.ts is
gitignored, and SVG/image imports need it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
shadcn-form Ready Ready Preview Sep 27, 2026 5:11pm UTC

@hasanharman hasanharman self-assigned this Sep 28, 2026
@hasanharman
hasanharman merged commit 81c83f6 into main Sep 28, 2026
3 checks passed

This branch was successfully deployed

1 active deployment
Preview — 940ae709 Deployed Sep 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant