Add design-sync inputs for claude.ai/design
Docker / build (push) Failing after 1m43s

Import this repo's components into a Claude Design project so the design
agent builds with the real Cloudrite library instead of generic components.

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

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

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

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ASOwnerYT
2026-08-05 17:04:10 +12:00
co-authored by Claude Opus 5
parent d321ed44a8
commit 3970cf9a00
116 changed files with 3403 additions and 1 deletions
+41
View File
@@ -0,0 +1,41 @@
import type { ReactNode } from 'react';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
const faqs = [
['hosting', 'Where are your servers?', 'All Cloudrite hosting runs on infrastructure in New Zealand, so your visitors get low-latency responses and your data stays onshore.'],
['support', 'Do you offer after-hours support?', 'Standard support is by appointment. Emergency support is available 24/7 for hosting and business-critical outages.'],
['migration', 'Can you move my existing site?', "Yes — WordPress, Squarespace, Wix and Shopify migrations are included free when you move hosting to us."],
];
export const Faq = () => (
<Surface>
<Accordion type="single" collapsible defaultValue="hosting" className="w-96">
{faqs.map(([value, q, a]) => (
<AccordionItem key={value} value={value}>
<AccordionTrigger>{q}</AccordionTrigger>
<AccordionContent>{a}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</Surface>
);
export const Multiple = () => (
<Surface>
<Accordion type="multiple" defaultValue={['hosting', 'support']} className="w-96">
{faqs.map(([value, q, a]) => (
<AccordionItem key={value} value={value}>
<AccordionTrigger>{q}</AccordionTrigger>
<AccordionContent>{a}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</Surface>
);
+43
View File
@@ -0,0 +1,43 @@
import type { ReactNode } from 'react';
import { Alert, AlertDescription, AlertTitle } from 'cloudrite';
import { CircleAlert, Info, TriangleAlert } from 'lucide-react';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Default = () => (
<Surface>
<Alert className="max-w-md">
<Info />
<AlertTitle>Scheduled maintenance</AlertTitle>
<AlertDescription>
Our Auckland edge nodes will be patched on Sunday 02:0003:00 NZST.
</AlertDescription>
</Alert>
</Surface>
);
export const Destructive = () => (
<Surface>
<Alert variant="destructive" className="max-w-md">
<CircleAlert />
<AlertTitle>SSL certificate expired</AlertTitle>
<AlertDescription>
cloudrite.co.nz stopped serving HTTPS 2 hours ago. Renew it to restore the site.
</AlertDescription>
</Alert>
</Surface>
);
export const TitleOnly = () => (
<Surface>
<Alert className="max-w-md">
<TriangleAlert />
<AlertTitle>Backup skipped disk almost full.</AlertTitle>
</Alert>
</Surface>
);
+61
View File
@@ -0,0 +1,61 @@
import type { ReactNode } from 'react';
import { Avatar, AvatarFallback, AvatarImage } from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
// AvatarImage points at a remote URL in real use; previews render offline, so
// these show the fallback path — which is also the state worth documenting,
// since it is what every avatar shows before the image resolves.
export const Fallbacks = () => (
<Surface>
<div className="flex items-center gap-3">
{['JL', 'CR', 'AM', 'TK'].map((initials) => (
<Avatar key={initials}>
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
))}
</div>
</Surface>
);
export const Sizes = () => (
<Surface>
<div className="flex items-center gap-4">
<Avatar className="size-6">
<AvatarFallback className="text-[10px]">CR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarFallback>CR</AvatarFallback>
</Avatar>
<Avatar className="size-12">
<AvatarFallback>CR</AvatarFallback>
</Avatar>
<Avatar className="size-16">
<AvatarFallback className="text-lg">CR</AvatarFallback>
</Avatar>
</div>
</Surface>
);
// size-10 with -space-x-3: at the default size-8 the overlap eats enough of each
// circle that the initials clip.
export const Stacked = () => (
<Surface>
<div className="flex -space-x-3">
{['JL', 'CR', 'AM'].map((initials) => (
<Avatar key={initials} className="size-10 ring-2 ring-background">
<AvatarImage alt="" />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
))}
<Avatar className="size-10 ring-2 ring-background">
<AvatarFallback className="bg-primary text-primary-foreground">+5</AvatarFallback>
</Avatar>
</div>
</Surface>
);
+38
View File
@@ -0,0 +1,38 @@
import type { ReactNode } from 'react';
import { Badge } from 'cloudrite';
import { Check, CircleAlert } from 'lucide-react';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Variants = () => (
<Surface>
<div className="flex flex-wrap items-center gap-2">
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
</div>
</Surface>
);
export const InContext = () => (
<Surface>
<div className="flex flex-wrap items-center gap-2">
<Badge>
<Check />
Online
</Badge>
<Badge variant="secondary">NZ Hosted</Badge>
<Badge variant="destructive">
<CircleAlert />
Expired
</Badge>
<Badge variant="outline">v16.2.0</Badge>
</div>
</Surface>
);
+57
View File
@@ -0,0 +1,57 @@
import type { ReactNode } from 'react';
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Default = () => (
<Surface>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">Dashboard</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="#">Hosting</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>cloudrite.co.nz</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</Surface>
);
export const Collapsed = () => (
<Surface>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">Dashboard</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbEllipsis />
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Backups</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</Surface>
);
+71
View File
@@ -0,0 +1,71 @@
import type { ReactNode } from 'react';
import { Button } from 'cloudrite';
import { ArrowRight, Download, Loader2, Phone, Trash2 } from 'lucide-react';
// Cloudrite is a dark-only design system: `ghost`, `link` and `outline` are all
// foreground-coloured, so they vanish on the preview harness's white card body.
// Every story renders on the brand surface — which is also how a real screen is
// built (see the README's conventions section).
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Variants = () => (
<Surface>
<div className="flex flex-wrap items-center gap-3">
<Button>Get a quote</Button>
<Button variant="secondary">Learn more</Button>
<Button variant="outline">View services</Button>
<Button variant="ghost">Cancel</Button>
<Button variant="link">Read the case study</Button>
<Button variant="destructive">Delete site</Button>
</div>
</Surface>
);
export const Sizes = () => (
<Surface>
<div className="flex flex-wrap items-center gap-3">
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon" aria-label="Download invoice">
<Download />
</Button>
</div>
</Surface>
);
export const WithIcons = () => (
<Surface>
<div className="flex flex-wrap items-center gap-3">
<Button>
Book a callout
<ArrowRight />
</Button>
<Button variant="outline">
<Phone />
021 107 7483
</Button>
<Button variant="destructive">
<Trash2 />
Remove backup
</Button>
</div>
</Surface>
);
export const States = () => (
<Surface>
<div className="flex flex-wrap items-center gap-3">
<Button disabled>Disabled</Button>
<Button variant="outline" disabled>
Disabled outline
</Button>
<Button disabled>
<Loader2 className="animate-spin" />
Provisioning
</Button>
</div>
</Surface>
);
+56
View File
@@ -0,0 +1,56 @@
import type { ReactNode } from 'react';
import { Button, ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from 'cloudrite';
import { Copy, RefreshCw, Trash2 } from 'lucide-react';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Horizontal = () => (
<Surface>
<ButtonGroup>
<Button variant="outline">Day</Button>
<Button variant="outline">Week</Button>
<Button variant="outline">Month</Button>
</ButtonGroup>
</Surface>
);
export const WithIcons = () => (
<Surface>
<ButtonGroup>
<Button variant="outline" size="icon" aria-label="Refresh">
<RefreshCw />
</Button>
<Button variant="outline" size="icon" aria-label="Duplicate">
<Copy />
</Button>
<ButtonGroupSeparator />
<Button variant="outline" size="icon" aria-label="Delete">
<Trash2 />
</Button>
</ButtonGroup>
</Surface>
);
export const WithText = () => (
<Surface>
<ButtonGroup>
<ButtonGroupText>https://</ButtonGroupText>
<Button variant="outline">cloudrite.co.nz</Button>
</ButtonGroup>
</Surface>
);
export const Vertical = () => (
<Surface>
<ButtonGroup orientation="vertical">
<Button variant="outline">Restart</Button>
<Button variant="outline">Rebuild</Button>
<Button variant="outline">Snapshot</Button>
</ButtonGroup>
</Surface>
);
+94
View File
@@ -0,0 +1,94 @@
import type { ReactNode } from 'react';
import {
Button,
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from 'cloudrite';
import { ArrowUpRight, Cloud, TrendingUp } from 'lucide-react';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const ServiceCard = () => (
<Surface>
<Card className="max-w-sm">
<CardHeader>
<div className="flex size-12 items-center justify-center rounded-xl bg-primary/10 text-primary">
<Cloud className="size-6" />
</div>
<CardTitle className="mt-4">Cloud Hosting</CardTitle>
<CardDescription>
Fast, easy hosting for WordPress, game servers and more proudly hosted here in New
Zealand.
</CardDescription>
</CardHeader>
<CardContent>
<ul className="flex flex-wrap gap-2">
{['NZ Hosted', 'WordPress', 'Game Servers', '24/7 Uptime'].map((f) => (
<li
key={f}
className="rounded-full border border-border px-3 py-1 text-xs text-muted-foreground"
>
{f}
</li>
))}
</ul>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full">
Explore hosting
<ArrowUpRight />
</Button>
</CardFooter>
</Card>
</Surface>
);
export const WithAction = () => (
<Surface>
<Card className="max-w-sm">
<CardHeader>
<CardTitle>Managed WordPress</CardTitle>
<CardDescription>Renews 12 March 2027</CardDescription>
<CardAction>
<Button variant="ghost" size="sm">
Manage
</Button>
</CardAction>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
Backups, updates and uptime monitoring handled for you. Includes a free NZ-hosted staging
site.
</CardContent>
</Card>
</Surface>
);
export const StatCard = () => (
<Surface>
<Card className="max-w-xs">
<CardHeader>
<CardDescription>Uptime this month</CardDescription>
<CardTitle className="font-sans text-3xl">99.98%</CardTitle>
<CardAction>
<span className="flex items-center gap-1 text-xs text-primary">
<TrendingUp className="size-3" />
+0.04%
</span>
</CardAction>
</CardHeader>
<CardFooter className="text-xs text-muted-foreground">
Measured across all Auckland edge nodes
</CardFooter>
</Card>
</Surface>
);
+37
View File
@@ -0,0 +1,37 @@
import type { ReactNode } from 'react';
import { Checkbox, Label } from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const States = () => (
<Surface>
<div className="flex items-center gap-6">
<Checkbox />
<Checkbox defaultChecked />
<Checkbox disabled />
<Checkbox defaultChecked disabled />
</div>
</Surface>
);
export const WithLabels = () => (
<Surface>
<div className="flex flex-col gap-3">
{[
['backups', 'Nightly off-site backups', true],
['ssl', 'Managed SSL certificate', true],
['cdn', 'Global CDN', false],
].map(([id, label, on]) => (
<Label key={id as string} className="flex items-center gap-2 font-normal">
<Checkbox id={id as string} defaultChecked={on as boolean} />
{label}
</Label>
))}
</div>
</Surface>
);
+50
View File
@@ -0,0 +1,50 @@
import type { ReactNode } from 'react';
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'cloudrite';
import { ChevronsUpDown } from 'lucide-react';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Open = () => (
<Surface>
<Collapsible defaultOpen className="w-80">
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-medium">What's included</span>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Toggle">
<ChevronsUpDown />
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="mt-2 flex flex-col gap-2">
{['Nightly backups', 'Managed SSL', 'Uptime monitoring'].map((item) => (
<div key={item} className="rounded-md border border-border px-3 py-2 text-sm">
{item}
</div>
))}
</CollapsibleContent>
</Collapsible>
</Surface>
);
export const Closed = () => (
<Surface>
<Collapsible className="w-80">
<div className="flex items-center justify-between gap-4">
<span className="text-sm font-medium">What's included</span>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Toggle">
<ChevronsUpDown />
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="mt-2 text-sm text-muted-foreground">
Hidden until opened.
</CollapsibleContent>
</Collapsible>
</Surface>
);
+19
View File
@@ -0,0 +1,19 @@
import { Contact } from 'cloudrite';
// The contact section — details plus the enquiry form that POSTs to /api/send-email.
//
// The section draws light-on-dark against the page background, which the preview
// card harness paints white — hence the surface wrapper. Transitions and
// animation delays are zeroed because the capture harness only waits on fonts
// and images, so an entrance animation would otherwise be screenshotted mid-flight.
const still = `*,*::before,*::after{
transition-duration:0s !important;transition-delay:0s !important;
animation-duration:0s !important;animation-delay:0s !important;
}`;
export const Default = () => (
<div className="bg-background text-foreground">
<style>{still}</style>
<Contact />
</div>
);
+46
View File
@@ -0,0 +1,46 @@
import type { ReactNode } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
Label,
} from 'cloudrite';
// Cloudrite is dark-only — the overlay surfaces read correctly only against the
// brand background, not the harness's white card body.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
// `defaultOpen` is what makes the card worth looking at — a bare trigger shows
// none of Dialog's parts. Paired with cardMode:single in .design-sync/config.json.
export const Open = () => (
<Surface>
<Dialog defaultOpen modal={false}>
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle>Add a site</DialogTitle>
<DialogDescription>
We'll point the DNS and issue an SSL certificate automatically.
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2">
<Label htmlFor="dlg-domain">Domain</Label>
<Input id="dlg-domain" defaultValue="cloudrite.co.nz" />
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Add site</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Surface>
);
+58
View File
@@ -0,0 +1,58 @@
import type { ReactNode } from 'react';
import {
Button,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from 'cloudrite';
import { Download, RefreshCw, Settings, Trash2 } from 'lucide-react';
// Cloudrite is dark-only — the overlay surfaces read correctly only against the
// brand background, not the harness's white card body.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Open = () => (
<Surface>
<div className="flex h-72 items-start justify-center">
<DropdownMenu defaultOpen modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="outline">Site actions</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuLabel>cloudrite.co.nz</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<RefreshCw />
Clear cache
<DropdownMenuShortcut>R</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<Download />
Download backup
</DropdownMenuItem>
<DropdownMenuItem>
<Settings />
Settings
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Auto-renew SSL</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<Trash2 />
Delete site
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Surface>
);
+40
View File
@@ -0,0 +1,40 @@
import type { ReactNode } from 'react';
import {
Button,
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from 'cloudrite';
import { CloudOff, Plus } from 'lucide-react';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Default = () => (
<Surface>
<Empty className="w-96 rounded-xl border border-border">
<EmptyHeader>
<EmptyMedia variant="icon">
<CloudOff />
</EmptyMedia>
<EmptyTitle>No sites yet</EmptyTitle>
<EmptyDescription>
Add your first site and we'll handle DNS, SSL and backups for you.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>
<Plus />
Add a site
</Button>
</EmptyContent>
</Empty>
</Surface>
);
+19
View File
@@ -0,0 +1,19 @@
import { Features } from 'cloudrite';
// The "why Cloudrite" feature showcase. Rotates its active feature on a 4s interval; the still captures whichever is active on first paint.
//
// The section draws light-on-dark against the page background, which the preview
// card harness paints white — hence the surface wrapper. Transitions and
// animation delays are zeroed because the capture harness only waits on fonts
// and images, so an entrance animation would otherwise be screenshotted mid-flight.
const still = `*,*::before,*::after{
transition-duration:0s !important;transition-delay:0s !important;
animation-duration:0s !important;animation-delay:0s !important;
}`;
export const Default = () => (
<div className="bg-background text-foreground">
<style>{still}</style>
<Features />
</div>
);
+78
View File
@@ -0,0 +1,78 @@
import type { ReactNode } from 'react';
import {
Checkbox,
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
Input,
} from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Vertical = () => (
<Surface>
<FieldGroup className="w-80">
<Field>
<FieldLabel htmlFor="domain">Domain</FieldLabel>
<Input id="domain" defaultValue="cloudrite.co.nz" />
<FieldDescription>We'll point the DNS for you at no charge.</FieldDescription>
</Field>
</FieldGroup>
</Surface>
);
export const Horizontal = () => (
<Surface>
<FieldGroup className="w-96">
<Field orientation="horizontal">
<Checkbox id="tos" defaultChecked />
<FieldContent>
<FieldLabel htmlFor="tos">Managed backups</FieldLabel>
<FieldDescription>Nightly snapshots kept for 30 days.</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
</Surface>
);
export const WithError = () => (
<Surface>
<FieldGroup className="w-80">
<Field data-invalid>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" defaultValue="not-an-email" aria-invalid />
<FieldError>Enter a valid email address.</FieldError>
</Field>
</FieldGroup>
</Surface>
);
export const Fieldset = () => (
<Surface>
<FieldSet className="w-96">
<FieldLegend>Hosting</FieldLegend>
<FieldGroup>
<Field>
<FieldLabel htmlFor="plan-name">Plan name</FieldLabel>
<Input id="plan-name" defaultValue="Business" />
</Field>
<FieldSeparator />
<Field>
<FieldLabel htmlFor="sites">Site limit</FieldLabel>
<Input id="sites" type="number" defaultValue={10} />
</Field>
</FieldGroup>
</FieldSet>
</Surface>
);
+19
View File
@@ -0,0 +1,19 @@
import { Footer } from 'cloudrite';
// The site footer: service/company link columns, contact details and the Cloudrite mark.
//
// The section draws light-on-dark against the page background, which the preview
// card harness paints white — hence the surface wrapper. Transitions and
// animation delays are zeroed because the capture harness only waits on fonts
// and images, so an entrance animation would otherwise be screenshotted mid-flight.
const still = `*,*::before,*::after{
transition-duration:0s !important;transition-delay:0s !important;
animation-duration:0s !important;animation-delay:0s !important;
}`;
export const Default = () => (
<div className="bg-background text-foreground">
<style>{still}</style>
<Footer />
</div>
);
+79
View File
@@ -0,0 +1,79 @@
import type { ReactNode } from 'react';
import { useForm } from 'react-hook-form';
import {
Button,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
Textarea,
} from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
type Values = { name: string; email: string; message: string };
// `Form` is react-hook-form's FormProvider — it needs a useForm() instance, and
// FormField wires each control through Controller. This is the enquiry form on
// the Cloudrite contact section.
export const EnquiryForm = () => {
const form = useForm<Values>({
defaultValues: { name: 'Jamie Lambert', email: '[email protected]', message: '' },
});
return (
<Surface>
<Form {...form}>
<form className="flex w-80 flex-col gap-5" onSubmit={(e) => e.preventDefault()}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormDescription>We reply within one business day.</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>How can we help?</FormLabel>
<FormControl>
<Textarea rows={3} placeholder="Tell us about your setup…" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Send enquiry</Button>
</form>
</Form>
</Surface>
);
};
+20
View File
@@ -0,0 +1,20 @@
import { Header } from 'cloudrite';
// Header is `position: fixed`, so it contributes no height to its container — an
// unwrapped render collapses the preview root to zero and the card comes up
// empty. The sized, `relative` shell below gives it something to pin to and
// shows it over page content the way it actually appears.
const still = `*,*::before,*::after{
transition-duration:0s !important;transition-delay:0s !important;
animation-duration:0s !important;animation-delay:0s !important;
}`;
export const Default = () => (
<div className="relative h-64 overflow-hidden bg-background text-foreground">
<style>{still}</style>
<Header />
<div className="px-6 pt-28 text-sm text-muted-foreground">
Page content scrolls beneath the fixed header.
</div>
</div>
);
+23
View File
@@ -0,0 +1,23 @@
import { Hero } from 'cloudrite';
// Hero takes no props — it is the cloudrite.co.nz landing hero, rendered exactly
// as app/page.tsx does.
//
// Two framing details, both needed for a truthful still:
// · Hero fades in (setTimeout 300ms, then `transition-all duration-1000`) and
// the capture harness only waits on fonts and images, so an unmodified
// render screenshots mid-fade at ~10% opacity. Zeroing transition/animation
// time lands every element on its final state instead.
// · It draws light-on-dark against the page background, which the card
// harness paints white.
const still = `*,*::before,*::after{
transition-duration:0s !important;transition-delay:0s !important;
animation-duration:0s !important;animation-delay:0s !important;
}`;
export const Default = () => (
<div className="bg-background text-foreground">
<style>{still}</style>
<Hero />
</div>
);
+45
View File
@@ -0,0 +1,45 @@
import type { ReactNode } from 'react';
import { Input, Label } from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Default = () => (
<Surface>
<Input className="w-72" placeholder="[email protected]" />
</Surface>
);
export const WithLabel = () => (
<Surface>
<div className="flex w-72 flex-col gap-2">
<Label htmlFor="site">Website to migrate</Label>
<Input id="site" defaultValue="https://cloudrite.co.nz" />
</div>
</Surface>
);
export const Types = () => (
<Surface>
<div className="flex w-72 flex-col gap-3">
<Input type="email" placeholder="Email address" />
<Input type="tel" defaultValue="021 107 7483" />
<Input type="password" defaultValue="hunter2hunter2" />
<Input type="file" />
</div>
</Surface>
);
export const States = () => (
<Surface>
<div className="flex w-72 flex-col gap-3">
<Input placeholder="Enabled" />
<Input placeholder="Disabled" disabled />
<Input defaultValue="not-an-email" aria-invalid />
</div>
</Surface>
);
+33
View File
@@ -0,0 +1,33 @@
import type { ReactNode } from 'react';
import { Checkbox, Input, Label } from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Default = () => (
<Surface>
<Label>Business name</Label>
</Surface>
);
export const WithInput = () => (
<Surface>
<div className="flex w-72 flex-col gap-2">
<Label htmlFor="business">Business name</Label>
<Input id="business" placeholder="Cloudrite Ltd" />
</div>
</Surface>
);
export const WithCheckbox = () => (
<Surface>
<Label className="flex items-center gap-2">
<Checkbox defaultChecked />
Send me the monthly uptime report
</Label>
</Surface>
);
+46
View File
@@ -0,0 +1,46 @@
import type { ReactNode } from 'react';
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Default = () => (
<Surface>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" isActive>
2
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>
</Surface>
);
+30
View File
@@ -0,0 +1,30 @@
import type { ReactNode } from 'react';
import { Button, Input, Label, Popover, PopoverContent, PopoverTrigger } from 'cloudrite';
// Cloudrite is dark-only — the overlay surfaces read correctly only against the
// brand background, not the harness's white card body.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Open = () => (
<Surface>
<div className="flex h-64 items-start justify-center">
<Popover defaultOpen modal={false}>
<PopoverTrigger asChild>
<Button variant="outline">Rename site</Button>
</PopoverTrigger>
<PopoverContent className="w-72" onOpenAutoFocus={(e) => e.preventDefault()}>
<div className="flex flex-col gap-3">
<div className="text-sm font-medium">Rename site</div>
<div className="flex flex-col gap-2">
<Label htmlFor="pop-name">Display name</Label>
<Input id="pop-name" defaultValue="Cloudrite marketing" />
</div>
<Button size="sm">Save</Button>
</div>
</PopoverContent>
</Popover>
</div>
</Surface>
);
+19
View File
@@ -0,0 +1,19 @@
import { Process } from 'cloudrite';
// The four-step engagement timeline. Reveals on IntersectionObserver, which fires normally in the capture viewport.
//
// The section draws light-on-dark against the page background, which the preview
// card harness paints white — hence the surface wrapper. Transitions and
// animation delays are zeroed because the capture harness only waits on fonts
// and images, so an entrance animation would otherwise be screenshotted mid-flight.
const still = `*,*::before,*::after{
transition-duration:0s !important;transition-delay:0s !important;
animation-duration:0s !important;animation-delay:0s !important;
}`;
export const Default = () => (
<div className="bg-background text-foreground">
<style>{still}</style>
<Process />
</div>
);
+32
View File
@@ -0,0 +1,32 @@
import type { ReactNode } from 'react';
import { Progress } from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Values = () => (
<Surface>
<div className="flex w-80 flex-col gap-4">
<Progress value={0} />
<Progress value={35} />
<Progress value={72} />
<Progress value={100} />
</div>
</Surface>
);
export const Labelled = () => (
<Surface>
<div className="flex w-80 flex-col gap-2">
<div className="flex items-center justify-between text-sm">
<span>Migrating site files</span>
<span className="text-muted-foreground">72%</span>
</div>
<Progress value={72} />
</div>
</Surface>
);
+41
View File
@@ -0,0 +1,41 @@
import type { ReactNode } from 'react';
import { Label, RadioGroup, RadioGroupItem } from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Default = () => (
<Surface>
<RadioGroup defaultValue="business">
{[
['starter', 'Starter — 1 site'],
['business', 'Business — 10 sites'],
['vps', 'Dedicated VPS'],
].map(([value, label]) => (
<Label key={value} className="flex items-center gap-2 font-normal">
<RadioGroupItem value={value} />
{label}
</Label>
))}
</RadioGroup>
</Surface>
);
export const Disabled = () => (
<Surface>
<RadioGroup defaultValue="remote" disabled>
<Label className="flex items-center gap-2 font-normal">
<RadioGroupItem value="remote" />
Remote support
</Label>
<Label className="flex items-center gap-2 font-normal">
<RadioGroupItem value="onsite" />
On-site callout
</Label>
</RadioGroup>
</Surface>
);
+93
View File
@@ -0,0 +1,93 @@
import type { ReactNode } from 'react';
import {
Label,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
// The open state is the primary story: a closed trigger shows none of the
// compound parts, and SelectContent/SelectItem/SelectGroup/SelectLabel are the
// half of this API worth seeing.
export const Open = () => (
<Surface>
<Select defaultValue="hosting" defaultOpen>
<SelectTrigger className="w-64">
<SelectValue placeholder="Choose a service" />
</SelectTrigger>
<SelectContent>
<SelectItem value="web">Web Development</SelectItem>
<SelectItem value="hosting">Cloud Hosting</SelectItem>
<SelectItem value="support">I.T Support &amp; Repairs</SelectItem>
<SelectItem value="ai">AI Solutions</SelectItem>
</SelectContent>
</Select>
</Surface>
);
export const Default = () => (
<Surface>
<Select defaultValue="hosting">
<SelectTrigger className="w-64">
<SelectValue placeholder="Choose a service" />
</SelectTrigger>
<SelectContent>
<SelectItem value="web">Web Development</SelectItem>
<SelectItem value="hosting">Cloud Hosting</SelectItem>
<SelectItem value="support">I.T Support &amp; Repairs</SelectItem>
<SelectItem value="ai">AI Solutions</SelectItem>
</SelectContent>
</Select>
</Surface>
);
export const WithLabelAndGroups = () => (
<Surface>
<div className="flex w-64 flex-col gap-2">
<Label htmlFor="plan">Hosting plan</Label>
<Select>
<SelectTrigger id="plan" className="w-full">
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Shared</SelectLabel>
<SelectItem value="starter">Starter 1 site</SelectItem>
<SelectItem value="business">Business 10 sites</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Dedicated</SelectLabel>
<SelectItem value="vps">VPS</SelectItem>
<SelectItem value="game">Game server</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</Surface>
);
export const Disabled = () => (
<Surface>
<Select defaultValue="starter" disabled>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="starter">Starter 1 site</SelectItem>
</SelectContent>
</Select>
</Surface>
);
+32
View File
@@ -0,0 +1,32 @@
import type { ReactNode } from 'react';
import { Separator } from 'cloudrite';
// Cloudrite is dark-only — foreground-coloured text and `border-border` are
// invisible on the preview harness's white card body. Stories render on the
// brand surface, which is also how a real screen is built.
const Surface = ({ children }: { children: ReactNode }) => (
<div className="bg-background text-foreground rounded-lg p-6">{children}</div>
);
export const Horizontal = () => (
<Surface>
<div className="w-80">
<div className="text-sm font-medium">Cloud Hosting</div>
<div className="text-sm text-muted-foreground">Proudly hosted in New Zealand.</div>
<Separator className="my-4" />
<div className="text-sm text-muted-foreground">From $29/month</div>
</div>
</Surface>
);
export const Vertical = () => (
<Surface>
<div className="flex h-6 items-center gap-4 text-sm">
<span>Services</span>
<Separator orientation="vertical" />
<span>Hosting</span>
<Separator orientation="vertical" />
<span>Support</span>
</div>
</Surface>
);
+19
View File
@@ -0,0 +1,19 @@
import { Services } from 'cloudrite';
// The four Cloudrite service cards — Web Development, Cloud Hosting, I.T Support & Repairs, AI Solutions.
//
// 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>
<Services />
</div>
);
+42
View File
@@ -0,0 +1,42 @@
import type { ReactNode } from 'react';
import {
Button,
Input,
Label,
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} 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>
<Sheet defaultOpen modal={false}>
<SheetContent side="right" onOpenAutoFocus={(e) => e.preventDefault()}>
<SheetHeader>
<SheetTitle>Site settings</SheetTitle>
<SheetDescription>cloudrite.co.nz Business plan</SheetDescription>
</SheetHeader>
<div className="flex flex-col gap-2 px-4">
<Label htmlFor="sheet-alias">Domain alias</Label>
<Input id="sheet-alias" defaultValue="www.cloudrite.co.nz" />
</div>
<SheetFooter>
<Button>Save changes</Button>
<SheetClose asChild>
<Button variant="outline">Cancel</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
</Surface>
);
+31
View File
@@ -0,0 +1,31 @@
import type { ReactNode } from 'react';
import { Skeleton } 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 TextLines = () => (
<Surface>
<div className="flex w-80 flex-col gap-2">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
</div>
</Surface>
);
export const CardPlaceholder = () => (
<Surface>
<div className="flex w-80 items-center gap-4 rounded-xl border border-border p-4">
<Skeleton className="size-12 shrink-0 rounded-xl" />
<div className="flex w-full flex-col gap-2">
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-3 w-full" />
</div>
</div>
</Surface>
);
+33
View File
@@ -0,0 +1,33 @@
import type { ReactNode } from 'react';
import { Label, Slider } 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>
<Slider className="w-72" defaultValue={[60]} max={100} step={1} />
</Surface>
);
export const WithLabel = () => (
<Surface>
<div className="flex w-72 flex-col gap-3">
<div className="flex items-center justify-between text-sm">
<Label>Storage</Label>
<span className="text-muted-foreground">40 GB</span>
</div>
<Slider defaultValue={[40]} max={200} step={10} />
</div>
</Surface>
);
export const Range = () => (
<Surface>
<Slider className="w-72" defaultValue={[25, 75]} max={100} step={5} />
</Surface>
);
+29
View File
@@ -0,0 +1,29 @@
import type { ReactNode } from 'react';
import { Button, Spinner } 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 Sizes = () => (
<Surface>
<div className="flex items-center gap-6 text-foreground">
<Spinner />
<Spinner className="size-6" />
<Spinner className="size-10" />
<Spinner className="size-10 text-primary" />
</div>
</Surface>
);
export const InButton = () => (
<Surface>
<Button disabled>
<Spinner />
Provisioning server
</Button>
</Surface>
);
+39
View File
@@ -0,0 +1,39 @@
import type { ReactNode } from 'react';
import { Label, Switch } 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">
<Switch />
<Switch defaultChecked />
<Switch disabled />
<Switch defaultChecked disabled />
</div>
</Surface>
);
export const WithLabels = () => (
<Surface>
<div className="flex flex-col gap-4">
{[
['auto-updates', 'Automatic WordPress updates', true],
['maintenance', 'Maintenance mode', false],
['alerts', 'Downtime SMS alerts', true],
].map(([id, label, on]) => (
<div key={id as string} className="flex items-center justify-between gap-8">
<Label htmlFor={id as string} className="font-normal">
{label}
</Label>
<Switch id={id as string} defaultChecked={on as boolean} />
</div>
))}
</div>
</Surface>
);
+60
View File
@@ -0,0 +1,60 @@
import type { ReactNode } from 'react';
import {
Badge,
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} 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 invoices = [
['CR-1042', 'Managed WordPress', 'Paid', '$149.00'],
['CR-1041', 'Cloud Hosting — Business', 'Paid', '$89.00'],
['CR-1040', 'On-site callout (2 hrs)', 'Overdue', '$240.00'],
['CR-1039', 'Domain renewal', 'Paid', '$38.00'],
];
export const Default = () => (
<Surface>
<Table>
<TableCaption>Recent invoices</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Service</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map(([id, service, status, amount]) => (
<TableRow key={id}>
<TableCell className="font-medium">{id}</TableCell>
<TableCell>{service}</TableCell>
<TableCell>
<Badge variant={status === 'Paid' ? 'secondary' : 'destructive'}>{status}</Badge>
</TableCell>
<TableCell className="text-right">{amount}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total</TableCell>
<TableCell className="text-right">$516.00</TableCell>
</TableRow>
</TableFooter>
</Table>
</Surface>
);
+30
View File
@@ -0,0 +1,30 @@
import type { ReactNode } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } 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>
<Tabs defaultValue="overview" className="w-96">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="text-sm text-muted-foreground">
cloudrite.co.nz Business plan, Auckland region, 99.98% uptime this month.
</TabsContent>
<TabsContent value="backups" className="text-sm text-muted-foreground">
Last snapshot 03:00 NZST. 30 restore points retained.
</TabsContent>
<TabsContent value="domains" className="text-sm text-muted-foreground">
1 domain, auto-renewing 12 March 2027.
</TabsContent>
</Tabs>
</Surface>
);
+36
View File
@@ -0,0 +1,36 @@
import type { ReactNode } from 'react';
import { Label, 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>
);
export const Default = () => (
<Surface>
<Textarea className="w-80" placeholder="Tell us what you need help with…" />
</Surface>
);
export const WithLabel = () => (
<Surface>
<div className="flex w-80 flex-col gap-2">
<Label htmlFor="brief">Project brief</Label>
<Textarea
id="brief"
rows={4}
defaultValue={
'We run a small Auckland cafe and need our Squarespace site moved to NZ hosting, plus email set up on a custom domain.'
}
/>
</div>
</Surface>
);
export const Disabled = () => (
<Surface>
<Textarea className="w-80" disabled defaultValue="Submitted — we'll be in touch within one business day." />
</Surface>
);
+53
View File
@@ -0,0 +1,53 @@
import type { ReactNode } from 'react';
import { Toggle } from 'cloudrite';
import { Bold, Italic, Underline } 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 States = () => (
<Surface>
<div className="flex items-center gap-2">
<Toggle aria-label="Bold">
<Bold />
</Toggle>
<Toggle defaultPressed aria-label="Italic">
<Italic />
</Toggle>
<Toggle disabled aria-label="Underline">
<Underline />
</Toggle>
</div>
</Surface>
);
export const Outline = () => (
<Surface>
<div className="flex items-center gap-2">
<Toggle variant="outline">Auto-renew</Toggle>
<Toggle variant="outline" defaultPressed>
Maintenance mode
</Toggle>
</div>
</Surface>
);
export const Sizes = () => (
<Surface>
<div className="flex items-center gap-2">
<Toggle size="sm" defaultPressed aria-label="Small">
<Bold />
</Toggle>
<Toggle size="default" defaultPressed aria-label="Default">
<Bold />
</Toggle>
<Toggle size="lg" defaultPressed aria-label="Large">
<Bold />
</Toggle>
</div>
</Surface>
);
+28
View File
@@ -0,0 +1,28 @@
import type { ReactNode } from 'react';
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'cloudrite';
import { Info } 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>
);
// Tooltip must be inside TooltipProvider — outside one it throws rather than
// rendering, so the provider is part of every composition.
export const Open = () => (
<Surface>
<div className="flex h-40 items-end justify-center">
<TooltipProvider>
<Tooltip defaultOpen>
<TooltipTrigger asChild>
<Button variant="outline" size="icon" aria-label="About uptime">
<Info />
</Button>
</TooltipTrigger>
<TooltipContent side="top">Measured across all Auckland edge nodes</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</Surface>
);