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,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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
Reference in New Issue
Block a user