Author SHA1 Message Date
ASOwnerYT d0bbae4a31 Remove unnecessary comments and Arcane deployment trigger from Docker publish workflow
Docker / build (push) Successful in 1m24s
2026-08-05 17:09:24 +12:00
ASOwnerYTandClaude Opus 5 3970cf9a00 Add design-sync inputs for claude.ai/design
Docker / build (push) Failing after 1m43s
Import this repo's components into a Claude Design project so the design
agent builds with the real Cloudrite library instead of generic components.

The repo is a Next.js app, not a component library: no dist/, no exports
map, no .d.ts tree, and Tailwind exists only as build directives. So
.design-sync/ carries a small build that manufactures a consumable package
from source (staged to the gitignored .ds-pkg/):

- tsconfig.dts.json  declaration emit, for real <Name>Props contracts
- build-css.mjs      Tailwind v4 -> a static stylesheet via @tailwindcss/postcss
- make-pkg.mjs       stages the package; index.js exports all 292 symbols
                     while index.d.ts exports only the 61 roots, so shadcn's
                     flat compound parts don't each become a preview card.
                     Also generates the per-root parts tables in docs/
- shims/next-link    router-free anchor; the real next/link needs
                     AppRouterContext and drags in the App Router runtime
- fonts/             Space Grotesk + Inter woff2 (SIL OFL). next/font supplies
                     these at runtime in the app, so the bundle has none

