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]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d321ed44a8
commit
3970cf9a00
@@ -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`.
|
||||
@@ -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}`);
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: layout
|
||||
---
|
||||
|
||||
# AspectRatio
|
||||
|
||||
Source: `components/ui/aspect-ratio.tsx`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Badge
|
||||
|
||||
Source: `components/ui/badge.tsx`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: actions
|
||||
---
|
||||
|
||||
# Button
|
||||
|
||||
Source: `components/ui/button.tsx`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Checkbox
|
||||
|
||||
Source: `components/ui/checkbox.tsx`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Contact
|
||||
|
||||
Source: `components/contact.tsx`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Features
|
||||
|
||||
Source: `components/features.tsx`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Footer
|
||||
|
||||
Source: `components/footer.tsx`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Header
|
||||
|
||||
Source: `components/header.tsx`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Hero
|
||||
|
||||
Source: `components/hero.tsx`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Input
|
||||
|
||||
Source: `components/ui/input.tsx`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Label
|
||||
|
||||
Source: `components/ui/label.tsx`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Process
|
||||
|
||||
Source: `components/process.tsx`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Progress
|
||||
|
||||
Source: `components/ui/progress.tsx`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: layout
|
||||
---
|
||||
|
||||
# Separator
|
||||
|
||||
Source: `components/ui/separator.tsx`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: sections
|
||||
---
|
||||
|
||||
# Services
|
||||
|
||||
Source: `components/services.tsx`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Skeleton
|
||||
|
||||
Source: `components/ui/skeleton.tsx`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Slider
|
||||
|
||||
Source: `components/ui/slider.tsx`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Spinner
|
||||
|
||||
Source: `components/ui/spinner.tsx`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Switch
|
||||
|
||||
Source: `components/ui/switch.tsx`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: forms
|
||||
---
|
||||
|
||||
# Textarea
|
||||
|
||||
Source: `components/ui/textarea.tsx`.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: feedback
|
||||
---
|
||||
|
||||
# Toaster
|
||||
|
||||
Source: `components/ui/sonner.tsx`.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
category: actions
|
||||
---
|
||||
|
||||
# Toggle
|
||||
|
||||
Source: `components/ui/toggle.tsx`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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.
@@ -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(' ')}`);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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:00–03:00 NZST.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const Destructive = () => (
|
||||
<Surface>
|
||||
<Alert variant="destructive" className="max-w-md">
|
||||
<CircleAlert />
|
||||
<AlertTitle>SSL certificate expired</AlertTitle>
|
||||
<AlertDescription>
|
||||
cloudrite.co.nz stopped serving HTTPS 2 hours ago. Renew it to restore the site.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
export const TitleOnly = () => (
|
||||
<Surface>
|
||||
<Alert className="max-w-md">
|
||||
<TriangleAlert />
|
||||
<AlertTitle>Backup skipped — disk almost full.</AlertTitle>
|
||||
</Alert>
|
||||
</Surface>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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 & 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 & 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>
|
||||
);
|
||||
@@ -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
Reference in New Issue
Block a user