@sableui/react 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +84 -0
  3. package/CONTRIBUTING.md +8 -0
  4. package/README.md +1 -0
  5. package/bin/sable-lint.mjs +0 -0
  6. package/bin/sable.mjs +68 -26
  7. package/dist/blocks/index.d.ts +396 -0
  8. package/dist/blocks/index.js +1997 -0
  9. package/dist/chunk-6LCIBMMG.js +4908 -0
  10. package/dist/{chunk-AOWBYEQ6.js → chunk-CKNM7KSB.js} +1598 -6464
  11. package/dist/{chunk-PAOZBZME.js → chunk-NGBDP3BM.js} +1 -1
  12. package/dist/{chunk-5Z5KLOAU.js → chunk-TH563WLW.js} +14 -1
  13. package/dist/chunk-WCFLQVK5.js +5097 -0
  14. package/dist/icon/index.js +2 -2
  15. package/dist/icon-sets/reicon.js +1 -1
  16. package/dist/index.d.ts +471 -34
  17. package/dist/index.js +588 -2589
  18. package/dist/ios/index.d.ts +8 -1
  19. package/dist/ios/index.js +6 -5
  20. package/dist/ios/preview.d.ts +11 -1
  21. package/dist/ios/preview.js +13 -8
  22. package/dist/ios/styles.css +14 -0
  23. package/dist/{meta-6dUzgC6M.d.ts → meta-CcE_SeFu.d.ts} +37 -1
  24. package/dist/patterns/index.d.ts +1 -1
  25. package/dist/patterns/index.js +29 -30
  26. package/dist/presets/index.js +10 -1
  27. package/dist/themes/base/base.css +23 -0
  28. package/dist/themes/index.js +2 -2
  29. package/llms/blocks/api-keys-block.md +79 -0
  30. package/llms/blocks/chat-thread-block.md +51 -0
  31. package/llms/blocks/command-palette-block.md +48 -0
  32. package/llms/blocks/cta-band-block.md +62 -0
  33. package/llms/blocks/danger-zone-block.md +69 -0
  34. package/llms/blocks/device-comparison-block.md +91 -0
  35. package/llms/blocks/event-card-block.md +84 -0
  36. package/llms/blocks/faq-block.md +72 -0
  37. package/llms/blocks/feature-grid-block.md +75 -0
  38. package/llms/blocks/feedback-form-block.md +66 -0
  39. package/llms/blocks/goal-progress-block.md +54 -0
  40. package/llms/blocks/hero-block.md +65 -0
  41. package/llms/blocks/invite-team-block.md +61 -0
  42. package/llms/blocks/invoice-history-block.md +84 -0
  43. package/llms/blocks/kpi-sparkline-block.md +57 -0
  44. package/llms/blocks/media-player-block.md +100 -0
  45. package/llms/blocks/newsletter-block.md +64 -0
  46. package/llms/blocks/not-found-block.md +56 -0
  47. package/llms/blocks/notification-list-block.md +63 -0
  48. package/llms/blocks/notification-settings-block.md +54 -0
  49. package/llms/blocks/order-tracking-block.md +62 -0
  50. package/llms/blocks/pricing-card-block.md +82 -0
  51. package/llms/blocks/pricing-table-block.md +129 -0
  52. package/llms/blocks/product-card-block.md +69 -0
  53. package/llms/blocks/profile-card-block.md +103 -0
  54. package/llms/blocks/profile-form-block.md +114 -0
  55. package/llms/blocks/revenue-breakdown-block.md +70 -0
  56. package/llms/blocks/sign-in-block.md +64 -0
  57. package/llms/blocks/sign-up-block.md +78 -0
  58. package/llms/blocks/site-footer-block.md +96 -0
  59. package/llms/blocks/stat-row-block.md +59 -0
  60. package/llms/blocks/steps-block.md +73 -0
  61. package/llms/blocks/task-list-block.md +81 -0
  62. package/llms/blocks/team-members-block.md +91 -0
  63. package/llms/blocks/testimonial-block.md +78 -0
  64. package/llms/blocks/traffic-sources-block.md +63 -0
  65. package/llms/blocks/traffic-trend-block.md +95 -0
  66. package/llms/blocks/upload-list-block.md +66 -0
  67. package/llms/blocks/verify-code-block.md +59 -0
  68. package/llms/blocks/wallet-block.md +84 -0
  69. package/llms/components/area-chart.md +112 -0
  70. package/llms/components/bar-chart.md +131 -0
  71. package/llms/components/button.md +1 -1
  72. package/llms/components/card.md +2 -2
  73. package/llms/components/context-menu.md +1 -1
  74. package/llms/components/date-tile.md +79 -0
  75. package/llms/components/entity.md +4 -2
  76. package/llms/components/line-chart.md +101 -0
  77. package/llms/components/menu.md +1 -1
  78. package/llms/components/{dots-menu.md → overflow-menu.md} +9 -9
  79. package/llms/components/pie-chart.md +103 -0
  80. package/llms/components/radar-chart.md +106 -0
  81. package/llms/components/radial-chart.md +102 -0
  82. package/llms/components/segmented-bar.md +102 -0
  83. package/llms/components/settings-menu.md +2 -2
  84. package/llms/components/sparkline.md +80 -0
  85. package/llms/components/table.md +1 -1
  86. package/llms/patterns/auth-page.md +2 -2
  87. package/llms/patterns/dashboard-page.md +6 -7
  88. package/llms/patterns/feed-page.md +7 -7
  89. package/llms/patterns/list-detail-page.md +7 -15
  90. package/llms/patterns/settings-page.md +10 -21
  91. package/llms/tasks/building-an-ios-prototype.md +4 -1
  92. package/llms/tasks/choosing-a-component.md +2 -2
  93. package/llms-full.txt +4296 -277
  94. package/llms.txt +55 -3
  95. package/package.json +5 -1
  96. package/registry.json +6383 -3741
  97. package/targets/ios.json +90 -9
  98. package/dist/{chunk-L6Y46C62.js → chunk-MXJX3NZI.js} +1 -1
