Compare commits

...
2 Commits
Author SHA1 Message Date
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
117 changed files with 3428 additions and 1 deletions
+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>
);
+78
View File
@@ -0,0 +1,78 @@
import type { ReactNode } from 'react';
import {
Checkbox,
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
Input,
} 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 Vertical = () => (
<Surface>
<FieldGroup className="w-80">
<Field>
<FieldLabel htmlFor="domain">Domain</FieldLabel>
<Input id="domain" defaultValue="cloudrite.co.nz" />
<FieldDescription>We'll point the DNS for you at no charge.</FieldDescription>
</Field>
</FieldGroup>
</Surface>
);
export const Horizontal = () => (
<Surface>
<FieldGroup className="w-96">
<Field orientation="horizontal">
<Checkbox id="tos" defaultChecked />
<FieldContent>
<FieldLabel htmlFor="tos">Managed backups</FieldLabel>
<FieldDescription>Nightly snapshots kept for 30 days.</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
</Surface>
);
export const WithError = () => (
<Surface>
<FieldGroup className="w-80">
<Field data-invalid>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" defaultValue="not-an-email" aria-invalid />
<FieldError>Enter a valid email address.</FieldError>
</Field>
</FieldGroup>
</Surface>
);
export const Fieldset = () => (
<Surface>
<FieldSet className="w-96">
<FieldLegend>Hosting</FieldLegend>
<FieldGroup>
<Field>
<FieldLabel htmlFor="plan-name">Plan name</FieldLabel>
<Input id="plan-name" defaultValue="Business" />
</Field>
<FieldSeparator />
<Field>
<FieldLabel htmlFor="sites">Site limit</FieldLabel>
<Input id="sites" type="number" defaultValue={10} />
</Field>
</FieldGroup>
</FieldSet>
</Surface>
);
+19
View File
@@ -0,0 +1,19 @@
import { Footer } from 'cloudrite';
// The site footer: service/company link columns, contact details and the Cloudrite mark.
//
// 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>
<Footer />
</div>
);
+79
View File
@@ -0,0 +1,79 @@
import type { ReactNode } from 'react';
import { useForm } from 'react-hook-form';
import {
Button,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
Textarea,
} 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>
);
type Values = { name: string; email: string; message: string };
// `Form` is react-hook-form's FormProvider — it needs a useForm() instance, and
// FormField wires each control through Controller. This is the enquiry form on
// the Cloudrite contact section.
export const EnquiryForm = () => {
const form = useForm<Values>({
defaultValues: { name: 'Jamie Lambert', email: '[email protected]', message: '' },
});
return (
<Surface>
<Form {...form}>
<form className="flex w-80 flex-col gap-5" onSubmit={(e) => e.preventDefault()}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormDescription>We reply within one business day.</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>How can we help?</FormLabel>
<FormControl>
<Textarea rows={3} placeholder="Tell us about your setup…" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Send enquiry</Button>
</form>
</Form>
</Surface>
);
};
+20
View File
@@ -0,0 +1,20 @@
import { Header } from 'cloudrite';
// Header is `position: fixed`, so it contributes no height to its container — an
// unwrapped render collapses the preview root to zero and the card comes up
// empty. The sized, `relative` shell below gives it something to pin to and
// shows it over page content the way it actually appears.
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="relative h-64 overflow-hidden bg-background text-foreground">
<style>{still}</style>
<Header />
<div className="px-6 pt-28 text-sm text-muted-foreground">
Page content scrolls beneath the fixed header.
</div>
</div>
);
+23
View File
@@ -0,0 +1,23 @@
import { Hero } from 'cloudrite';
// Hero takes no props — it is the cloudrite.co.nz landing hero, rendered exactly
// as app/page.tsx does.
//
// Two framing details, both needed for a truthful still:
// · Hero fades in (setTimeout 300ms, then `transition-all duration-1000`) and
// the capture harness only waits on fonts and images, so an unmodified
// render screenshots mid-fade at ~10% opacity. Zeroing transition/animation
// time lands every element on its final state instead.
// · It draws light-on-dark against the page background, which the card
// harness paints white.
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>
<Hero />
</div>
);
+45
View File
@@ -0,0 +1,45 @@
import type { ReactNode } from 'react';
import { Input, 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 Default = () => (
<Surface>
<Input className="w-72" placeholder="[email protected]" />
</Surface>
);
export const WithLabel = () => (
<Surface>
<div className="flex w-72 flex-col gap-2">
<Label htmlFor="site">Website to migrate</Label>
<Input id="site" defaultValue="https://cloudrite.co.nz" />
</div>
</Surface>
);
export const Types = () => (
<Surface>
<div className="flex w-72 flex-col gap-3">
<Input type="email" placeholder="Email address" />
<Input type="tel" defaultValue="021 107 7483" />
<Input type="password" defaultValue="hunter2hunter2" />
<Input type="file" />
</div>
</Surface>
);
export const States = () => (
<Surface>
<div className="flex w-72 flex-col gap-3">
<Input placeholder="Enabled" />
<Input placeholder="Disabled" disabled />
<Input defaultValue="not-an-email" aria-invalid />
</div>
</Surface>
);
+33
View File
@@ -0,0 +1,33 @@
import type { ReactNode } from 'react';
import { Checkbox, Input, 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 Default = () => (
<Surface>
<Label>Business name</Label>
</Surface>
);
export const WithInput = () => (
<Surface>
<div className="flex w-72 flex-col gap-2">
<Label htmlFor="business">Business name</Label>
<Input id="business" placeholder="Cloudrite Ltd" />
</div>
</Surface>
);
export const WithCheckbox = () => (
<Surface>
<Label className="flex items-center gap-2">
<Checkbox defaultChecked />
Send me the monthly uptime report
</Label>
</Surface>
);
+46
View File
@@ -0,0 +1,46 @@
import type { ReactNode } from 'react';
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} 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>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" isActive>
2
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>
</Surface>
);
+30
View File
@@ -0,0 +1,30 @@
import type { ReactNode } from 'react';
import { Button, Input, Label, Popover, PopoverContent, PopoverTrigger } 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>
);
export const Open = () => (
<Surface>
<div className="flex h-64 items-start justify-center">
<Popover defaultOpen modal={false}>
<PopoverTrigger asChild>
<Button variant="outline">Rename site</Button>
</PopoverTrigger>
<PopoverContent className="w-72" onOpenAutoFocus={(e) => e.preventDefault()}>
<div className="flex flex-col gap-3">
<div className="text-sm font-medium">Rename site</div>
<div className="flex flex-col gap-2">
<Label htmlFor="pop-name">Display name</Label>
<Input id="pop-name" defaultValue="Cloudrite marketing" />
</div>
<Button size="sm">Save</Button>
</div>
</PopoverContent>
</Popover>
</div>
</Surface>
);
+19
View File
@@ -0,0 +1,19 @@
import { Process } from 'cloudrite';
// The four-step engagement timeline. Reveals on IntersectionObserver, which fires normally in the capture viewport.
//
// 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>
<Process />
</div>
);
+32
View File
@@ -0,0 +1,32 @@
import type { ReactNode } from 'react';
import { Progress } 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 Values = () => (
<Surface>
<div className="flex w-80 flex-col gap-4">
<Progress value={0} />
<Progress value={35} />
<Progress value={72} />
<Progress value={100} />
</div>
</Surface>
);
export const Labelled = () => (
<Surface>
<div className="flex w-80 flex-col gap-2">
<div className="flex items-center justify-between text-sm">
<span>Migrating site files</span>
<span className="text-muted-foreground">72%</span>
</div>
<Progress value={72} />
</div>
</Surface>
);
+41
View File
@@ -0,0 +1,41 @@
import type { ReactNode } from 'react';
import { Label, RadioGroup, RadioGroupItem } 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>
<RadioGroup defaultValue="business">
{[
['starter', 'Starter — 1 site'],
['business', 'Business — 10 sites'],
['vps', 'Dedicated VPS'],
].map(([value, label]) => (
<Label key={value} className="flex items-center gap-2 font-normal">
<RadioGroupItem value={value} />
{label}
</Label>
))}
</RadioGroup>
</Surface>
);
export const Disabled = () => (
<Surface>
<RadioGroup defaultValue="remote" disabled>
<Label className="flex items-center gap-2 font-normal">
<RadioGroupItem value="remote" />
Remote support
</Label>
<Label className="flex items-center gap-2 font-normal">
<RadioGroupItem value="onsite" />
On-site callout
</Label>
</RadioGroup>
</Surface>
);
+93
View File
@@ -0,0 +1,93 @@
import type { ReactNode } from 'react';
import {
Label,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} 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>
);
// The open state is the primary story: a closed trigger shows none of the
// compound parts, and SelectContent/SelectItem/SelectGroup/SelectLabel are the
// half of this API worth seeing.
export const Open = () => (
<Surface>
<Select defaultValue="hosting" defaultOpen>
<SelectTrigger className="w-64">
<SelectValue placeholder="Choose a service" />
</SelectTrigger>
<SelectContent>
<SelectItem value="web">Web Development</SelectItem>
<SelectItem value="hosting">Cloud Hosting</SelectItem>
<SelectItem value="support">I.T Support &amp; Repairs</SelectItem>
<SelectItem value="ai">AI Solutions</SelectItem>
</SelectContent>
</Select>
</Surface>
);
export const Default = () => (
<Surface>
<Select defaultValue="hosting">
<SelectTrigger className="w-64">
<SelectValue placeholder="Choose a service" />
</SelectTrigger>
<SelectContent>
<SelectItem value="web">Web Development</SelectItem>
<SelectItem value="hosting">Cloud Hosting</SelectItem>
<SelectItem value="support">I.T Support &amp; Repairs</SelectItem>
<SelectItem value="ai">AI Solutions</SelectItem>
</SelectContent>
</Select>
</Surface>
);
export const WithLabelAndGroups = () => (
<Surface>
<div className="flex w-64 flex-col gap-2">
<Label htmlFor="plan">Hosting plan</Label>
<Select>
<SelectTrigger id="plan" className="w-full">
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Shared</SelectLabel>
<SelectItem value="starter">Starter 1 site</SelectItem>
<SelectItem value="business">Business 10 sites</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Dedicated</SelectLabel>
<SelectItem value="vps">VPS</SelectItem>
<SelectItem value="game">Game server</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</Surface>
);
export const Disabled = () => (
<Surface>
<Select defaultValue="starter" disabled>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="starter">Starter 1 site</SelectItem>
</SelectContent>
</Select>
</Surface>
);
+32
View File
@@ -0,0 +1,32 @@
import type { ReactNode } from 'react';
import { Separator } 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 Horizontal = () => (
<Surface>
<div className="w-80">
<div className="text-sm font-medium">Cloud Hosting</div>
<div className="text-sm text-muted-foreground">Proudly hosted in New Zealand.</div>
<Separator className="my-4" />
<div className="text-sm text-muted-foreground">From $29/month</div>
</div>
</Surface>
);
export const Vertical = () => (
<Surface>
<div className="flex h-6 items-center gap-4 text-sm">
<span>Services</span>
<Separator orientation="vertical" />
<span>Hosting</span>
<Separator orientation="vertical" />
<span>Support</span>
</div>
</Surface>
);

Some files were not shown because too many files have changed in this diff Show More