@sableui/react 0.1.1 → 0.3.0

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 (137) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +120 -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/{_contract-CYScOdf5.d.ts → _contract-o3tUc8Ig.d.ts} +1 -1
  8. package/dist/blocks/index.d.ts +396 -0
  9. package/dist/blocks/index.js +1997 -0
  10. package/dist/chunk-6LCIBMMG.js +4908 -0
  11. package/dist/{chunk-J23ELQQZ.js → chunk-CKNM7KSB.js} +1664 -4270
  12. package/dist/chunk-K6LII3OE.js +5093 -0
  13. package/dist/{chunk-RWP4XBSK.js → chunk-KFZ43RKU.js} +1 -1
  14. package/dist/{chunk-PAOZBZME.js → chunk-NGBDP3BM.js} +1 -1
  15. package/dist/{chunk-5Z5KLOAU.js → chunk-TH563WLW.js} +14 -1
  16. package/dist/{chunk-JI7WJP2L.js → chunk-X276MF2W.js} +5 -0
  17. package/dist/foundations/index.js +1 -1
  18. package/dist/icon/index.js +2 -2
  19. package/dist/icon-sets/reicon.js +1 -1
  20. package/dist/index.d.ts +624 -133
  21. package/dist/index.js +767 -4964
  22. package/dist/ios/index.d.ts +14 -2
  23. package/dist/ios/index.js +10 -9
  24. package/dist/ios/preview.d.ts +11 -1
  25. package/dist/ios/preview.js +14 -9
  26. package/dist/ios/styles.css +26 -6
  27. package/dist/{meta-6dUzgC6M.d.ts → meta-CcE_SeFu.d.ts} +37 -1
  28. package/dist/patterns/index.d.ts +1 -1
  29. package/dist/patterns/index.js +1684 -471
  30. package/dist/presets/index.d.ts +1 -1
  31. package/dist/presets/index.js +1 -1
  32. package/dist/styles.css +238 -40
  33. package/dist/{theme-provider-BWcizhAt.d.ts → theme-provider-BE-KqUG-.d.ts} +1 -1
  34. package/dist/themes/atelier/atelier.css +3 -0
  35. package/dist/themes/atelier/index.d.ts +1 -1
  36. package/dist/themes/base/base.css +34 -4
  37. package/dist/themes/base/index.d.ts +1 -1
  38. package/dist/themes/ember/ember.css +3 -0
  39. package/dist/themes/ember/index.d.ts +1 -1
  40. package/dist/themes/index.d.ts +2 -2
  41. package/dist/themes/index.js +2 -2
  42. package/dist/themes/meridian/index.d.ts +1 -1
  43. package/dist/themes/meridian/meridian.css +10 -5
  44. package/dist/themes/poppy/index.d.ts +1 -1
  45. package/dist/themes/poppy/poppy.css +16 -0
  46. package/dist/themes/prism/index.d.ts +1 -1
  47. package/dist/themes/prism/prism.css +4 -1
  48. package/dist/themes/verdant/index.d.ts +1 -1
  49. package/dist/themes/verdant/verdant.css +7 -2
  50. package/llms/blocks/api-keys-block.md +79 -0
  51. package/llms/blocks/chat-thread-block.md +51 -0
  52. package/llms/blocks/command-palette-block.md +48 -0
  53. package/llms/blocks/cta-band-block.md +62 -0
  54. package/llms/blocks/danger-zone-block.md +69 -0
  55. package/llms/blocks/device-comparison-block.md +91 -0
  56. package/llms/blocks/event-card-block.md +84 -0
  57. package/llms/blocks/faq-block.md +72 -0
  58. package/llms/blocks/feature-grid-block.md +75 -0
  59. package/llms/blocks/feedback-form-block.md +66 -0
  60. package/llms/blocks/goal-progress-block.md +54 -0
  61. package/llms/blocks/hero-block.md +65 -0
  62. package/llms/blocks/invite-team-block.md +61 -0
  63. package/llms/blocks/invoice-history-block.md +84 -0
  64. package/llms/blocks/kpi-sparkline-block.md +57 -0
  65. package/llms/blocks/media-player-block.md +100 -0
  66. package/llms/blocks/newsletter-block.md +64 -0
  67. package/llms/blocks/not-found-block.md +56 -0
  68. package/llms/blocks/notification-list-block.md +63 -0
  69. package/llms/blocks/notification-settings-block.md +54 -0
  70. package/llms/blocks/order-tracking-block.md +62 -0
  71. package/llms/blocks/pricing-card-block.md +82 -0
  72. package/llms/blocks/pricing-table-block.md +129 -0
  73. package/llms/blocks/product-card-block.md +69 -0
  74. package/llms/blocks/profile-card-block.md +103 -0
  75. package/llms/blocks/profile-form-block.md +114 -0
  76. package/llms/blocks/revenue-breakdown-block.md +70 -0
  77. package/llms/blocks/sign-in-block.md +64 -0
  78. package/llms/blocks/sign-up-block.md +78 -0
  79. package/llms/blocks/site-footer-block.md +96 -0
  80. package/llms/blocks/stat-row-block.md +59 -0
  81. package/llms/blocks/steps-block.md +73 -0
  82. package/llms/blocks/task-list-block.md +81 -0
  83. package/llms/blocks/team-members-block.md +91 -0
  84. package/llms/blocks/testimonial-block.md +78 -0
  85. package/llms/blocks/traffic-sources-block.md +63 -0
  86. package/llms/blocks/traffic-trend-block.md +95 -0
  87. package/llms/blocks/upload-list-block.md +66 -0
  88. package/llms/blocks/verify-code-block.md +59 -0
  89. package/llms/blocks/wallet-block.md +84 -0
  90. package/llms/components/area-chart.md +112 -0
  91. package/llms/components/bar-chart.md +131 -0
  92. package/llms/components/bubble.md +5 -5
  93. package/llms/components/button.md +1 -1
  94. package/llms/components/card.md +2 -2
  95. package/llms/components/checkbox-card.md +1 -1
  96. package/llms/components/context-card.md +2 -2
  97. package/llms/components/context-menu.md +1 -1
  98. package/llms/components/date-tile.md +79 -0
  99. package/llms/components/dialog.md +1 -1
  100. package/llms/components/empty-state.md +4 -4
  101. package/llms/components/entity.md +5 -3
  102. package/llms/components/fieldset.md +1 -1
  103. package/llms/components/kbd.md +18 -6
  104. package/llms/components/line-chart.md +101 -0
  105. package/llms/components/menu.md +1 -1
  106. package/llms/components/{dots-menu.md → overflow-menu.md} +9 -9
  107. package/llms/components/pie-chart.md +103 -0
  108. package/llms/components/pill.md +1 -1
  109. package/llms/components/radar-chart.md +106 -0
  110. package/llms/components/radial-chart.md +102 -0
  111. package/llms/components/segmented-bar.md +102 -0
  112. package/llms/components/settings-menu.md +2 -2
  113. package/llms/components/side-nav.md +1 -1
  114. package/llms/components/skeleton.md +1 -1
  115. package/llms/components/sparkline.md +80 -0
  116. package/llms/components/table.md +1 -1
  117. package/llms/components/theme-selector.md +1 -1
  118. package/llms/foundations/shape.md +1 -1
  119. package/llms/patterns/auth-page.md +132 -42
  120. package/llms/patterns/brand-landing-page.md +271 -175
  121. package/llms/patterns/chat-panel.md +120 -39
  122. package/llms/patterns/dashboard-page.md +276 -65
  123. package/llms/patterns/feed-page.md +245 -64
  124. package/llms/patterns/landing-page.md +473 -75
  125. package/llms/patterns/list-detail-page.md +377 -68
  126. package/llms/patterns/settings-page.md +268 -67
  127. package/llms/reference/token-intent.md +4 -2
  128. package/llms/reference/utilities.md +20 -5
  129. package/llms/tasks/building-an-ios-prototype.md +4 -1
  130. package/llms/tasks/choosing-a-component.md +2 -2
  131. package/llms-full.txt +6428 -892
  132. package/llms.txt +67 -16
  133. package/package.json +5 -1
  134. package/registry.json +3217 -552
  135. package/targets/ios.json +88 -18
  136. package/llms/components/description.md +0 -96
  137. package/dist/{chunk-L6Y46C62.js → chunk-MXJX3NZI.js} +1 -1