previews/ holds 40 authored preview stories, conventions.md is prepended to
the generated README as the design agent's usage guide, and NOTES.md records
the gotchas a future sync would otherwise rediscover.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-05 17:04:10 +12:00
ASOwnerYTandClaude Opus 5 d321ed44a8 Add webfinger endpoint
Serve /.well-known/webfinger for federated-identity discovery.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-05 17:03:54 +12:00
asowneryt 19dcce5d59 Chatwoot, Arcane deployment
Docker / build (push) Successful in 1m35s
2026-07-18 00:40:53 +12:00
asowneryt 1da82b9d30 Make Docker pnpm install resilient to sharp ignored-builds; drop
Docker / build (push) Successful in 1m26s
unreachable gha cache
2026-07-17 01:31:30 +12:00
asowneryt 8857163e11 Update next-env.d.ts
Docker / build (push) Failing after 1m14s
2026-07-17 01:19:08 +12:00
asowneryt ebfdd74ca2 Update docker-publish.yml
Docker / build (push) Failing after 1m18s
2026-07-17 01:02:25 +12:00
asowneryt 32ca000d79 Update workflow
Docker / build (push) Failing after 4m33s
2026-07-17 00:47:56 +12:00
asowneryt 53df8dcefc Add login button, UI tweaks
Docker / build (push) Canceled after 46s
2026-07-17 00:32:39 +12:00
ASOwnerYT 7cb01d7ad0 Create chatwidget.tsx 2026-04-09 20:23:07 +12:00
ASOwnerYT 1a77c8db16 Replace chatwoot with odoo 2026-04-09 20:22:39 +12:00
ASOwnerYT ed398bae75 Update favicon 2026-04-07 18:39:06 +12:00
ASOwnerYTandGitHub 8ab699bba7 Merge pull request #3 from ASOwnerYT/claude/practical-curie
Add AI Solutions service card and update settings
2026-04-02 10:21:50 +13:00
ASOwnerYT 7dbc3c9f14 Improve build times of Docker image and remove unneeded AI skills 2026-04-02 10:18:53 +13:00
ASOwnerYTandClaude Sonnet 4.6 2ba8a9222e Add AI Solutions service card and update settings
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-04-02 09:49:59 +13:00
ASOwnerYT ae2066d02f Some AI stuff 2026-03-23 18:18:46 +13:00
ASOwnerYT 587af8a74a Fix email, remove docker-compose 2026-03-23 09:05:15 +13:00
ASOwnerYT 3bc7f332de Update Docker workflow to use main and develop branches for pushes and pull requests 2026-03-20 23:41:43 +13:00
ASOwnerYT 84aa8d8093 Docker support v2 2026-03-20 23:36:24 +13:00
ASOwnerYT d36959fa96 Fix docker again 2026-03-20 23:15:51 +13:00
ASOwnerYT ddbee83185 Fix docker 2026-03-20 23:14:54 +13:00
162 changed files with 5898 additions and 259 deletions
+242
View File
@@ -0,0 +1,242 @@
---
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 (27 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 25 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
+5
View File
@@ -0,0 +1,5 @@
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.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+257
View File
@@ -0,0 +1,257 @@
# 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.
+202
View File
@@ -0,0 +1,202 @@
# 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 (01), chroma (0 = gray), and hue (0360).
---
## 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.
+47
View File
@@ -0,0 +1,47 @@
{
"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-*"
]
}
]
}
+94
View File
@@ -0,0 +1,94 @@
# 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`
@@ -0,0 +1,306 @@
# 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>
```
+195
View File
@@ -0,0 +1,195 @@
# 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">` |
+192
View File
@@ -0,0 +1,192 @@
# Forms & Inputs
## Contents
- Forms use FieldGroup + Field
- InputGroup requires InputGroupInput/InputGroupTextarea
- Buttons inside inputs use InputGroup + InputGroupAddon
- Option sets (27 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 25 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 (27 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`.
+101
View File
@@ -0,0 +1,101 @@
# 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} />
```
+162
View File
@@ -0,0 +1,162 @@
# 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]`.
+6
View File
@@ -0,0 +1,6 @@
{
"enabledPlugins": {
"frontend-design@claude-plugins-official": true,
"typescript-lsp@claude-plugins-official": true
}
}
+157
View File
@@ -0,0 +1,157 @@
# 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`.
+27
View File
@@ -0,0 +1,27 @@
#!/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}`);
+45
View File
@@ -0,0 +1,45 @@
{
"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"
}
+102
View File
@@ -0,0 +1,102 @@
## 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.
+18
View File
@@ -0,0 +1,18 @@
---
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`.
+17
View File
@@ -0,0 +1,17 @@
---
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`.
+25
View File
@@ -0,0 +1,25 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: layout
---
# AspectRatio
Source: `components/ui/aspect-ratio.tsx`.
+17
View File
@@ -0,0 +1,17 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: feedback
---
# Badge
Source: `components/ui/badge.tsx`.
+21
View File
@@ -0,0 +1,21 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: actions
---
# Button
Source: `components/ui/button.tsx`.
+17
View File
@@ -0,0 +1,17 @@
---
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`.
+16
View File
@@ -0,0 +1,16 @@
---
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`.
+21
View File
@@ -0,0 +1,21 @@
---
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`.
+19
View File
@@ -0,0 +1,19 @@
---
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`.
+20
View File
@@ -0,0 +1,20 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: forms
---
# Checkbox
Source: `components/ui/checkbox.tsx`.
+17
View File
@@ -0,0 +1,17 @@
---
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`.
+23
View File
@@ -0,0 +1,23 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: sections
---
# Contact
Source: `components/contact.tsx`.
+29
View File
@@ -0,0 +1,29 @@
---
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`.
+24
View File
@@ -0,0 +1,24 @@
---
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`.
+24
View File
@@ -0,0 +1,24 @@
---
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`.
+29
View File
@@ -0,0 +1,29 @@
---
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`.
+20
View File
@@ -0,0 +1,20 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: sections
---
# Features
Source: `components/features.tsx`.
+24
View File
@@ -0,0 +1,24 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: sections
---
# Footer
Source: `components/footer.tsx`.
+21
View File
@@ -0,0 +1,21 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: sections
---
# Header
Source: `components/header.tsx`.
+7
View File
@@ -0,0 +1,7 @@
---
category: sections
---
# Hero
Source: `components/hero.tsx`.
+17
View File
@@ -0,0 +1,17 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: forms
---
# Input
Source: `components/ui/input.tsx`.
+20
View File
@@ -0,0 +1,20 @@
---
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`.
+18
View File
@@ -0,0 +1,18 @@
---
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`.
+24
View File
@@ -0,0 +1,24 @@
---
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`.
+16
View File
@@ -0,0 +1,16 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: forms
---
# Label
Source: `components/ui/label.tsx`.
+30
View File
@@ -0,0 +1,30 @@
---
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`.
+22
View File
@@ -0,0 +1,22 @@
---
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`.
+21
View File
@@ -0,0 +1,21 @@
---
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`.
+18
View File
@@ -0,0 +1,18 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: sections
---
# Process
Source: `components/process.tsx`.
+7
View File
@@ -0,0 +1,7 @@
---
category: feedback
---
# Progress
Source: `components/ui/progress.tsx`.
+16
View File
@@ -0,0 +1,16 @@
---
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`.
+17
View File
@@ -0,0 +1,17 @@
---
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`.
+16
View File
@@ -0,0 +1,16 @@
---
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`.
+24
View File
@@ -0,0 +1,24 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: layout
---
# Separator
Source: `components/ui/separator.tsx`.
+7
View File
@@ -0,0 +1,7 @@
---
category: sections
---
# Services
Source: `components/services.tsx`.
+22
View File
@@ -0,0 +1,22 @@
---
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`.
+37
View File
@@ -0,0 +1,37 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: feedback
---
# Skeleton
Source: `components/ui/skeleton.tsx`.
+7
View File
@@ -0,0 +1,7 @@
---
category: forms
---
# Slider
Source: `components/ui/slider.tsx`.
+7
View File
@@ -0,0 +1,7 @@
---
category: feedback
---
# Spinner
Source: `components/ui/spinner.tsx`.
+7
View File
@@ -0,0 +1,7 @@
---
category: forms
---
# Switch
Source: `components/ui/switch.tsx`.
+22
View File
@@ -0,0 +1,22 @@
---
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`.
+18
View File
@@ -0,0 +1,18 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: forms
---
# Textarea
Source: `components/ui/textarea.tsx`.
+21
View File
@@ -0,0 +1,21 @@
---
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`.
+7
View File
@@ -0,0 +1,7 @@
---
category: feedback
---
# Toaster
Source: `components/ui/sonner.tsx`.
+7
View File
@@ -0,0 +1,7 @@
---
category: actions
---
# Toggle
Source: `components/ui/toggle.tsx`.
+16
View File
@@ -0,0 +1,16 @@
---
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`.
+18
View File
@@ -0,0 +1,18 @@
---
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`.
+40
View File
@@ -0,0 +1,40 @@
/* 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.
+212
View File
@@ -0,0 +1,212 @@
#!/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(' ')}`);
+41
View File
@@ -0,0 +1,41 @@
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>
);
+43
View File
@@ -0,0 +1,43 @@
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:0003: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>
);
+61
View File
@@ -0,0 +1,61 @@
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>
);
+38
View File
@@ -0,0 +1,38 @@
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>
);
+57
View File
@@ -0,0 +1,57 @@
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>
);
+71
View File
@@ -0,0 +1,71 @@
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>
);
+56
View File
@@ -0,0 +1,56 @@
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>
);
+94
View File
@@ -0,0 +1,94 @@
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>
);
+37
View File
@@ -0,0 +1,37 @@
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>
);
+50
View File
@@ -0,0 +1,50 @@
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>
);
+19
View File
@@ -0,0 +1,19 @@
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>
);
+46
View File
@@ -0,0 +1,46 @@
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>
);
+58
View File
@@ -0,0 +1,58 @@
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>
);
+40
View File
@@ -0,0 +1,40 @@
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>
);
+19
View File
@@ -0,0 +1,19 @@
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