@@ -0,0 +1,78 @@
1
+ # SignUpBlock
2
+
3
+ A sign-up card: name, work email and password as labelled Fields, one full-width Create account, and a link to sign in instead.
4
+
5
+ **Intent:** Reach for this to create an account inside a surface you already have, or as the form of a sign-up route. For the whole route with brand art beside it, start from the AuthPage page shell and drop this card in.
6
+
7
+ **Block:** Auth · card · designed for 18rem–24rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - Visible labels, not placeholders: someone filling a new form needs to see what each field is after typing in it.
12
+ - The password rule is stated up front in the Field description, before anyone fails it.
13
+ - `onSignUp` receives the fields only after the browser's own validation (required, type=email, minLength) passes.
14
+
15
+ ## Accessibility
16
+
17
+ - Field wires each label, description and error to its input; never hand-roll <label> + <input> pairs.
18
+ - autoComplete hints (name, email, new-password) let password managers offer to generate and save the password.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, Field, Form, Input } from "@sableui/react"
24
+
25
+ export interface SignUpBlockProps {
26
+ className?: string
27
+ /** false drops the card's surface and border — for a Sheet, Dialog or muted section that already frames it. */
28
+ framed?: boolean
29
+ /** Called with the submitted fields once the browser's validation passes. */
30
+ onSignUp?: (account: { name: string; email: string; password: string }) => void
31
+ }
32
+
33
+ export function SignUpBlock({ className, framed = true, onSignUp }: SignUpBlockProps) {
34
+ return (
35
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-sm", className)}>
36
+ <CardHeader>
37
+ <CardTitle>Create your account</CardTitle>
38
+ <CardDescription>Free for 14 days. No card needed.</CardDescription>
39
+ </CardHeader>
40
+ <Form
41
+ className="gap-0"
42
+ onSubmit={(event) => {
43
+ event.preventDefault()
44
+ const data = new FormData(event.currentTarget)
45
+ onSignUp?.({
46
+ name: String(data.get("name") ?? ""),
47
+ email: String(data.get("email") ?? ""),
48
+ password: String(data.get("password") ?? ""),
49
+ })
50
+ }}
51
+ >
52
+ <CardContent className="flex flex-col gap-3">
53
+ <Field label="Name" name="name" required>
54
+ <Input autoComplete="name" />
55
+ </Field>
56
+ <Field label="Work email" name="email" required>
57
+ <Input type="email" autoComplete="email" placeholder="you@studio.com" />
58
+ </Field>
59
+ <Field label="Password" name="password" description="At least 12 characters." required>
60
+ <Input type="password" autoComplete="new-password" minLength={12} />
61
+ </Field>
62
+ </CardContent>
63
+ <CardFooter className="flex-col items-stretch gap-3">
64
+ <Button type="submit" className="w-full">
65
+ Create account
66
+ </Button>
67
+ <p className="text-center text-caption text-muted-foreground">
68
+ Already have an account?{" "}
69
+ <a href="/sign-in" className="text-foreground underline underline-offset-4 motion-interactive hover:text-muted-foreground">
70
+ Sign in
71
+ </a>
72
+ </p>
73
+ </CardFooter>
74
+ </Form>
75
+ </Card>
76
+ )
77
+ }
78
+ ```
@@ -0,0 +1,96 @@
1
+ # SiteFooterBlock
2
+
3
+ A site footer: the brand and a one-line tagline beside link groups in a labelled Footer nav, over a copyright line.
4
+
5
+ **Intent:** Reach for this at the foot of a marketing or docs site. App screens inside an AppShell take no footer; their navigation lives in the sidebar.
6
+
7
+ **Block:** Marketing · section
8
+
9
+ ## Notes
10
+
11
+ - Copied from the LandingPage page shell's footer.
12
+ - Every link is at least 36px tall (min-h-9), so the dense list is still an easy touch target.
13
+ - Groups go two up on a phone and three up from sm; add a group only if it has three links or more.
14
+
15
+ ## Accessibility
16
+
17
+ - Renders <footer> with a <nav aria-label="Footer">, distinct from the page's main navigation.
18
+ - Group titles are h2s, so the footer's structure is navigable by heading.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { cn, Region, Stack } from "@sableui/react"
24
+
25
+ const brand = { name: "Sable", tagline: "React components, patterns, and tokens for products built with AI agents." }
26
+
27
+ const groups = [
28
+ {
29
+ title: "Product",
30
+ links: [
31
+ { label: "Components", href: "/components" },
32
+ { label: "Patterns", href: "/patterns" },
33
+ { label: "Themes", href: "/themes" },
34
+ { label: "Canvas", href: "/canvas" },
35
+ ],
36
+ },
37
+ {
38
+ title: "Resources",
39
+ links: [
40
+ { label: "Documentation", href: "/docs" },
41
+ { label: "Changelog", href: "/changelog" },
42
+ { label: "llms.txt", href: "/llms.txt" },
43
+ ],
44
+ },
45
+ {
46
+ title: "Community",
47
+ links: [
48
+ { label: "GitHub", href: "/github" },
49
+ { label: "npm", href: "/npm" },
50
+ { label: "Discussions", href: "/discussions" },
51
+ ],
52
+ },
53
+ ]
54
+
55
+ export interface SiteFooterBlockProps {
56
+ className?: string
57
+ }
58
+
59
+ export function SiteFooterBlock({ className }: SiteFooterBlockProps) {
60
+ return (
61
+ <footer className={cn("w-full border-t border-border", className)}>
62
+ <Region max="wide" className="py-12">
63
+ <div className="grid gap-10 md:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]">
64
+ <Stack gap={3} className="max-w-xs">
65
+ <a href="/" className="w-fit text-title-lg motion-interactive hover:text-muted-foreground">
66
+ {brand.name}
67
+ </a>
68
+ <p className="text-body-sm text-muted-foreground">{brand.tagline}</p>
69
+ </Stack>
70
+ <nav aria-label="Footer" className="grid grid-cols-2 gap-8 sm:grid-cols-3">
71
+ {groups.map((group) => (
72
+ <div key={group.title}>
73
+ <h2 className="text-label-md text-muted-foreground">{group.title}</h2>
74
+ <ul className="mt-3 flex flex-col gap-1">
75
+ {group.links.map((link) => (
76
+ <li key={link.label}>
77
+ <a href={link.href} className="flex min-h-9 items-center text-body-sm motion-interactive hover:text-muted-foreground">
78
+ {link.label}
79
+ </a>
80
+ </li>
81
+ ))}
82
+ </ul>
83
+ </div>
84
+ ))}
85
+ </nav>
86
+ </div>
87
+ <div className="mt-12 border-t border-border pt-6">
88
+ <p className="text-caption text-muted-foreground">
89
+ © 2026 {brand.name}. Released under the MIT license.
90
+ </p>
91
+ </div>
92
+ </Region>
93
+ </footer>
94
+ )
95
+ }
96
+ ```
@@ -0,0 +1,59 @@
1
+ # StatRowBlock
2
+
3
+ A KPI row: four stat cards, each a label, the value in the metric mono tier, and a trend Badge with its comparison note — 2 × 2 on a phone, one row from xl.
4
+
5
+ **Intent:** Reach for this at the top of a dashboard or report, above the tables and charts it summarises. One headline number worth splitting into categories is RevenueBreakdownBlock.
6
+
7
+ **Block:** Dashboard · section
8
+
9
+ ## Notes
10
+
11
+ - Copied from the DashboardPage page shell's stat row.
12
+ - Values are `text-mono-xl` — tabular figures, never a heading tier — so the four numbers align digit for digit.
13
+ - The trend Badge sits with its note in CardContent, so the value keeps the full card width when the row folds to 2 × 2.
14
+ - Keep it to four. A fifth stat is a sign one of them belongs in a table.
15
+
16
+ ## Accessibility
17
+
18
+ - The trend is stated in text (+8.2%) with an arrow icon; the tone is reinforcement, not the signal.
19
+ - Each card's label is read before its value, in source order.
20
+
21
+ ## Source
22
+
23
+ ```tsx
24
+ import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, Grid, Icon, Row } from "@sableui/react"
25
+
26
+ const stats = [
27
+ { label: "Revenue", value: "$18,240", delta: "+8.2%", up: true, note: "vs. $16,857" },
28
+ { label: "Collects", value: "1,284", delta: "+3.1%", up: true, note: "182 first-time" },
29
+ { label: "Collectors", value: "312", delta: "+12.4%", up: true, note: "Best month yet" },
30
+ { label: "Sell-through", value: "64%", delta: "−1.8%", up: false, note: "6 open editions" },
31
+ ]
32
+
33
+ export interface StatRowBlockProps {
34
+ className?: string
35
+ }
36
+
37
+ export function StatRowBlock({ className }: StatRowBlockProps) {
38
+ return (
39
+ <Grid cols={{ base: 2, xl: 4 }} gap={3} className={cn("w-full", className)}>
40
+ {stats.map((stat) => (
41
+ <Card key={stat.label}>
42
+ <CardHeader>
43
+ <CardDescription>{stat.label}</CardDescription>
44
+ <CardTitle className="text-mono-xl">{stat.value}</CardTitle>
45
+ </CardHeader>
46
+ <CardContent>
47
+ <Row gap={1} wrap>
48
+ <Badge tone={stat.up ? "success" : "warning"} size="sm" icon={<Icon name={stat.up ? "arrow-up" : "arrow-down"} />}>
49
+ {stat.delta}
50
+ </Badge>
51
+ <span className="text-caption text-muted-foreground">{stat.note}</span>
52
+ </Row>
53
+ </CardContent>
54
+ </Card>
55
+ ))}
56
+ </Grid>
57
+ )
58
+ }
59
+ ```
@@ -0,0 +1,73 @@
1
+ # StepsBlock
2
+
3
+ A how-it-works section: the heading, lede and a guide link on the left; three numbered steps on the right, each under a strong rule.
4
+
5
+ **Intent:** Reach for this when the pitch is a sequence — onboarding, a workflow, a setup. Parallel benefits with no order are FeatureGridBlock; progress through a live process is a Stepper.
6
+
7
+ **Block:** Marketing · section
8
+
9
+ ## Notes
10
+
11
+ - Copied from the BrandLandingPage page shell's how-it-works section.
12
+ - The steps are an <ol>, so the order is semantic and the 01 / 02 / 03 numbers are its visual echo, set in the mono tier.
13
+ - Three steps fit the 1 : 2 split on desktop; on a phone everything stacks with the intro first.
14
+
15
+ ## Accessibility
16
+
17
+ - Each step has an h3 under the section's h2.
18
+ - The guide link is an <a> styled as an outline Button, because it navigates.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { useId } from "react"
24
+ import { Button, cn, Icon, Region, Stack } from "@sableui/react"
25
+
26
+ const intro = {
27
+ eyebrow: "How it works",
28
+ title: "From request to sealed record.",
29
+ lede: "Teams are usually live within a week. Cove adapts to the routes you already run instead of asking you to start over.",
30
+ link: { label: "Read the rollout guide", href: "/guides/rollout" },
31
+ }
32
+
33
+ const steps = [
34
+ { title: "Connect your spaces", body: "Bring teams, vendors, and record stores into one workspace, with roles that mirror how you already work." },
35
+ { title: "Route every approval", body: "Requests follow the path you define, with reviewers, deadlines, and escalation built in from the start." },
36
+ { title: "Seal the record", body: "Approved work is archived with its full context and is ready for review the moment it closes." },
37
+ ]
38
+
39
+ export interface StepsBlockProps {
40
+ className?: string
41
+ }
42
+
43
+ export function StepsBlock({ className }: StepsBlockProps) {
44
+ const titleId = useId()
45
+ return (
46
+ <Region as="section" aria-labelledby={titleId} max="wide" className={cn("w-full py-16 sm:py-24", className)}>
47
+ <div className="grid gap-12 lg:grid-cols-[1fr_2fr] lg:gap-16">
48
+ <Stack gap={2} className="max-w-md">
49
+ <p className="text-overline text-muted-foreground">{intro.eyebrow}</p>
50
+ <h2 id={titleId} className="text-balance text-heading-1">
51
+ {intro.title}
52
+ </h2>
53
+ <p className="text-body-md text-muted-foreground">{intro.lede}</p>
54
+ <div className="pt-3">
55
+ <Button variant="outline" iconEnd={<Icon name="arrow-right" />} render={<a href={intro.link.href} />}>
56
+ {intro.link.label}
57
+ </Button>
58
+ </div>
59
+ </Stack>
60
+ <ol className="grid gap-10 sm:grid-cols-3 sm:gap-6">
61
+ {steps.map((step, index) => (
62
+ <li key={step.title} className="flex flex-col gap-2 border-t border-border-strong pt-6">
63
+ <span className="text-mono-sm text-muted-foreground">{String(index + 1).padStart(2, "0")}</span>
64
+ <h3 className="mt-3 text-title-lg sm:mt-6">{step.title}</h3>
65
+ <p className="text-body-sm text-muted-foreground">{step.body}</p>
66
+ </li>
67
+ ))}
68
+ </ol>
69
+ </div>
70
+ </Region>
71
+ )
72
+ }
73
+ ```
@@ -0,0 +1,81 @@
1
+ # TaskListBlock
2
+
3
+ A checklist card: a titled list of Checkbox rows with a category Badge each, a live count of what is left, and a quiet Add task action.
4
+
5
+ **Intent:** Reach for this for a short, personal to-do list on a dashboard or in onboarding. A team's task board with assignees and status columns needs a Table or DataTable.
6
+
7
+ **Block:** Dashboard · card · designed for 18rem–20rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - Finishing a task strikes it through and mutes it rather than removing it, so the list does not jump under the pointer.
12
+ - The count in CardAction updates with every check, so progress reads without counting rows.
13
+ - Add task is a ghost Button in the footer — present, but quieter than the list.
14
+
15
+ ## Accessibility
16
+
17
+ - Every Checkbox has a real <Label htmlFor>, so the whole title is the click target and the accessible name.
18
+ - Category Badges carry text; the tone only groups them visually.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { useState, useId } from "react"
24
+ import { Badge, Button, Card, CardAction, CardContent, CardFooter, CardHeader, CardTitle, Checkbox, cn, Icon, Label } from "@sableui/react"
25
+
26
+ const tasks = [
27
+ { title: "Pick the cover type", tag: "Design", tone: "collectible", done: true },
28
+ { title: "Wire up checkout", tag: "Engineering", tone: "info", done: false },
29
+ { title: "Draft launch post", tag: "Launch", tone: "edition", done: false },
30
+ ] as const
31
+
32
+ export interface TaskListBlockProps {
33
+ className?: string
34
+ /** false drops the card's surface and border — for a muted section that already frames it. */
35
+ framed?: boolean
36
+ onAddTask?: () => void
37
+ }
38
+
39
+ export function TaskListBlock({ className, framed = true, onAddTask }: TaskListBlockProps) {
40
+ const [done, setDone] = useState<boolean[]>(tasks.map((t) => t.done))
41
+ const id = useId()
42
+ const left = done.filter((d) => !d).length
43
+ return (
44
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs", className)}>
45
+ <CardHeader>
46
+ <CardTitle>This week</CardTitle>
47
+ <CardAction>
48
+ <span className="text-caption text-muted-foreground">{left} left</span>
49
+ </CardAction>
50
+ </CardHeader>
51
+ <CardContent>
52
+ <ul className="flex flex-col gap-1">
53
+ {tasks.map((task, i) => (
54
+ <li key={task.title} className="flex items-center gap-3 py-1.5">
55
+ <Checkbox
56
+ id={`${id}-${i}`}
57
+ checked={done[i]}
58
+ onCheckedChange={(v: boolean) => setDone(done.map((d, j) => (j === i ? v : d)))}
59
+ />
60
+ <Label
61
+ htmlFor={`${id}-${i}`}
62
+ className={cn("min-w-0 flex-1 motion-interactive", done[i] && "text-muted-foreground line-through")}
63
+ >
64
+ {task.title}
65
+ </Label>
66
+ <Badge tone={task.tone} size="sm">
67
+ {task.tag}
68
+ </Badge>
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ </CardContent>
73
+ <CardFooter>
74
+ <Button variant="ghost" size="sm" iconStart={<Icon name="plus" />} onClick={onAddTask}>
75
+ Add task
76
+ </Button>
77
+ </CardFooter>
78
+ </Card>
79
+ )
80
+ }
81
+ ```
@@ -0,0 +1,91 @@
1
+ # TeamMembersBlock
2
+
3
+ A members card: Entity rows with avatar, name and email, a role Badge, and an OverflowMenu of role changes and removal; pending invites sit in the same list.
4
+
5
+ **Intent:** Reach for this to manage who is in a workspace, project or team. For inviting people when the list is empty or short, InviteTeamBlock is the lighter shape; for hundreds of members with filters, use a DataTable.
6
+
7
+ **Block:** Settings & team · card · designed for 20rem–42rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - Each row's menu offers only what applies to it: Make admin or Make member, Resend invite for pending rows, then the destructive action after a separator.
12
+ - The owner has no menu — ownership is transferred elsewhere — and a control-sized spacer keeps the role badges aligned.
13
+ - Pending invites are outline Badges in the same list, so the team's full size reads in one place.
14
+
15
+ ## Accessibility
16
+
17
+ - Each OverflowMenu is labelled with the member's name ("Manage Mara Okafor"), so the triggers are distinguishable in a list.
18
+ - Roles are text in a Badge; tone only distinguishes the owner.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Avatar, Badge, Button, Card, CardAction, CardDescription, CardHeader, CardList, CardTitle, cn, Entity, Icon, MenuItem, MenuSeparator, OverflowMenu } from "@sableui/react"
24
+
25
+ const members = [
26
+ { id: "u1", name: "Ada Lovelace", initials: "AL", email: "ada@studio.com", role: "Owner" },
27
+ { id: "u2", name: "Mara Okafor", initials: "MO", email: "mara@studio.com", role: "Admin" },
28
+ { id: "u3", name: "Sam Rivera", initials: "SR", email: "sam@studio.com", role: "Member" },
29
+ { id: "u4", name: "jun@studio.com", initials: "J", email: "Invited 2 days ago", role: "Pending" },
30
+ ]
31
+
32
+ export interface TeamMembersBlockProps {
33
+ className?: string
34
+ /** false drops the card's surface and border — for a muted section that already frames it. */
35
+ framed?: boolean
36
+ onInvite?: () => void
37
+ /** Called with the member's id and the action chosen from their menu. */
38
+ onMemberAction?: (id: string, action: "make-admin" | "make-member" | "resend" | "remove") => void
39
+ }
40
+
41
+ export function TeamMembersBlock({ className, framed = true, onInvite, onMemberAction }: TeamMembersBlockProps) {
42
+ return (
43
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-2xl", className)}>
44
+ <CardHeader>
45
+ <CardTitle render={<h2 />}>Members</CardTitle>
46
+ <CardDescription>{members.length} people in this workspace.</CardDescription>
47
+ <CardAction>
48
+ <Button size="sm" iconStart={<Icon name="plus" />} onClick={onInvite}>
49
+ Invite
50
+ </Button>
51
+ </CardAction>
52
+ </CardHeader>
53
+ <CardList>
54
+ {members.map((m) => (
55
+ <Entity
56
+ key={m.id}
57
+ leading={<Avatar size="sm" fallback={m.initials} alt={m.name} />}
58
+ title={m.name}
59
+ subtitle={m.email}
60
+ action={
61
+ <div className="flex items-center gap-2">
62
+ <Badge appearance={m.role === "Pending" ? "outline" : "plain"} tone={m.role === "Owner" ? "edition" : "neutral"} size="sm">
63
+ {m.role}
64
+ </Badge>
65
+ {/* The owner is not managed from here; a placeholder keeps the column aligned. */}
66
+ {m.role === "Owner" ? (
67
+ <span aria-hidden className="size-(--control-sm)" />
68
+ ) : (
69
+ <OverflowMenu label={`Manage ${m.name}`} size="icon-sm">
70
+ {m.role === "Pending" ? (
71
+ <MenuItem onClick={() => onMemberAction?.(m.id, "resend")}>Resend invite</MenuItem>
72
+ ) : m.role === "Admin" ? (
73
+ <MenuItem onClick={() => onMemberAction?.(m.id, "make-member")}>Make member</MenuItem>
74
+ ) : (
75
+ <MenuItem onClick={() => onMemberAction?.(m.id, "make-admin")}>Make admin</MenuItem>
76
+ )}
77
+ <MenuSeparator />
78
+ <MenuItem variant="destructive" onClick={() => onMemberAction?.(m.id, "remove")}>
79
+ {m.role === "Pending" ? "Cancel invite" : "Remove from workspace"}
80
+ </MenuItem>
81
+ </OverflowMenu>
82
+ )}
83
+ </div>
84
+ }
85
+ />
86
+ ))}
87
+ </CardList>
88
+ </Card>
89
+ )
90
+ }
91
+ ```
@@ -0,0 +1,78 @@
1
+ # TestimonialBlock
2
+
3
+ A customer story: one large blockquote with the person's avatar, name and role, beside three outcome figures separated by rules.
4
+
5
+ **Intent:** Reach for this for one strong, attributable quote backed by numbers. A wall of short quotes is a different section; a logo row is Marquee.
6
+
7
+ **Block:** Marketing · section
8
+
9
+ ## Notes
10
+
11
+ - Copied from the BrandLandingPage page shell's customer story.
12
+ - The quote and the outcomes share one Card as a <figure>; the outcomes move under the quote below lg.
13
+ - Outcome values use display sizes because they are the proof; their labels say what each number counts.
14
+
15
+ ## Accessibility
16
+
17
+ - <figure> + <blockquote> + <figcaption> tie the quote to the person who said it.
18
+ - The section is labelled "Customer story" so it is reachable as a landmark.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Avatar, Card, CardContent, cn, Region, Separator, Stack } from "@sableui/react"
24
+
25
+ const story = {
26
+ quote: "We used to chase approvals across four tools. Now every request has an owner, a deadline, and a record, and our auditors stopped asking for screenshots.",
27
+ name: "Maya Okafor",
28
+ initials: "MO",
29
+ role: "Head of operations, Fielddesk",
30
+ }
31
+
32
+ const outcomes = [
33
+ { value: "3.2×", label: "faster approvals in the first quarter" },
34
+ { value: "68%", label: "fewer follow-ups on handoffs" },
35
+ { value: "1 day", label: "to prepare a quarterly audit" },
36
+ ]
37
+
38
+ export interface TestimonialBlockProps {
39
+ className?: string
40
+ }
41
+
42
+ export function TestimonialBlock({ className }: TestimonialBlockProps) {
43
+ return (
44
+ <Region as="section" aria-label="Customer story" max="wide" className={cn("w-full py-16", className)}>
45
+ <Card render={<figure />}>
46
+ <div className="grid lg:grid-cols-[1.5fr_1fr]">
47
+ <CardContent className="flex flex-col justify-between gap-10 sm:p-10">
48
+ <Stack gap={3}>
49
+ <p className="text-overline text-muted-foreground">Customer story</p>
50
+ <blockquote className="text-balance text-heading-3 sm:text-heading-2">“{story.quote}”</blockquote>
51
+ </Stack>
52
+ <figcaption className="flex items-center gap-3">
53
+ <Avatar size="lg" fallback={story.initials} alt={story.name} />
54
+ <div>
55
+ <p className="text-title-sm">{story.name}</p>
56
+ <p className="text-body-sm text-muted-foreground">{story.role}</p>
57
+ </div>
58
+ </figcaption>
59
+ </CardContent>
60
+ <div className="border-border max-lg:border-t lg:border-s">
61
+ <CardContent className="flex h-full flex-col justify-center gap-6 sm:p-10">
62
+ {outcomes.map((outcome, index) => (
63
+ <Stack key={outcome.label} gap={3}>
64
+ {index > 0 && <Separator />}
65
+ <div>
66
+ <p className="text-heading-1 sm:text-display-lg">{outcome.value}</p>
67
+ <p className="mt-1 text-body-sm text-muted-foreground">{outcome.label}</p>
68
+ </div>
69
+ </Stack>
70
+ ))}
71
+ </CardContent>
72
+ </div>
73
+ </div>
74
+ </Card>
75
+ </Region>
76
+ )
77
+ }
78
+ ```
@@ -0,0 +1,63 @@
1
+ # TrafficSourcesBlock
2
+
3
+ A share card: a donut PieChart of where a total comes from, the total in the hole, a legend, and the headline share in words in the footer.
4
+
5
+ **Intent:** Reach for this for a split of one total into four or fewer parts — traffic sources, revenue by plan, tickets by channel. More parts, or values to compare exactly, belong in a horizontal BarChart.
6
+
7
+ **Block:** Dashboard · card · designed for 18rem–24rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - The total sits in the donut's hole, so the card answers "how many" and "from where" at once.
12
+ - The footer puts the biggest share in a sentence — the takeaway does not depend on reading the slices.
13
+ - The slices sweep in clockwise as the card scrolls into view.
14
+
15
+ ## Accessibility
16
+
17
+ - The chart's data table lists every source and its count; the legend repeats it visually.
18
+ - Hovering or arrowing through the slices pulls each out and announces it.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, PieChart } from "@sableui/react"
24
+
25
+ const sources = [
26
+ { source: "Search", visitors: 4280 },
27
+ { source: "Direct", visitors: 2910 },
28
+ { source: "Social", visitors: 1840 },
29
+ { source: "Email", visitors: 960 },
30
+ ]
31
+
32
+ const total = sources.reduce((sum, s) => sum + s.visitors, 0)
33
+
34
+ export interface TrafficSourcesBlockProps {
35
+ className?: string
36
+ /** false drops the card's surface and border — for a muted section that already frames it. */
37
+ framed?: boolean
38
+ }
39
+
40
+ export function TrafficSourcesBlock({ className, framed = true }: TrafficSourcesBlockProps) {
41
+ return (
42
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-sm", className)}>
43
+ <CardHeader>
44
+ <CardTitle render={<h2 />}>Traffic sources</CardTitle>
45
+ <CardDescription>Where this month's visitors came from.</CardDescription>
46
+ </CardHeader>
47
+ <CardContent>
48
+ <PieChart
49
+ label="Visitors by source, this month"
50
+ data={sources}
51
+ index="source"
52
+ valueKey="visitors"
53
+ donut
54
+ centerLabel={{ value: total.toLocaleString("en-US"), label: "Visitors" }}
55
+ />
56
+ </CardContent>
57
+ <CardFooter className="text-body-sm text-muted-foreground">
58
+ Search brings {Math.round((sources[0].visitors / total) * 100)}% of visitors.
59
+ </CardFooter>
60
+ </Card>
61
+ )
62
+ }
63
+ ```