653 lines
19 KiB
Markdown
653 lines
19 KiB
Markdown
|
|
# Base UI Shadcn Wrapper Implementation Plan
|
||
|
|
|
||
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
|
|
||
|
|
**Goal:** Build a source-only internal `components/ui` library that wraps all Base UI React components with Tailwind styles and supports both Base UI-style default namespace imports and shadcn-style named imports.
|
||
|
|
|
||
|
|
**Architecture:** Each UI component is a folder. Base UI parts are split into focused files such as `root.tsx`, `trigger.tsx`, and `popup.tsx`; `index.ts` assembles the default namespace export and named shadcn-style exports. Shared class merging lives in `lib/utils.ts`, and styles are static Tailwind class strings adapted from the Base UI documentation examples.
|
||
|
|
|
||
|
|
**Tech Stack:** React, TypeScript, `@base-ui/react`, Tailwind CSS, `clsx`, `tailwind-merge`, `class-variance-authority`.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## File Map
|
||
|
|
|
||
|
|
- Create `package.json`: local validation scripts and dependencies only.
|
||
|
|
- Create `tsconfig.json`: framework-neutral React TypeScript settings.
|
||
|
|
- Create `components/ui/**`: source templates, one folder per Base UI component.
|
||
|
|
- Create `lib/utils.ts`: `cn()` helper used by every styled part.
|
||
|
|
- Create `examples/smoke.tsx`: compile-only examples for default namespace and named shadcn-style APIs.
|
||
|
|
- Create `scripts/check-ui-exports.mjs`: verifies every planned component folder exists and exports a default.
|
||
|
|
- Create `README.md`: usage, dependency, import, and customization guidance.
|
||
|
|
|
||
|
|
## Component Folders
|
||
|
|
|
||
|
|
Create these component folders under `components/ui`:
|
||
|
|
|
||
|
|
```txt
|
||
|
|
accordion
|
||
|
|
alert-dialog
|
||
|
|
autocomplete
|
||
|
|
avatar
|
||
|
|
button
|
||
|
|
checkbox
|
||
|
|
checkbox-group
|
||
|
|
collapsible
|
||
|
|
combobox
|
||
|
|
context-menu
|
||
|
|
dialog
|
||
|
|
drawer
|
||
|
|
field
|
||
|
|
fieldset
|
||
|
|
form
|
||
|
|
input
|
||
|
|
menu
|
||
|
|
menubar
|
||
|
|
meter
|
||
|
|
navigation-menu
|
||
|
|
number-field
|
||
|
|
otp-field
|
||
|
|
popover
|
||
|
|
preview-card
|
||
|
|
progress
|
||
|
|
radio
|
||
|
|
scroll-area
|
||
|
|
select
|
||
|
|
separator
|
||
|
|
slider
|
||
|
|
switch
|
||
|
|
tabs
|
||
|
|
toast
|
||
|
|
toggle
|
||
|
|
toggle-group
|
||
|
|
toolbar
|
||
|
|
tooltip
|
||
|
|
```
|
||
|
|
|
||
|
|
## Task 1: Project Skeleton
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `package.json`
|
||
|
|
- Create: `tsconfig.json`
|
||
|
|
- Create: `lib/utils.ts`
|
||
|
|
- Create: `components/ui/.gitkeep`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Create `package.json`**
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"private": true,
|
||
|
|
"type": "module",
|
||
|
|
"scripts": {
|
||
|
|
"typecheck": "tsc --noEmit",
|
||
|
|
"check:exports": "node scripts/check-ui-exports.mjs",
|
||
|
|
"check": "npm run typecheck && npm run check:exports"
|
||
|
|
},
|
||
|
|
"dependencies": {
|
||
|
|
"@base-ui/react": "^1.6.0",
|
||
|
|
"class-variance-authority": "^0.7.1",
|
||
|
|
"clsx": "^2.1.1",
|
||
|
|
"tailwind-merge": "^3.3.1"
|
||
|
|
},
|
||
|
|
"devDependencies": {
|
||
|
|
"@types/react": "^19.0.0",
|
||
|
|
"@types/react-dom": "^19.0.0",
|
||
|
|
"typescript": "^5.8.0"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Create `tsconfig.json`**
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"compilerOptions": {
|
||
|
|
"allowSyntheticDefaultImports": true,
|
||
|
|
"baseUrl": ".",
|
||
|
|
"declaration": false,
|
||
|
|
"esModuleInterop": true,
|
||
|
|
"jsx": "react-jsx",
|
||
|
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||
|
|
"module": "ESNext",
|
||
|
|
"moduleResolution": "Bundler",
|
||
|
|
"noEmit": true,
|
||
|
|
"paths": {
|
||
|
|
"@/*": ["./*"]
|
||
|
|
},
|
||
|
|
"skipLibCheck": true,
|
||
|
|
"strict": true,
|
||
|
|
"target": "ES2022"
|
||
|
|
},
|
||
|
|
"include": ["components", "examples", "lib", "scripts"]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Create `lib/utils.ts`**
|
||
|
|
|
||
|
|
```ts
|
||
|
|
import { clsx, type ClassValue } from "clsx"
|
||
|
|
import { twMerge } from "tailwind-merge"
|
||
|
|
|
||
|
|
export function cn(...inputs: ClassValue[]) {
|
||
|
|
return twMerge(clsx(inputs))
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Install dependencies**
|
||
|
|
|
||
|
|
Run: `npm install`
|
||
|
|
|
||
|
|
Expected: `package-lock.json` and `node_modules` are created.
|
||
|
|
|
||
|
|
- [ ] **Step 5: Run typecheck**
|
||
|
|
|
||
|
|
Run: `npm run typecheck`
|
||
|
|
|
||
|
|
Expected: TypeScript exits successfully, or fails only because component files are not present yet.
|
||
|
|
|
||
|
|
## Task 2: Export Checker
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `scripts/check-ui-exports.mjs`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Create `scripts/check-ui-exports.mjs`**
|
||
|
|
|
||
|
|
```js
|
||
|
|
import { access, readFile } from "node:fs/promises"
|
||
|
|
import path from "node:path"
|
||
|
|
|
||
|
|
const components = [
|
||
|
|
"accordion",
|
||
|
|
"alert-dialog",
|
||
|
|
"autocomplete",
|
||
|
|
"avatar",
|
||
|
|
"button",
|
||
|
|
"checkbox",
|
||
|
|
"checkbox-group",
|
||
|
|
"collapsible",
|
||
|
|
"combobox",
|
||
|
|
"context-menu",
|
||
|
|
"dialog",
|
||
|
|
"drawer",
|
||
|
|
"field",
|
||
|
|
"fieldset",
|
||
|
|
"form",
|
||
|
|
"input",
|
||
|
|
"menu",
|
||
|
|
"menubar",
|
||
|
|
"meter",
|
||
|
|
"navigation-menu",
|
||
|
|
"number-field",
|
||
|
|
"otp-field",
|
||
|
|
"popover",
|
||
|
|
"preview-card",
|
||
|
|
"progress",
|
||
|
|
"radio",
|
||
|
|
"scroll-area",
|
||
|
|
"select",
|
||
|
|
"separator",
|
||
|
|
"slider",
|
||
|
|
"switch",
|
||
|
|
"tabs",
|
||
|
|
"toast",
|
||
|
|
"toggle",
|
||
|
|
"toggle-group",
|
||
|
|
"toolbar",
|
||
|
|
"tooltip",
|
||
|
|
]
|
||
|
|
|
||
|
|
const failures = []
|
||
|
|
|
||
|
|
for (const component of components) {
|
||
|
|
const indexPath = path.join("components", "ui", component, "index.ts")
|
||
|
|
try {
|
||
|
|
await access(indexPath)
|
||
|
|
const source = await readFile(indexPath, "utf8")
|
||
|
|
if (!source.includes("export default")) {
|
||
|
|
failures.push(`${indexPath}: missing default export`)
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
failures.push(`${indexPath}: missing file`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (failures.length > 0) {
|
||
|
|
console.error(failures.join("\n"))
|
||
|
|
process.exit(1)
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`Checked ${components.length} component exports.`)
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Run checker and verify it fails before components exist**
|
||
|
|
|
||
|
|
Run: `npm run check:exports`
|
||
|
|
|
||
|
|
Expected: FAIL with missing `components/ui/*/index.ts` files.
|
||
|
|
|
||
|
|
## Task 3: Implement Foundation Components
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `components/ui/button/root.tsx`
|
||
|
|
- Create: `components/ui/button/index.ts`
|
||
|
|
- Create: `components/ui/input/root.tsx`
|
||
|
|
- Create: `components/ui/input/index.ts`
|
||
|
|
- Create: `components/ui/separator/root.tsx`
|
||
|
|
- Create: `components/ui/separator/index.ts`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Implement Button**
|
||
|
|
|
||
|
|
`components/ui/button/root.tsx`:
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
"use client"
|
||
|
|
|
||
|
|
import * as React from "react"
|
||
|
|
import { Button as BaseButton } from "@base-ui/react/button"
|
||
|
|
import { cva, type VariantProps } from "class-variance-authority"
|
||
|
|
import { cn } from "@/lib/utils"
|
||
|
|
|
||
|
|
const buttonVariants = cva(
|
||
|
|
"inline-flex h-10 items-center justify-center rounded-md border border-neutral-950 bg-neutral-950 px-4 text-sm font-medium text-white select-none hover:bg-neutral-800 active:bg-neutral-700 data-disabled:pointer-events-none data-disabled:opacity-50 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 dark:border-white dark:bg-white dark:text-neutral-950 dark:hover:bg-neutral-200 dark:active:bg-neutral-300 dark:focus-visible:outline-white",
|
||
|
|
{
|
||
|
|
variants: {
|
||
|
|
variant: {
|
||
|
|
default: "",
|
||
|
|
outline:
|
||
|
|
"border-neutral-300 bg-transparent text-neutral-950 hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-900 dark:active:bg-neutral-800",
|
||
|
|
ghost:
|
||
|
|
"border-transparent bg-transparent text-neutral-950 hover:bg-neutral-100 active:bg-neutral-200 dark:text-white dark:hover:bg-neutral-900 dark:active:bg-neutral-800",
|
||
|
|
destructive:
|
||
|
|
"border-red-600 bg-red-600 text-white hover:bg-red-700 active:bg-red-800 focus-visible:outline-red-600 dark:border-red-500 dark:bg-red-500 dark:hover:bg-red-600 dark:active:bg-red-700 dark:focus-visible:outline-red-500",
|
||
|
|
},
|
||
|
|
size: {
|
||
|
|
sm: "h-8 px-3 text-sm",
|
||
|
|
default: "h-10 px-4",
|
||
|
|
lg: "h-11 px-5",
|
||
|
|
icon: "size-10 px-0",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
defaultVariants: {
|
||
|
|
variant: "default",
|
||
|
|
size: "default",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
export type ButtonProps = React.ComponentPropsWithoutRef<typeof BaseButton> &
|
||
|
|
VariantProps<typeof buttonVariants>
|
||
|
|
|
||
|
|
export const Button = React.forwardRef<
|
||
|
|
React.ElementRef<typeof BaseButton>,
|
||
|
|
ButtonProps
|
||
|
|
>(({ className, variant, size, ...props }, ref) => (
|
||
|
|
<BaseButton
|
||
|
|
ref={ref}
|
||
|
|
className={cn(buttonVariants({ variant, size }), className)}
|
||
|
|
{...props}
|
||
|
|
/>
|
||
|
|
))
|
||
|
|
|
||
|
|
Button.displayName = "Button"
|
||
|
|
```
|
||
|
|
|
||
|
|
`components/ui/button/index.ts`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
export { Button, type ButtonProps } from "./root"
|
||
|
|
export { Button as default } from "./root"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Implement Input**
|
||
|
|
|
||
|
|
`components/ui/input/root.tsx`:
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
"use client"
|
||
|
|
|
||
|
|
import * as React from "react"
|
||
|
|
import { Input as BaseInput } from "@base-ui/react/input"
|
||
|
|
import { cn } from "@/lib/utils"
|
||
|
|
|
||
|
|
export type InputProps = React.ComponentPropsWithoutRef<typeof BaseInput>
|
||
|
|
|
||
|
|
export const Input = React.forwardRef<
|
||
|
|
React.ElementRef<typeof BaseInput>,
|
||
|
|
InputProps
|
||
|
|
>(({ className, ...props }, ref) => (
|
||
|
|
<BaseInput
|
||
|
|
ref={ref}
|
||
|
|
className={cn(
|
||
|
|
"h-10 w-full rounded-md border border-neutral-300 bg-transparent px-3 text-base text-neutral-950 outline-none placeholder:text-neutral-500 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:text-white dark:placeholder:text-neutral-400 dark:focus-visible:outline-white",
|
||
|
|
className,
|
||
|
|
)}
|
||
|
|
{...props}
|
||
|
|
/>
|
||
|
|
))
|
||
|
|
|
||
|
|
Input.displayName = "Input"
|
||
|
|
```
|
||
|
|
|
||
|
|
`components/ui/input/index.ts`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
export { Input, type InputProps } from "./root"
|
||
|
|
export { Input as default } from "./root"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Implement Separator**
|
||
|
|
|
||
|
|
`components/ui/separator/root.tsx`:
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
"use client"
|
||
|
|
|
||
|
|
import * as React from "react"
|
||
|
|
import { Separator as BaseSeparator } from "@base-ui/react/separator"
|
||
|
|
import { cn } from "@/lib/utils"
|
||
|
|
|
||
|
|
export type SeparatorProps = React.ComponentPropsWithoutRef<
|
||
|
|
typeof BaseSeparator
|
||
|
|
>
|
||
|
|
|
||
|
|
export const Separator = React.forwardRef<
|
||
|
|
React.ElementRef<typeof BaseSeparator>,
|
||
|
|
SeparatorProps
|
||
|
|
>(({ className, orientation = "horizontal", ...props }, ref) => (
|
||
|
|
<BaseSeparator
|
||
|
|
ref={ref}
|
||
|
|
orientation={orientation}
|
||
|
|
className={cn(
|
||
|
|
"shrink-0 bg-neutral-200 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px dark:bg-neutral-800",
|
||
|
|
className,
|
||
|
|
)}
|
||
|
|
{...props}
|
||
|
|
/>
|
||
|
|
))
|
||
|
|
|
||
|
|
Separator.displayName = "Separator"
|
||
|
|
```
|
||
|
|
|
||
|
|
`components/ui/separator/index.ts`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
export { Separator, type SeparatorProps } from "./root"
|
||
|
|
export { Separator as default } from "./root"
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Run typecheck**
|
||
|
|
|
||
|
|
Run: `npm run typecheck`
|
||
|
|
|
||
|
|
Expected: PASS for the files added in this task.
|
||
|
|
|
||
|
|
## Task 4: Implement Dialog Pattern
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `components/ui/dialog/root.tsx`
|
||
|
|
- Create: `components/ui/dialog/trigger.tsx`
|
||
|
|
- Create: `components/ui/dialog/portal.tsx`
|
||
|
|
- Create: `components/ui/dialog/backdrop.tsx`
|
||
|
|
- Create: `components/ui/dialog/viewport.tsx`
|
||
|
|
- Create: `components/ui/dialog/popup.tsx`
|
||
|
|
- Create: `components/ui/dialog/title.tsx`
|
||
|
|
- Create: `components/ui/dialog/description.tsx`
|
||
|
|
- Create: `components/ui/dialog/close.tsx`
|
||
|
|
- Create: `components/ui/dialog/content.tsx`
|
||
|
|
- Create: `components/ui/dialog/index.ts`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Create styled primitive files**
|
||
|
|
|
||
|
|
Use `@base-ui/react/dialog` parts. Keep the class model from the Base UI dialog examples: neutral trigger button, fixed translucent backdrop, centered popup, compact title and muted description. `content.tsx` composes `Portal`, `Backdrop`, and `Popup`.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Create namespace and named exports**
|
||
|
|
|
||
|
|
`index.ts` must default export:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const DialogPrimitive = {
|
||
|
|
Root: DialogRoot,
|
||
|
|
Trigger: DialogTrigger,
|
||
|
|
Portal: DialogPortal,
|
||
|
|
Backdrop: DialogBackdrop,
|
||
|
|
Viewport: DialogViewport,
|
||
|
|
Popup: DialogPopup,
|
||
|
|
Title: DialogTitle,
|
||
|
|
Description: DialogDescription,
|
||
|
|
Close: DialogClose,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
It must also named export `Dialog`, `DialogTrigger`, `DialogPortal`, `DialogBackdrop`, `DialogViewport`, `DialogPopup`, `DialogContent`, `DialogTitle`, `DialogDescription`, and `DialogClose`.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Run typecheck**
|
||
|
|
|
||
|
|
Run: `npm run typecheck`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
## Task 5: Implement Dialog-Like Overlay Components
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create folders and part files for `alert-dialog`, `drawer`, `popover`, `preview-card`, and `tooltip`.
|
||
|
|
|
||
|
|
- [ ] **Step 1: Implement Alert Dialog**
|
||
|
|
|
||
|
|
Mirror the dialog folder pattern with `@base-ui/react/alert-dialog`. Include styled parts for `Root`, `Trigger`, `Portal`, `Backdrop`, `Viewport` if present, `Popup`, `Title`, `Description`, and `Close`. Include shadcn-style `AlertDialog`, `AlertDialogTrigger`, `AlertDialogContent`, `AlertDialogTitle`, `AlertDialogDescription`, and `AlertDialogClose`.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Implement Drawer**
|
||
|
|
|
||
|
|
Mirror Base UI Drawer anatomy. Split each part into a file. Preserve snap/gesture props by extending Base UI part props. Use Tailwind classes from Base UI drawer examples for backdrop, popup, handle, title, and description.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Implement Popover and Preview Card**
|
||
|
|
|
||
|
|
Split `Root`, `Trigger`, `Portal`, `Positioner`, `Popup`, `Arrow`, and close/action parts where documented. Include shadcn-style `PopoverContent` and `PreviewCardContent` convenience components.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Implement Tooltip**
|
||
|
|
|
||
|
|
Split `Provider` if documented, `Root`, `Trigger`, `Portal`, `Positioner`, `Popup`, and `Arrow`. Include `TooltipContent` as the shadcn-style convenience component.
|
||
|
|
|
||
|
|
- [ ] **Step 5: Run typecheck**
|
||
|
|
|
||
|
|
Run: `npm run typecheck`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
## Task 6: Implement Selection and Menu Components
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create folders and part files for `select`, `combobox`, `autocomplete`, `menu`, `menubar`, `context-menu`, and `navigation-menu`.
|
||
|
|
|
||
|
|
- [ ] **Step 1: Implement Select**
|
||
|
|
|
||
|
|
Split every documented Base UI Select part into files. Include styled trigger, value, icon, portal, positioner, popup, item, item indicator, item text, group, label, separator, and scroll buttons where documented. Add shadcn-style named exports such as `Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`, `SelectGroup`, `SelectLabel`, and `SelectSeparator`.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Implement Combobox and Autocomplete**
|
||
|
|
|
||
|
|
Split root, input, trigger, portal, positioner, popup, list, item, item text, item indicator, status, empty, and group/label parts where documented. Use Base UI example classes for popup, list, and highlighted item states.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Implement Menu, Menubar, and Context Menu**
|
||
|
|
|
||
|
|
Use the same menu part style vocabulary across the three components: trigger buttons, popups, items, checkbox/radio items, separators, labels, groups, submenus, arrows, and positioners. Preserve all Base UI part props and shadcn-style convenience exports.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Implement Navigation Menu**
|
||
|
|
|
||
|
|
Split root, list, item, trigger, content, viewport, positioner, link, indicator, and portal parts where documented. Keep layout classes static and allow `className` overrides on every part.
|
||
|
|
|
||
|
|
- [ ] **Step 5: Run typecheck**
|
||
|
|
|
||
|
|
Run: `npm run typecheck`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
## Task 7: Implement Form and Field Components
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create folders and part files for `form`, `field`, `fieldset`, `checkbox`, `checkbox-group`, `radio`, `switch`, `number-field`, `otp-field`, and `slider`.
|
||
|
|
|
||
|
|
- [ ] **Step 1: Implement Field, Fieldset, and Form**
|
||
|
|
|
||
|
|
Split root/control/label/description/error/validity parts according to Base UI docs. Use compact label, muted description, and red error text classes from the examples.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Implement Checkbox and Checkbox Group**
|
||
|
|
|
||
|
|
Split root, indicator, group, label, description, and error parts where documented. Include shadcn-style `Checkbox` with indicator composed inside when appropriate, plus primitive namespace exports.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Implement Radio and Switch**
|
||
|
|
|
||
|
|
Split radio root/indicator/group parts and switch root/thumb parts. Use Base UI data attribute styling for checked, unchecked, and disabled states.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Implement Number Field, OTP Field, and Slider**
|
||
|
|
|
||
|
|
Split increment/decrement buttons, input, scrubbing area, group, track, thumb, indicator, and item slots according to each anatomy page. Preserve CSS variable driven styles for slider and number-field controls.
|
||
|
|
|
||
|
|
- [ ] **Step 5: Run typecheck**
|
||
|
|
|
||
|
|
Run: `npm run typecheck`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
## Task 8: Implement Disclosure, Navigation, and Feedback Components
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create folders and part files for `accordion`, `collapsible`, `tabs`, `toolbar`, `toggle`, `toggle-group`, `toast`, `progress`, `meter`, `avatar`, and `scroll-area`.
|
||
|
|
|
||
|
|
- [ ] **Step 1: Implement Accordion and Collapsible**
|
||
|
|
|
||
|
|
Split root, item, header, trigger, panel/content parts. Use Base UI example classes for border grouping, trigger focus, and panel transition states.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Implement Tabs and Toolbar**
|
||
|
|
|
||
|
|
Split list, tab, panel, indicator, root, group, button, link, and separator parts where documented. Use data selected/orientation selectors for visual states.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Implement Toggle and Toggle Group**
|
||
|
|
|
||
|
|
Use `class-variance-authority` for `variant` and `size` on single toggle controls, preserving the Base UI example as `default`.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Implement Toast**
|
||
|
|
|
||
|
|
Split provider, viewport, root, title, description, close, action, and portal parts where documented. Preserve swipe/animation data attributes and static Tailwind classes.
|
||
|
|
|
||
|
|
- [ ] **Step 5: Implement Progress, Meter, Avatar, and Scroll Area**
|
||
|
|
|
||
|
|
Split root, indicator, image, fallback, viewport, scrollbar, thumb, and corner parts where documented. Use Base UI CSS variables for progress/meter values and scroll area sizing.
|
||
|
|
|
||
|
|
- [ ] **Step 6: Run typecheck**
|
||
|
|
|
||
|
|
Run: `npm run typecheck`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
## Task 9: Smoke Examples and Export Verification
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `examples/smoke.tsx`
|
||
|
|
- Modify: `scripts/check-ui-exports.mjs` if any official component folder name changed during implementation.
|
||
|
|
|
||
|
|
- [ ] **Step 1: Create `examples/smoke.tsx`**
|
||
|
|
|
||
|
|
```tsx
|
||
|
|
import Dialog, {
|
||
|
|
Dialog as ShadcnDialog,
|
||
|
|
DialogContent,
|
||
|
|
DialogTitle,
|
||
|
|
DialogTrigger,
|
||
|
|
} from "@/components/ui/dialog"
|
||
|
|
import Button, { Button as UIButton } from "@/components/ui/button"
|
||
|
|
|
||
|
|
export function Smoke() {
|
||
|
|
return (
|
||
|
|
<div>
|
||
|
|
<Button focusableWhenDisabled>Primitive-style button</Button>
|
||
|
|
<UIButton variant="outline" size="sm">
|
||
|
|
Named button
|
||
|
|
</UIButton>
|
||
|
|
|
||
|
|
<Dialog.Root>
|
||
|
|
<Dialog.Trigger>Primitive dialog</Dialog.Trigger>
|
||
|
|
<Dialog.Portal>
|
||
|
|
<Dialog.Backdrop />
|
||
|
|
<Dialog.Popup>
|
||
|
|
<Dialog.Title>Primitive dialog</Dialog.Title>
|
||
|
|
<Dialog.Description>Default namespace API.</Dialog.Description>
|
||
|
|
<Dialog.Close>Close</Dialog.Close>
|
||
|
|
</Dialog.Popup>
|
||
|
|
</Dialog.Portal>
|
||
|
|
</Dialog.Root>
|
||
|
|
|
||
|
|
<ShadcnDialog>
|
||
|
|
<DialogTrigger>Named dialog</DialogTrigger>
|
||
|
|
<DialogContent>
|
||
|
|
<DialogTitle>Named dialog</DialogTitle>
|
||
|
|
</DialogContent>
|
||
|
|
</ShadcnDialog>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Run export checker**
|
||
|
|
|
||
|
|
Run: `npm run check:exports`
|
||
|
|
|
||
|
|
Expected: `Checked 37 component exports.`
|
||
|
|
|
||
|
|
- [ ] **Step 3: Run full check**
|
||
|
|
|
||
|
|
Run: `npm run check`
|
||
|
|
|
||
|
|
Expected: TypeScript and export checks pass.
|
||
|
|
|
||
|
|
## Task 10: README
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `README.md`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Create README**
|
||
|
|
|
||
|
|
Include:
|
||
|
|
|
||
|
|
```md
|
||
|
|
# Base UI Tailwind Components
|
||
|
|
|
||
|
|
Source-owned Base UI React wrappers styled with Tailwind CSS.
|
||
|
|
|
||
|
|
## Install Dependencies
|
||
|
|
|
||
|
|
\`\`\`bash
|
||
|
|
npm install @base-ui/react clsx tailwind-merge class-variance-authority
|
||
|
|
\`\`\`
|
||
|
|
|
||
|
|
## Import Styles
|
||
|
|
|
||
|
|
Default imports expose styled Base UI part namespaces:
|
||
|
|
|
||
|
|
\`\`\`tsx
|
||
|
|
import Dialog from "@/components/ui/dialog"
|
||
|
|
\`\`\`
|
||
|
|
|
||
|
|
Named imports expose shadcn-style components:
|
||
|
|
|
||
|
|
\`\`\`tsx
|
||
|
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
|
||
|
|
\`\`\`
|
||
|
|
|
||
|
|
Every styled part accepts \`className\`, and custom classes are merged after defaults.
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Run full check**
|
||
|
|
|
||
|
|
Run: `npm run check`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
## Self-Review Checklist
|
||
|
|
|
||
|
|
- [ ] Every component in the spec has a folder in `components/ui`.
|
||
|
|
- [ ] Every folder has `index.ts`.
|
||
|
|
- [ ] Every `index.ts` has a default export.
|
||
|
|
- [ ] Composite components split Base UI parts into separate files.
|
||
|
|
- [ ] Named exports support shadcn-style usage.
|
||
|
|
- [ ] `examples/smoke.tsx` compiles.
|
||
|
|
- [ ] README explains default and named imports.
|