@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,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
+ ```
@@ -0,0 +1,95 @@
1
+ # TrafficTrendBlock
2
+
3
+ A trend card: a stacked gradient AreaChart of two series by day, a time-range ToggleGroup in the header, and a one-line takeaway with its direction in the footer.
4
+
5
+ **Intent:** Reach for this as the headline chart of a dashboard or an analytics section on a landing page. For a single number with a small trend beside it, KpiSparklineBlock is lighter.
6
+
7
+ **Block:** Dashboard · card · designed for 20rem–48rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - Switching the range eases the chart from one shape to the next instead of redrawing it.
12
+ - The footer states the takeaway in words, so the card says something even before the chart is read.
13
+ - The tooltip heading spells out the weekday; the axis keeps short dates so they fit.
14
+
15
+ ## Accessibility
16
+
17
+ - The chart's accessible name includes the range showing, and its data table updates with it.
18
+ - The range control is a labelled ToggleGroup; the chart is one tab stop with arrow-key navigation.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { useState } from "react"
24
+ import { AreaChart, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, Icon, Toggle, ToggleGroup } from "@sableui/react"
25
+
26
+ /** 90 days of visitors — replace with your own rows ({ date, desktop, mobile }). */
27
+ const days = Array.from({ length: 90 }, (_, i) => {
28
+ const wave = Math.sin(i / 6) * 60 + Math.sin(i / 2.3) * 25
29
+ return {
30
+ date: new Date(2026, 6, 1 + i).toISOString().slice(0, 10),
31
+ desktop: Math.round(240 + wave + (i % 7) * 9),
32
+ mobile: Math.round(160 + wave * 0.7 + ((i * 3) % 11) * 6),
33
+ }
34
+ })
35
+
36
+ const series = [
37
+ { key: "desktop", label: "Desktop" },
38
+ { key: "mobile", label: "Mobile" },
39
+ ]
40
+
41
+ const ranges = [
42
+ { id: "90d", label: "3 months", days: 90 },
43
+ { id: "30d", label: "30 days", days: 30 },
44
+ { id: "7d", label: "7 days", days: 7 },
45
+ ]
46
+
47
+ const shortDate = (iso: string) => new Date(`${iso}T00:00:00`).toLocaleDateString("en-US", { month: "short", day: "numeric" })
48
+
49
+ export interface TrafficTrendBlockProps {
50
+ className?: string
51
+ /** false drops the card's surface and border — for a muted section that already frames it. */
52
+ framed?: boolean
53
+ }
54
+
55
+ export function TrafficTrendBlock({ className, framed = true }: TrafficTrendBlockProps) {
56
+ const [range, setRange] = useState("90d")
57
+ const shown = days.slice(-(ranges.find((r) => r.id === range)?.days ?? 90))
58
+ return (
59
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-3xl", className)}>
60
+ <CardHeader>
61
+ <CardTitle render={<h2 />}>Visitors</CardTitle>
62
+ <CardDescription>Desktop and mobile, by day.</CardDescription>
63
+ <CardAction>
64
+ <ToggleGroup value={[range]} onValueChange={(v: unknown[]) => v[0] && setRange(String(v[0]))} aria-label="Time range">
65
+ {ranges.map((r) => (
66
+ <Toggle key={r.id} value={r.id}>
67
+ {r.label}
68
+ </Toggle>
69
+ ))}
70
+ </ToggleGroup>
71
+ </CardAction>
72
+ </CardHeader>
73
+ <CardContent>
74
+ <AreaChart
75
+ label={`Visitors per day, last ${ranges.find((r) => r.id === range)?.label}`}
76
+ data={shown}
77
+ index="date"
78
+ series={series}
79
+ indexFormatter={shortDate}
80
+ tooltip={{ labelFormatter: (d) => new Date(`${d}T00:00:00`).toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" }) }}
81
+ stacked
82
+ gradient
83
+ className="h-64"
84
+ />
85
+ </CardContent>
86
+ <CardFooter className="gap-1.5 text-body-sm">
87
+ <Icon name="arrow-up" className="text-success" />
88
+ <span>
89
+ Up 5.2% on the previous period <span className="text-muted-foreground">· desktop leads every week</span>
90
+ </span>
91
+ </CardFooter>
92
+ </Card>
93
+ )
94
+ }
95
+ ```
@@ -0,0 +1,66 @@
1
+ # UploadListBlock
2
+
3
+ An upload queue: full-width Attachment rows — one uploading with progress and a cancel button, one finished with a success check.
4
+
5
+ **Intent:** Reach for this under a FileDropzone, or anywhere files are in flight — a composer, an import step, a document form. For files attached to a message, Attachment chips in the composer fit better.
6
+
7
+ **Block:** Media & files · card · designed for 16rem–20rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - Rows, not chips: each Attachment drops its chip cap (`max-w-none`) to fill the block's width.
12
+ - The success check sits in a control-sized box so it lines up with the cancel button on the row above.
13
+ - No card: Attachment rows are their own surfaces. Wrap the block in a Card if it needs a header.
14
+
15
+ ## Accessibility
16
+
17
+ - Attachment announces upload progress; the cancel Button carries aria-label.
18
+ - The finished row's check carries aria-label="Uploaded", so the state is spoken, not only coloured.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Attachment, Button, cn, Icon } from "@sableui/react"
24
+
25
+ const uploading = { name: "brand-guidelines.pdf", meta: "4.2 MB of 6.8 MB", progress: 62 }
26
+ const uploaded = { name: "cover-final.png", meta: "Uploaded · 2.1 MB" }
27
+
28
+ export interface UploadListBlockProps {
29
+ className?: string
30
+ /** Called when the in-progress upload is cancelled. */
31
+ onCancel?: () => void
32
+ }
33
+
34
+ export function UploadListBlock({ className, onCancel }: UploadListBlockProps) {
35
+ return (
36
+ // An upload list, not chips: rows fill the block's width.
37
+ <div className={cn("flex w-full max-w-xs flex-col gap-2", className)}>
38
+ <Attachment
39
+ className="max-w-none"
40
+ name={uploading.name}
41
+ meta={uploading.meta}
42
+ status="uploading"
43
+ progress={uploading.progress}
44
+ actions={
45
+ <Button variant="ghost" size="icon-sm" aria-label="Cancel upload" onClick={onCancel}>
46
+ <Icon name="xmark" />
47
+ </Button>
48
+ }
49
+ />
50
+ <Attachment
51
+ className="max-w-none"
52
+ name={uploaded.name}
53
+ icon="image-stack"
54
+ meta={uploaded.meta}
55
+ // The same box as the icon-sm button above, so the check centres on
56
+ // the cancel button's line instead of hugging the edge.
57
+ actions={
58
+ <span className="grid size-(--control-sm) place-items-center">
59
+ <Icon name="circle-check" className="text-success" aria-label="Uploaded" />
60
+ </span>
61
+ }
62
+ />
63
+ </div>
64
+ )
65
+ }
66
+ ```
@@ -0,0 +1,59 @@
1
+ # VerifyCodeBlock
2
+
3
+ A one-time-code card: where the code went, a six-slot OtpField grouped 3 + 3 under a visible label, and a quiet Resend.
4
+
5
+ **Intent:** Reach for this for email or SMS verification, two-factor sign-in, or confirming a sensitive change. For a code someone copies out (a recovery key), show it with Code and a copy button instead.
6
+
7
+ **Block:** Auth · card · designed for 18rem–24rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - No submit button: the code verifies the moment the last slot fills, which is what people expect from a code field.
12
+ - The destination is named in the description, so a mistyped address is caught before the wait.
13
+ - Resend is a ghost Button — present for the stuck case, never competing with the field.
14
+
15
+ ## Accessibility
16
+
17
+ - OtpField names each slot ("Character 1 of 6") and accepts a pasted code into all six at once.
18
+ - Field labels the group and reads the expiry as its description.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cn, Field, OtpField } from "@sableui/react"
24
+
25
+ const sentTo = "ada@studio.com"
26
+
27
+ export interface VerifyCodeBlockProps {
28
+ className?: string
29
+ /** false drops the card's surface and border — for a Sheet, Dialog or muted section that already frames it. */
30
+ framed?: boolean
31
+ /** Called with the code as soon as the last digit is entered. */
32
+ onVerify?: (code: string) => void
33
+ onResend?: () => void
34
+ }
35
+
36
+ export function VerifyCodeBlock({ className, framed = true, onVerify, onResend }: VerifyCodeBlockProps) {
37
+ return (
38
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-sm", className)}>
39
+ <CardHeader>
40
+ <CardTitle>Check your email</CardTitle>
41
+ <CardDescription>
42
+ We sent a 6-digit code to <span className="text-foreground">{sentTo}</span>.
43
+ </CardDescription>
44
+ </CardHeader>
45
+ <CardContent>
46
+ <Field label="Verification code" description="The code expires in 10 minutes.">
47
+ <OtpField length={6} groupSize={3} onValueComplete={(code: string) => onVerify?.(code)} />
48
+ </Field>
49
+ </CardContent>
50
+ <CardFooter className="justify-between gap-3">
51
+ <span className="text-caption text-muted-foreground">Didn’t get it?</span>
52
+ <Button variant="ghost" size="sm" onClick={onResend}>
53
+ Resend code
54
+ </Button>
55
+ </CardFooter>
56
+ </Card>
57
+ )
58
+ }
59
+ ```