Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60100e9c52 | ||
|
|
1f5631552c | ||
|
|
da5a2e6aab | ||
|
|
7b1a2051a0 | ||
|
|
1386f6e549 |
@@ -1,242 +0,0 @@
|
||||
---
|
||||
name: shadcn
|
||||
description: Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI. Provides project context, component docs, and usage examples. Applies when working with shadcn/ui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for "shadcn init", "create an app with --preset", or "switch to --preset".
|
||||
user-invocable: false
|
||||
allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
|
||||
---
|
||||
|
||||
# shadcn/ui
|
||||
|
||||
A framework for building ui, components and design systems. Components are added as source code to the user's project via the CLI.
|
||||
|
||||
> **IMPORTANT:** Run all CLI commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest` — based on the project's `packageManager`. Examples below use `npx shadcn@latest` but substitute the correct runner for the project.
|
||||
|
||||
## Current Project Context
|
||||
|
||||
```json
|
||||
!`npx shadcn@latest info --json`
|
||||
```
|
||||
|
||||
The JSON above contains the project config and installed components. Use `npx shadcn@latest docs <component>` to get documentation and example URLs for any component.
|
||||
|
||||
## Principles
|
||||
|
||||
1. **Use existing components first.** Use `npx shadcn@latest search` to check registries before writing custom UI. Check community registries too.
|
||||
2. **Compose, don't reinvent.** Settings page = Tabs + Card + form controls. Dashboard = Sidebar + Card + Chart + Table.
|
||||
3. **Use built-in variants before custom styles.** `variant="outline"`, `size="sm"`, etc.
|
||||
4. **Use semantic colors.** `bg-primary`, `text-muted-foreground` — never raw values like `bg-blue-500`.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
These rules are **always enforced**. Each links to a file with Incorrect/Correct code pairs.
|
||||
|
||||
### Styling & Tailwind → [styling.md](./rules/styling.md)
|
||||
|
||||
- **`className` for layout, not styling.** Never override component colors or typography.
|
||||
- **No `space-x-*` or `space-y-*`.** Use `flex` with `gap-*`. For vertical stacks, `flex flex-col gap-*`.
|
||||
- **Use `size-*` when width and height are equal.** `size-10` not `w-10 h-10`.
|
||||
- **Use `truncate` shorthand.** Not `overflow-hidden text-ellipsis whitespace-nowrap`.
|
||||
- **No manual `dark:` color overrides.** Use semantic tokens (`bg-background`, `text-muted-foreground`).
|
||||
- **Use `cn()` for conditional classes.** Don't write manual template literal ternaries.
|
||||
- **No manual `z-index` on overlay components.** Dialog, Sheet, Popover, etc. handle their own stacking.
|
||||
|
||||
### Forms & Inputs → [forms.md](./rules/forms.md)
|
||||
|
||||
- **Forms use `FieldGroup` + `Field`.** Never use raw `div` with `space-y-*` or `grid gap-*` for form layout.
|
||||
- **`InputGroup` uses `InputGroupInput`/`InputGroupTextarea`.** Never raw `Input`/`Textarea` inside `InputGroup`.
|
||||
- **Buttons inside inputs use `InputGroup` + `InputGroupAddon`.**
|
||||
- **Option sets (2–7 choices) use `ToggleGroup`.** Don't loop `Button` with manual active state.
|
||||
- **`FieldSet` + `FieldLegend` for grouping related checkboxes/radios.** Don't use a `div` with a heading.
|
||||
- **Field validation uses `data-invalid` + `aria-invalid`.** `data-invalid` on `Field`, `aria-invalid` on the control. For disabled: `data-disabled` on `Field`, `disabled` on the control.
|
||||
|
||||
### Component Structure → [composition.md](./rules/composition.md)
|
||||
|
||||
- **Items always inside their Group.** `SelectItem` → `SelectGroup`. `DropdownMenuItem` → `DropdownMenuGroup`. `CommandItem` → `CommandGroup`.
|
||||
- **Use `asChild` (radix) or `render` (base) for custom triggers.** Check `base` field from `npx shadcn@latest info`. → [base-vs-radix.md](./rules/base-vs-radix.md)
|
||||
- **Dialog, Sheet, and Drawer always need a Title.** `DialogTitle`, `SheetTitle`, `DrawerTitle` required for accessibility. Use `className="sr-only"` if visually hidden.
|
||||
- **Use full Card composition.** `CardHeader`/`CardTitle`/`CardDescription`/`CardContent`/`CardFooter`. Don't dump everything in `CardContent`.
|
||||
- **Button has no `isPending`/`isLoading`.** Compose with `Spinner` + `data-icon` + `disabled`.
|
||||
- **`TabsTrigger` must be inside `TabsList`.** Never render triggers directly in `Tabs`.
|
||||
- **`Avatar` always needs `AvatarFallback`.** For when the image fails to load.
|
||||
|
||||
### Use Components, Not Custom Markup → [composition.md](./rules/composition.md)
|
||||
|
||||
- **Use existing components before custom markup.** Check if a component exists before writing a styled `div`.
|
||||
- **Callouts use `Alert`.** Don't build custom styled divs.
|
||||
- **Empty states use `Empty`.** Don't build custom empty state markup.
|
||||
- **Toast via `sonner`.** Use `toast()` from `sonner`.
|
||||
- **Use `Separator`** instead of `<hr>` or `<div className="border-t">`.
|
||||
- **Use `Skeleton`** for loading placeholders. No custom `animate-pulse` divs.
|
||||
- **Use `Badge`** instead of custom styled spans.
|
||||
|
||||
### Icons → [icons.md](./rules/icons.md)
|
||||
|
||||
- **Icons in `Button` use `data-icon`.** `data-icon="inline-start"` or `data-icon="inline-end"` on the icon.
|
||||
- **No sizing classes on icons inside components.** Components handle icon sizing via CSS. No `size-4` or `w-4 h-4`.
|
||||
- **Pass icons as objects, not string keys.** `icon={CheckIcon}`, not a string lookup.
|
||||
|
||||
### CLI
|
||||
|
||||
- **Never decode or fetch preset codes manually.** Pass them directly to `npx shadcn@latest init --preset <code>`.
|
||||
|
||||
## Key Patterns
|
||||
|
||||
These are the most common patterns that differentiate correct shadcn/ui code. For edge cases, see the linked rule files above.
|
||||
|
||||
```tsx
|
||||
// Form layout: FieldGroup + Field, not div + Label.
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input id="email" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
// Validation: data-invalid on Field, aria-invalid on the control.
|
||||
<Field data-invalid>
|
||||
<FieldLabel>Email</FieldLabel>
|
||||
<Input aria-invalid />
|
||||
<FieldDescription>Invalid email.</FieldDescription>
|
||||
</Field>
|
||||
|
||||
// Icons in buttons: data-icon, no sizing classes.
|
||||
<Button>
|
||||
<SearchIcon data-icon="inline-start" />
|
||||
Search
|
||||
</Button>
|
||||
|
||||
// Spacing: gap-*, not space-y-*.
|
||||
<div className="flex flex-col gap-4"> // correct
|
||||
<div className="space-y-4"> // wrong
|
||||
|
||||
// Equal dimensions: size-*, not w-* h-*.
|
||||
<Avatar className="size-10"> // correct
|
||||
<Avatar className="w-10 h-10"> // wrong
|
||||
|
||||
// Status colors: Badge variants or semantic tokens, not raw colors.
|
||||
<Badge variant="secondary">+20.1%</Badge> // correct
|
||||
<span className="text-emerald-600">+20.1%</span> // wrong
|
||||
```
|
||||
|
||||
## Component Selection
|
||||
|
||||
| Need | Use |
|
||||
| -------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| Button/action | `Button` with appropriate variant |
|
||||
| Form inputs | `Input`, `Select`, `Combobox`, `Switch`, `Checkbox`, `RadioGroup`, `Textarea`, `InputOTP`, `Slider` |
|
||||
| Toggle between 2–5 options | `ToggleGroup` + `ToggleGroupItem` |
|
||||
| Data display | `Table`, `Card`, `Badge`, `Avatar` |
|
||||
| Navigation | `Sidebar`, `NavigationMenu`, `Breadcrumb`, `Tabs`, `Pagination` |
|
||||
| Overlays | `Dialog` (modal), `Sheet` (side panel), `Drawer` (bottom sheet), `AlertDialog` (confirmation) |
|
||||
| Feedback | `sonner` (toast), `Alert`, `Progress`, `Skeleton`, `Spinner` |
|
||||
| Command palette | `Command` inside `Dialog` |
|
||||
| Charts | `Chart` (wraps Recharts) |
|
||||
| Layout | `Card`, `Separator`, `Resizable`, `ScrollArea`, `Accordion`, `Collapsible` |
|
||||
| Empty states | `Empty` |
|
||||
| Menus | `DropdownMenu`, `ContextMenu`, `Menubar` |
|
||||
| Tooltips/info | `Tooltip`, `HoverCard`, `Popover` |
|
||||
|
||||
## Key Fields
|
||||
|
||||
The injected project context contains these key fields:
|
||||
|
||||
- **`aliases`** → use the actual alias prefix for imports (e.g. `@/`, `~/`), never hardcode.
|
||||
- **`isRSC`** → when `true`, components using `useState`, `useEffect`, event handlers, or browser APIs need `"use client"` at the top of the file. Always reference this field when advising on the directive.
|
||||
- **`tailwindVersion`** → `"v4"` uses `@theme inline` blocks; `"v3"` uses `tailwind.config.js`.
|
||||
- **`tailwindCssFile`** → the global CSS file where custom CSS variables are defined. Always edit this file, never create a new one.
|
||||
- **`style`** → component visual treatment (e.g. `nova`, `vega`).
|
||||
- **`base`** → primitive library (`radix` or `base`). Affects component APIs and available props.
|
||||
- **`iconLibrary`** → determines icon imports. Use `lucide-react` for `lucide`, `@tabler/icons-react` for `tabler`, etc. Never assume `lucide-react`.
|
||||
- **`resolvedPaths`** → exact file-system destinations for components, utils, hooks, etc.
|
||||
- **`framework`** → routing and file conventions (e.g. Next.js App Router vs Vite SPA).
|
||||
- **`packageManager`** → use this for any non-shadcn dependency installs (e.g. `pnpm add date-fns` vs `npm install date-fns`).
|
||||
|
||||
See [cli.md — `info` command](./cli.md) for the full field reference.
|
||||
|
||||
## Component Docs, Examples, and Usage
|
||||
|
||||
Run `npx shadcn@latest docs <component>` to get the URLs for a component's documentation, examples, and API reference. Fetch these URLs to get the actual content.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest docs button dialog select
|
||||
```
|
||||
|
||||
**When creating, fixing, debugging, or using a component, always run `npx shadcn@latest docs` and fetch the URLs first.** This ensures you're working with the correct API and usage patterns rather than guessing.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Get project context** — already injected above. Run `npx shadcn@latest info` again if you need to refresh.
|
||||
2. **Check installed components first** — before running `add`, always check the `components` list from project context or list the `resolvedPaths.ui` directory. Don't import components that haven't been added, and don't re-add ones already installed.
|
||||
3. **Find components** — `npx shadcn@latest search`.
|
||||
4. **Get docs and examples** — run `npx shadcn@latest docs <component>` to get URLs, then fetch them. Use `npx shadcn@latest view` to browse registry items you haven't installed. To preview changes to installed components, use `npx shadcn@latest add --diff`.
|
||||
5. **Install or update** — `npx shadcn@latest add`. When updating existing components, use `--dry-run` and `--diff` to preview changes first (see [Updating Components](#updating-components) below).
|
||||
6. **Fix imports in third-party components** — After adding components from community registries (e.g. `@bundui`, `@magicui`), check the added non-UI files for hardcoded import paths like `@/components/ui/...`. These won't match the project's actual aliases. Use `npx shadcn@latest info` to get the correct `ui` alias (e.g. `@workspace/ui/components`) and rewrite the imports accordingly. The CLI rewrites imports for its own UI files, but third-party registry components may use default paths that don't match the project.
|
||||
7. **Review added components** — After adding a component or block from any registry, **always read the added files and verify they are correct**. Check for missing sub-components (e.g. `SelectItem` without `SelectGroup`), missing imports, incorrect composition, or violations of the [Critical Rules](#critical-rules). Also replace any icon imports with the project's `iconLibrary` from the project context (e.g. if the registry item uses `lucide-react` but the project uses `hugeicons`, swap the imports and icon names accordingly). Fix all issues before moving on.
|
||||
8. **Registry must be explicit** — When the user asks to add a block or component, **do not guess the registry**. If no registry is specified (e.g. user says "add a login block" without specifying `@shadcn`, `@tailark`, etc.), ask which registry to use. Never default to a registry on behalf of the user.
|
||||
9. **Switching presets** — Ask the user first: **reinstall**, **merge**, or **skip**?
|
||||
- **Reinstall**: `npx shadcn@latest init --preset <code> --force --reinstall`. Overwrites all components.
|
||||
- **Merge**: `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to list installed components, then for each installed component use `--dry-run` and `--diff` to [smart merge](#updating-components) it individually.
|
||||
- **Skip**: `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS, leaves components as-is.
|
||||
- **Important**: Always run preset commands inside the user's project directory. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base <current-base>` explicitly — preset codes do not encode the base.
|
||||
|
||||
## Updating Components
|
||||
|
||||
When the user asks to update a component from upstream while keeping their local changes, use `--dry-run` and `--diff` to intelligently merge. **NEVER fetch raw files from GitHub manually — always use the CLI.**
|
||||
|
||||
1. Run `npx shadcn@latest add <component> --dry-run` to see all files that would be affected.
|
||||
2. For each file, run `npx shadcn@latest add <component> --diff <file>` to see what changed upstream vs local.
|
||||
3. Decide per file based on the diff:
|
||||
- No local changes → safe to overwrite.
|
||||
- Has local changes → read the local file, analyze the diff, and apply upstream updates while preserving local modifications.
|
||||
- User says "just update everything" → use `--overwrite`, but confirm first.
|
||||
4. **Never use `--overwrite` without the user's explicit approval.**
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Create a new project.
|
||||
npx shadcn@latest init --name my-app --preset base-nova
|
||||
npx shadcn@latest init --name my-app --preset a2r6bw --template vite
|
||||
|
||||
# Create a monorepo project.
|
||||
npx shadcn@latest init --name my-app --preset base-nova --monorepo
|
||||
npx shadcn@latest init --name my-app --preset base-nova --template next --monorepo
|
||||
|
||||
# Initialize existing project.
|
||||
npx shadcn@latest init --preset base-nova
|
||||
npx shadcn@latest init --defaults # shortcut: --template=next --preset=base-nova
|
||||
|
||||
# Add components.
|
||||
npx shadcn@latest add button card dialog
|
||||
npx shadcn@latest add @magicui/shimmer-button
|
||||
npx shadcn@latest add --all
|
||||
|
||||
# Preview changes before adding/updating.
|
||||
npx shadcn@latest add button --dry-run
|
||||
npx shadcn@latest add button --diff button.tsx
|
||||
npx shadcn@latest add @acme/form --view button.tsx
|
||||
|
||||
# Search registries.
|
||||
npx shadcn@latest search @shadcn -q "sidebar"
|
||||
npx shadcn@latest search @tailark -q "stats"
|
||||
|
||||
# Get component docs and example URLs.
|
||||
npx shadcn@latest docs button dialog select
|
||||
|
||||
# View registry item details (for items not yet installed).
|
||||
npx shadcn@latest view @shadcn/button
|
||||
```
|
||||
|
||||
**Named presets:** `base-nova`, `radix-nova`
|
||||
**Templates:** `next`, `vite`, `start`, `react-router`, `astro` (all support `--monorepo`) and `laravel` (not supported for monorepo)
|
||||
**Preset codes:** Base62 strings starting with `a` (e.g. `a2r6bw`), from [ui.shadcn.com](https://ui.shadcn.com).
|
||||
|
||||
## Detailed References
|
||||
|
||||
- [rules/forms.md](./rules/forms.md) — FieldGroup, Field, InputGroup, ToggleGroup, FieldSet, validation states
|
||||
- [rules/composition.md](./rules/composition.md) — Groups, overlays, Card, Tabs, Avatar, Alert, Empty, Toast, Separator, Skeleton, Badge, Button loading
|
||||
- [rules/icons.md](./rules/icons.md) — data-icon, icon sizing, passing icons as objects
|
||||
- [rules/styling.md](./rules/styling.md) — Semantic colors, variants, className, spacing, size, truncate, dark mode, cn(), z-index
|
||||
- [rules/base-vs-radix.md](./rules/base-vs-radix.md) — asChild vs render, Select, ToggleGroup, Slider, Accordion
|
||||
- [cli.md](./cli.md) — Commands, flags, presets, templates
|
||||
- [customization.md](./customization.md) — Theming, CSS variables, extending components
|
||||
@@ -1,5 +0,0 @@
|
||||
interface:
|
||||
display_name: "shadcn/ui"
|
||||
short_description: "Manages shadcn/ui components — adding, searching, fixing, debugging, styling, and composing UI."
|
||||
icon_small: "./assets/shadcn-small.png"
|
||||
icon_large: "./assets/shadcn.png"
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,257 +0,0 @@
|
||||
# shadcn CLI Reference
|
||||
|
||||
Configuration is read from `components.json`.
|
||||
|
||||
> **IMPORTANT:** Always run commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest`. Check `packageManager` from project context to choose the right one. Examples below use `npx shadcn@latest` but substitute the correct runner for the project.
|
||||
|
||||
> **IMPORTANT:** Only use the flags documented below. Do not invent or guess flags — if a flag isn't listed here, it doesn't exist. The CLI auto-detects the package manager from the project's lockfile; there is no `--package-manager` flag.
|
||||
|
||||
## Contents
|
||||
|
||||
- Commands: init, add (dry-run, smart merge), search, view, docs, info, build
|
||||
- Templates: next, vite, start, react-router, astro
|
||||
- Presets: named, code, URL formats and fields
|
||||
- Switching presets
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
### `init` — Initialize or create a project
|
||||
|
||||
```bash
|
||||
npx shadcn@latest init [components...] [options]
|
||||
```
|
||||
|
||||
Initializes shadcn/ui in an existing project or creates a new project (when `--name` is provided). Optionally installs components in the same step.
|
||||
|
||||
| Flag | Short | Description | Default |
|
||||
| ----------------------- | ----- | --------------------------------------------------------- | ------- |
|
||||
| `--template <template>` | `-t` | Template (next, start, vite, next-monorepo, react-router) | — |
|
||||
| `--preset [name]` | `-p` | Preset configuration (named, code, or URL) | — |
|
||||
| `--yes` | `-y` | Skip confirmation prompt | `true` |
|
||||
| `--defaults` | `-d` | Use defaults (`--template=next --preset=base-nova`) | `false` |
|
||||
| `--force` | `-f` | Force overwrite existing configuration | `false` |
|
||||
| `--cwd <cwd>` | `-c` | Working directory | current |
|
||||
| `--name <name>` | `-n` | Name for new project | — |
|
||||
| `--silent` | `-s` | Mute output | `false` |
|
||||
| `--rtl` | | Enable RTL support | — |
|
||||
| `--reinstall` | | Re-install existing UI components | `false` |
|
||||
| `--monorepo` | | Scaffold a monorepo project | — |
|
||||
| `--no-monorepo` | | Skip the monorepo prompt | — |
|
||||
|
||||
`npx shadcn@latest create` is an alias for `npx shadcn@latest init`.
|
||||
|
||||
### `add` — Add components
|
||||
|
||||
> **IMPORTANT:** To compare local components against upstream or to preview changes, ALWAYS use `npx shadcn@latest add <component> --dry-run`, `--diff`, or `--view`. NEVER fetch raw files from GitHub or other sources manually. The CLI handles registry resolution, file paths, and CSS diffing automatically.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add [components...] [options]
|
||||
```
|
||||
|
||||
Accepts component names, registry-prefixed names (`@magicui/shimmer-button`), URLs, or local paths.
|
||||
|
||||
| Flag | Short | Description | Default |
|
||||
| --------------- | ----- | -------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `--yes` | `-y` | Skip confirmation prompt | `false` |
|
||||
| `--overwrite` | `-o` | Overwrite existing files | `false` |
|
||||
| `--cwd <cwd>` | `-c` | Working directory | current |
|
||||
| `--all` | `-a` | Add all available components | `false` |
|
||||
| `--path <path>` | `-p` | Target path for the component | — |
|
||||
| `--silent` | `-s` | Mute output | `false` |
|
||||
| `--dry-run` | | Preview all changes without writing files | `false` |
|
||||
| `--diff [path]` | | Show diffs. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | — |
|
||||
| `--view [path]` | | Show file contents. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | — |
|
||||
|
||||
#### Dry-Run Mode
|
||||
|
||||
Use `--dry-run` to preview what `add` would do without writing any files. `--diff` and `--view` both imply `--dry-run`.
|
||||
|
||||
```bash
|
||||
# Preview all changes.
|
||||
npx shadcn@latest add button --dry-run
|
||||
|
||||
# Show diffs for all files (top 5).
|
||||
npx shadcn@latest add button --diff
|
||||
|
||||
# Show the diff for a specific file.
|
||||
npx shadcn@latest add button --diff button.tsx
|
||||
|
||||
# Show contents for all files (top 5).
|
||||
npx shadcn@latest add button --view
|
||||
|
||||
# Show the full content of a specific file.
|
||||
npx shadcn@latest add button --view button.tsx
|
||||
|
||||
# Works with URLs too.
|
||||
npx shadcn@latest add https://api.npoint.io/abc123 --dry-run
|
||||
|
||||
# CSS diffs.
|
||||
npx shadcn@latest add button --diff globals.css
|
||||
```
|
||||
|
||||
**When to use dry-run:**
|
||||
|
||||
- When the user asks "what files will this add?" or "what will this change?" — use `--dry-run`.
|
||||
- Before overwriting existing components — use `--diff` to preview the changes first.
|
||||
- When the user wants to inspect component source code without installing — use `--view`.
|
||||
- When checking what CSS changes would be made to `globals.css` — use `--diff globals.css`.
|
||||
- When the user asks to review or audit third-party registry code before installing — use `--view` to inspect the source.
|
||||
|
||||
> **`npx shadcn@latest add --dry-run` vs `npx shadcn@latest view`:** Prefer `npx shadcn@latest add --dry-run/--diff/--view` over `npx shadcn@latest view` when the user wants to preview changes to their project. `npx shadcn@latest view` only shows raw registry metadata. `npx shadcn@latest add --dry-run` shows exactly what would happen in the user's project: resolved file paths, diffs against existing files, and CSS updates. Use `npx shadcn@latest view` only when the user wants to browse registry info without a project context.
|
||||
|
||||
#### Smart Merge from Upstream
|
||||
|
||||
See [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full workflow.
|
||||
|
||||
### `search` — Search registries
|
||||
|
||||
```bash
|
||||
npx shadcn@latest search <registries...> [options]
|
||||
```
|
||||
|
||||
Fuzzy search across registries. Also aliased as `npx shadcn@latest list`. Without `-q`, lists all items.
|
||||
|
||||
| Flag | Short | Description | Default |
|
||||
| ------------------- | ----- | ---------------------- | ------- |
|
||||
| `--query <query>` | `-q` | Search query | — |
|
||||
| `--limit <number>` | `-l` | Max items per registry | `100` |
|
||||
| `--offset <number>` | `-o` | Items to skip | `0` |
|
||||
| `--cwd <cwd>` | `-c` | Working directory | current |
|
||||
|
||||
### `view` — View item details
|
||||
|
||||
```bash
|
||||
npx shadcn@latest view <items...> [options]
|
||||
```
|
||||
|
||||
Displays item info including file contents. Example: `npx shadcn@latest view @shadcn/button`.
|
||||
|
||||
### `docs` — Get component documentation URLs
|
||||
|
||||
```bash
|
||||
npx shadcn@latest docs <components...> [options]
|
||||
```
|
||||
|
||||
Outputs resolved URLs for component documentation, examples, and API references. Accepts one or more component names. Fetch the URLs to get the actual content.
|
||||
|
||||
Example output for `npx shadcn@latest docs input button`:
|
||||
|
||||
```
|
||||
base radix
|
||||
|
||||
input
|
||||
docs https://ui.shadcn.com/docs/components/radix/input
|
||||
examples https://raw.githubusercontent.com/.../examples/input-example.tsx
|
||||
|
||||
button
|
||||
docs https://ui.shadcn.com/docs/components/radix/button
|
||||
examples https://raw.githubusercontent.com/.../examples/button-example.tsx
|
||||
```
|
||||
|
||||
Some components include an `api` link to the underlying library (e.g. `cmdk` for the command component).
|
||||
|
||||
### `diff` — Check for updates
|
||||
|
||||
Do not use this command. Use `npx shadcn@latest add --diff` instead.
|
||||
|
||||
### `info` — Project information
|
||||
|
||||
```bash
|
||||
npx shadcn@latest info [options]
|
||||
```
|
||||
|
||||
Displays project info and `components.json` configuration. Run this first to discover the project's framework, aliases, Tailwind version, and resolved paths.
|
||||
|
||||
| Flag | Short | Description | Default |
|
||||
| ------------- | ----- | ----------------- | ------- |
|
||||
| `--cwd <cwd>` | `-c` | Working directory | current |
|
||||
|
||||
**Project Info fields:**
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| -------------------- | --------- | ------------------------------------------------------------------ |
|
||||
| `framework` | `string` | Detected framework (`next`, `vite`, `react-router`, `start`, etc.) |
|
||||
| `frameworkVersion` | `string` | Framework version (e.g. `15.2.4`) |
|
||||
| `isSrcDir` | `boolean` | Whether the project uses a `src/` directory |
|
||||
| `isRSC` | `boolean` | Whether React Server Components are enabled |
|
||||
| `isTsx` | `boolean` | Whether the project uses TypeScript |
|
||||
| `tailwindVersion` | `string` | `"v3"` or `"v4"` |
|
||||
| `tailwindConfigFile` | `string` | Path to the Tailwind config file |
|
||||
| `tailwindCssFile` | `string` | Path to the global CSS file |
|
||||
| `aliasPrefix` | `string` | Import alias prefix (e.g. `@`, `~`, `@/`) |
|
||||
| `packageManager` | `string` | Detected package manager (`npm`, `pnpm`, `yarn`, `bun`) |
|
||||
|
||||
**Components.json fields:**
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| -------------------- | --------- | ------------------------------------------------------------------------------------------ |
|
||||
| `base` | `string` | Primitive library (`radix` or `base`) — determines component APIs and available props |
|
||||
| `style` | `string` | Visual style (e.g. `nova`, `vega`) |
|
||||
| `rsc` | `boolean` | RSC flag from config |
|
||||
| `tsx` | `boolean` | TypeScript flag |
|
||||
| `tailwind.config` | `string` | Tailwind config path |
|
||||
| `tailwind.css` | `string` | Global CSS path — this is where custom CSS variables go |
|
||||
| `iconLibrary` | `string` | Icon library — determines icon import package (e.g. `lucide-react`, `@tabler/icons-react`) |
|
||||
| `aliases.components` | `string` | Component import alias (e.g. `@/components`) |
|
||||
| `aliases.utils` | `string` | Utils import alias (e.g. `@/lib/utils`) |
|
||||
| `aliases.ui` | `string` | UI component alias (e.g. `@/components/ui`) |
|
||||
| `aliases.lib` | `string` | Lib alias (e.g. `@/lib`) |
|
||||
| `aliases.hooks` | `string` | Hooks alias (e.g. `@/hooks`) |
|
||||
| `resolvedPaths` | `object` | Absolute file-system paths for each alias |
|
||||
| `registries` | `object` | Configured custom registries |
|
||||
|
||||
**Links fields:**
|
||||
|
||||
The `info` output includes a **Links** section with templated URLs for component docs, source, and examples. For resolved URLs, use `npx shadcn@latest docs <component>` instead.
|
||||
|
||||
### `build` — Build a custom registry
|
||||
|
||||
```bash
|
||||
npx shadcn@latest build [registry] [options]
|
||||
```
|
||||
|
||||
Builds `registry.json` into individual JSON files for distribution. Default input: `./registry.json`, default output: `./public/r`.
|
||||
|
||||
| Flag | Short | Description | Default |
|
||||
| ----------------- | ----- | ----------------- | ------------ |
|
||||
| `--output <path>` | `-o` | Output directory | `./public/r` |
|
||||
| `--cwd <cwd>` | `-c` | Working directory | current |
|
||||
|
||||
---
|
||||
|
||||
## Templates
|
||||
|
||||
| Value | Framework | Monorepo support |
|
||||
| -------------- | -------------- | ---------------- |
|
||||
| `next` | Next.js | Yes |
|
||||
| `vite` | Vite | Yes |
|
||||
| `start` | TanStack Start | Yes |
|
||||
| `react-router` | React Router | Yes |
|
||||
| `astro` | Astro | Yes |
|
||||
| `laravel` | Laravel | No |
|
||||
|
||||
All templates support monorepo scaffolding via the `--monorepo` flag. When passed, the CLI uses a monorepo-specific template directory (e.g. `next-monorepo`, `vite-monorepo`). When neither `--monorepo` nor `--no-monorepo` is passed, the CLI prompts interactively. Laravel does not support monorepo scaffolding.
|
||||
|
||||
---
|
||||
|
||||
## Presets
|
||||
|
||||
Three ways to specify a preset via `--preset`:
|
||||
|
||||
1. **Named:** `--preset base-nova` or `--preset radix-nova`
|
||||
2. **Code:** `--preset a2r6bw` (base62 string, starts with lowercase `a`)
|
||||
3. **URL:** `--preset "https://ui.shadcn.com/init?base=radix&style=nova&..."`
|
||||
|
||||
> **IMPORTANT:** Never try to decode, fetch, or resolve preset codes manually. Preset codes are opaque — pass them directly to `npx shadcn@latest init --preset <code>` and let the CLI handle resolution.
|
||||
|
||||
## Switching Presets
|
||||
|
||||
Ask the user first: **reinstall**, **merge**, or **skip** existing components?
|
||||
|
||||
- **Re-install** → `npx shadcn@latest init --preset <code> --force --reinstall`. Overwrites all component files with the new preset styles. Use when the user hasn't customized components.
|
||||
- **Merge** → `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to get the list of installed components and use the [smart merge workflow](./SKILL.md#updating-components) to update them one by one, preserving local changes. Use when the user has customized components.
|
||||
- **Skip** → `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS variables, leaves existing components as-is.
|
||||
|
||||
Always run preset commands inside the user's project directory. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base <current-base>` explicitly — preset codes do not encode the base.
|
||||
@@ -1,202 +0,0 @@
|
||||
# Customization & Theming
|
||||
|
||||
Components reference semantic CSS variable tokens. Change the variables to change every component.
|
||||
|
||||
## Contents
|
||||
|
||||
- How it works (CSS variables → Tailwind utilities → components)
|
||||
- Color variables and OKLCH format
|
||||
- Dark mode setup
|
||||
- Changing the theme (presets, CSS variables)
|
||||
- Adding custom colors (Tailwind v3 and v4)
|
||||
- Border radius
|
||||
- Customizing components (variants, className, wrappers)
|
||||
- Checking for updates
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
1. CSS variables defined in `:root` (light) and `.dark` (dark mode).
|
||||
2. Tailwind maps them to utilities: `bg-primary`, `text-muted-foreground`, etc.
|
||||
3. Components use these utilities — changing a variable changes all components that reference it.
|
||||
|
||||
---
|
||||
|
||||
## Color Variables
|
||||
|
||||
Every color follows the `name` / `name-foreground` convention. The base variable is for backgrounds, `-foreground` is for text/icons on that background.
|
||||
|
||||
| Variable | Purpose |
|
||||
| -------------------------------------------- | -------------------------------- |
|
||||
| `--background` / `--foreground` | Page background and default text |
|
||||
| `--card` / `--card-foreground` | Card surfaces |
|
||||
| `--primary` / `--primary-foreground` | Primary buttons and actions |
|
||||
| `--secondary` / `--secondary-foreground` | Secondary actions |
|
||||
| `--muted` / `--muted-foreground` | Muted/disabled states |
|
||||
| `--accent` / `--accent-foreground` | Hover and accent states |
|
||||
| `--destructive` / `--destructive-foreground` | Error and destructive actions |
|
||||
| `--border` | Default border color |
|
||||
| `--input` | Form input borders |
|
||||
| `--ring` | Focus ring color |
|
||||
| `--chart-1` through `--chart-5` | Chart/data visualization |
|
||||
| `--sidebar-*` | Sidebar-specific colors |
|
||||
| `--surface` / `--surface-foreground` | Secondary surface |
|
||||
|
||||
Colors use OKLCH: `--primary: oklch(0.205 0 0)` where values are lightness (0–1), chroma (0 = gray), and hue (0–360).
|
||||
|
||||
---
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Class-based toggle via `.dark` on the root element. In Next.js, use `next-themes`:
|
||||
|
||||
```tsx
|
||||
import { ThemeProvider } from "next-themes"
|
||||
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Changing the Theme
|
||||
|
||||
```bash
|
||||
# Apply a preset code from ui.shadcn.com.
|
||||
npx shadcn@latest init --preset a2r6bw --force
|
||||
|
||||
# Switch to a named preset.
|
||||
npx shadcn@latest init --preset radix-nova --force
|
||||
npx shadcn@latest init --reinstall # update existing components to match
|
||||
|
||||
# Use a custom theme URL.
|
||||
npx shadcn@latest init --preset "https://ui.shadcn.com/init?base=radix&style=nova&theme=blue&..." --force
|
||||
```
|
||||
|
||||
Or edit CSS variables directly in `globals.css`.
|
||||
|
||||
---
|
||||
|
||||
## Adding Custom Colors
|
||||
|
||||
Add variables to the file at `tailwindCssFile` from `npx shadcn@latest info` (typically `globals.css`). Never create a new CSS file for this.
|
||||
|
||||
```css
|
||||
/* 1. Define in the global CSS file. */
|
||||
:root {
|
||||
--warning: oklch(0.84 0.16 84);
|
||||
--warning-foreground: oklch(0.28 0.07 46);
|
||||
}
|
||||
.dark {
|
||||
--warning: oklch(0.41 0.11 46);
|
||||
--warning-foreground: oklch(0.99 0.02 95);
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* 2a. Register with Tailwind v4 (@theme inline). */
|
||||
@theme inline {
|
||||
--color-warning: var(--warning);
|
||||
--color-warning-foreground: var(--warning-foreground);
|
||||
}
|
||||
```
|
||||
|
||||
When `tailwindVersion` is `"v3"` (check via `npx shadcn@latest info`), register in `tailwind.config.js` instead:
|
||||
|
||||
```js
|
||||
// 2b. Register with Tailwind v3 (tailwind.config.js).
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
warning: "oklch(var(--warning) / <alpha-value>)",
|
||||
"warning-foreground":
|
||||
"oklch(var(--warning-foreground) / <alpha-value>)",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// 3. Use in components.
|
||||
<div className="bg-warning text-warning-foreground">Warning</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Border Radius
|
||||
|
||||
`--radius` controls border radius globally. Components derive values from it (`rounded-lg` = `var(--radius)`, `rounded-md` = `calc(var(--radius) - 2px)`).
|
||||
|
||||
---
|
||||
|
||||
## Customizing Components
|
||||
|
||||
See also: [rules/styling.md](./rules/styling.md) for Incorrect/Correct examples.
|
||||
|
||||
Prefer these approaches in order:
|
||||
|
||||
### 1. Built-in variants
|
||||
|
||||
```tsx
|
||||
<Button variant="outline" size="sm">Click</Button>
|
||||
```
|
||||
|
||||
### 2. Tailwind classes via `className`
|
||||
|
||||
```tsx
|
||||
<Card className="max-w-md mx-auto">...</Card>
|
||||
```
|
||||
|
||||
### 3. Add a new variant
|
||||
|
||||
Edit the component source to add a variant via `cva`:
|
||||
|
||||
```tsx
|
||||
// components/ui/button.tsx
|
||||
warning: "bg-warning text-warning-foreground hover:bg-warning/90",
|
||||
```
|
||||
|
||||
### 4. Wrapper components
|
||||
|
||||
Compose shadcn/ui primitives into higher-level components:
|
||||
|
||||
```tsx
|
||||
export function ConfirmDialog({ title, description, onConfirm, children }) {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onConfirm}>Confirm</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checking for Updates
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add button --diff
|
||||
```
|
||||
|
||||
To preview exactly what would change before updating, use `--dry-run` and `--diff`:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add button --dry-run # see all affected files
|
||||
npx shadcn@latest add button --diff button.tsx # see the diff for a specific file
|
||||
```
|
||||
|
||||
See [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full smart merge workflow.
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"skill_name": "shadcn",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"prompt": "I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Create a settings form component with fields for: full name, email address, and notification preferences (email, SMS, push notifications as toggle options). Add validation states for required fields.",
|
||||
"expected_output": "A React component using FieldGroup, Field, ToggleGroup, data-invalid/aria-invalid validation, gap-* spacing, and semantic colors.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Uses FieldGroup and Field components for form layout instead of raw div with space-y",
|
||||
"Uses Switch for independent on/off notification toggles (not looping Button with manual active state)",
|
||||
"Uses data-invalid on Field and aria-invalid on the input control for validation states",
|
||||
"Uses gap-* (e.g. gap-4, gap-6) instead of space-y-* or space-x-* for spacing",
|
||||
"Uses semantic color tokens (e.g. bg-background, text-muted-foreground, text-destructive) instead of raw colors like bg-red-500",
|
||||
"No manual dark: color overrides"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"prompt": "Create a dialog component for editing a user profile. It should have the user's avatar at the top, input fields for name and bio, and Save/Cancel buttons with appropriate icons. Using shadcn/ui with radix-nova preset and tabler icons.",
|
||||
"expected_output": "A React component with DialogTitle, Avatar+AvatarFallback, data-icon on icon buttons, no icon sizing classes, tabler icon imports.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Includes DialogTitle for accessibility (visible or with sr-only class)",
|
||||
"Avatar component includes AvatarFallback",
|
||||
"Icons on buttons use the data-icon attribute (data-icon=\"inline-start\" or data-icon=\"inline-end\")",
|
||||
"No sizing classes on icons inside components (no size-4, w-4, h-4, etc.)",
|
||||
"Uses tabler icons (@tabler/icons-react) instead of lucide-react",
|
||||
"Uses asChild for custom triggers (radix preset)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"prompt": "Create a dashboard component that shows 4 stat cards in a grid. Each card has a title, large number, percentage change badge, and a loading skeleton state. Using shadcn/ui with base-nova preset and lucide icons.",
|
||||
"expected_output": "A React component with full Card composition, Skeleton for loading, Badge for changes, semantic colors, gap-* spacing.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Uses full Card composition with CardHeader, CardTitle, CardContent (not dumping everything into CardContent)",
|
||||
"Uses Skeleton component for loading placeholders instead of custom animate-pulse divs",
|
||||
"Uses Badge component for percentage change instead of custom styled spans",
|
||||
"Uses semantic color tokens instead of raw color values like bg-green-500 or text-red-600",
|
||||
"Uses gap-* instead of space-y-* or space-x-* for spacing",
|
||||
"Uses size-* when width and height are equal instead of separate w-* h-*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
# shadcn MCP Server
|
||||
|
||||
The CLI includes an MCP server that lets AI assistants search, browse, view, and install components from registries.
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
shadcn mcp # start the MCP server (stdio)
|
||||
shadcn mcp init # write config for your editor
|
||||
```
|
||||
|
||||
Editor config files:
|
||||
|
||||
| Editor | Config file |
|
||||
|--------|------------|
|
||||
| Claude Code | `.mcp.json` |
|
||||
| Cursor | `.cursor/mcp.json` |
|
||||
| VS Code | `.vscode/mcp.json` |
|
||||
| OpenCode | `opencode.json` |
|
||||
| Codex | `~/.codex/config.toml` (manual) |
|
||||
|
||||
---
|
||||
|
||||
## Tools
|
||||
|
||||
> **Tip:** MCP tools handle registry operations (search, view, install). For project configuration (aliases, framework, Tailwind version), use `npx shadcn@latest info` — there is no MCP equivalent.
|
||||
|
||||
### `shadcn:get_project_registries`
|
||||
|
||||
Returns registry names from `components.json`. Errors if no `components.json` exists.
|
||||
|
||||
**Input:** none
|
||||
|
||||
### `shadcn:list_items_in_registries`
|
||||
|
||||
Lists all items from one or more registries.
|
||||
|
||||
**Input:** `registries` (string[]), `limit` (number, optional), `offset` (number, optional)
|
||||
|
||||
### `shadcn:search_items_in_registries`
|
||||
|
||||
Fuzzy search across registries.
|
||||
|
||||
**Input:** `registries` (string[]), `query` (string), `limit` (number, optional), `offset` (number, optional)
|
||||
|
||||
### `shadcn:view_items_in_registries`
|
||||
|
||||
View item details including full file contents.
|
||||
|
||||
**Input:** `items` (string[]) — e.g. `["@shadcn/button", "@shadcn/card"]`
|
||||
|
||||
### `shadcn:get_item_examples_from_registries`
|
||||
|
||||
Find usage examples and demos with source code.
|
||||
|
||||
**Input:** `registries` (string[]), `query` (string) — e.g. `"accordion-demo"`, `"button example"`
|
||||
|
||||
### `shadcn:get_add_command_for_items`
|
||||
|
||||
Returns the CLI install command.
|
||||
|
||||
**Input:** `items` (string[]) — e.g. `["@shadcn/button"]`
|
||||
|
||||
### `shadcn:get_audit_checklist`
|
||||
|
||||
Returns a checklist for verifying components (imports, deps, lint, TypeScript).
|
||||
|
||||
**Input:** none
|
||||
|
||||
---
|
||||
|
||||
## Configuring Registries
|
||||
|
||||
Registries are set in `components.json`. The `@shadcn` registry is always built-in.
|
||||
|
||||
```json
|
||||
{
|
||||
"registries": {
|
||||
"@acme": "https://acme.com/r/{name}.json",
|
||||
"@private": {
|
||||
"url": "https://private.com/r/{name}.json",
|
||||
"headers": { "Authorization": "Bearer ${MY_TOKEN}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Names must start with `@`.
|
||||
- URLs must contain `{name}`.
|
||||
- `${VAR}` references are resolved from environment variables.
|
||||
|
||||
Community registry index: `https://ui.shadcn.com/r/registries.json`
|
||||
@@ -1,306 +0,0 @@
|
||||
# Base vs Radix
|
||||
|
||||
API differences between `base` and `radix`. Check the `base` field from `npx shadcn@latest info`.
|
||||
|
||||
## Contents
|
||||
|
||||
- Composition: asChild vs render
|
||||
- Button / trigger as non-button element
|
||||
- Select (items prop, placeholder, positioning, multiple, object values)
|
||||
- ToggleGroup (type vs multiple)
|
||||
- Slider (scalar vs array)
|
||||
- Accordion (type and defaultValue)
|
||||
|
||||
---
|
||||
|
||||
## Composition: asChild (radix) vs render (base)
|
||||
|
||||
Radix uses `asChild` to replace the default element. Base uses `render`. Don't wrap triggers in extra elements.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<DialogTrigger>
|
||||
<div>
|
||||
<Button>Open</Button>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
```
|
||||
|
||||
**Correct (radix):**
|
||||
|
||||
```tsx
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open</Button>
|
||||
</DialogTrigger>
|
||||
```
|
||||
|
||||
**Correct (base):**
|
||||
|
||||
```tsx
|
||||
<DialogTrigger render={<Button />}>Open</DialogTrigger>
|
||||
```
|
||||
|
||||
This applies to all trigger and close components: `DialogTrigger`, `SheetTrigger`, `AlertDialogTrigger`, `DropdownMenuTrigger`, `PopoverTrigger`, `TooltipTrigger`, `CollapsibleTrigger`, `DialogClose`, `SheetClose`, `NavigationMenuLink`, `BreadcrumbLink`, `SidebarMenuButton`, `Badge`, `Item`.
|
||||
|
||||
---
|
||||
|
||||
## Button / trigger as non-button element (base only)
|
||||
|
||||
When `render` changes an element to a non-button (`<a>`, `<span>`), add `nativeButton={false}`.
|
||||
|
||||
**Incorrect (base):** missing `nativeButton={false}`.
|
||||
|
||||
```tsx
|
||||
<Button render={<a href="/docs" />}>Read the docs</Button>
|
||||
```
|
||||
|
||||
**Correct (base):**
|
||||
|
||||
```tsx
|
||||
<Button render={<a href="/docs" />} nativeButton={false}>
|
||||
Read the docs
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Correct (radix):**
|
||||
|
||||
```tsx
|
||||
<Button asChild>
|
||||
<a href="/docs">Read the docs</a>
|
||||
</Button>
|
||||
```
|
||||
|
||||
Same for triggers whose `render` is not a `Button`:
|
||||
|
||||
```tsx
|
||||
// base.
|
||||
<PopoverTrigger render={<InputGroupAddon />} nativeButton={false}>
|
||||
Pick date
|
||||
</PopoverTrigger>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Select
|
||||
|
||||
**items prop (base only).** Base requires an `items` prop on the root. Radix uses inline JSX only.
|
||||
|
||||
**Incorrect (base):**
|
||||
|
||||
```tsx
|
||||
<Select>
|
||||
<SelectTrigger><SelectValue placeholder="Select a fruit" /></SelectTrigger>
|
||||
</Select>
|
||||
```
|
||||
|
||||
**Correct (base):**
|
||||
|
||||
```tsx
|
||||
const items = [
|
||||
{ label: "Select a fruit", value: null },
|
||||
{ label: "Apple", value: "apple" },
|
||||
{ label: "Banana", value: "banana" },
|
||||
]
|
||||
|
||||
<Select items={items}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{items.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
```
|
||||
|
||||
**Correct (radix):**
|
||||
|
||||
```tsx
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a fruit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
```
|
||||
|
||||
**Placeholder.** Base uses a `{ value: null }` item in the items array. Radix uses `<SelectValue placeholder="...">`.
|
||||
|
||||
**Content positioning.** Base uses `alignItemWithTrigger`. Radix uses `position`.
|
||||
|
||||
```tsx
|
||||
// base.
|
||||
<SelectContent alignItemWithTrigger={false} side="bottom">
|
||||
|
||||
// radix.
|
||||
<SelectContent position="popper">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Select — multiple selection and object values (base only)
|
||||
|
||||
Base supports `multiple`, render-function children on `SelectValue`, and object values with `itemToStringValue`. Radix is single-select with string values only.
|
||||
|
||||
**Correct (base — multiple selection):**
|
||||
|
||||
```tsx
|
||||
<Select items={items} multiple defaultValue={[]}>
|
||||
<SelectTrigger>
|
||||
<SelectValue>
|
||||
{(value: string[]) => value.length === 0 ? "Select fruits" : `${value.length} selected`}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
...
|
||||
</Select>
|
||||
```
|
||||
|
||||
**Correct (base — object values):**
|
||||
|
||||
```tsx
|
||||
<Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}>
|
||||
<SelectTrigger>
|
||||
<SelectValue>{(value) => value.name}</SelectValue>
|
||||
</SelectTrigger>
|
||||
...
|
||||
</Select>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ToggleGroup
|
||||
|
||||
Base uses a `multiple` boolean prop. Radix uses `type="single"` or `type="multiple"`.
|
||||
|
||||
**Incorrect (base):**
|
||||
|
||||
```tsx
|
||||
<ToggleGroup type="single" defaultValue="daily">
|
||||
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
```
|
||||
|
||||
**Correct (base):**
|
||||
|
||||
```tsx
|
||||
// Single (no prop needed), defaultValue is always an array.
|
||||
<ToggleGroup defaultValue={["daily"]} spacing={2}>
|
||||
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
||||
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
// Multi-selection.
|
||||
<ToggleGroup multiple>
|
||||
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
|
||||
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
```
|
||||
|
||||
**Correct (radix):**
|
||||
|
||||
```tsx
|
||||
// Single, defaultValue is a string.
|
||||
<ToggleGroup type="single" defaultValue="daily" spacing={2}>
|
||||
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
||||
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
// Multi-selection.
|
||||
<ToggleGroup type="multiple">
|
||||
<ToggleGroupItem value="bold">Bold</ToggleGroupItem>
|
||||
<ToggleGroupItem value="italic">Italic</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
```
|
||||
|
||||
**Controlled single value:**
|
||||
|
||||
```tsx
|
||||
// base — wrap/unwrap arrays.
|
||||
const [value, setValue] = React.useState("normal")
|
||||
<ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}>
|
||||
|
||||
// radix — plain string.
|
||||
const [value, setValue] = React.useState("normal")
|
||||
<ToggleGroup type="single" value={value} onValueChange={setValue}>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Slider
|
||||
|
||||
Base accepts a plain number for a single thumb. Radix always requires an array.
|
||||
|
||||
**Incorrect (base):**
|
||||
|
||||
```tsx
|
||||
<Slider defaultValue={[50]} max={100} step={1} />
|
||||
```
|
||||
|
||||
**Correct (base):**
|
||||
|
||||
```tsx
|
||||
<Slider defaultValue={50} max={100} step={1} />
|
||||
```
|
||||
|
||||
**Correct (radix):**
|
||||
|
||||
```tsx
|
||||
<Slider defaultValue={[50]} max={100} step={1} />
|
||||
```
|
||||
|
||||
Both use arrays for range sliders. Controlled `onValueChange` in base may need a cast:
|
||||
|
||||
```tsx
|
||||
// base.
|
||||
const [value, setValue] = React.useState([0.3, 0.7])
|
||||
<Slider value={value} onValueChange={(v) => setValue(v as number[])} />
|
||||
|
||||
// radix.
|
||||
const [value, setValue] = React.useState([0.3, 0.7])
|
||||
<Slider value={value} onValueChange={setValue} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accordion
|
||||
|
||||
Radix requires `type="single"` or `type="multiple"` and supports `collapsible`. `defaultValue` is a string. Base uses no `type` prop, uses `multiple` boolean, and `defaultValue` is always an array.
|
||||
|
||||
**Incorrect (base):**
|
||||
|
||||
```tsx
|
||||
<Accordion type="single" collapsible defaultValue="item-1">
|
||||
<AccordionItem value="item-1">...</AccordionItem>
|
||||
</Accordion>
|
||||
```
|
||||
|
||||
**Correct (base):**
|
||||
|
||||
```tsx
|
||||
<Accordion defaultValue={["item-1"]}>
|
||||
<AccordionItem value="item-1">...</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
// Multi-select.
|
||||
<Accordion multiple defaultValue={["item-1", "item-2"]}>
|
||||
<AccordionItem value="item-1">...</AccordionItem>
|
||||
<AccordionItem value="item-2">...</AccordionItem>
|
||||
</Accordion>
|
||||
```
|
||||
|
||||
**Correct (radix):**
|
||||
|
||||
```tsx
|
||||
<Accordion type="single" collapsible defaultValue="item-1">
|
||||
<AccordionItem value="item-1">...</AccordionItem>
|
||||
</Accordion>
|
||||
```
|
||||
@@ -1,195 +0,0 @@
|
||||
# Component Composition
|
||||
|
||||
## Contents
|
||||
|
||||
- Items always inside their Group component
|
||||
- Callouts use Alert
|
||||
- Empty states use Empty component
|
||||
- Toast notifications use sonner
|
||||
- Choosing between overlay components
|
||||
- Dialog, Sheet, and Drawer always need a Title
|
||||
- Card structure
|
||||
- Button has no isPending or isLoading prop
|
||||
- TabsTrigger must be inside TabsList
|
||||
- Avatar always needs AvatarFallback
|
||||
- Use Separator instead of raw hr or border divs
|
||||
- Use Skeleton for loading placeholders
|
||||
- Use Badge instead of custom styled spans
|
||||
|
||||
---
|
||||
|
||||
## Items always inside their Group component
|
||||
|
||||
Never render items directly inside the content container.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<SelectContent>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
</SelectContent>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
```
|
||||
|
||||
This applies to all group-based components:
|
||||
|
||||
| Item | Group |
|
||||
|------|-------|
|
||||
| `SelectItem`, `SelectLabel` | `SelectGroup` |
|
||||
| `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSub` | `DropdownMenuGroup` |
|
||||
| `MenubarItem` | `MenubarGroup` |
|
||||
| `ContextMenuItem` | `ContextMenuGroup` |
|
||||
| `CommandItem` | `CommandGroup` |
|
||||
|
||||
---
|
||||
|
||||
## Callouts use Alert
|
||||
|
||||
```tsx
|
||||
<Alert>
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
<AlertDescription>Something needs attention.</AlertDescription>
|
||||
</Alert>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Empty states use Empty component
|
||||
|
||||
```tsx
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>
|
||||
<EmptyTitle>No projects yet</EmptyTitle>
|
||||
<EmptyDescription>Get started by creating a new project.</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Create Project</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Toast notifications use sonner
|
||||
|
||||
```tsx
|
||||
import { toast } from "sonner"
|
||||
|
||||
toast.success("Changes saved.")
|
||||
toast.error("Something went wrong.")
|
||||
toast("File deleted.", {
|
||||
action: { label: "Undo", onClick: () => undoDelete() },
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Choosing between overlay components
|
||||
|
||||
| Use case | Component |
|
||||
|----------|-----------|
|
||||
| Focused task that requires input | `Dialog` |
|
||||
| Destructive action confirmation | `AlertDialog` |
|
||||
| Side panel with details or filters | `Sheet` |
|
||||
| Mobile-first bottom panel | `Drawer` |
|
||||
| Quick info on hover | `HoverCard` |
|
||||
| Small contextual content on click | `Popover` |
|
||||
|
||||
---
|
||||
|
||||
## Dialog, Sheet, and Drawer always need a Title
|
||||
|
||||
`DialogTitle`, `SheetTitle`, `DrawerTitle` are required for accessibility. Use `className="sr-only"` if visually hidden.
|
||||
|
||||
```tsx
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Profile</DialogTitle>
|
||||
<DialogDescription>Update your profile.</DialogDescription>
|
||||
</DialogHeader>
|
||||
...
|
||||
</DialogContent>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Card structure
|
||||
|
||||
Use full composition — don't dump everything into `CardContent`:
|
||||
|
||||
```tsx
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Team Members</CardTitle>
|
||||
<CardDescription>Manage your team.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>...</CardContent>
|
||||
<CardFooter>
|
||||
<Button>Invite</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Button has no isPending or isLoading prop
|
||||
|
||||
Compose with `Spinner` + `data-icon` + `disabled`:
|
||||
|
||||
```tsx
|
||||
<Button disabled>
|
||||
<Spinner data-icon="inline-start" />
|
||||
Saving...
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TabsTrigger must be inside TabsList
|
||||
|
||||
Never render `TabsTrigger` directly inside `Tabs` — always wrap in `TabsList`:
|
||||
|
||||
```tsx
|
||||
<Tabs defaultValue="account">
|
||||
<TabsList>
|
||||
<TabsTrigger value="account">Account</TabsTrigger>
|
||||
<TabsTrigger value="password">Password</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="account">...</TabsContent>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Avatar always needs AvatarFallback
|
||||
|
||||
Always include `AvatarFallback` for when the image fails to load:
|
||||
|
||||
```tsx
|
||||
<Avatar>
|
||||
<AvatarImage src="/avatar.png" alt="User" />
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use existing components instead of custom markup
|
||||
|
||||
| Instead of | Use |
|
||||
|---|---|
|
||||
| `<hr>` or `<div className="border-t">` | `<Separator />` |
|
||||
| `<div className="animate-pulse">` with styled divs | `<Skeleton className="h-4 w-3/4" />` |
|
||||
| `<span className="rounded-full bg-green-100 ...">` | `<Badge variant="secondary">` |
|
||||
@@ -1,192 +0,0 @@
|
||||
# Forms & Inputs
|
||||
|
||||
## Contents
|
||||
|
||||
- Forms use FieldGroup + Field
|
||||
- InputGroup requires InputGroupInput/InputGroupTextarea
|
||||
- Buttons inside inputs use InputGroup + InputGroupAddon
|
||||
- Option sets (2–7 choices) use ToggleGroup
|
||||
- FieldSet + FieldLegend for grouping related fields
|
||||
- Field validation and disabled states
|
||||
|
||||
---
|
||||
|
||||
## Forms use FieldGroup + Field
|
||||
|
||||
Always use `FieldGroup` + `Field` — never raw `div` with `space-y-*`:
|
||||
|
||||
```tsx
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input id="email" type="email" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="password">Password</FieldLabel>
|
||||
<Input id="password" type="password" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
```
|
||||
|
||||
Use `Field orientation="horizontal"` for settings pages. Use `FieldLabel className="sr-only"` for visually hidden labels.
|
||||
|
||||
**Choosing form controls:**
|
||||
|
||||
- Simple text input → `Input`
|
||||
- Dropdown with predefined options → `Select`
|
||||
- Searchable dropdown → `Combobox`
|
||||
- Native HTML select (no JS) → `native-select`
|
||||
- Boolean toggle → `Switch` (for settings) or `Checkbox` (for forms)
|
||||
- Single choice from few options → `RadioGroup`
|
||||
- Toggle between 2–5 options → `ToggleGroup` + `ToggleGroupItem`
|
||||
- OTP/verification code → `InputOTP`
|
||||
- Multi-line text → `Textarea`
|
||||
|
||||
---
|
||||
|
||||
## InputGroup requires InputGroupInput/InputGroupTextarea
|
||||
|
||||
Never use raw `Input` or `Textarea` inside an `InputGroup`.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<InputGroup>
|
||||
<Input placeholder="Search..." />
|
||||
</InputGroup>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
import { InputGroup, InputGroupInput } from "@/components/ui/input-group"
|
||||
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Search..." />
|
||||
</InputGroup>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Buttons inside inputs use InputGroup + InputGroupAddon
|
||||
|
||||
Never place a `Button` directly inside or adjacent to an `Input` with custom positioning.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<div className="relative">
|
||||
<Input placeholder="Search..." className="pr-10" />
|
||||
<Button className="absolute right-0 top-0" size="icon">
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
import { InputGroup, InputGroupInput, InputGroupAddon } from "@/components/ui/input-group"
|
||||
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Search..." />
|
||||
<InputGroupAddon>
|
||||
<Button size="icon">
|
||||
<SearchIcon data-icon="inline-start" />
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Option sets (2–7 choices) use ToggleGroup
|
||||
|
||||
Don't manually loop `Button` components with active state.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
const [selected, setSelected] = useState("daily")
|
||||
|
||||
<div className="flex gap-2">
|
||||
{["daily", "weekly", "monthly"].map((option) => (
|
||||
<Button
|
||||
key={option}
|
||||
variant={selected === option ? "default" : "outline"}
|
||||
onClick={() => setSelected(option)}
|
||||
>
|
||||
{option}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
||||
|
||||
<ToggleGroup spacing={2}>
|
||||
<ToggleGroupItem value="daily">Daily</ToggleGroupItem>
|
||||
<ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
|
||||
<ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
```
|
||||
|
||||
Combine with `Field` for labelled toggle groups:
|
||||
|
||||
```tsx
|
||||
<Field orientation="horizontal">
|
||||
<FieldTitle id="theme-label">Theme</FieldTitle>
|
||||
<ToggleGroup aria-labelledby="theme-label" spacing={2}>
|
||||
<ToggleGroupItem value="light">Light</ToggleGroupItem>
|
||||
<ToggleGroupItem value="dark">Dark</ToggleGroupItem>
|
||||
<ToggleGroupItem value="system">System</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</Field>
|
||||
```
|
||||
|
||||
> **Note:** `defaultValue` and `type`/`multiple` props differ between base and radix. See [base-vs-radix.md](./base-vs-radix.md#togglegroup).
|
||||
|
||||
---
|
||||
|
||||
## FieldSet + FieldLegend for grouping related fields
|
||||
|
||||
Use `FieldSet` + `FieldLegend` for related checkboxes, radios, or switches — not `div` with a heading:
|
||||
|
||||
```tsx
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Preferences</FieldLegend>
|
||||
<FieldDescription>Select all that apply.</FieldDescription>
|
||||
<FieldGroup className="gap-3">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="dark" />
|
||||
<FieldLabel htmlFor="dark" className="font-normal">Dark mode</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field validation and disabled states
|
||||
|
||||
Both attributes are needed — `data-invalid`/`data-disabled` styles the field (label, description), while `aria-invalid`/`disabled` styles the control.
|
||||
|
||||
```tsx
|
||||
// Invalid.
|
||||
<Field data-invalid>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input id="email" aria-invalid />
|
||||
<FieldDescription>Invalid email address.</FieldDescription>
|
||||
</Field>
|
||||
|
||||
// Disabled.
|
||||
<Field data-disabled>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input id="email" disabled />
|
||||
</Field>
|
||||
```
|
||||
|
||||
Works for all controls: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroupItem`, `Switch`, `Slider`, `NativeSelect`, `InputOTP`.
|
||||
@@ -1,101 +0,0 @@
|
||||
# Icons
|
||||
|
||||
**Always use the project's configured `iconLibrary` for imports.** Check the `iconLibrary` field from project context: `lucide` → `lucide-react`, `tabler` → `@tabler/icons-react`, etc. Never assume `lucide-react`.
|
||||
|
||||
---
|
||||
|
||||
## Icons in Button use data-icon attribute
|
||||
|
||||
Add `data-icon="inline-start"` (prefix) or `data-icon="inline-end"` (suffix) to the icon. No sizing classes on the icon.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<Button>
|
||||
<SearchIcon className="mr-2 size-4" />
|
||||
Search
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
<Button>
|
||||
<SearchIcon data-icon="inline-start"/>
|
||||
Search
|
||||
</Button>
|
||||
|
||||
<Button>
|
||||
Next
|
||||
<ArrowRightIcon data-icon="inline-end"/>
|
||||
</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## No sizing classes on icons inside components
|
||||
|
||||
Components handle icon sizing via CSS. Don't add `size-4`, `w-4 h-4`, or other sizing classes to icons inside `Button`, `DropdownMenuItem`, `Alert`, `Sidebar*`, or other shadcn components. Unless the user explicitly asks for custom icon sizes.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<Button>
|
||||
<SearchIcon className="size-4" data-icon="inline-start" />
|
||||
Search
|
||||
</Button>
|
||||
|
||||
<DropdownMenuItem>
|
||||
<SettingsIcon className="mr-2 size-4" />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
<Button>
|
||||
<SearchIcon data-icon="inline-start" />
|
||||
Search
|
||||
</Button>
|
||||
|
||||
<DropdownMenuItem>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pass icons as component objects, not string keys
|
||||
|
||||
Use `icon={CheckIcon}`, not a string key to a lookup map.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
const iconMap = {
|
||||
check: CheckIcon,
|
||||
alert: AlertIcon,
|
||||
}
|
||||
|
||||
function StatusBadge({ icon }: { icon: string }) {
|
||||
const Icon = iconMap[icon]
|
||||
return <Icon />
|
||||
}
|
||||
|
||||
<StatusBadge icon="check" />
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
// Import from the project's configured iconLibrary (e.g. lucide-react, @tabler/icons-react).
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
function StatusBadge({ icon: Icon }: { icon: React.ComponentType }) {
|
||||
return <Icon />
|
||||
}
|
||||
|
||||
<StatusBadge icon={CheckIcon} />
|
||||
```
|
||||
@@ -1,162 +0,0 @@
|
||||
# Styling & Customization
|
||||
|
||||
See [customization.md](../customization.md) for theming, CSS variables, and adding custom colors.
|
||||
|
||||
## Contents
|
||||
|
||||
- Semantic colors
|
||||
- Built-in variants first
|
||||
- className for layout only
|
||||
- No space-x-* / space-y-*
|
||||
- Prefer size-* over w-* h-* when equal
|
||||
- Prefer truncate shorthand
|
||||
- No manual dark: color overrides
|
||||
- Use cn() for conditional classes
|
||||
- No manual z-index on overlay components
|
||||
|
||||
---
|
||||
|
||||
## Semantic colors
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<div className="bg-blue-500 text-white">
|
||||
<p className="text-gray-600">Secondary text</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
<div className="bg-primary text-primary-foreground">
|
||||
<p className="text-muted-foreground">Secondary text</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## No raw color values for status/state indicators
|
||||
|
||||
For positive, negative, or status indicators, use Badge variants, semantic tokens like `text-destructive`, or define custom CSS variables — don't reach for raw Tailwind colors.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<span className="text-emerald-600">+20.1%</span>
|
||||
<span className="text-green-500">Active</span>
|
||||
<span className="text-red-600">-3.2%</span>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
<Badge variant="secondary">+20.1%</Badge>
|
||||
<Badge>Active</Badge>
|
||||
<span className="text-destructive">-3.2%</span>
|
||||
```
|
||||
|
||||
If you need a success/positive color that doesn't exist as a semantic token, use a Badge variant or ask the user about adding a custom CSS variable to the theme (see [customization.md](../customization.md)).
|
||||
|
||||
---
|
||||
|
||||
## Built-in variants first
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<Button className="border border-input bg-transparent hover:bg-accent">
|
||||
Click me
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
<Button variant="outline">Click me</Button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## className for layout only
|
||||
|
||||
Use `className` for layout (e.g. `max-w-md`, `mx-auto`, `mt-4`), **not** for overriding component colors or typography. To change colors, use semantic tokens, built-in variants, or CSS variables.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<Card className="bg-blue-100 text-blue-900 font-bold">
|
||||
<CardContent>Dashboard</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
<Card className="max-w-md mx-auto">
|
||||
<CardContent>Dashboard</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
To customize a component's appearance, prefer these approaches in order:
|
||||
1. **Built-in variants** — `variant="outline"`, `variant="destructive"`, etc.
|
||||
2. **Semantic color tokens** — `bg-primary`, `text-muted-foreground`.
|
||||
3. **CSS variables** — define custom colors in the global CSS file (see [customization.md](../customization.md)).
|
||||
|
||||
---
|
||||
|
||||
## No space-x-* / space-y-*
|
||||
|
||||
Use `gap-*` instead. `space-y-4` → `flex flex-col gap-4`. `space-x-2` → `flex gap-2`.
|
||||
|
||||
```tsx
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input />
|
||||
<Input />
|
||||
<Button>Submit</Button>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prefer size-* over w-* h-* when equal
|
||||
|
||||
`size-10` not `w-10 h-10`. Applies to icons, avatars, skeletons, etc.
|
||||
|
||||
---
|
||||
|
||||
## Prefer truncate shorthand
|
||||
|
||||
`truncate` not `overflow-hidden text-ellipsis whitespace-nowrap`.
|
||||
|
||||
---
|
||||
|
||||
## No manual dark: color overrides
|
||||
|
||||
Use semantic tokens — they handle light/dark via CSS variables. `bg-background text-foreground` not `bg-white dark:bg-gray-950`.
|
||||
|
||||
---
|
||||
|
||||
## Use cn() for conditional classes
|
||||
|
||||
Use the `cn()` utility from the project for conditional or merged class names. Don't write manual ternaries in className strings.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
<div className={`flex items-center ${isActive ? "bg-primary text-primary-foreground" : "bg-muted"}`}>
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
<div className={cn("flex items-center", isActive ? "bg-primary text-primary-foreground" : "bg-muted")}>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## No manual z-index on overlay components
|
||||
|
||||
`Dialog`, `Sheet`, `Drawer`, `AlertDialog`, `DropdownMenu`, `Popover`, `Tooltip`, `HoverCard` handle their own stacking. Never add `z-50` or `z-[999]`.
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"frontend-design@claude-plugins-official": true,
|
||||
"typescript-lsp@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
# design-sync notes — Cloudrite
|
||||
|
||||
Repo-specific gotchas for future syncs. Read this before re-running.
|
||||
|
||||
## What this repo is
|
||||
|
||||
Not a component library — a Next.js 16 marketing site (`private: true`, no `dist/`,
|
||||
no `exports`, no `.d.ts` tree). The DS surface is `components/ui/*` (54 shadcn/ui
|
||||
primitives, new-york style) plus 7 hand-built Cloudrite page sections.
|
||||
|
||||
## The staging package (`.ds-pkg/`)
|
||||
|
||||
The converter needs a package with a built entry and a `.d.ts` tree. Three scripts
|
||||
under `.design-sync/` build one from source; run them **in this order** (this is
|
||||
`cfg.buildCmd`):
|
||||
|
||||
1. `node_modules/.bin/tsc -p .design-sync/tsconfig.dts.json` — declaration emit,
|
||||
69 `.d.ts` files. Exits 0; all three steps are chained with `&&`, so a real
|
||||
failure stops the run.
|
||||
**`../next-env.d.ts` must stay in that config's `include`.** It pulls in Next's
|
||||
JSX augmentation for styled-jsx; without it the `<style jsx>` blocks in
|
||||
`hero.tsx` and `footer.tsx` raise TS2322 and the emit exits non-zero. Those are
|
||||
not repo errors — `pnpm build` and `tsc --noEmit -p tsconfig.json` are both clean.
|
||||
2. `node .design-sync/build-css.mjs` — compiles Tailwind v4 → `.design-sync/compiled.css`.
|
||||
3. `node .design-sync/make-pkg.mjs` — writes `.ds-pkg/{package.json,index.js,index.d.ts}`,
|
||||
copies the stylesheet + fonts in, and regenerates `.design-sync/docs/*.md`.
|
||||
|
||||
`index.js` re-exports **every** symbol from source (292 exports → `window.Cloudrite`),
|
||||
while `index.d.ts` re-exports only the 61 **roots**. That split is deliberate: shadcn
|
||||
exports compound parts flat (`CardHeader`, not `Card.Header`), and the converter's
|
||||
subcomponent grouping only recognises TS-namespace compounds — so without the split
|
||||
all 292 exports would each get their own preview card. The parts are instead
|
||||
documented per-root in the generated `.design-sync/docs/<Root>.md` parts tables,
|
||||
which become the `.prompt.md` the design agent reads.
|
||||
|
||||
## Known render warns
|
||||
|
||||
- `[DTS_STYLE_SYSTEM] filtering @types/react props` — expected. Most shadcn
|
||||
primitives type their props as `React.ComponentProps<'div'>`, so the CSS-shorthand
|
||||
filter trips on React's own DOM prop bag. The emitted `<Name>Props` still carry the
|
||||
real API (variant unions, `asChild`, etc.). Not a defect.
|
||||
- `inlined npm packages: 1` in the build log is a pnpm artifact, not a real count —
|
||||
the metafile regex matches `.pnpm` first in pnpm's `node_modules/.pnpm/<pkg>@ver/...`
|
||||
layout. The bundle really does inline radix, lucide, cva, etc.
|
||||
|
||||
## Gotchas that cost a debugging cycle
|
||||
|
||||
- **`cfg.tsconfig` must use `/* */` comments only.** The converter strips `//`
|
||||
comments with a regex that also mangles a `"//"` JSON *key*, leaving unparseable
|
||||
JSON. The failure is **silent**: `tsconfigPathsPlugin` returns null, esbuild falls
|
||||
back to autodiscovering the repo's own `tsconfig.json`, and `next/link` resolves to
|
||||
the real Next runtime — which bundles the App Router and makes every preview die
|
||||
with `ReferenceError: process is not defined`.
|
||||
- **`next/link` is shimmed** to a plain anchor (`.design-sync/shims/next-link.tsx`)
|
||||
via `compilerOptions.paths`. Header, Hero and Footer import it; the real one needs
|
||||
`AppRouterContext`, which doesn't exist outside a Next app.
|
||||
- **CSS edits need a full `package-build.mjs`.** `lib/preview-rebuild.mjs` only
|
||||
recompiles `_preview/*.js`; it does not re-copy the stylesheet, so a CSS fix appears
|
||||
to do nothing until the full build runs.
|
||||
- **Never set `html { background }` in the DS stylesheet.** The card harness paints
|
||||
`body{background:#fff}` in a later inline `<style>`, so a dark `html` under a white
|
||||
`body` box paints a black band across the bottom of every card. `body` alone is
|
||||
correct: designs get the brand surface, cards keep the harness's white chrome.
|
||||
- **Fonts are self-hosted.** `next/font/google` supplies Space Grotesk + Inter at
|
||||
runtime in the app, so the repo has no font files. Latin + latin-ext woff2 subsets
|
||||
were downloaded from Google Fonts into `.design-sync/fonts/` (SIL OFL) and are
|
||||
committed. `app/globals.css` declares `--font-sans: var(--font-sans), …`, which is a
|
||||
self-reference that is invalid outside Next — `.design-sync/tailwind-entry.css`
|
||||
re-declares both families with real stacks.
|
||||
|
||||
## Preview conventions (apply to every new preview)
|
||||
|
||||
- **Wrap every story in a `Surface`** — `<div className="bg-background text-foreground rounded-lg p-6">`.
|
||||
Cloudrite is dark-only; `ghost`, `link`, `outline` and `border-border` are all
|
||||
invisible against the harness's white card body.
|
||||
- **Overlay / menu components need an open state.** Give them a `defaultOpen` story
|
||||
and set `cfg.overrides.<Name> = {"cardMode":"single","primaryStory":"Open","viewport":"WxH"}`
|
||||
— see `Select`. A closed trigger shows none of the compound parts.
|
||||
- **Animated sections need transitions zeroed.** `Hero` fades in on a 300 ms timer
|
||||
plus a 1 s transition; the capture harness only waits on fonts and images, so an
|
||||
unmodified render screenshots at ~10 % opacity. See the `still` style in
|
||||
`previews/Hero.tsx`. `Features` and `Process` use IntersectionObserver instead and
|
||||
settle on their own.
|
||||
- Use real Cloudrite copy (Auckland IT services, 021 107 7483, the four service
|
||||
lines), never `foo`/`bar`.
|
||||
- Recompile the CSS after authoring previews: Tailwind only emits utilities for
|
||||
classes it can see, and `@source './previews'` covers the preview files.
|
||||
|
||||
## Excluded from the DS, on purpose
|
||||
|
||||
- `components/chatwidget.tsx` — Chatwoot script injector. Renders no markup and
|
||||
fetches `/api/chatwoot` on mount.
|
||||
- `components/ui/toaster.tsx` — legacy `Toaster`, colliding with `ui/sonner.tsx`'s.
|
||||
Sonner wins; the site uses neither.
|
||||
- `ThemeProvider` — in the bundle (wrap-able) but given no card.
|
||||
|
||||
## Re-sync risks
|
||||
|
||||
- **`.ds-pkg/` and `.design-sync/compiled.css` are gitignored and regenerated.** A
|
||||
fresh clone must run all three `buildCmd` steps before the converter, or the entry
|
||||
and stylesheet won't exist.
|
||||
- **The scaffolding is inert to the app build, but only because of dot-prefixing.**
|
||||
`tsconfig.json` includes `**/*.ts(x)` from the repo root with only `node_modules`
|
||||
excluded; TypeScript's glob matcher skips directories beginning with `.`, which is
|
||||
the sole reason `.design-sync/previews/*.tsx` (they import `'cloudrite'`, which
|
||||
does not resolve in the app) and `.ds-pkg/types/**` don't enter the app's program.
|
||||
Verified: `pnpm build` and a cold `tsc --noEmit` both exit 0. If any of this is ever
|
||||
moved to a non-dotted path, add it to the app tsconfig's `exclude` first.
|
||||
- **`next build` is not a substitute for the staging package**, but it *does* compile
|
||||
the Tailwind stylesheet (~136 KB, hash-named under `.next/static/chunks/*.css`).
|
||||
That is smaller than `.design-sync/compiled.css` (~170 KB) because Tailwind only
|
||||
emits classes it can see and the app never references the utilities used by
|
||||
`.design-sync/previews/`. The app build supplies no library entry and no
|
||||
declarations, which is what `.ds-pkg/` exists to provide.
|
||||
- **The fonts are a point-in-time copy.** If Google Fonts revs the woff2, nothing here
|
||||
notices; the committed files keep working. Re-download only deliberately.
|
||||
- **The parts tables in `.design-sync/docs/` are generated** by `make-pkg.mjs` from the
|
||||
declaration emit — never hand-edit them; they are overwritten every run. The prop
|
||||
signature column falls back to `—` for declarations that aren't plain functions.
|
||||
- **`ROOT_OVERRIDE` in `make-pkg.mjs` is a hand-maintained exception list** (currently
|
||||
just `toast.tsx → Toast`, whose first export is the provider). A new shadcn component
|
||||
whose first export isn't the root needs an entry there.
|
||||
- Only 40 of 61 components have authored previews; the other 21 ship the floor card and
|
||||
can be authored on any later sync. `.design-sync/previews/` and the grades carry forward.
|
||||
- Playwright drives the machine's installed Google Chrome via
|
||||
`DS_CHROMIUM_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"`
|
||||
(no 200 MB chromium download). Export it before `package-validate.mjs` /
|
||||
`package-capture.mjs`, or the render check is skipped.
|
||||
|
||||
## Findings for the Cloudrite team (not sync bugs)
|
||||
|
||||
- **`--accent` is set to the brand green**, byte-identical to `--primary`
|
||||
(`oklch(0.75 0.18 145)`), in both the `:root` and `.dark` blocks of
|
||||
`app/globals.css`. shadcn's default is a low-contrast neutral used for hover and
|
||||
placeholder surfaces, so every `bg-accent` consumer is now vivid green — most
|
||||
visibly `Skeleton` (loading blocks render as solid green bars), plus the hover
|
||||
states in `dropdown-menu`, `command`, `menubar`, `navigation-menu`, `calendar`,
|
||||
`item`, `toggle`, `button` and `dialog`. The previews render this faithfully
|
||||
rather than papering over it. If it wasn't intentional, giving `--accent` its own
|
||||
muted value is a one-line change.
|
||||
- `styles/globals.css` is an unused light-theme duplicate of `app/globals.css`
|
||||
(v0 scaffolding). Nothing imports it.
|
||||
|
||||
## Gotchas found after the first NOTES pass
|
||||
|
||||
- **`package-build.mjs` wipes the whole `--out` dir**, `_screenshots/` included. Any
|
||||
review sheet captured before a full rebuild is gone; re-run `package-capture.mjs`
|
||||
for anything still awaiting a grade.
|
||||
- **Overlay content must not be given `className="relative"`.** Radix positions
|
||||
`DialogContent` with `fixed` plus `translate(-50%,-50%)`; switching it to `relative`
|
||||
keeps the translate but drops the centring anchor, so the card clips the title.
|
||||
Size the card via `cfg.overrides.<Name>.viewport` instead.
|
||||
- Radix autofocuses the first focusable in an open overlay, which screenshots as a
|
||||
green text selection. `onOpenAutoFocus={(e) => e.preventDefault()}` gives a clean
|
||||
still.
|
||||
- `DS_CHROMIUM_PATH` must be exported for `resync.mjs` too — without it the driver's
|
||||
validate stage fails `[RENDER_SKIPPED]` and skips capture on `prior_failure`.
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Compile .design-sync/tailwind-entry.css → .design-sync/compiled.css.
|
||||
//
|
||||
// The repo only depends on @tailwindcss/postcss (Next drives it through
|
||||
// postcss.config.mjs), not @tailwindcss/cli — so this runs the same plugin
|
||||
// directly rather than adding a devDependency just for the sync.
|
||||
//
|
||||
// Re-run before every package-build.mjs: Tailwind generates utilities by
|
||||
// scanning sources, and newly authored .design-sync/previews/*.tsx introduce
|
||||
// classes that aren't in the previous compile.
|
||||
//
|
||||
// node .design-sync/build-css.mjs
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import postcss from 'postcss';
|
||||
import tailwind from '@tailwindcss/postcss';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const from = resolve(HERE, 'tailwind-entry.css');
|
||||
const to = resolve(HERE, 'compiled.css');
|
||||
|
||||
const result = await postcss([tailwind()]).process(readFileSync(from, 'utf8'), { from, to });
|
||||
for (const w of result.warnings()) console.error(`! ${w.toString()}`);
|
||||
writeFileSync(to, result.css);
|
||||
console.error(`css: ${(result.css.length / 1024).toFixed(0)} KB → ${to}`);
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"projectId": "6ff3d4ca-e5fe-4093-8572-9f00a21fd4f6",
|
||||
"shape": "package",
|
||||
"pkg": "cloudrite",
|
||||
"globalName": "Cloudrite",
|
||||
"entry": ".ds-pkg/index.js",
|
||||
"buildCmd": "node_modules/.bin/tsc -p .design-sync/tsconfig.dts.json && node .design-sync/build-css.mjs && node .design-sync/make-pkg.mjs",
|
||||
"srcDir": "../components",
|
||||
"tsconfig": "../.design-sync/tsconfig.ds.json",
|
||||
"cssEntry": "./styles.css",
|
||||
"docsDir": "../.design-sync/docs",
|
||||
"overrides": {
|
||||
"Select": {
|
||||
"cardMode": "single",
|
||||
"primaryStory": "Open",
|
||||
"viewport": "560x420"
|
||||
},
|
||||
"Dialog": {
|
||||
"cardMode": "single",
|
||||
"primaryStory": "Open",
|
||||
"viewport": "720x600"
|
||||
},
|
||||
"Sheet": {
|
||||
"cardMode": "single",
|
||||
"primaryStory": "Open",
|
||||
"viewport": "820x560"
|
||||
},
|
||||
"Popover": {
|
||||
"cardMode": "single",
|
||||
"primaryStory": "Open",
|
||||
"viewport": "560x420"
|
||||
},
|
||||
"Tooltip": {
|
||||
"cardMode": "single",
|
||||
"primaryStory": "Open",
|
||||
"viewport": "480x320"
|
||||
},
|
||||
"DropdownMenu": {
|
||||
"cardMode": "single",
|
||||
"primaryStory": "Open",
|
||||
"viewport": "560x460"
|
||||
}
|
||||
},
|
||||
"readmeHeader": ".design-sync/conventions.md"
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
## Building with Cloudrite
|
||||
|
||||
Cloudrite is the component library behind cloudrite.co.nz — 54 shadcn/ui (new-york)
|
||||
primitives plus 7 ready-made page sections, styled with Tailwind v4 utilities over
|
||||
CSS custom properties.
|
||||
|
||||
### Dark is the only theme
|
||||
|
||||
`:root` carries the dark palette — there is no light mode. `styles.css` already puts
|
||||
`background-color: var(--background)` and `color: var(--foreground)` on `body`, so a
|
||||
plain page is correct by default. **Any container that paints its own background must
|
||||
also set the pair**, or foreground-coloured content vanishes:
|
||||
|
||||
```jsx
|
||||
<div className="bg-background text-foreground">…</div>
|
||||
```
|
||||
|
||||
`ghost` and `link` buttons, `border-border` hairlines and `text-muted-foreground` are
|
||||
all near-invisible on a light surface. If something renders blank, this is why.
|
||||
|
||||
### No global provider — four local exceptions
|
||||
|
||||
Components work unwrapped. These four do not:
|
||||
|
||||
- `Tooltip` — must be inside `TooltipProvider`, or it throws.
|
||||
- `Sidebar` and its parts — must be inside `SidebarProvider`.
|
||||
- `Form` — this **is** react-hook-form's `FormProvider`. Spread a `useForm()` instance
|
||||
into it (`<Form {...form}>`) and wire controls with `FormField`.
|
||||
- `Toast`/`ToastViewport` — need `ToastProvider`. Prefer `Toaster` (sonner) for new work.
|
||||
|
||||
### Compound parts are flat exports, not namespaces
|
||||
|
||||
Write `<CardHeader>`, never `<Card.Header>`. Every part is its own top-level import:
|
||||
|
||||
```jsx
|
||||
import { Card, CardHeader, CardTitle, CardContent } from 'cloudrite';
|
||||
```
|
||||
|
||||
Each component's `.prompt.md` carries a **parts table** listing its exports and their
|
||||
props — read it before composing a compound.
|
||||
|
||||
### The class vocabulary
|
||||
|
||||
Semantic Tailwind utilities backed by tokens. Use these, not raw colours:
|
||||
|
||||
| Family | Names |
|
||||
| --- | --- |
|
||||
| Surfaces | `bg-background` `bg-card` `bg-popover` `bg-muted` `bg-secondary` `bg-primary` `bg-accent` `bg-destructive` `bg-sidebar` |
|
||||
| Text | `text-foreground` `text-muted-foreground` `text-primary` `text-card-foreground` `text-primary-foreground` `text-secondary-foreground` `text-accent-foreground` `text-destructive` |
|
||||
| Lines | `border-border` `border-input` `ring-ring` |
|
||||
| Radius | `rounded-md` `rounded-lg` `rounded-xl` |
|
||||
| Type | `font-sans` (Space Grotesk — headings) `font-body` (Inter — body copy) `font-mono` |
|
||||
|
||||
Opacity modifiers are idiomatic here: `bg-primary/10`, `border-primary/50`,
|
||||
`bg-primary/20 blur-[100px]` for the brand glow.
|
||||
|
||||
**`--accent` is the brand green, identical to `--primary`** — not shadcn's usual neutral
|
||||
hover tint. So `bg-accent` is a saturated green, and `Skeleton` (`bg-accent`) is a green
|
||||
block. For a quiet hover or placeholder surface use `bg-muted` or `bg-secondary`.
|
||||
|
||||
The brand accent is `--primary: oklch(0.75 0.18 145)` on `--background: oklch(0.08 0 0)`.
|
||||
|
||||
### Where the truth is
|
||||
|
||||
- `styles.css` — the entry; `@import`s `fonts/fonts.css` and `_ds_bundle.css`.
|
||||
- `_ds_bundle.css` — every token definition and compiled utility. Grep it before
|
||||
inventing a class name.
|
||||
- `components/<group>/<Name>/<Name>.prompt.md` — per-component API and parts table.
|
||||
|
||||
### Idiomatic composition
|
||||
|
||||
Library components for the controls, these utilities for your own layout glue:
|
||||
|
||||
```jsx
|
||||
import { Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'cloudrite';
|
||||
import { Cloud } from 'lucide-react';
|
||||
|
||||
<section className="bg-background text-foreground p-8">
|
||||
<Card className="max-w-sm">
|
||||
<CardHeader>
|
||||
<div className="flex size-12 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
||||
<Cloud className="size-6" />
|
||||
</div>
|
||||
<CardTitle className="mt-4">Cloud Hosting</CardTitle>
|
||||
<CardDescription>Fast, easy hosting — proudly hosted in New Zealand.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Badge variant="secondary">NZ Hosted</Badge>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Explore hosting</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</section>
|
||||
```
|
||||
|
||||
### The `sections` group
|
||||
|
||||
`Header` `Hero` `Services` `Features` `Process` `Contact` `Footer` are complete,
|
||||
prop-less cloudrite.co.nz page sections. Compose a full marketing page from them
|
||||
directly; use the primitives for anything new. `Header` is `position: fixed` — give it a
|
||||
`relative` parent with height, or it contributes none.
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
category: display
|
||||
---
|
||||
|
||||
# Accordion
|
||||
|
||||
Source: `components/ui/accordion.tsx`.
|
||||
|
||||
`Accordion` is a compound component. Its parts are exported **flat** (`AccordionItem`, not
|
||||
`Accordion.Item`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `AccordionItem` | `React.ComponentProps<typeof AccordionPrimitive.Item>` |
|
||||
| `AccordionTrigger` | `React.ComponentProps<typeof AccordionPrimitive.Trigger>` |
|
||||
| `AccordionContent` | `React.ComponentProps<typeof AccordionPrimitive.Content>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Accordion`.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Alert
|
||||
|
||||
Source: `components/ui/alert.tsx`.
|
||||
|
||||
`Alert` is a compound component. Its parts are exported **flat** (`AlertTitle`, not
|
||||
`Alert.Title`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `AlertTitle` | `React.ComponentProps<'div'>` |
|
||||
| `AlertDescription` | `React.ComponentProps<'div'>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Alert`.
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
category: overlays
|
||||
---
|
||||
|
||||
# AlertDialog
|
||||
|
||||
Source: `components/ui/alert-dialog.tsx`.
|
||||
|
||||
`AlertDialog` is a compound component. Its parts are exported **flat** (`AlertDialogPortal`, not
|
||||
`AlertDialog.Portal`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `AlertDialogPortal` | `React.ComponentProps<typeof AlertDialogPrimitive.Portal>` |
|
||||
| `AlertDialogOverlay` | `React.ComponentProps<typeof AlertDialogPrimitive.Overlay>` |
|
||||
| `AlertDialogTrigger` | `React.ComponentProps<typeof AlertDialogPrimitive.Trigger>` |
|
||||
| `AlertDialogContent` | `React.ComponentProps<typeof AlertDialogPrimitive.Content>` |
|
||||
| `AlertDialogHeader` | `React.ComponentProps<'div'>` |
|
||||
| `AlertDialogFooter` | `React.ComponentProps<'div'>` |
|
||||
| `AlertDialogTitle` | `React.ComponentProps<typeof AlertDialogPrimitive.Title>` |
|
||||
| `AlertDialogDescription` | `React.ComponentProps<typeof AlertDialogPrimitive.Description>` |
|
||||
| `AlertDialogAction` | `React.ComponentProps<typeof AlertDialogPrimitive.Action>` |
|
||||
| `AlertDialogCancel` | `React.ComponentProps<typeof AlertDialogPrimitive.Cancel>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `AlertDialog`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: layout
|
||||
---
|
||||
|
||||
# AspectRatio
|
||||
|
||||
Source: `components/ui/aspect-ratio.tsx`.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
category: display
|
||||
---
|
||||
|
||||
# Avatar
|
||||
|
||||
Source: `components/ui/avatar.tsx`.
|
||||
|
||||
`Avatar` is a compound component. Its parts are exported **flat** (`AvatarImage`, not
|
||||
`Avatar.Image`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `AvatarImage` | `React.ComponentProps<typeof AvatarPrimitive.Image>` |
|
||||
| `AvatarFallback` | `React.ComponentProps<typeof AvatarPrimitive.Fallback>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Avatar`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Badge
|
||||
|
||||
Source: `components/ui/badge.tsx`.
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
category: navigation
|
||||
---
|
||||
|
||||
# Breadcrumb
|
||||
|
||||
Source: `components/ui/breadcrumb.tsx`.
|
||||
|
||||
`Breadcrumb` is a compound component. Its parts are exported **flat** (`BreadcrumbList`, not
|
||||
`Breadcrumb.List`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `BreadcrumbList` | `React.ComponentProps<'ol'>` |
|
||||
| `BreadcrumbItem` | `React.ComponentProps<'li'>` |
|
||||
| `BreadcrumbLink` | `React.ComponentProps<'a'> & {…}` |
|
||||
| `BreadcrumbPage` | `React.ComponentProps<'span'>` |
|
||||
| `BreadcrumbSeparator` | `React.ComponentProps<'li'>` |
|
||||
| `BreadcrumbEllipsis` | `React.ComponentProps<'span'>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Breadcrumb`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: actions
|
||||
---
|
||||
|
||||
# Button
|
||||
|
||||
Source: `components/ui/button.tsx`.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
category: actions
|
||||
---
|
||||
|
||||
# ButtonGroup
|
||||
|
||||
Source: `components/ui/button-group.tsx`.
|
||||
|
||||
`ButtonGroup` is a compound component. Its parts are exported **flat** (`ButtonGroupSeparator`, not
|
||||
`ButtonGroup.Separator`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `ButtonGroupSeparator` | `React.ComponentProps<typeof Separator>` |
|
||||
| `ButtonGroupText` | `React.ComponentProps<'div'> & {…}` |
|
||||
|
||||
All parts accept `className` and are composed as children of `ButtonGroup`.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Calendar
|
||||
|
||||
Source: `components/ui/calendar.tsx`.
|
||||
|
||||
`Calendar` is a compound component. Its parts are exported **flat** (`CalendarDayButton`, not
|
||||
`Calendar.DayButton`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `CalendarDayButton` | `React.ComponentProps<typeof DayButton>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Calendar`.
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
category: layout
|
||||
---
|
||||
|
||||
# Card
|
||||
|
||||
Source: `components/ui/card.tsx`.
|
||||
|
||||
`Card` is a compound component. Its parts are exported **flat** (`CardHeader`, not
|
||||
`Card.Header`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `CardHeader` | `React.ComponentProps<'div'>` |
|
||||
| `CardFooter` | `React.ComponentProps<'div'>` |
|
||||
| `CardTitle` | `React.ComponentProps<'div'>` |
|
||||
| `CardAction` | `React.ComponentProps<'div'>` |
|
||||
| `CardDescription` | `React.ComponentProps<'div'>` |
|
||||
| `CardContent` | `React.ComponentProps<'div'>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Card`.
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
category: display
|
||||
---
|
||||
|
||||
# Carousel
|
||||
|
||||
Source: `components/ui/carousel.tsx`.
|
||||
|
||||
`Carousel` is a compound component. Its parts are exported **flat** (`CarouselContent`, not
|
||||
`Carousel.Content`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `CarouselContent` | `React.ComponentProps<'div'>` |
|
||||
| `CarouselItem` | `React.ComponentProps<'div'>` |
|
||||
| `CarouselPrevious` | `React.ComponentProps<typeof Button>` |
|
||||
| `CarouselNext` | `React.ComponentProps<typeof Button>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Carousel`.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
category: display
|
||||
---
|
||||
|
||||
# ChartContainer
|
||||
|
||||
Source: `components/ui/chart.tsx`.
|
||||
|
||||
`ChartContainer` is a compound component. Its parts are exported **flat** (`ChartTooltip`, not
|
||||
`ChartContainer.ChartTooltip`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `ChartTooltip` | — |
|
||||
| `ChartTooltipContent` | `React.ComponentProps<typeof RechartsPrimitive.Tooltip> & React.ComponentProps<'div'> & {…}` |
|
||||
| `ChartLegend` | — |
|
||||
| `ChartLegendContent` | `React.ComponentProps<'div'> & Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {…}` |
|
||||
| `ChartStyle` | — |
|
||||
|
||||
All parts accept `className` and are composed as children of `ChartContainer`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Checkbox
|
||||
|
||||
Source: `components/ui/checkbox.tsx`.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
category: display
|
||||
---
|
||||
|
||||
# Collapsible
|
||||
|
||||
Source: `components/ui/collapsible.tsx`.
|
||||
|
||||
`Collapsible` is a compound component. Its parts are exported **flat** (`CollapsibleTrigger`, not
|
||||
`Collapsible.Trigger`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `CollapsibleTrigger` | `React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>` |
|
||||
| `CollapsibleContent` | `React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Collapsible`.
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
category: navigation
|
||||
---
|
||||
|
||||
# Command
|
||||
|
||||
Source: `components/ui/command.tsx`.
|
||||
|
||||
`Command` is a compound component. Its parts are exported **flat** (`CommandDialog`, not
|
||||
`Command.Dialog`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `CommandDialog` | `React.ComponentProps<typeof Dialog> & {…}` |
|
||||
| `CommandInput` | `React.ComponentProps<typeof CommandPrimitive.Input>` |
|
||||
| `CommandList` | `React.ComponentProps<typeof CommandPrimitive.List>` |
|
||||
| `CommandEmpty` | `React.ComponentProps<typeof CommandPrimitive.Empty>` |
|
||||
| `CommandGroup` | `React.ComponentProps<typeof CommandPrimitive.Group>` |
|
||||
| `CommandItem` | `React.ComponentProps<typeof CommandPrimitive.Item>` |
|
||||
| `CommandShortcut` | `React.ComponentProps<'span'>` |
|
||||
| `CommandSeparator` | `React.ComponentProps<typeof CommandPrimitive.Separator>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Command`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Contact
|
||||
|
||||
Source: `components/contact.tsx`.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
category: overlays
|
||||
---
|
||||
|
||||
# ContextMenu
|
||||
|
||||
Source: `components/ui/context-menu.tsx`.
|
||||
|
||||
`ContextMenu` is a compound component. Its parts are exported **flat** (`ContextMenuTrigger`, not
|
||||
`ContextMenu.Trigger`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `ContextMenuTrigger` | `React.ComponentProps<typeof ContextMenuPrimitive.Trigger>` |
|
||||
| `ContextMenuContent` | `React.ComponentProps<typeof ContextMenuPrimitive.Content>` |
|
||||
| `ContextMenuItem` | `React.ComponentProps<typeof ContextMenuPrimitive.Item> & {…}` |
|
||||
| `ContextMenuCheckboxItem` | `React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>` |
|
||||
| `ContextMenuRadioItem` | `React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>` |
|
||||
| `ContextMenuLabel` | `React.ComponentProps<typeof ContextMenuPrimitive.Label> & {…}` |
|
||||
| `ContextMenuSeparator` | `React.ComponentProps<typeof ContextMenuPrimitive.Separator>` |
|
||||
| `ContextMenuShortcut` | `React.ComponentProps<'span'>` |
|
||||
| `ContextMenuGroup` | `React.ComponentProps<typeof ContextMenuPrimitive.Group>` |
|
||||
| `ContextMenuPortal` | `React.ComponentProps<typeof ContextMenuPrimitive.Portal>` |
|
||||
| `ContextMenuSub` | `React.ComponentProps<typeof ContextMenuPrimitive.Sub>` |
|
||||
| `ContextMenuSubContent` | `React.ComponentProps<typeof ContextMenuPrimitive.SubContent>` |
|
||||
| `ContextMenuSubTrigger` | `React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {…}` |
|
||||
| `ContextMenuRadioGroup` | `React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `ContextMenu`.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
category: overlays
|
||||
---
|
||||
|
||||
# Dialog
|
||||
|
||||
Source: `components/ui/dialog.tsx`.
|
||||
|
||||
`Dialog` is a compound component. Its parts are exported **flat** (`DialogClose`, not
|
||||
`Dialog.Close`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `DialogClose` | `React.ComponentProps<typeof DialogPrimitive.Close>` |
|
||||
| `DialogContent` | `React.ComponentProps<typeof DialogPrimitive.Content> & {…}` |
|
||||
| `DialogDescription` | `React.ComponentProps<typeof DialogPrimitive.Description>` |
|
||||
| `DialogFooter` | `React.ComponentProps<'div'>` |
|
||||
| `DialogHeader` | `React.ComponentProps<'div'>` |
|
||||
| `DialogOverlay` | `React.ComponentProps<typeof DialogPrimitive.Overlay>` |
|
||||
| `DialogPortal` | `React.ComponentProps<typeof DialogPrimitive.Portal>` |
|
||||
| `DialogTitle` | `React.ComponentProps<typeof DialogPrimitive.Title>` |
|
||||
| `DialogTrigger` | `React.ComponentProps<typeof DialogPrimitive.Trigger>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Dialog`.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
category: overlays
|
||||
---
|
||||
|
||||
# Drawer
|
||||
|
||||
Source: `components/ui/drawer.tsx`.
|
||||
|
||||
`Drawer` is a compound component. Its parts are exported **flat** (`DrawerPortal`, not
|
||||
`Drawer.Portal`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `DrawerPortal` | `React.ComponentProps<typeof DrawerPrimitive.Portal>` |
|
||||
| `DrawerOverlay` | `React.ComponentProps<typeof DrawerPrimitive.Overlay>` |
|
||||
| `DrawerTrigger` | `React.ComponentProps<typeof DrawerPrimitive.Trigger>` |
|
||||
| `DrawerClose` | `React.ComponentProps<typeof DrawerPrimitive.Close>` |
|
||||
| `DrawerContent` | `React.ComponentProps<typeof DrawerPrimitive.Content>` |
|
||||
| `DrawerHeader` | `React.ComponentProps<'div'>` |
|
||||
| `DrawerFooter` | `React.ComponentProps<'div'>` |
|
||||
| `DrawerTitle` | `React.ComponentProps<typeof DrawerPrimitive.Title>` |
|
||||
| `DrawerDescription` | `React.ComponentProps<typeof DrawerPrimitive.Description>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Drawer`.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
category: overlays
|
||||
---
|
||||
|
||||
# DropdownMenu
|
||||
|
||||
Source: `components/ui/dropdown-menu.tsx`.
|
||||
|
||||
`DropdownMenu` is a compound component. Its parts are exported **flat** (`DropdownMenuPortal`, not
|
||||
`DropdownMenu.Portal`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `DropdownMenuPortal` | `React.ComponentProps<typeof DropdownMenuPrimitive.Portal>` |
|
||||
| `DropdownMenuTrigger` | `React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>` |
|
||||
| `DropdownMenuContent` | `React.ComponentProps<typeof DropdownMenuPrimitive.Content>` |
|
||||
| `DropdownMenuGroup` | `React.ComponentProps<typeof DropdownMenuPrimitive.Group>` |
|
||||
| `DropdownMenuLabel` | `React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {…}` |
|
||||
| `DropdownMenuItem` | `React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {…}` |
|
||||
| `DropdownMenuCheckboxItem` | `React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>` |
|
||||
| `DropdownMenuRadioGroup` | `React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>` |
|
||||
| `DropdownMenuRadioItem` | `React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>` |
|
||||
| `DropdownMenuSeparator` | `React.ComponentProps<typeof DropdownMenuPrimitive.Separator>` |
|
||||
| `DropdownMenuShortcut` | `React.ComponentProps<'span'>` |
|
||||
| `DropdownMenuSub` | `React.ComponentProps<typeof DropdownMenuPrimitive.Sub>` |
|
||||
| `DropdownMenuSubTrigger` | `React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {…}` |
|
||||
| `DropdownMenuSubContent` | `React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `DropdownMenu`.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
category: layout
|
||||
---
|
||||
|
||||
# Empty
|
||||
|
||||
Source: `components/ui/empty.tsx`.
|
||||
|
||||
`Empty` is a compound component. Its parts are exported **flat** (`EmptyHeader`, not
|
||||
`Empty.Header`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `EmptyHeader` | `React.ComponentProps<'div'>` |
|
||||
| `EmptyTitle` | `React.ComponentProps<'div'>` |
|
||||
| `EmptyDescription` | `React.ComponentProps<'p'>` |
|
||||
| `EmptyContent` | `React.ComponentProps<'div'>` |
|
||||
| `EmptyMedia` | `React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Empty`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Features
|
||||
|
||||
Source: `components/features.tsx`.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Field
|
||||
|
||||
Source: `components/ui/field.tsx`.
|
||||
|
||||
`Field` is a compound component. Its parts are exported **flat** (`FieldLabel`, not
|
||||
`Field.Label`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `FieldLabel` | `React.ComponentProps<typeof Label>` |
|
||||
| `FieldDescription` | `React.ComponentProps<'p'>` |
|
||||
| `FieldError` | `React.ComponentProps<'div'> & { errors?: Array<{…} | undefined>; }` |
|
||||
| `FieldGroup` | `React.ComponentProps<'div'>` |
|
||||
| `FieldLegend` | `React.ComponentProps<'legend'> & {…}` |
|
||||
| `FieldSeparator` | `React.ComponentProps<'div'> & {…}` |
|
||||
| `FieldSet` | `React.ComponentProps<'fieldset'>` |
|
||||
| `FieldContent` | `React.ComponentProps<'div'>` |
|
||||
| `FieldTitle` | `React.ComponentProps<'div'>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Field`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Footer
|
||||
|
||||
Source: `components/footer.tsx`.
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Form
|
||||
|
||||
Source: `components/ui/form.tsx`.
|
||||
|
||||
`Form` is a compound component. Its parts are exported **flat** (`FormItem`, not
|
||||
`Form.Item`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `FormItem` | `React.ComponentProps<'div'>` |
|
||||
| `FormLabel` | `React.ComponentProps<typeof LabelPrimitive.Root>` |
|
||||
| `FormControl` | `React.ComponentProps<typeof Slot>` |
|
||||
| `FormDescription` | `React.ComponentProps<'p'>` |
|
||||
| `FormMessage` | `React.ComponentProps<'p'>` |
|
||||
| `FormField` | — |
|
||||
|
||||
All parts accept `className` and are composed as children of `Form`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Header
|
||||
|
||||
Source: `components/header.tsx`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Hero
|
||||
|
||||
Source: `components/hero.tsx`.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
category: overlays
|
||||
---
|
||||
|
||||
# HoverCard
|
||||
|
||||
Source: `components/ui/hover-card.tsx`.
|
||||
|
||||
`HoverCard` is a compound component. Its parts are exported **flat** (`HoverCardTrigger`, not
|
||||
`HoverCard.Trigger`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `HoverCardTrigger` | `React.ComponentProps<typeof HoverCardPrimitive.Trigger>` |
|
||||
| `HoverCardContent` | `React.ComponentProps<typeof HoverCardPrimitive.Content>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `HoverCard`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Input
|
||||
|
||||
Source: `components/ui/input.tsx`.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# InputGroup
|
||||
|
||||
Source: `components/ui/input-group.tsx`.
|
||||
|
||||
`InputGroup` is a compound component. Its parts are exported **flat** (`InputGroupAddon`, not
|
||||
`InputGroup.Addon`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `InputGroupAddon` | `React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>` |
|
||||
| `InputGroupButton` | `Omit<React.ComponentProps<typeof Button>, 'size'> & VariantProps<typeof inputGroupButtonVariants>` |
|
||||
| `InputGroupText` | `React.ComponentProps<'span'>` |
|
||||
| `InputGroupInput` | `React.ComponentProps<'input'>` |
|
||||
| `InputGroupTextarea` | `React.ComponentProps<'textarea'>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `InputGroup`.
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# InputOTP
|
||||
|
||||
Source: `components/ui/input-otp.tsx`.
|
||||
|
||||
`InputOTP` is a compound component. Its parts are exported **flat** (`InputOTPGroup`, not
|
||||
`InputOTP.Group`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `InputOTPGroup` | `React.ComponentProps<'div'>` |
|
||||
| `InputOTPSlot` | `React.ComponentProps<'div'> & {…}` |
|
||||
| `InputOTPSeparator` | `React.ComponentProps<'div'>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `InputOTP`.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
category: layout
|
||||
---
|
||||
|
||||
# Item
|
||||
|
||||
Source: `components/ui/item.tsx`.
|
||||
|
||||
`Item` is a compound component. Its parts are exported **flat** (`ItemMedia`, not
|
||||
`Item.Media`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `ItemMedia` | `React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>` |
|
||||
| `ItemContent` | `React.ComponentProps<'div'>` |
|
||||
| `ItemActions` | `React.ComponentProps<'div'>` |
|
||||
| `ItemGroup` | `React.ComponentProps<'div'>` |
|
||||
| `ItemSeparator` | `React.ComponentProps<typeof Separator>` |
|
||||
| `ItemTitle` | `React.ComponentProps<'div'>` |
|
||||
| `ItemDescription` | `React.ComponentProps<'p'>` |
|
||||
| `ItemHeader` | `React.ComponentProps<'div'>` |
|
||||
| `ItemFooter` | `React.ComponentProps<'div'>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Item`.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
category: display
|
||||
---
|
||||
|
||||
# Kbd
|
||||
|
||||
Source: `components/ui/kbd.tsx`.
|
||||
|
||||
`Kbd` is a compound component. Its parts are exported **flat** (`KbdGroup`, not
|
||||
`Kbd.Group`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `KbdGroup` | `React.ComponentProps<'div'>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Kbd`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Label
|
||||
|
||||
Source: `components/ui/label.tsx`.
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
category: navigation
|
||||
---
|
||||
|
||||
# Menubar
|
||||
|
||||
Source: `components/ui/menubar.tsx`.
|
||||
|
||||
`Menubar` is a compound component. Its parts are exported **flat** (`MenubarPortal`, not
|
||||
`Menubar.Portal`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `MenubarPortal` | `React.ComponentProps<typeof MenubarPrimitive.Portal>` |
|
||||
| `MenubarMenu` | `React.ComponentProps<typeof MenubarPrimitive.Menu>` |
|
||||
| `MenubarTrigger` | `React.ComponentProps<typeof MenubarPrimitive.Trigger>` |
|
||||
| `MenubarContent` | `React.ComponentProps<typeof MenubarPrimitive.Content>` |
|
||||
| `MenubarGroup` | `React.ComponentProps<typeof MenubarPrimitive.Group>` |
|
||||
| `MenubarSeparator` | `React.ComponentProps<typeof MenubarPrimitive.Separator>` |
|
||||
| `MenubarLabel` | `React.ComponentProps<typeof MenubarPrimitive.Label> & {…}` |
|
||||
| `MenubarItem` | `React.ComponentProps<typeof MenubarPrimitive.Item> & {…}` |
|
||||
| `MenubarShortcut` | `React.ComponentProps<'span'>` |
|
||||
| `MenubarCheckboxItem` | `React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>` |
|
||||
| `MenubarRadioGroup` | `React.ComponentProps<typeof MenubarPrimitive.RadioGroup>` |
|
||||
| `MenubarRadioItem` | `React.ComponentProps<typeof MenubarPrimitive.RadioItem>` |
|
||||
| `MenubarSub` | `React.ComponentProps<typeof MenubarPrimitive.Sub>` |
|
||||
| `MenubarSubTrigger` | `React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {…}` |
|
||||
| `MenubarSubContent` | `React.ComponentProps<typeof MenubarPrimitive.SubContent>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Menubar`.
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
category: navigation
|
||||
---
|
||||
|
||||
# NavigationMenu
|
||||
|
||||
Source: `components/ui/navigation-menu.tsx`.
|
||||
|
||||
`NavigationMenu` is a compound component. Its parts are exported **flat** (`NavigationMenuList`, not
|
||||
`NavigationMenu.List`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `NavigationMenuList` | `React.ComponentProps<typeof NavigationMenuPrimitive.List>` |
|
||||
| `NavigationMenuItem` | `React.ComponentProps<typeof NavigationMenuPrimitive.Item>` |
|
||||
| `NavigationMenuContent` | `React.ComponentProps<typeof NavigationMenuPrimitive.Content>` |
|
||||
| `NavigationMenuTrigger` | `React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>` |
|
||||
| `NavigationMenuLink` | `React.ComponentProps<typeof NavigationMenuPrimitive.Link>` |
|
||||
| `NavigationMenuIndicator` | `React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>` |
|
||||
| `NavigationMenuViewport` | `React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `NavigationMenu`.
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
category: navigation
|
||||
---
|
||||
|
||||
# Pagination
|
||||
|
||||
Source: `components/ui/pagination.tsx`.
|
||||
|
||||
`Pagination` is a compound component. Its parts are exported **flat** (`PaginationContent`, not
|
||||
`Pagination.Content`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `PaginationContent` | `React.ComponentProps<'ul'>` |
|
||||
| `PaginationLink` | `PaginationLinkProps` |
|
||||
| `PaginationItem` | `React.ComponentProps<'li'>` |
|
||||
| `PaginationPrevious` | `React.ComponentProps<typeof PaginationLink>` |
|
||||
| `PaginationNext` | `React.ComponentProps<typeof PaginationLink>` |
|
||||
| `PaginationEllipsis` | `React.ComponentProps<'span'>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Pagination`.
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
category: overlays
|
||||
---
|
||||
|
||||
# Popover
|
||||
|
||||
Source: `components/ui/popover.tsx`.
|
||||
|
||||
`Popover` is a compound component. Its parts are exported **flat** (`PopoverTrigger`, not
|
||||
`Popover.Trigger`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `PopoverTrigger` | `React.ComponentProps<typeof PopoverPrimitive.Trigger>` |
|
||||
| `PopoverContent` | `React.ComponentProps<typeof PopoverPrimitive.Content>` |
|
||||
| `PopoverAnchor` | `React.ComponentProps<typeof PopoverPrimitive.Anchor>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Popover`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Process
|
||||
|
||||
Source: `components/process.tsx`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Progress
|
||||
|
||||
Source: `components/ui/progress.tsx`.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# RadioGroup
|
||||
|
||||
Source: `components/ui/radio-group.tsx`.
|
||||
|
||||
`RadioGroup` is a compound component. Its parts are exported **flat** (`RadioGroupItem`, not
|
||||
`RadioGroup.Item`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `RadioGroupItem` | `React.ComponentProps<typeof RadioGroupPrimitive.Item>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `RadioGroup`.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
category: layout
|
||||
---
|
||||
|
||||
# ResizablePanelGroup
|
||||
|
||||
Source: `components/ui/resizable.tsx`.
|
||||
|
||||
`ResizablePanelGroup` is a compound component. Its parts are exported **flat** (`ResizablePanel`, not
|
||||
`ResizablePanelGroup.ResizablePanel`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `ResizablePanel` | `React.ComponentProps<typeof ResizablePrimitive.Panel>` |
|
||||
| `ResizableHandle` | `React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {…}` |
|
||||
|
||||
All parts accept `className` and are composed as children of `ResizablePanelGroup`.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
category: layout
|
||||
---
|
||||
|
||||
# ScrollArea
|
||||
|
||||
Source: `components/ui/scroll-area.tsx`.
|
||||
|
||||
`ScrollArea` is a compound component. Its parts are exported **flat** (`ScrollBar`, not
|
||||
`ScrollArea.ScrollBar`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `ScrollBar` | `React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `ScrollArea`.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Select
|
||||
|
||||
Source: `components/ui/select.tsx`.
|
||||
|
||||
`Select` is a compound component. Its parts are exported **flat** (`SelectContent`, not
|
||||
`Select.Content`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `SelectContent` | `React.ComponentProps<typeof SelectPrimitive.Content>` |
|
||||
| `SelectGroup` | `React.ComponentProps<typeof SelectPrimitive.Group>` |
|
||||
| `SelectItem` | `React.ComponentProps<typeof SelectPrimitive.Item>` |
|
||||
| `SelectLabel` | `React.ComponentProps<typeof SelectPrimitive.Label>` |
|
||||
| `SelectScrollDownButton` | `React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>` |
|
||||
| `SelectScrollUpButton` | `React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>` |
|
||||
| `SelectSeparator` | `React.ComponentProps<typeof SelectPrimitive.Separator>` |
|
||||
| `SelectTrigger` | `React.ComponentProps<typeof SelectPrimitive.Trigger> & {…}` |
|
||||
| `SelectValue` | `React.ComponentProps<typeof SelectPrimitive.Value>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Select`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: layout
|
||||
---
|
||||
|
||||
# Separator
|
||||
|
||||
Source: `components/ui/separator.tsx`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Services
|
||||
|
||||
Source: `components/services.tsx`.
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
category: overlays
|
||||
---
|
||||
|
||||
# Sheet
|
||||
|
||||
Source: `components/ui/sheet.tsx`.
|
||||
|
||||
`Sheet` is a compound component. Its parts are exported **flat** (`SheetTrigger`, not
|
||||
`Sheet.Trigger`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `SheetTrigger` | `React.ComponentProps<typeof SheetPrimitive.Trigger>` |
|
||||
| `SheetClose` | `React.ComponentProps<typeof SheetPrimitive.Close>` |
|
||||
| `SheetContent` | `React.ComponentProps<typeof SheetPrimitive.Content> & {…}` |
|
||||
| `SheetHeader` | `React.ComponentProps<'div'>` |
|
||||
| `SheetFooter` | `React.ComponentProps<'div'>` |
|
||||
| `SheetTitle` | `React.ComponentProps<typeof SheetPrimitive.Title>` |
|
||||
| `SheetDescription` | `React.ComponentProps<typeof SheetPrimitive.Description>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Sheet`.
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
category: layout
|
||||
---
|
||||
|
||||
# Sidebar
|
||||
|
||||
Source: `components/ui/sidebar.tsx`.
|
||||
|
||||
`Sidebar` is a compound component. Its parts are exported **flat** (`SidebarContent`, not
|
||||
`Sidebar.Content`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `SidebarContent` | `React.ComponentProps<'div'>` |
|
||||
| `SidebarFooter` | `React.ComponentProps<'div'>` |
|
||||
| `SidebarGroup` | `React.ComponentProps<'div'>` |
|
||||
| `SidebarGroupAction` | `React.ComponentProps<'button'> & {…}` |
|
||||
| `SidebarGroupContent` | `React.ComponentProps<'div'>` |
|
||||
| `SidebarGroupLabel` | `React.ComponentProps<'div'> & {…}` |
|
||||
| `SidebarHeader` | `React.ComponentProps<'div'>` |
|
||||
| `SidebarInput` | `React.ComponentProps<typeof Input>` |
|
||||
| `SidebarInset` | `React.ComponentProps<'main'>` |
|
||||
| `SidebarMenu` | `React.ComponentProps<'ul'>` |
|
||||
| `SidebarMenuAction` | `React.ComponentProps<'button'> & {…}` |
|
||||
| `SidebarMenuBadge` | `React.ComponentProps<'div'>` |
|
||||
| `SidebarMenuButton` | `React.ComponentProps<'button'> & {…} & VariantProps<typeof sidebarMenuButtonVariants>` |
|
||||
| `SidebarMenuItem` | `React.ComponentProps<'li'>` |
|
||||
| `SidebarMenuSkeleton` | `React.ComponentProps<'div'> & {…}` |
|
||||
| `SidebarMenuSub` | `React.ComponentProps<'ul'>` |
|
||||
| `SidebarMenuSubButton` | `React.ComponentProps<'a'> & {…}` |
|
||||
| `SidebarMenuSubItem` | `React.ComponentProps<'li'>` |
|
||||
| `SidebarProvider` | `React.ComponentProps<'div'> & {…}` |
|
||||
| `SidebarRail` | `React.ComponentProps<'button'>` |
|
||||
| `SidebarSeparator` | `React.ComponentProps<typeof Separator>` |
|
||||
| `SidebarTrigger` | `React.ComponentProps<typeof Button>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Sidebar`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Skeleton
|
||||
|
||||
Source: `components/ui/skeleton.tsx`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Slider
|
||||
|
||||
Source: `components/ui/slider.tsx`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Spinner
|
||||
|
||||
Source: `components/ui/spinner.tsx`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Switch
|
||||
|
||||
Source: `components/ui/switch.tsx`.
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
category: display
|
||||
---
|
||||
|
||||
# Table
|
||||
|
||||
Source: `components/ui/table.tsx`.
|
||||
|
||||
`Table` is a compound component. Its parts are exported **flat** (`TableHeader`, not
|
||||
`Table.Header`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `TableHeader` | `React.ComponentProps<'thead'>` |
|
||||
| `TableBody` | `React.ComponentProps<'tbody'>` |
|
||||
| `TableFooter` | `React.ComponentProps<'tfoot'>` |
|
||||
| `TableHead` | `React.ComponentProps<'th'>` |
|
||||
| `TableRow` | `React.ComponentProps<'tr'>` |
|
||||
| `TableCell` | `React.ComponentProps<'td'>` |
|
||||
| `TableCaption` | `React.ComponentProps<'caption'>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Table`.
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
category: navigation
|
||||
---
|
||||
|
||||
# Tabs
|
||||
|
||||
Source: `components/ui/tabs.tsx`.
|
||||
|
||||
`Tabs` is a compound component. Its parts are exported **flat** (`TabsList`, not
|
||||
`Tabs.List`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `TabsList` | `React.ComponentProps<typeof TabsPrimitive.List>` |
|
||||
| `TabsTrigger` | `React.ComponentProps<typeof TabsPrimitive.Trigger>` |
|
||||
| `TabsContent` | `React.ComponentProps<typeof TabsPrimitive.Content>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Tabs`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Textarea
|
||||
|
||||
Source: `components/ui/textarea.tsx`.
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Toast
|
||||
|
||||
Source: `components/ui/toast.tsx`.
|
||||
|
||||
`Toast` is a compound component. Its parts are exported **flat** (`ToastProvider`, not
|
||||
`Toast.Provider`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `ToastProvider` | — |
|
||||
| `ToastViewport` | — |
|
||||
| `ToastTitle` | — |
|
||||
| `ToastDescription` | — |
|
||||
| `ToastClose` | — |
|
||||
| `ToastAction` | — |
|
||||
|
||||
All parts accept `className` and are composed as children of `Toast`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Toaster
|
||||
|
||||
Source: `components/ui/sonner.tsx`.
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
category: actions
|
||||
---
|
||||
|
||||
# Toggle
|
||||
|
||||
Source: `components/ui/toggle.tsx`.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
category: actions
|
||||
---
|
||||
|
||||
# ToggleGroup
|
||||
|
||||
Source: `components/ui/toggle-group.tsx`.
|
||||
|
||||
`ToggleGroup` is a compound component. Its parts are exported **flat** (`ToggleGroupItem`, not
|
||||
`ToggleGroup.Item`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `ToggleGroupItem` | `React.ComponentProps<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `ToggleGroup`.
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
category: overlays
|
||||
---
|
||||
|
||||
# Tooltip
|
||||
|
||||
Source: `components/ui/tooltip.tsx`.
|
||||
|
||||
`Tooltip` is a compound component. Its parts are exported **flat** (`TooltipTrigger`, not
|
||||
`Tooltip.Trigger`) and each is a separate top-level export of the bundle:
|
||||
|
||||
| Part | Props |
|
||||
| --- | --- |
|
||||
| `TooltipTrigger` | `React.ComponentProps<typeof TooltipPrimitive.Trigger>` |
|
||||
| `TooltipContent` | `React.ComponentProps<typeof TooltipPrimitive.Content>` |
|
||||
| `TooltipProvider` | `React.ComponentProps<typeof TooltipPrimitive.Provider>` |
|
||||
|
||||
All parts accept `className` and are composed as children of `Tooltip`.
|
||||
@@ -1,40 +0,0 @@
|
||||
/* Brand fonts, self-hosted for the design system.
|
||||
In the Next app these come from next/font/google at runtime; the DS bundle
|
||||
has to ship them. Latin + latin-ext subsets, variable weight axes.
|
||||
Space Grotesk & Inter are licensed under the SIL Open Font License 1.1. */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url(./inter-latin-ext.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url(./inter-latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(./space-grotesk-latin-ext.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 300 700;
|
||||
font-display: swap;
|
||||
src: url(./space-grotesk-latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,212 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Stage .ds-pkg/ — the synthetic "published package" the design-sync converter
|
||||
// consumes.
|
||||
//
|
||||
// This repo is a Next.js app, not a component library: no dist/, no exports
|
||||
// map, no .d.ts tree. The converter's synth-entry fallback would work, but it
|
||||
// leaves every <Name>Props body empty (props are resolved by ts-morph from a
|
||||
// .d.ts tree that doesn't exist here), and an empty props contract is what the
|
||||
// claude.ai/design agent would code against.
|
||||
//
|
||||
// So we give it a real one, derived entirely from the repo's own source:
|
||||
//
|
||||
// .ds-pkg/package.json name/version/module/types, so PKG_DIR + findTypesRoot
|
||||
// resolve the way they would for a published package
|
||||
// .ds-pkg/index.d.ts re-exports the tsc declaration emit (tsconfig.dts.json)
|
||||
// for the ROOT of each module only. The converter reads
|
||||
// this entry as the component list, and shadcn ships
|
||||
// ~292 flat exports (CardHeader, CardTitle, …) — every
|
||||
// one of which would otherwise become its own preview
|
||||
// card. Roots here, parts in the docs table below.
|
||||
// .ds-pkg/index.js re-exports the SOURCE .tsx — every export, parts
|
||||
// included, so window.Cloudrite carries the whole API.
|
||||
// esbuild bundles from source, so the runtime bundle is
|
||||
// the real components, never a recompiled copy
|
||||
// .ds-pkg/styles.css copy of .design-sync/compiled.css, with fonts/
|
||||
// .ds-pkg/fonts/*.woff2 alongside it. cfg.cssEntry is bounded to PKG_DIR by
|
||||
// the converter, so the stylesheet has to live inside
|
||||
// the staged package; the relative url(./fonts/…)
|
||||
// references survive the copy because both move together
|
||||
// .design-sync/docs/*.md per-root docs: the group, and the compound parts
|
||||
// table (shadcn exports parts flat — CardHeader, not
|
||||
// Card.Header — so the converter's namespace-based
|
||||
// subcomponent grouping can't see them, and without
|
||||
// this the agent never learns the parts exist)
|
||||
//
|
||||
// Run order (all three, in this order — see cfg.buildCmd):
|
||||
// node_modules/.bin/tsc -p .design-sync/tsconfig.dts.json
|
||||
// node .design-sync/build-css.mjs
|
||||
// node .design-sync/make-pkg.mjs
|
||||
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { basename, dirname, join, relative, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO = resolve(HERE, '..');
|
||||
const PKG_DIR = join(REPO, '.ds-pkg');
|
||||
const TYPES = join(PKG_DIR, 'types');
|
||||
const DOCS = join(HERE, 'docs');
|
||||
|
||||
// ── source files to expose ───────────────────────────────────────────────
|
||||
// Skipped, with reasons:
|
||||
// ui/toaster.tsx — exports `Toaster`, colliding with ui/sonner.tsx's. The
|
||||
// app ships both (v0 scaffolding); sonner is the current
|
||||
// one, so it wins and the legacy pair stays out of the DS.
|
||||
// chatwidget.tsx — a Chatwoot script injector. Renders no markup and fires a
|
||||
// fetch('/api/chatwoot') on mount; nothing to design with.
|
||||
// ui/use-mobile — hook only, no component export.
|
||||
const SKIP = new Set(['components/ui/toaster.tsx', 'components/chatwidget.tsx', 'components/ui/use-mobile.tsx']);
|
||||
|
||||
// The root component of each file is its first PascalCase export — true for
|
||||
// every shadcn primitive except toast.tsx, which lists the provider first.
|
||||
const ROOT_OVERRIDE = { 'components/ui/toast.tsx': 'Toast' };
|
||||
|
||||
// Not a design component — a context wrapper. Stays in the bundle (the agent
|
||||
// may need to wrap), but gets no card.
|
||||
const NO_CARD = new Set(['ThemeProvider']);
|
||||
|
||||
const GROUPS = {
|
||||
sections: ['Header', 'Hero', 'Services', 'Features', 'Process', 'Contact', 'Footer'],
|
||||
actions: ['Button', 'ButtonGroup', 'Toggle', 'ToggleGroup'],
|
||||
forms: ['Input', 'Textarea', 'Label', 'Checkbox', 'RadioGroup', 'Select', 'Switch', 'Slider',
|
||||
'Form', 'Field', 'InputGroup', 'InputOTP', 'Calendar'],
|
||||
layout: ['Card', 'Separator', 'AspectRatio', 'ScrollArea', 'ResizablePanelGroup', 'Sidebar', 'Item', 'Empty'],
|
||||
navigation: ['Breadcrumb', 'NavigationMenu', 'Menubar', 'Pagination', 'Tabs', 'Command'],
|
||||
overlays: ['Dialog', 'AlertDialog', 'Sheet', 'Drawer', 'Popover', 'HoverCard', 'Tooltip',
|
||||
'DropdownMenu', 'ContextMenu'],
|
||||
feedback: ['Alert', 'Badge', 'Progress', 'Skeleton', 'Spinner', 'Toast', 'Toaster'],
|
||||
display: ['Table', 'Avatar', 'Accordion', 'Collapsible', 'Carousel', 'ChartContainer', 'Kbd'],
|
||||
};
|
||||
const groupOf = (name) =>
|
||||
Object.entries(GROUPS).find(([, names]) => names.includes(name))?.[0] ?? 'misc';
|
||||
|
||||
// ── collect exports per source file, in declaration order ────────────────
|
||||
function walk(dir, test, out = []) {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isDirectory()) walk(p, test, out);
|
||||
else if (test(e.name)) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Ordered PascalCase exports of a .d.ts: `export { A, B }` lists win (they
|
||||
// carry the file's own ordering), else `export declare` order.
|
||||
function exportsOf(dtsPath) {
|
||||
const s = readFileSync(dtsPath, 'utf8');
|
||||
const names = [];
|
||||
const push = (n) => { if (/^[A-Z]/.test(n) && !names.includes(n)) names.push(n); };
|
||||
const lists = [...s.matchAll(/export\s*\{([^}]*)\}/g)];
|
||||
if (lists.length) {
|
||||
for (const m of lists) {
|
||||
for (const raw of m[1].split(',')) {
|
||||
const n = raw.trim();
|
||||
if (!n) continue;
|
||||
const as = n.split(/\s+as\s+/);
|
||||
push((as[1] ?? as[0]).trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const m of s.matchAll(/export\s+declare\s+(?:const|function|class)\s+([A-Za-z0-9_$]+)/g)) push(m[1]);
|
||||
return names;
|
||||
}
|
||||
|
||||
// Prop signature of a part, for the docs table. Falls back to '—' when the
|
||||
// declaration isn't a plain function (forwardRef consts etc.).
|
||||
function propSigOf(dtsPath, name) {
|
||||
const s = readFileSync(dtsPath, 'utf8');
|
||||
const re = new RegExp(`declare (?:function|const) ${name}\\b([\\s\\S]*?)(?=\\ndeclare |\\nexport |$)`);
|
||||
const m = re.exec(s);
|
||||
if (!m) return null;
|
||||
const props = /:\s*([^)]*?)\)\s*:/.exec(m[1].replace(/\{[^{}]*\}/g, '{…}'));
|
||||
return props ? props[1].replace(/\s+/g, ' ').trim() : null;
|
||||
}
|
||||
|
||||
const srcFiles = walk(join(REPO, 'components'), (n) => /\.tsx$/.test(n))
|
||||
.map((p) => relative(REPO, p).split('\\').join('/'))
|
||||
.filter((p) => !SKIP.has(p));
|
||||
|
||||
const units = [];
|
||||
for (const src of srcFiles) {
|
||||
const dts = join(TYPES, src.replace(/\.tsx$/, '.d.ts'));
|
||||
if (!existsSync(dts)) { console.error(`! no declaration for ${src} — skipped`); continue; }
|
||||
const names = exportsOf(dts);
|
||||
if (!names.length) continue;
|
||||
const root = ROOT_OVERRIDE[src] ?? names[0];
|
||||
units.push({ src, dts, names, root, parts: names.filter((n) => n !== root) });
|
||||
}
|
||||
|
||||
// ── emit the package ─────────────────────────────────────────────────────
|
||||
mkdirSync(PKG_DIR, { recursive: true });
|
||||
const appPkg = JSON.parse(readFileSync(join(REPO, 'package.json'), 'utf8'));
|
||||
writeFileSync(join(PKG_DIR, 'package.json'), JSON.stringify({
|
||||
name: 'cloudrite',
|
||||
version: appPkg.version ?? '0.1.0',
|
||||
private: true,
|
||||
type: 'module',
|
||||
module: 'index.js',
|
||||
main: 'index.js',
|
||||
types: 'index.d.ts',
|
||||
}, null, 2) + '\n');
|
||||
|
||||
// Stylesheet + fonts move into the package together so url(./fonts/…) still
|
||||
// resolves. build-css.mjs must have run first.
|
||||
const compiled = join(HERE, 'compiled.css');
|
||||
if (!existsSync(compiled)) {
|
||||
console.error('! .design-sync/compiled.css missing — run node .design-sync/build-css.mjs first');
|
||||
process.exit(1);
|
||||
}
|
||||
copyFileSync(compiled, join(PKG_DIR, 'styles.css'));
|
||||
mkdirSync(join(PKG_DIR, 'fonts'), { recursive: true });
|
||||
for (const f of readdirSync(join(HERE, 'fonts')).filter((f) => /\.(woff2?|ttf|otf)$/.test(f))) {
|
||||
copyFileSync(join(HERE, 'fonts', f), join(PKG_DIR, 'fonts', f));
|
||||
}
|
||||
|
||||
const banner = '// Generated by .design-sync/make-pkg.mjs — do not edit.\n';
|
||||
writeFileSync(join(PKG_DIR, 'index.js'),
|
||||
banner + units.map((u) => `export * from '../${u.src.replace(/\.tsx$/, '')}';`).join('\n') + '\n');
|
||||
writeFileSync(join(PKG_DIR, 'index.d.ts'),
|
||||
banner + units
|
||||
.filter((u) => !NO_CARD.has(u.root))
|
||||
.map((u) => `export { ${u.root} } from './types/${u.src.replace(/\.tsx$/, '')}';`)
|
||||
.join('\n') + '\n');
|
||||
|
||||
// ── per-root docs ────────────────────────────────────────────────────────
|
||||
rmSync(DOCS, { recursive: true, force: true });
|
||||
mkdirSync(DOCS, { recursive: true });
|
||||
let withParts = 0;
|
||||
for (const u of units) {
|
||||
if (NO_CARD.has(u.root)) continue;
|
||||
const lines = [
|
||||
'---',
|
||||
`category: ${groupOf(u.root)}`,
|
||||
'---',
|
||||
'',
|
||||
`# ${u.root}`,
|
||||
'',
|
||||
`Source: \`${u.src}\`.`,
|
||||
'',
|
||||
];
|
||||
if (u.parts.length) {
|
||||
withParts++;
|
||||
lines.push(
|
||||
`\`${u.root}\` is a compound component. Its parts are exported **flat** (\`${u.parts[0]}\`, not`,
|
||||
`\`${u.root}.${u.parts[0].startsWith(u.root) ? u.parts[0].slice(u.root.length) : u.parts[0]}\`) and each is a separate top-level export of the bundle:`,
|
||||
'',
|
||||
'| Part | Props |',
|
||||
'| --- | --- |',
|
||||
);
|
||||
for (const p of u.parts) {
|
||||
const sig = propSigOf(u.dts, p);
|
||||
lines.push(`| \`${p}\` | ${sig ? `\`${sig}\`` : '—'} |`);
|
||||
}
|
||||
lines.push('', `All parts accept \`className\` and are composed as children of \`${u.root}\`.`, '');
|
||||
}
|
||||
writeFileSync(join(DOCS, `${u.root}.md`), lines.join('\n'));
|
||||
}
|
||||
|
||||
const roots = units.filter((u) => !NO_CARD.has(u.root)).map((u) => u.root);
|
||||
console.error(`.ds-pkg: ${units.length} modules, ${units.reduce((n, u) => n + u.names.length, 0)} exports`);
|
||||
console.error(`docs: ${roots.length} roots (${withParts} compound) → ${relative(REPO, DOCS)}`);
|
||||
console.error(`roots: ${roots.join(' ')}`);
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from 'cloudrite';
|
||||
|
||||
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
|
||||
// invisible on the preview harness's white card body. Stories render on the
|
||||
// brand surface, which is also how a real screen is built.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
const faqs = [
|
||||
['hosting', 'Where are your servers?', 'All Cloudrite hosting runs on infrastructure in New Zealand, so your visitors get low-latency responses and your data stays onshore.'],
|
||||
['support', 'Do you offer after-hours support?', 'Standard support is by appointment. Emergency support is available 24/7 for hosting and business-critical outages.'],
|
||||
['migration', 'Can you move my existing site?', "Yes — WordPress, Squarespace, Wix and Shopify migrations are included free when you move hosting to us."],
|
||||
];
|
||||
|
||||
export const Faq = () => (
|
||||
<Surface>
|
||||
<Accordion type="single" collapsible defaultValue="hosting" className="w-96">
|
||||
{faqs.map(([value, q, a]) => (
|
||||
<AccordionItem key={value} value={value}>
|
||||
<AccordionTrigger>{q}</AccordionTrigger>
|
||||
<AccordionContent>{a}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Multiple = () => (
|
||||
<Surface>
|
||||
<Accordion type="multiple" defaultValue={['hosting', 'support']} className="w-96">
|
||||
{faqs.map(([value, q, a]) => (
|
||||
<AccordionItem key={value} value={value}>
|
||||
<AccordionTrigger>{q}</AccordionTrigger>
|
||||
<AccordionContent>{a}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Alert, AlertDescription, AlertTitle } from 'cloudrite';
|
||||
import { CircleAlert, Info, TriangleAlert } from 'lucide-react';
|
||||
|
||||
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
|
||||
// invisible on the preview harness's white card body. Stories render on the
|
||||
// brand surface, which is also how a real screen is built.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
export const Default = () => (
|
||||
<Surface>
|
||||
<Alert className="max-w-md">
|
||||
<Info />
|
||||
<AlertTitle>Scheduled maintenance</AlertTitle>
|
||||
<AlertDescription>
|
||||
Our Auckland edge nodes will be patched on Sunday 02:00–03:00 NZST.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Destructive = () => (
|
||||
<Surface>
|
||||
<Alert variant="destructive" className="max-w-md">
|
||||
<CircleAlert />
|
||||
<AlertTitle>SSL certificate expired</AlertTitle>
|
||||
<AlertDescription>
|
||||
cloudrite.co.nz stopped serving HTTPS 2 hours ago. Renew it to restore the site.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const TitleOnly = () => (
|
||||
<Surface>
|
||||
<Alert className="max-w-md">
|
||||
<TriangleAlert />
|
||||
<AlertTitle>Backup skipped — disk almost full.</AlertTitle>
|
||||
</Alert>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,61 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from 'cloudrite';
|
||||
|
||||
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
|
||||
// invisible on the preview harness's white card body. Stories render on the
|
||||
// brand surface, which is also how a real screen is built.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
// AvatarImage points at a remote URL in real use; previews render offline, so
|
||||
// these show the fallback path — which is also the state worth documenting,
|
||||
// since it is what every avatar shows before the image resolves.
|
||||
export const Fallbacks = () => (
|
||||
<Surface>
|
||||
<div className="flex items-center gap-3">
|
||||
{['JL', 'CR', 'AM', 'TK'].map((initials) => (
|
||||
<Avatar key={initials}>
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Sizes = () => (
|
||||
<Surface>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="size-6">
|
||||
<AvatarFallback className="text-[10px]">CR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarFallback>CR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="size-12">
|
||||
<AvatarFallback>CR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="size-16">
|
||||
<AvatarFallback className="text-lg">CR</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
// size-10 with -space-x-3: at the default size-8 the overlap eats enough of each
|
||||
// circle that the initials clip.
|
||||
export const Stacked = () => (
|
||||
<Surface>
|
||||
<div className="flex -space-x-3">
|
||||
{['JL', 'CR', 'AM'].map((initials) => (
|
||||
<Avatar key={initials} className="size-10 ring-2 ring-background">
|
||||
<AvatarImage alt="" />
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
<Avatar className="size-10 ring-2 ring-background">
|
||||
<AvatarFallback className="bg-primary text-primary-foreground">+5</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Badge } from 'cloudrite';
|
||||
import { Check, CircleAlert } from 'lucide-react';
|
||||
|
||||
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
|
||||
// invisible on the preview harness's white card body. Stories render on the
|
||||
// brand surface, which is also how a real screen is built.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
export const Variants = () => (
|
||||
<Surface>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge>Default</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const InContext = () => (
|
||||
<Surface>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge>
|
||||
<Check />
|
||||
Online
|
||||
</Badge>
|
||||
<Badge variant="secondary">NZ Hosted</Badge>
|
||||
<Badge variant="destructive">
|
||||
<CircleAlert />
|
||||
Expired
|
||||
</Badge>
|
||||
<Badge variant="outline">v16.2.0</Badge>
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbEllipsis,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from 'cloudrite';
|
||||
|
||||
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
|
||||
// invisible on the preview harness's white card body. Stories render on the
|
||||
// brand surface, which is also how a real screen is built.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
export const Default = () => (
|
||||
<Surface>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Dashboard</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Hosting</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>cloudrite.co.nz</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Collapsed = () => (
|
||||
<Surface>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="#">Dashboard</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbEllipsis />
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Backups</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Button } from 'cloudrite';
|
||||
import { ArrowRight, Download, Loader2, Phone, Trash2 } from 'lucide-react';
|
||||
|
||||
// Cloudrite is a dark-only design system: `ghost`, `link` and `outline` are all
|
||||
// foreground-coloured, so they vanish on the preview harness's white card body.
|
||||
// Every story renders on the brand surface — which is also how a real screen is
|
||||
// built (see the README's conventions section).
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
export const Variants = () => (
|
||||
<Surface>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button>Get a quote</Button>
|
||||
<Button variant="secondary">Learn more</Button>
|
||||
<Button variant="outline">View services</Button>
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
<Button variant="link">Read the case study</Button>
|
||||
<Button variant="destructive">Delete site</Button>
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Sizes = () => (
|
||||
<Surface>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="default">Default</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
<Button size="icon" aria-label="Download invoice">
|
||||
<Download />
|
||||
</Button>
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const WithIcons = () => (
|
||||
<Surface>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button>
|
||||
Book a callout
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<Phone />
|
||||
021 107 7483
|
||||
</Button>
|
||||
<Button variant="destructive">
|
||||
<Trash2 />
|
||||
Remove backup
|
||||
</Button>
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const States = () => (
|
||||
<Surface>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button disabled>Disabled</Button>
|
||||
<Button variant="outline" disabled>
|
||||
Disabled outline
|
||||
</Button>
|
||||
<Button disabled>
|
||||
<Loader2 className="animate-spin" />
|
||||
Provisioning…
|
||||
</Button>
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from 'cloudrite';
|
||||
import { Copy, RefreshCw, Trash2 } from 'lucide-react';
|
||||
|
||||
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
|
||||
// invisible on the preview harness's white card body. Stories render on the
|
||||
// brand surface, which is also how a real screen is built.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
export const Horizontal = () => (
|
||||
<Surface>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">Day</Button>
|
||||
<Button variant="outline">Week</Button>
|
||||
<Button variant="outline">Month</Button>
|
||||
</ButtonGroup>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const WithIcons = () => (
|
||||
<Surface>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline" size="icon" aria-label="Refresh">
|
||||
<RefreshCw />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" aria-label="Duplicate">
|
||||
<Copy />
|
||||
</Button>
|
||||
<ButtonGroupSeparator />
|
||||
<Button variant="outline" size="icon" aria-label="Delete">
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const WithText = () => (
|
||||
<Surface>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupText>https://</ButtonGroupText>
|
||||
<Button variant="outline">cloudrite.co.nz</Button>
|
||||
</ButtonGroup>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Vertical = () => (
|
||||
<Surface>
|
||||
<ButtonGroup orientation="vertical">
|
||||
<Button variant="outline">Restart</Button>
|
||||
<Button variant="outline">Rebuild</Button>
|
||||
<Button variant="outline">Snapshot</Button>
|
||||
</ButtonGroup>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,94 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from 'cloudrite';
|
||||
import { ArrowUpRight, Cloud, TrendingUp } from 'lucide-react';
|
||||
|
||||
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
|
||||
// invisible on the preview harness's white card body. Stories render on the
|
||||
// brand surface, which is also how a real screen is built.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
export const ServiceCard = () => (
|
||||
<Surface>
|
||||
<Card className="max-w-sm">
|
||||
<CardHeader>
|
||||
<div className="flex size-12 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
||||
<Cloud className="size-6" />
|
||||
</div>
|
||||
<CardTitle className="mt-4">Cloud Hosting</CardTitle>
|
||||
<CardDescription>
|
||||
Fast, easy hosting for WordPress, game servers and more — proudly hosted here in New
|
||||
Zealand.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="flex flex-wrap gap-2">
|
||||
{['NZ Hosted', 'WordPress', 'Game Servers', '24/7 Uptime'].map((f) => (
|
||||
<li
|
||||
key={f}
|
||||
className="rounded-full border border-border px-3 py-1 text-xs text-muted-foreground"
|
||||
>
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" className="w-full">
|
||||
Explore hosting
|
||||
<ArrowUpRight />
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const WithAction = () => (
|
||||
<Surface>
|
||||
<Card className="max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Managed WordPress</CardTitle>
|
||||
<CardDescription>Renews 12 March 2027</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="ghost" size="sm">
|
||||
Manage
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Backups, updates and uptime monitoring handled for you. Includes a free NZ-hosted staging
|
||||
site.
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const StatCard = () => (
|
||||
<Surface>
|
||||
<Card className="max-w-xs">
|
||||
<CardHeader>
|
||||
<CardDescription>Uptime this month</CardDescription>
|
||||
<CardTitle className="font-sans text-3xl">99.98%</CardTitle>
|
||||
<CardAction>
|
||||
<span className="flex items-center gap-1 text-xs text-primary">
|
||||
<TrendingUp className="size-3" />
|
||||
+0.04%
|
||||
</span>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardFooter className="text-xs text-muted-foreground">
|
||||
Measured across all Auckland edge nodes
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Checkbox, Label } from 'cloudrite';
|
||||
|
||||
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
|
||||
// invisible on the preview harness's white card body. Stories render on the
|
||||
// brand surface, which is also how a real screen is built.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
export const States = () => (
|
||||
<Surface>
|
||||
<div className="flex items-center gap-6">
|
||||
<Checkbox />
|
||||
<Checkbox defaultChecked />
|
||||
<Checkbox disabled />
|
||||
<Checkbox defaultChecked disabled />
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const WithLabels = () => (
|
||||
<Surface>
|
||||
<div className="flex flex-col gap-3">
|
||||
{[
|
||||
['backups', 'Nightly off-site backups', true],
|
||||
['ssl', 'Managed SSL certificate', true],
|
||||
['cdn', 'Global CDN', false],
|
||||
].map(([id, label, on]) => (
|
||||
<Label key={id as string} className="flex items-center gap-2 font-normal">
|
||||
<Checkbox id={id as string} defaultChecked={on as boolean} />
|
||||
{label}
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'cloudrite';
|
||||
import { ChevronsUpDown } from 'lucide-react';
|
||||
|
||||
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
|
||||
// invisible on the preview harness's white card body. Stories render on the
|
||||
// brand surface, which is also how a real screen is built.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
export const Open = () => (
|
||||
<Surface>
|
||||
<Collapsible defaultOpen className="w-80">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-sm font-medium">What's included</span>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label="Toggle">
|
||||
<ChevronsUpDown />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent className="mt-2 flex flex-col gap-2">
|
||||
{['Nightly backups', 'Managed SSL', 'Uptime monitoring'].map((item) => (
|
||||
<div key={item} className="rounded-md border border-border px-3 py-2 text-sm">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Closed = () => (
|
||||
<Surface>
|
||||
<Collapsible className="w-80">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span className="text-sm font-medium">What's included</span>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label="Toggle">
|
||||
<ChevronsUpDown />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent className="mt-2 text-sm text-muted-foreground">
|
||||
Hidden until opened.
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Contact } from 'cloudrite';
|
||||
|
||||
// The contact section — details plus the enquiry form that POSTs to /api/send-email.
|
||||
//
|
||||
// The section draws light-on-dark against the page background, which the preview
|
||||
// card harness paints white — hence the surface wrapper. Transitions and
|
||||
// animation delays are zeroed because the capture harness only waits on fonts
|
||||
// and images, so an entrance animation would otherwise be screenshotted mid-flight.
|
||||
const still = `*,*::before,*::after{
|
||||
transition-duration:0s !important;transition-delay:0s !important;
|
||||
animation-duration:0s !important;animation-delay:0s !important;
|
||||
}`;
|
||||
|
||||
export const Default = () => (
|
||||
<div className="bg-background text-foreground">
|
||||
<style>{still}</style>
|
||||
<Contact />
|
||||
</div>
|
||||
);
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
} from 'cloudrite';
|
||||
|
||||
// Cloudrite is dark-only — the overlay surfaces read correctly only against the
|
||||
// brand background, not the harness's white card body.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
// `defaultOpen` is what makes the card worth looking at — a bare trigger shows
|
||||
// none of Dialog's parts. Paired with cardMode:single in .design-sync/config.json.
|
||||
export const Open = () => (
|
||||
<Surface>
|
||||
<Dialog defaultOpen modal={false}>
|
||||
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add a site</DialogTitle>
|
||||
<DialogDescription>
|
||||
We'll point the DNS and issue an SSL certificate automatically.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="dlg-domain">Domain</Label>
|
||||
<Input id="dlg-domain" defaultValue="cloudrite.co.nz" />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button>Add site</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from 'cloudrite';
|
||||
import { Download, RefreshCw, Settings, Trash2 } from 'lucide-react';
|
||||
|
||||
// Cloudrite is dark-only — the overlay surfaces read correctly only against the
|
||||
// brand background, not the harness's white card body.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
export const Open = () => (
|
||||
<Surface>
|
||||
<div className="flex h-72 items-start justify-center">
|
||||
<DropdownMenu defaultOpen modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Site actions</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuLabel>cloudrite.co.nz</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<RefreshCw />
|
||||
Clear cache
|
||||
<DropdownMenuShortcut>⌘R</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Download />
|
||||
Download backup
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Settings />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuCheckboxItem checked>Auto-renew SSL</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive">
|
||||
<Trash2 />
|
||||
Delete site
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from 'cloudrite';
|
||||
import { CloudOff, Plus } from 'lucide-react';
|
||||
|
||||
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
|
||||
// invisible on the preview harness's white card body. Stories render on the
|
||||
// brand surface, which is also how a real screen is built.
|
||||
const Surface = ({ children }: { children: ReactNode }) => (
|
||||
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
|
||||
);
|
||||
|
||||
export const Default = () => (
|
||||
<Surface>
|
||||
<Empty className="w-96 rounded-xl border border-border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<CloudOff />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No sites yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Add your first site and we'll handle DNS, SSL and backups for you.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>
|
||||
<Plus />
|
||||
Add a site
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</Surface>
|
||||
);
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Features } from 'cloudrite';
|
||||
|
||||
// The "why Cloudrite" feature showcase. Rotates its active feature on a 4s interval; the still captures whichever is active on first paint.
|
||||
//
|
||||
// The section draws light-on-dark against the page background, which the preview
|
||||
// card harness paints white — hence the surface wrapper. Transitions and
|
||||
// animation delays are zeroed because the capture harness only waits on fonts
|
||||
// and images, so an entrance animation would otherwise be screenshotted mid-flight.
|
||||
const still = `*,*::before,*::after{
|
||||
transition-duration:0s !important;transition-delay:0s !important;
|
||||
animation-duration:0s !important;animation-delay:0s !important;
|
||||
}`;
|
||||
|
||||
export const Default = () => (
|
||||
<div className="bg-background text-foreground">
|
||||
<style>{still}</style>
|
||||
<Features />
|
||||
</div>
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user