@@ -0,0 +1,84 @@
1
+ # EventCardBlock
2
+
3
+ An upcoming-event card: a DateTile beside the title and time, a "starts in" Badge, attendees as an AvatarGroup, and Decline / Join.
4
+
5
+ **Intent:** Reach for this for the next meeting, booking or live session on a dashboard or home screen. For a list of many upcoming events, use Entity rows with a DateTile as `leading`.
6
+
7
+ **Block:** Dashboard · card · designed for 18rem–20rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - The DateTile names the day; the full weekday and time range sit in text beside it.
12
+ - The Badge is one title line tall and wraps under the title rather than squeezing it onto two lines.
13
+ - AvatarGroup `fit` trades avatars for the +N count before it pushes the actions to their own line.
14
+
15
+ ## Accessibility
16
+
17
+ - DateTile is a <time> with a machine-readable date; the time range is plain text read in order after the title.
18
+ - The AvatarGroup's aria-label states the total attendee count.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Avatar, AvatarGroup, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardTitle, cn, DateTile } from "@sableui/react"
24
+
25
+ const event = {
26
+ title: "Design review",
27
+ date: new Date(2026, 9, 14),
28
+ when: "Tuesday, 2:00 – 2:45 PM",
29
+ starts: "In 10 min",
30
+ attendees: ["MO", "SR", "JL"],
31
+ total: 7,
32
+ }
33
+
34
+ export interface EventCardBlockProps {
35
+ className?: string
36
+ /** false drops the card's surface and border — for a muted section that already frames it. */
37
+ framed?: boolean
38
+ /** Called with the person's answer. */
39
+ onRespond?: (answer: "join" | "decline") => void
40
+ }
41
+
42
+ export function EventCardBlock({ className, framed = true, onRespond }: EventCardBlockProps) {
43
+ return (
44
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs", className)}>
45
+ <CardContent className="flex gap-4">
46
+ <DateTile date={event.date} />
47
+ <div className="flex min-w-0 flex-1 flex-col gap-1">
48
+ {/* The row carries the title's type, so the badge's box is exactly
49
+ one title line (`h-[1lh]`): it centres on the title and never
50
+ makes the row taller than the title alone. When a bolder or
51
+ roomier look leaves no space beside the title, the badge wraps
52
+ to its own line instead of squeezing the title onto two. */}
53
+ <div className="flex flex-wrap items-start justify-between gap-x-2 text-title-lg">
54
+ <CardTitle>{event.title}</CardTitle>
55
+ <span className="flex h-[1lh] shrink-0 items-center">
56
+ <Badge tone="info" size="sm">
57
+ {event.starts}
58
+ </Badge>
59
+ </span>
60
+ </div>
61
+ <CardDescription>{event.when}</CardDescription>
62
+ </div>
63
+ </CardContent>
64
+ {/* `fit`: when a roomy look tightens the row, the stack gives up
65
+ avatars to the count instead of pushing the actions to a new line. */}
66
+ <CardFooter className="flex-wrap justify-between gap-3">
67
+ <AvatarGroup aria-label={`${event.total} attendees`} total={event.total} size="sm" surface={framed ? "card" : undefined} fit>
68
+ {event.attendees.map((initials) => (
69
+ <Avatar key={initials} fallback={initials} alt={initials} />
70
+ ))}
71
+ </AvatarGroup>
72
+ <div className="flex gap-2">
73
+ <Button variant="outline" size="sm" onClick={() => onRespond?.("decline")}>
74
+ Decline
75
+ </Button>
76
+ <Button size="sm" onClick={() => onRespond?.("join")}>
77
+ Join
78
+ </Button>
79
+ </div>
80
+ </CardFooter>
81
+ </Card>
82
+ )
83
+ }
84
+ ```
@@ -0,0 +1,72 @@
1
+ # FaqBlock
2
+
3
+ A FAQ section: the heading and a way to ask beside an Accordion of questions, each answer one or two sentences.
4
+
5
+ **Intent:** Reach for this near pricing or at the end of a landing page, for the questions that block a decision. Long help content belongs in docs, not an accordion.
6
+
7
+ **Block:** Marketing · section
8
+
9
+ ## Notes
10
+
11
+ - Every question starts closed: the list of questions is itself the useful summary.
12
+ - The way to ask a human sits beside the heading, so it is found before the reader scrolls the list.
13
+ - Capped at the `content` region width; questions wider than that are hard to scan.
14
+
15
+ ## Accessibility
16
+
17
+ - Accordion triggers are buttons with aria-expanded, inside headings, so the questions are navigable by heading and operable by keyboard.
18
+ - The contact link is a real <a> inside the lede sentence.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { useId } from "react"
24
+ import { Accordion, AccordionItem, AccordionPanel, AccordionTrigger, cn, Region, Stack } from "@sableui/react"
25
+
26
+ const intro = {
27
+ title: "Questions, answered",
28
+ lede: "Can't find what you need?",
29
+ contact: { label: "Write to us", href: "/contact" },
30
+ }
31
+
32
+ const questions = [
33
+ { q: "Is there a free plan?", a: "Yes. Starter is free forever for three live editions, with no fees on your first $1,000 in sales." },
34
+ { q: "Can I change plans later?", a: "Any time. Upgrades apply immediately and are prorated; downgrades take effect at the end of the billing period." },
35
+ { q: "Who owns the work I sell?", a: "You do. Collectors own the editions they buy, and you keep the rights to the work itself." },
36
+ { q: "How do payouts work?", a: "Sales clear after seven days and are paid out every Friday to the account you connect in settings." },
37
+ ]
38
+
39
+ export interface FaqBlockProps {
40
+ className?: string
41
+ }
42
+
43
+ export function FaqBlock({ className }: FaqBlockProps) {
44
+ const titleId = useId()
45
+ return (
46
+ <Region as="section" aria-labelledby={titleId} max="content" className={cn("w-full py-16", className)}>
47
+ <div className="grid gap-10 md:grid-cols-[1fr_2fr]">
48
+ <Stack gap={2}>
49
+ <h2 id={titleId} className="text-balance text-heading-2">
50
+ {intro.title}
51
+ </h2>
52
+ <p className="text-body-md text-muted-foreground">
53
+ {intro.lede}{" "}
54
+ <a href={intro.contact.href} className="text-foreground underline underline-offset-4 motion-interactive hover:text-muted-foreground">
55
+ {intro.contact.label}
56
+ </a>
57
+ .
58
+ </p>
59
+ </Stack>
60
+ <Accordion>
61
+ {questions.map((item) => (
62
+ <AccordionItem key={item.q} value={item.q}>
63
+ <AccordionTrigger>{item.q}</AccordionTrigger>
64
+ <AccordionPanel>{item.a}</AccordionPanel>
65
+ </AccordionItem>
66
+ ))}
67
+ </Accordion>
68
+ </div>
69
+ </Region>
70
+ )
71
+ }
72
+ ```
@@ -0,0 +1,75 @@
1
+ # FeatureGridBlock
2
+
3
+ A feature section: an overline, an h2 and a lede over a grid of article cards, each with an icon in a well, a title and one sentence — 1, 2 then 3 columns.
4
+
5
+ **Intent:** Reach for this to explain what a product does in six or fewer points. For a sequence (first this, then that), use StepsBlock; for one proof point with numbers, TestimonialBlock.
6
+
7
+ **Block:** Marketing · section
8
+
9
+ ## Notes
10
+
11
+ - Copied from the LandingPage page shell's features section.
12
+ - Six features fill 3 × 2 evenly; keep the count a multiple of three, or drop to cols={{ lg: 2 }}.
13
+ - Each body is one sentence. A feature that needs a paragraph needs its own section.
14
+ - Icons sit in a `sable-well`, which derives its fill from the card surface it is on.
15
+
16
+ ## Accessibility
17
+
18
+ - Each feature is an <article> with an h3 under the section's h2.
19
+ - The icons are decorative; each title says what the feature is.
20
+
21
+ ## Source
22
+
23
+ ```tsx
24
+ import { useId } from "react"
25
+ import { Card, CardContent, cn, Grid, Icon, Region, Stack } from "@sableui/react"
26
+
27
+ const intro = {
28
+ eyebrow: "Why Sable",
29
+ title: "A design system with the rules written down",
30
+ lede: "The decisions a senior designer would make are encoded once, so every screen your team or your agent builds starts from them.",
31
+ }
32
+
33
+ const features = [
34
+ { icon: "component", title: "Behavior built in", body: "Focus management, keyboard navigation, and ARIA come from Base UI primitives under every control." },
35
+ { icon: "eyedropper", title: "Semantic tokens end to end", body: "Re-theme the whole product, light and dark, from one preset without touching a component." },
36
+ { icon: "layout", title: "Pages, not just parts", body: "Landing, dashboard, settings, auth, and list-detail shells to copy and adapt." },
37
+ { icon: "book", title: "A contract agents can read", body: "registry.json, llms.txt, and a CLI hand agents the exact props, examples, and rules." },
38
+ { icon: "circle-check", title: "Lint that knows the design", body: "sable-lint flags raw colors, hand-built controls, and motion that ignores reduced motion." },
39
+ { icon: "play", title: "Motion with an off switch", body: "Named recipes run on one scale variable, so reduced motion stops every transition at once." },
40
+ ]
41
+
42
+ export interface FeatureGridBlockProps {
43
+ className?: string
44
+ }
45
+
46
+ export function FeatureGridBlock({ className }: FeatureGridBlockProps) {
47
+ const titleId = useId()
48
+ return (
49
+ <Region as="section" aria-labelledby={titleId} max="wide" className={cn("w-full py-16 sm:py-24", className)}>
50
+ <Stack gap={10}>
51
+ <Stack gap={3} className="max-w-2xl">
52
+ <p className="text-overline text-muted-foreground">{intro.eyebrow}</p>
53
+ <h2 id={titleId} className="text-balance text-heading-1">
54
+ {intro.title}
55
+ </h2>
56
+ <p className="text-body-lg text-muted-foreground">{intro.lede}</p>
57
+ </Stack>
58
+ <Grid cols={{ base: 1, sm: 2, lg: 3 }} gap={4}>
59
+ {features.map((feature) => (
60
+ <Card key={feature.title} render={<article />} className="h-full">
61
+ <CardContent className="flex flex-col gap-3">
62
+ <span className="grid size-10 place-items-center rounded-md sable-well">
63
+ <Icon name={feature.icon} className="text-xl" />
64
+ </span>
65
+ <h3 className="pt-2 text-title-lg">{feature.title}</h3>
66
+ <p className="text-body-sm text-muted-foreground">{feature.body}</p>
67
+ </CardContent>
68
+ </Card>
69
+ ))}
70
+ </Grid>
71
+ </Stack>
72
+ </Region>
73
+ )
74
+ }
75
+ ```
@@ -0,0 +1,66 @@
1
+ # FeedbackFormBlock
2
+
3
+ A feedback card: a one-to-five rating as a full-width ToggleGroup, an optional notes field, and Skip / Send feedback.
4
+
5
+ **Intent:** Reach for this for a post-session or post-purchase survey in a Sheet, Dialog or end-of-flow screen. For a feedback control anchored to a button in the app chrome, use the Feedback component (a popover).
6
+
7
+ **Block:** Social & messaging · card · designed for 18rem–20rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - The rating is preselected, so one tap on Send is a complete answer; the notes are optional and say so.
12
+ - Skip is a ghost Button before the primary, so dismissing is always available and never competes with sending.
13
+ - `onDone` reports whether feedback was sent and, if so, the score and notes.
14
+
15
+ ## Accessibility
16
+
17
+ - The ToggleGroup carries aria-label="Rating"; each Toggle's number is its name.
18
+ - The Textarea carries aria-label="Notes"; its placeholder is a prompt, not the label.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { useState } from "react"
24
+ import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, Textarea, Toggle, ToggleGroup } from "@sableui/react"
25
+
26
+ const scale = ["1", "2", "3", "4", "5"]
27
+
28
+ export interface FeedbackFormBlockProps {
29
+ className?: string
30
+ /** false drops the card's surface and border — for a Sheet or Dialog that already frames it. */
31
+ framed?: boolean
32
+ /** Skip calls it with `sent: false`; Send with `sent: true` and the answers. */
33
+ onDone?: (sent: boolean, feedback?: { score: number; notes: string }) => void
34
+ }
35
+
36
+ export function FeedbackFormBlock({ className, framed = true, onDone }: FeedbackFormBlockProps) {
37
+ const [score, setScore] = useState("4")
38
+ const [notes, setNotes] = useState("")
39
+ return (
40
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs", className)}>
41
+ <CardHeader>
42
+ <CardTitle>How was the session?</CardTitle>
43
+ <CardDescription>One tap is enough. Notes help us more.</CardDescription>
44
+ </CardHeader>
45
+ <CardContent className="flex flex-col gap-4">
46
+ <ToggleGroup value={[score]} onValueChange={(v: unknown[]) => v[0] && setScore(String(v[0]))} aria-label="Rating" className="w-full">
47
+ {scale.map((n) => (
48
+ <Toggle key={n} value={n} className="flex-1">
49
+ {n}
50
+ </Toggle>
51
+ ))}
52
+ </ToggleGroup>
53
+ <Textarea placeholder="What worked, what didn’t…" aria-label="Notes" rows={3} value={notes} onChange={(e) => setNotes(e.target.value)} />
54
+ </CardContent>
55
+ <CardFooter className="justify-end">
56
+ <Button variant="ghost" size="sm" onClick={() => onDone?.(false)}>
57
+ Skip
58
+ </Button>
59
+ <Button size="sm" onClick={() => onDone?.(true, { score: Number(score), notes })}>
60
+ Send feedback
61
+ </Button>
62
+ </CardFooter>
63
+ </Card>
64
+ )
65
+ }
66
+ ```
@@ -0,0 +1,54 @@
1
+ # GoalProgressBlock
2
+
3
+ A goals card: a ring per goal in a RadialChart against its track (a full ring is 100%), a legend, and which goals are behind in the footer.
4
+
5
+ **Intent:** Reach for this for progress toward two to four targets — a quarter's goals, a plan's limits, onboarding steps. A single reading in a settings row is a Meter; many goals are a list of Progress bars.
6
+
7
+ **Block:** Dashboard · card · designed for 18rem–24rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - max={100} makes a full ring mean done; each ring draws round from 12 o'clock as the card scrolls into view.
12
+ - The footer names what is behind, so the card is actionable without reading the rings.
13
+ - Values are formatted as percents in the tooltip and data table.
14
+
15
+ ## Accessibility
16
+
17
+ - The rings' data table lists each goal and its percent.
18
+ - Arrowing through the rings announces each goal; the legend names every ring's colour.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, RadialChart } from "@sableui/react"
24
+
25
+ const goals = [
26
+ { goal: "Releases", progress: 82 },
27
+ { goal: "Collectors", progress: 64 },
28
+ { goal: "Revenue", progress: 91 },
29
+ ]
30
+
31
+ export interface GoalProgressBlockProps {
32
+ className?: string
33
+ /** false drops the card's surface and border — for a muted section that already frames it. */
34
+ framed?: boolean
35
+ }
36
+
37
+ export function GoalProgressBlock({ className, framed = true }: GoalProgressBlockProps) {
38
+ const behind = goals.filter((g) => g.progress < 70)
39
+ return (
40
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-sm", className)}>
41
+ <CardHeader>
42
+ <CardTitle render={<h2 />}>Quarter goals</CardTitle>
43
+ <CardDescription>Progress toward each target, in percent.</CardDescription>
44
+ </CardHeader>
45
+ <CardContent>
46
+ <RadialChart label="Progress toward quarter goals" data={goals} index="goal" valueKey="progress" max={100} className="h-64" valueFormatter={(v) => `${v}%`} />
47
+ </CardContent>
48
+ <CardFooter className="text-body-sm text-muted-foreground">
49
+ {behind.length ? `${behind.map((g) => g.goal).join(" and ")} behind pace.` : "Every goal on pace."}
50
+ </CardFooter>
51
+ </Card>
52
+ )
53
+ }
54
+ ```
@@ -0,0 +1,65 @@
1
+ # HeroBlock
2
+
3
+ A centred hero: an outline Badge eyebrow, the page's h1 at display size, a lede, a primary and an outline call to action, and an install command in a CodeBlock.
4
+
5
+ **Intent:** Reach for this to open a product or developer-tool landing page. A brand page that leads with a product scene beside the headline is the BrandLandingPage page shell's hero.
6
+
7
+ **Block:** Marketing · section
8
+
9
+ ## Notes
10
+
11
+ - Copied from the LandingPage page shell's hero.
12
+ - The headline is the page's only h1 — place one HeroBlock per page.
13
+ - Two actions, one primary. The install command is the third, quieter way in for developers who already decided.
14
+ - The lede is capped at the prose measure and the headline at max-w-4xl, so both stay readable on wide screens.
15
+
16
+ ## Accessibility
17
+
18
+ - Calls to action are links styled as Buttons (render={<a />}), because they navigate.
19
+ - The CodeBlock's copy button is labelled and announces when it has copied.
20
+
21
+ ## Source
22
+
23
+ ```tsx
24
+ import { Badge, Button, cn, CodeBlock, Icon, Region, Row, Stack } from "@sableui/react"
25
+
26
+ const hero = {
27
+ eyebrow: "Open source · MIT licensed",
28
+ title: "Product UI your agents can't get wrong",
29
+ lede: "Sable is a themeable React component library with enforced tokens, typography, and motion, documented for coding agents as carefully as for people.",
30
+ primary: { label: "Get started", href: "/docs/getting-started" },
31
+ secondary: { label: "Browse components", href: "/components" },
32
+ install: "pnpm add @sableui/react",
33
+ }
34
+
35
+ export interface HeroBlockProps {
36
+ className?: string
37
+ }
38
+
39
+ export function HeroBlock({ className }: HeroBlockProps) {
40
+ return (
41
+ <Region as="section" max="wide" className={cn("w-full py-16 sm:py-24", className)}>
42
+ <Stack gap={8} align="center">
43
+ <Stack gap={4} align="center" className="text-center">
44
+ <Badge appearance="outline" size="sm" icon={<Icon name="sparkles" />}>
45
+ {hero.eyebrow}
46
+ </Badge>
47
+ <h1 className="max-w-4xl text-balance text-display-lg sm:text-display-2xl">{hero.title}</h1>
48
+ <p className="max-w-(--measure-prose) text-pretty text-body-lg text-muted-foreground">{hero.lede}</p>
49
+ <Row gap={2} justify="center" wrap className="pt-2">
50
+ <Button size="cta" render={<a href={hero.primary.href} />}>
51
+ {hero.primary.label}
52
+ </Button>
53
+ <Button size="cta" variant="outline" render={<a href={hero.secondary.href} />}>
54
+ {hero.secondary.label}
55
+ </Button>
56
+ </Row>
57
+ </Stack>
58
+ <div className="w-full max-w-sm">
59
+ <CodeBlock prompt="$" code={hero.install} />
60
+ </div>
61
+ </Stack>
62
+ </Region>
63
+ )
64
+ }
65
+ ```
@@ -0,0 +1,61 @@
1
+ # InviteTeamBlock
2
+
3
+ An invite card: who is already on the team as an AvatarGroup, and an email field with Send.
4
+
5
+ **Intent:** Reach for this for team or project invites in onboarding, an empty workspace, or a settings section. For managing an existing member list with roles, use a CardList of Entity rows instead.
6
+
7
+ **Block:** Settings & team · card · designed for 18rem–20rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - The field and Send are a <form>, so Enter sends; `onSend` receives the typed address.
12
+ - The AvatarGroup ring matches the card surface when framed and inherits the host surface when not.
13
+
14
+ ## Accessibility
15
+
16
+ - The AvatarGroup carries an aria-label with the count, so the stack reads as one item, not four initials.
17
+ - The email Input carries aria-label; there is no visible label in this compact layout.
18
+
19
+ ## Source
20
+
21
+ ```tsx
22
+ import { Avatar, AvatarGroup, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, Input } from "@sableui/react"
23
+
24
+ const collaborators = ["MO", "SR", "JL", "AK"]
25
+
26
+ export interface InviteTeamBlockProps {
27
+ className?: string
28
+ /** false drops the card's surface and border — for a Sheet, Dialog or muted section that already frames it. */
29
+ framed?: boolean
30
+ /** Called with the address typed, on Send or Enter. */
31
+ onSend?: (email: string) => void
32
+ }
33
+
34
+ export function InviteTeamBlock({ className, framed = true, onSend }: InviteTeamBlockProps) {
35
+ return (
36
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs", className)}>
37
+ <CardHeader>
38
+ <CardTitle>Invite your team</CardTitle>
39
+ <CardDescription>Collaborators can edit every project.</CardDescription>
40
+ </CardHeader>
41
+ <CardContent className="flex flex-col gap-4">
42
+ <AvatarGroup aria-label={`${collaborators.length} collaborators`} size="sm" surface={framed ? "card" : undefined}>
43
+ {collaborators.map((initials) => (
44
+ <Avatar key={initials} fallback={initials} alt={initials} />
45
+ ))}
46
+ </AvatarGroup>
47
+ <form
48
+ className="flex gap-2"
49
+ onSubmit={(event) => {
50
+ event.preventDefault()
51
+ onSend?.(String(new FormData(event.currentTarget).get("email") ?? ""))
52
+ }}
53
+ >
54
+ <Input name="email" type="email" placeholder="name@company.com" aria-label="Email address" />
55
+ <Button type="submit">Send</Button>
56
+ </form>
57
+ </CardContent>
58
+ </Card>
59
+ )
60
+ }
61
+ ```
@@ -0,0 +1,84 @@
1
+ # InvoiceHistoryBlock
2
+
3
+ A billing history card: a flush Table of date, plan, amount and status per invoice, with a download button on each row.
4
+
5
+ **Intent:** Reach for this in billing settings, under the current plan. For a long, filterable history (hundreds of rows, date ranges, export), use DataTable on its own page.
6
+
7
+ **Block:** Commerce · card · designed for 20rem–42rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - The table runs edge to edge (CardContent flush); its first and last columns still line up with the card header's padding.
12
+ - Amounts are a numeric column — right-aligned with tabular figures — so they compare by eye.
13
+ - On a narrow screen the table scrolls sideways inside the card (with a quiet scrollbar) rather than squeezing its columns.
14
+
15
+ ## Accessibility
16
+
17
+ - The download column has a screen-reader-only header, and each button names its invoice ("Download INV-0042").
18
+ - Status is text in a Badge; the dot's colour only reinforces it.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, Icon, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@sableui/react"
24
+
25
+ const invoices = [
26
+ { id: "INV-0042", date: "Oct 1, 2026", plan: "Studio · yearly", amount: "$192.00", status: "Paid" },
27
+ { id: "INV-0031", date: "Sep 1, 2026", plan: "Studio · monthly", amount: "$20.00", status: "Paid" },
28
+ { id: "INV-0027", date: "Aug 1, 2026", plan: "Studio · monthly", amount: "$20.00", status: "Refunded" },
29
+ { id: "INV-0019", date: "Jul 1, 2026", plan: "Starter", amount: "$0.00", status: "Paid" },
30
+ ]
31
+
32
+ export interface InvoiceHistoryBlockProps {
33
+ className?: string
34
+ /** false drops the card's surface and border — for a muted section that already frames it. */
35
+ framed?: boolean
36
+ /** Called with the invoice id to download. */
37
+ onDownload?: (id: string) => void
38
+ }
39
+
40
+ export function InvoiceHistoryBlock({ className, framed = true, onDownload }: InvoiceHistoryBlockProps) {
41
+ return (
42
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-2xl", className)}>
43
+ <CardHeader>
44
+ <CardTitle render={<h2 />}>Invoices</CardTitle>
45
+ <CardDescription>Sent to billing@studio.com on the 1st of each month.</CardDescription>
46
+ </CardHeader>
47
+ <CardContent flush className="overflow-x-auto scrollbar-quiet">
48
+ <Table>
49
+ <TableHeader>
50
+ <TableRow>
51
+ <TableHead>Date</TableHead>
52
+ <TableHead>Plan</TableHead>
53
+ <TableHead numeric>Amount</TableHead>
54
+ <TableHead>Status</TableHead>
55
+ <TableHead className="w-0">
56
+ <span className="sr-only">Download</span>
57
+ </TableHead>
58
+ </TableRow>
59
+ </TableHeader>
60
+ <TableBody>
61
+ {invoices.map((invoice) => (
62
+ <TableRow key={invoice.id}>
63
+ <TableCell className="whitespace-nowrap">{invoice.date}</TableCell>
64
+ <TableCell className="text-muted-foreground">{invoice.plan}</TableCell>
65
+ <TableCell numeric>{invoice.amount}</TableCell>
66
+ <TableCell>
67
+ <Badge appearance="plain" tone={invoice.status === "Paid" ? "success" : "neutral"} dot>
68
+ {invoice.status}
69
+ </Badge>
70
+ </TableCell>
71
+ <TableCell>
72
+ <Button variant="ghost" size="icon-sm" aria-label={`Download ${invoice.id}`} onClick={() => onDownload?.(invoice.id)}>
73
+ <Icon name="download" />
74
+ </Button>
75
+ </TableCell>
76
+ </TableRow>
77
+ ))}
78
+ </TableBody>
79
+ </Table>
80
+ </CardContent>
81
+ </Card>
82
+ )
83
+ }
84
+ ```
@@ -0,0 +1,57 @@
1
+ # KpiSparklineBlock
2
+
3
+ A KPI row with trends: three stat cards, each the value in the metric mono tier, a Sparkline of its last twelve weeks, and the change as a Badge.
4
+
5
+ **Intent:** Reach for this at the top of a dashboard or a landing page's proof section, where each number needs its recent shape. Without the trend, StatRowBlock is calmer; for one headline trend, TrafficTrendBlock.
6
+
7
+ **Block:** Dashboard · section
8
+
9
+ ## Notes
10
+
11
+ - Each sparkline is coloured by what its measure is, and the change Badge by whether it is good: churn falling is good news in a warning-toned trend.
12
+ - The sparklines draw on as the row scrolls into view.
13
+ - One column on a phone, three from sm.
14
+
15
+ ## Accessibility
16
+
17
+ - Each Sparkline is an image named by the measure and the period, with its range spoken.
18
+ - The change is stated in text (+8.2% vs last quarter) with an arrow icon; colour only reinforces it.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Badge, Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, cn, Grid, Icon, Sparkline } from "@sableui/react"
24
+
25
+ const kpis = [
26
+ { label: "Revenue", value: "$18,240", delta: "+8.2%", up: true, trend: [12, 14, 13, 17, 16, 19, 18, 21, 20, 24, 23, 26], tone: "success" },
27
+ { label: "Active users", value: "3,912", delta: "+3.1%", up: true, trend: [30, 31, 29, 33, 34, 33, 36, 35, 37, 38, 37, 39], tone: "info" },
28
+ { label: "Churn", value: "4.1%", delta: "−0.6%", up: false, trend: [9, 8, 9, 7, 8, 6, 7, 5, 6, 4, 5, 4], tone: "warning" },
29
+ ] as const
30
+
31
+ export interface KpiSparklineBlockProps {
32
+ className?: string
33
+ }
34
+
35
+ export function KpiSparklineBlock({ className }: KpiSparklineBlockProps) {
36
+ return (
37
+ <Grid cols={{ base: 1, sm: 3 }} gap={3} className={cn("w-full", className)}>
38
+ {kpis.map((kpi) => (
39
+ <Card key={kpi.label}>
40
+ <CardHeader>
41
+ <CardDescription>{kpi.label}</CardDescription>
42
+ <CardTitle className="text-mono-xl">{kpi.value}</CardTitle>
43
+ <CardAction>
44
+ <Sparkline label={`${kpi.label}, last 12 weeks`} data={[...kpi.trend]} variant="area" tone={kpi.tone} />
45
+ </CardAction>
46
+ </CardHeader>
47
+ <CardContent>
48
+ <Badge tone={kpi.up ? "success" : "neutral"} size="sm" icon={<Icon name={kpi.up ? "arrow-up" : "arrow-down"} />}>
49
+ {kpi.delta} vs last quarter
50
+ </Badge>
51
+ </CardContent>
52
+ </Card>
53
+ ))}
54
+ </Grid>
55
+ )
56
+ }
57
+ ```