@sableui/react 0.2.0 → 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 (97) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +59 -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-K6LII3OE.js +5093 -0
  12. package/dist/{chunk-PAOZBZME.js → chunk-NGBDP3BM.js} +1 -1
  13. package/dist/{chunk-5Z5KLOAU.js → chunk-TH563WLW.js} +14 -1
  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 +6 -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/themes/base/base.css +23 -0
  27. package/dist/themes/index.js +2 -2
  28. package/llms/blocks/api-keys-block.md +79 -0
  29. package/llms/blocks/chat-thread-block.md +51 -0
  30. package/llms/blocks/command-palette-block.md +48 -0
  31. package/llms/blocks/cta-band-block.md +62 -0
  32. package/llms/blocks/danger-zone-block.md +69 -0
  33. package/llms/blocks/device-comparison-block.md +91 -0
  34. package/llms/blocks/event-card-block.md +84 -0
  35. package/llms/blocks/faq-block.md +72 -0
  36. package/llms/blocks/feature-grid-block.md +75 -0
  37. package/llms/blocks/feedback-form-block.md +66 -0
  38. package/llms/blocks/goal-progress-block.md +54 -0
  39. package/llms/blocks/hero-block.md +65 -0
  40. package/llms/blocks/invite-team-block.md +61 -0
  41. package/llms/blocks/invoice-history-block.md +84 -0
  42. package/llms/blocks/kpi-sparkline-block.md +57 -0
  43. package/llms/blocks/media-player-block.md +100 -0
  44. package/llms/blocks/newsletter-block.md +64 -0
  45. package/llms/blocks/not-found-block.md +56 -0
  46. package/llms/blocks/notification-list-block.md +63 -0
  47. package/llms/blocks/notification-settings-block.md +54 -0
  48. package/llms/blocks/order-tracking-block.md +62 -0
  49. package/llms/blocks/pricing-card-block.md +82 -0
  50. package/llms/blocks/pricing-table-block.md +129 -0
  51. package/llms/blocks/product-card-block.md +69 -0
  52. package/llms/blocks/profile-card-block.md +103 -0
  53. package/llms/blocks/profile-form-block.md +114 -0
  54. package/llms/blocks/revenue-breakdown-block.md +70 -0
  55. package/llms/blocks/sign-in-block.md +64 -0
  56. package/llms/blocks/sign-up-block.md +78 -0
  57. package/llms/blocks/site-footer-block.md +96 -0
  58. package/llms/blocks/stat-row-block.md +59 -0
  59. package/llms/blocks/steps-block.md +73 -0
  60. package/llms/blocks/task-list-block.md +81 -0
  61. package/llms/blocks/team-members-block.md +91 -0
  62. package/llms/blocks/testimonial-block.md +78 -0
  63. package/llms/blocks/traffic-sources-block.md +63 -0
  64. package/llms/blocks/traffic-trend-block.md +95 -0
  65. package/llms/blocks/upload-list-block.md +66 -0
  66. package/llms/blocks/verify-code-block.md +59 -0
  67. package/llms/blocks/wallet-block.md +84 -0
  68. package/llms/components/area-chart.md +112 -0
  69. package/llms/components/bar-chart.md +131 -0
  70. package/llms/components/button.md +1 -1
  71. package/llms/components/card.md +2 -2
  72. package/llms/components/context-menu.md +1 -1
  73. package/llms/components/date-tile.md +79 -0
  74. package/llms/components/entity.md +4 -2
  75. package/llms/components/line-chart.md +101 -0
  76. package/llms/components/menu.md +1 -1
  77. package/llms/components/{dots-menu.md → overflow-menu.md} +9 -9
  78. package/llms/components/pie-chart.md +103 -0
  79. package/llms/components/radar-chart.md +106 -0
  80. package/llms/components/radial-chart.md +102 -0
  81. package/llms/components/segmented-bar.md +102 -0
  82. package/llms/components/settings-menu.md +2 -2
  83. package/llms/components/sparkline.md +80 -0
  84. package/llms/components/table.md +1 -1
  85. package/llms/patterns/auth-page.md +2 -2
  86. package/llms/patterns/dashboard-page.md +6 -7
  87. package/llms/patterns/feed-page.md +7 -7
  88. package/llms/patterns/list-detail-page.md +7 -15
  89. package/llms/patterns/settings-page.md +10 -21
  90. package/llms/tasks/building-an-ios-prototype.md +4 -1
  91. package/llms/tasks/choosing-a-component.md +2 -2
  92. package/llms-full.txt +4296 -277
  93. package/llms.txt +55 -3
  94. package/package.json +5 -1
  95. package/registry.json +6383 -3741
  96. package/targets/ios.json +90 -9
  97. package/dist/{chunk-L6Y46C62.js → chunk-MXJX3NZI.js} +1 -1
@@ -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
+ ```
@@ -0,0 +1,84 @@
1
+ # WalletBlock
2
+
3
+ A balance card: the balance in the metric mono tier, Send and Receive as an equal pair, and recent activity as Entity rows with signed amounts.
4
+
5
+ **Intent:** Reach for this for a wallet, account balance or credits summary with its latest movements. For a full transaction history with filters, use a DataTable.
6
+
7
+ **Block:** Commerce · card · designed for 18rem–20rem wide (fills its container up to the max)
8
+
9
+ ## Notes
10
+
11
+ - The balance is CardTitle at `text-mono-xl` — tabular figures, never a heading tier — under its label as CardDescription.
12
+ - Send is primary and Receive secondary, side by side at equal width: two directions of one action, with sending the riskier and more frequent.
13
+ - Incoming amounts are `text-success`; outgoing stay muted rather than red, because a withdrawal is not an error.
14
+
15
+ ## Accessibility
16
+
17
+ - Each activity row's leading swatch is aria-hidden; the title and detail carry the meaning.
18
+ - Amounts keep their sign in text (+0.80 / -1.50), so the direction does not depend on colour.
19
+
20
+ ## Source
21
+
22
+ ```tsx
23
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, cn, Entity, Icon } from "@sableui/react"
24
+
25
+ const balance = "14.62 SOL"
26
+
27
+ const activity = [
28
+ { title: "Sold “Dusk, no. 4”", detail: "Edition · 2h ago", amount: "+0.80", tone: "edition" },
29
+ { title: "Royalty", detail: "Collectible · Yesterday", amount: "+0.12", tone: "collectible" },
30
+ { title: "Withdrawal", detail: "To Phantom · Mon", amount: "-1.50", tone: "standard" },
31
+ ]
32
+
33
+ export interface WalletBlockProps {
34
+ className?: string
35
+ /** false drops the card's surface and border — for a Sheet or muted section that already frames it. */
36
+ framed?: boolean
37
+ onSend?: () => void
38
+ onReceive?: () => void
39
+ }
40
+
41
+ export function WalletBlock({ className, framed = true, onSend, onReceive }: WalletBlockProps) {
42
+ return (
43
+ <Card variant={framed ? "default" : "flat"} className={cn("w-full max-w-xs", className)}>
44
+ <CardHeader>
45
+ <CardDescription>Balance</CardDescription>
46
+ <CardTitle className="text-mono-xl">{balance}</CardTitle>
47
+ </CardHeader>
48
+ <CardContent className="flex flex-col gap-3">
49
+ <div className="grid grid-cols-2 gap-2">
50
+ <Button iconStart={<Icon name="arrow-up" />} onClick={onSend}>
51
+ Send
52
+ </Button>
53
+ <Button variant="secondary" iconStart={<Icon name="arrow-down" />} onClick={onReceive}>
54
+ Receive
55
+ </Button>
56
+ </div>
57
+ <div className="flex flex-col">
58
+ {activity.map((item) => (
59
+ <Entity
60
+ key={item.title}
61
+ className="px-0"
62
+ leading={
63
+ <span
64
+ aria-hidden
65
+ className="block size-8 rounded-full"
66
+ // design-lint-disable-next-line inline-color -- relative colour derived from the theme's own --tone token, so it follows every look
67
+ style={{ background: `oklch(from var(--tone-${item.tone}) clamp(0.5, calc(l + 0.16), 0.9) max(calc(c * 1.25), 0.1) h)` }}
68
+ />
69
+ }
70
+ title={item.title}
71
+ subtitle={item.detail}
72
+ action={
73
+ <span className={cn("text-mono-md", item.amount.startsWith("+") ? "text-success" : "text-muted-foreground")}>
74
+ {item.amount}
75
+ </span>
76
+ }
77
+ />
78
+ ))}
79
+ </div>
80
+ </CardContent>
81
+ </Card>
82
+ )
83
+ }
84
+ ```
@@ -0,0 +1,112 @@
1
+ # AreaChart
2
+
3
+ **Status:** beta
4
+
5
+ A trend over a sequence, filled to the baseline: overlapping translucent series, stacked, or stacked to 100%, with smooth, linear or step curves and optional gradient fills. The line draws on and the fill rises as it scrolls into view.
6
+
7
+ **Keywords:** chart, area, trend, time series, stacked, graph, analytics, dashboard, visitors, revenue
8
+
9
+ ## Usage
10
+
11
+ Use AreaChart for volume over time — visitors, revenue, usage — where the filled shape helps the eye read the size of the total. Stack the series to show a total and its parts; `stacked="expand"` shows each category as 100%. For comparing how measures move relative to each other, LineChart is cleaner; for amounts per category, BarChart.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Keep to one to three series; past that the tints muddy — stack them or switch to LineChart.
16
+ - Do: Set a height on className (h-72, aspect-video h-auto); the chart fills the box it is given.
17
+ - Don't: Overlap more than two unstacked series — the translucent tints blend into a colour no series has.
18
+ - Don't: Use it for data-heavy views — streaming or real-time data, thousands of points, zoom and brushing, trading charts. Those belong on a charting library (Recharts, visx, ECharts); Sable's charts are for dashboards, landing pages and decks.
19
+
20
+ ## Props
21
+
22
+ | Name | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | data | Record<string, unknown>[] | — | The rows, in order along the category axis. |
25
+ | index | string | — | The datum field naming each row's category (a month, a day, a channel). |
26
+ | series | { key: string; label: string; tone?: ChartTone; color?: string; icon?: string }[] | — | The measures to plot: the datum field, its name in the legend/tooltip/table, and its colour (a tone by role, or any CSS colour). Omitted, series take the chart palette in order: --chart-1 … --chart-8, validated for colour-blind separation in light and dark; a theme may override the set. |
27
+ | label | string | — | What the chart shows. Its accessible name and the caption of the data table assistive tech reads. |
28
+ | className | string | "h-60" | Sizes the chart (plot + legend). Set a height: `h-72`, `aspect-video h-auto`. |
29
+ | valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |
30
+ | indexFormatter | (value: string) => string | — | Shorten category labels on the axis ("January" → "Jan"); the tooltip keeps the full value. |
31
+ | locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
32
+ | tooltip | boolean \| { indicator?: "dot" \| "line" \| "dashed" \| "none"; hideLabel?: boolean; labelFormatter?: (label, datum) => ReactNode; showTotal?: boolean } | true | Show a tooltip on hover and keyboard focus, or configure it: the row marker, hiding the heading, rewriting it, adding a Total row. |
33
+ | legend | boolean | true when there is more than one series | Show the legend under the chart. |
34
+ | grid | boolean | true (false for horizontal bars) | Hairline gridlines along the value axis. |
35
+ | xAxis | boolean | true for columns; false for horizontal bars | The horizontal axis. |
36
+ | yAxis | boolean | false for columns; true for horizontal bars | The vertical axis. |
37
+ | animate | boolean | true | Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. |
38
+ | activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
39
+ | defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
40
+ | onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
41
+ | curve | "smooth" \| "linear" \| "step" | "smooth" | How the line runs between points. `smooth` is monotone: it never overshoots a value the data does not contain. |
42
+ | stacked | boolean \| "expand" | false | Stack the series, or `"expand"` to show each category as 100%. |
43
+ | gradient | boolean | false | Fill each area with a fade from its colour to transparent instead of a flat tint. |
44
+
45
+ ## Tokens used
46
+
47
+ `tone-edition`, `tone-collectible`, `tone-standard`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
48
+
49
+ ## Motion
50
+
51
+ - **arrival** · Area + line · _custom (outside the recipe layer)_ — Once on screen, each line draws on (stroke-dashoffset) and its fill rises from the baseline, series staggered — the trend is read as it is drawn. On the motion tokens; reduced motion shows the finished chart.
52
+ - **response** · Cursor + tooltip · _no motion_ — Hover and focus move the cursor and tooltip instantly — a lagging cursor reads as a slow chart.
53
+ - **transition** · Area + line · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place.
54
+ - **activity** · AreaChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
55
+
56
+ ## Accessibility
57
+
58
+ - The drawing is aria-hidden; the same numbers are in a visually hidden data table (captioned by `label`) that the chart is aria-describedby, so assistive tech reads values, not shapes.
59
+ - With a tooltip, the chart is one tab stop (role=group, aria-roledescription="chart"): arrow keys, Home and End step through the data, Escape clears, and each step is announced in a polite live region.
60
+ - Colour is never the only key: series are named in the legend, tooltip and table.
61
+
62
+ ## Related
63
+
64
+ - Components: LineChart, BarChart, Sparkline, Card
65
+
66
+ ## Examples
67
+
68
+ ### Two series with a legend
69
+
70
+ ```tsx
71
+ import { AreaChart } from "@sableui/react"
72
+
73
+ const visitors = [
74
+ { month: "January", desktop: 186, mobile: 80 },
75
+ { month: "February", desktop: 305, mobile: 200 },
76
+ { month: "March", desktop: 237, mobile: 120 },
77
+ { month: "April", desktop: 73, mobile: 190 },
78
+ { month: "May", desktop: 209, mobile: 130 },
79
+ { month: "June", desktop: 214, mobile: 140 },
80
+ ]
81
+
82
+ <AreaChart
83
+ label="Visitors, January to June"
84
+ data={visitors}
85
+ index="month"
86
+ series={[
87
+ { key: "desktop", label: "Desktop" },
88
+ { key: "mobile", label: "Mobile" },
89
+ ]}
90
+ indexFormatter={(m) => m.slice(0, 3)}
91
+ gradient
92
+ />
93
+ ```
94
+
95
+ ### Stacked to 100%
96
+
97
+ ```tsx
98
+ import { AreaChart } from "@sableui/react"
99
+
100
+ <AreaChart
101
+ label="Share of visitors by device"
102
+ data={visitors}
103
+ index="month"
104
+ series={[
105
+ { key: "desktop", label: "Desktop" },
106
+ { key: "mobile", label: "Mobile" },
107
+ ]}
108
+ stacked="expand"
109
+ yAxis
110
+ tooltip={{ indicator: "line", showTotal: true }}
111
+ />
112
+ ```
@@ -0,0 +1,131 @@
1
+ # BarChart
2
+
3
+ **Status:** beta
4
+
5
+ Bars comparing amounts across categories: grouped, stacked or stacked to 100%, vertical columns or horizontal bars, with value labels, per-category colours and a separate tone for negative values. Bars rise from the baseline in sequence as the chart scrolls into view.
6
+
7
+ **Keywords:** chart, bar, column, histogram, compare, stacked, grouped, horizontal, analytics, dashboard
8
+
9
+ ## Usage
10
+
11
+ Use BarChart to compare amounts across categories — the default chart for "how much of each". Go horizontal (`orientation="horizontal"`) when category names are long or there are many of them; group series side by side to compare them, stack them to show a total. For parts of one whole, a stacked bar or PieChart; for a trend, AreaChart or LineChart.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Sort categories by value unless they have a natural order (months, steps).
16
+ - Do: Use negativeTone for diverging data (profit and loss), so a loss reads apart from a gain.
17
+ - Don't: Group more than three series side by side — the bars get too thin to compare; stack them or split the chart.
18
+ - Don't: Use it for data-heavy views — streaming or real-time data, thousands of points, zoom and brushing, trading charts. Those belong on a charting library (Recharts, visx, ECharts); Sable's charts are for dashboards, landing pages and decks.
19
+
20
+ ## Props
21
+
22
+ | Name | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | data | Record<string, unknown>[] | — | The rows, in order along the category axis. |
25
+ | index | string | — | The datum field naming each row's category (a month, a day, a channel). |
26
+ | series | { key: string; label: string; tone?: ChartTone; color?: string; icon?: string }[] | — | The measures to plot: the datum field, its name in the legend/tooltip/table, and its colour (a tone by role, or any CSS colour). Omitted, series take the chart palette in order: --chart-1 … --chart-8, validated for colour-blind separation in light and dark; a theme may override the set. |
27
+ | label | string | — | What the chart shows. Its accessible name and the caption of the data table assistive tech reads. |
28
+ | className | string | "h-60" | Sizes the chart (plot + legend). Set a height: `h-72`, `aspect-video h-auto`. |
29
+ | valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |
30
+ | indexFormatter | (value: string) => string | — | Shorten category labels on the axis ("January" → "Jan"); the tooltip keeps the full value. |
31
+ | locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
32
+ | tooltip | boolean \| { indicator?: "dot" \| "line" \| "dashed" \| "none"; hideLabel?: boolean; labelFormatter?: (label, datum) => ReactNode; showTotal?: boolean } | true | Show a tooltip on hover and keyboard focus, or configure it: the row marker, hiding the heading, rewriting it, adding a Total row. |
33
+ | legend | boolean | true when there is more than one series | Show the legend under the chart. |
34
+ | grid | boolean | true (false for horizontal bars) | Hairline gridlines along the value axis. |
35
+ | xAxis | boolean | true for columns; false for horizontal bars | The horizontal axis. |
36
+ | yAxis | boolean | false for columns; true for horizontal bars | The vertical axis. |
37
+ | animate | boolean | true | Play the arrival once the chart scrolls into view. Reduced motion shows the finished chart. |
38
+ | activeIndex | number \| null | — | The highlighted row (controlled); `null` for none. |
39
+ | defaultActiveIndex | number \| null | — | The row highlighted at first — a tooltip already open before anyone hovers. |
40
+ | onActiveIndexChange | (index: number \| null) => void | — | Called as the pointer or keyboard moves between rows. |
41
+ | orientation | "vertical" \| "horizontal" | "vertical" | `vertical` columns rise from the bottom; `horizontal` bars run from the left, for long category names. |
42
+ | stacked | boolean \| "expand" | false | Stack the series in one bar, or `"expand"` to show each category as 100%. Unstacked series sit side by side. |
43
+ | labels | boolean \| "inside" | false | Print each bar's value at its end, or (`"inside"`, horizontal) the category name inside the bar as well. |
44
+ | colorBy | "series" \| "index" | "series" | Colour by series, or give each category its own tone (one series). |
45
+ | negativeTone | "edition" \| "collectible" \| "standard" \| "info" \| "warning" \| "primary" \| "neutral" \| "success" \| "destructive" | — | Colour negative values with this tone. |
46
+ | radius | number | 4 | Corner radius of each bar's far end, in px. Stacks round only the outer segment. |
47
+ | barWidth | number | 24 | Maximum thickness of a bar, in px: the band's leftover is air, not a fatter bar. Raise it for a bolder presentation look. |
48
+
49
+ ## Tokens used
50
+
51
+ `tone-edition`, `tone-collectible`, `tone-standard`, `tone-info`, `tone-warning`, `primary`, `muted-foreground`, `border`, `popover`, `popover-foreground`, `foreground`, `background`
52
+
53
+ ## Motion
54
+
55
+ - **arrival** · Bar · _custom (outside the recipe layer)_ — Once on screen, bars rise from the baseline in sequence on the emphasized easing (scale from the zero line) and their labels fade in. On the motion tokens; reduced motion shows the finished chart.
56
+ - **response** · Cursor + tooltip · _no motion_ — The hovered category gets a band behind it and the other bars dim at once — a lagging highlight reads as a slow chart.
57
+ - **transition** · Bar · _custom (outside the recipe layer)_ — Geometry eases from the old shape to the new one through a CSS `d` transition on the motion tokens (no recipe animates path geometry); browsers without `d` transitions update in place.
58
+ - **activity** · BarChart · _no motion_ — A chart has no indeterminate state; show a Skeleton while the data loads.
59
+
60
+ ## Accessibility
61
+
62
+ - The drawing is aria-hidden; the same numbers are in a visually hidden data table (captioned by `label`) that the chart is aria-describedby, so assistive tech reads values, not shapes.
63
+ - With a tooltip, the chart is one tab stop (role=group, aria-roledescription="chart"): arrow keys, Home and End step through the data, Escape clears, and each step is announced in a polite live region.
64
+ - Colour is never the only key: series are named in the legend, tooltip and table.
65
+
66
+ ## Related
67
+
68
+ - Components: AreaChart, PieChart, SegmentedBar, Card
69
+
70
+ ## Examples
71
+
72
+ ### Grouped series
73
+
74
+ ```tsx
75
+ import { BarChart } from "@sableui/react"
76
+
77
+ <BarChart
78
+ label="Visitors by device"
79
+ data={visitors}
80
+ index="month"
81
+ series={[
82
+ { key: "desktop", label: "Desktop" },
83
+ { key: "mobile", label: "Mobile" },
84
+ ]}
85
+ indexFormatter={(m) => m.slice(0, 3)}
86
+ />
87
+ ```
88
+
89
+ ### Horizontal, labelled inside
90
+
91
+ ```tsx
92
+ import { BarChart } from "@sableui/react"
93
+
94
+ const browsers = [
95
+ { browser: "Chrome", visitors: 275 },
96
+ { browser: "Safari", visitors: 200 },
97
+ { browser: "Firefox", visitors: 187 },
98
+ { browser: "Edge", visitors: 173 },
99
+ ]
100
+
101
+ <BarChart
102
+ label="Visitors by browser"
103
+ data={browsers}
104
+ index="browser"
105
+ series={[{ key: "visitors", label: "Visitors" }]}
106
+ orientation="horizontal"
107
+ labels="inside"
108
+ yAxis={false}
109
+ grid={false}
110
+ />
111
+ ```
112
+
113
+ ### Profit and loss
114
+
115
+ ```tsx
116
+ import { BarChart } from "@sableui/react"
117
+
118
+ <BarChart
119
+ label="Net revenue by month"
120
+ data={[
121
+ { month: "Jan", net: 186 },
122
+ { month: "Feb", net: -95 },
123
+ { month: "Mar", net: 137 },
124
+ { month: "Apr", net: -60 },
125
+ ]}
126
+ index="month"
127
+ series={[{ key: "net", label: "Net", tone: "success" }]}
128
+ negativeTone="destructive"
129
+ labels
130
+ />
131
+ ```
@@ -72,7 +72,7 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
72
72
 
73
73
  ## Related
74
74
 
75
- - Components: Menu, DotsMenu, Icon, Banner
75
+ - Components: Menu, OverflowMenu, Icon, Banner
76
76
  - Patterns: SettingsPage, AuthPage, DashboardPage
77
77
 
78
78
  ## Examples
@@ -34,9 +34,9 @@ Use Card for a discrete thing with its own boundary, such as one product, one no
34
34
  | Name | Type | Description |
35
35
  | --- | --- | --- |
36
36
  | CardHeader | ComponentPropsWithRef<"div"> | Top section holding CardTitle/CardDescription (and optionally CardAction). Padding follows the density knob. |
37
- | CardTitle | ComponentPropsWithRef<"div"> | The card's name (text-title-lg). |
37
+ | CardTitle | ComponentPropsWithRef<"div"> & { render?: useRender.RenderProp } | The card's name (text-title-lg). A div by default; pass `render={<h2 />}` (or h3…) to make it a heading in the page outline, at the level the card's position calls for. |
38
38
  | CardDescription | ComponentPropsWithRef<"div"> | Muted supporting line under the title. |
39
- | CardAction | ComponentPropsWithRef<"div"> | Header-aligned top-right action slot (e.g. a dots menu, switch, or close button). Place inside CardHeader, which becomes a 2-column grid when present and collapses to a single column otherwise. |
39
+ | CardAction | ComponentPropsWithRef<"div"> | Header-aligned top-right action slot (e.g. an overflow menu, switch, or close button). Place inside CardHeader, which becomes a 2-column grid when present and collapses to a single column otherwise. |
40
40
  | CardContent | ComponentPropsWithRef<"div"> & { flush?: boolean } | The body region. Padding follows the density knob on all sides — does not assume a preceding CardHeader, so it is safe to use standalone (e.g. no CardHeader in the card). When paired with CardHeader, the seam stays tight because CardHeader zeroes its own bottom padding. `flush` runs it edge to edge for a table: the Table insets its first and last columns by exactly the card padding, so it lines up with the header — never hand-set pl-6/pr-6 on columns. |
41
41
  | CardList | ComponentPropsWithRef<"div"> | Rows that run edge to edge in the card (Entity rows, most often), divided by hairlines. Insets each row to the card's own padding, so a list card lines up with every other card at any density — no hand-tuned px-* on the rows. Under a CardHeader it opens with a hairline and a gap; alone it fills the card. |
42
42
  | CardFooter | ComponentPropsWithRef<"div"> | Bottom row for actions/meta. Padding follows the density knob. |
@@ -58,7 +58,7 @@ Use ContextMenu for actions that are specific to the object or area the user inv
58
58
 
59
59
  ## Related
60
60
 
61
- - Components: Menu, DotsMenu
61
+ - Components: Menu, OverflowMenu
62
62
 
63
63
  ## Examples
64
64
 
@@ -0,0 +1,79 @@
1
+ # DateTile
2
+
3
+ **Status:** stable
4
+
5
+ A calendar-page date: the short month on a primary band over the day number, in a <time> element. The leading visual of an event, booking or deadline row.
6
+
7
+ **Keywords:** date, calendar, event, day, month, schedule, booking, tile
8
+
9
+ ## Usage
10
+
11
+ Use DateTile as the leading visual when the date is what a reader scans a list or card by — events, bookings, deadlines, releases. It sits beside a title and a time line, usually in an Entity's `leading` slot or a flex row in a CardContent. For a date inside a sentence or a row's metadata, use text or RelativeTime instead.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Pair the tile with a title and the full time or range in text beside it; the tile only names the day.
16
+ - Do: Keep every row of a list on DateTile so the dates line up as one column.
17
+ - Don't: Use it for timestamps ("2 hours ago", "Oct 14, 10:02") — that is RelativeTime or plain metadata text.
18
+ - Don't: Stretch it to its neighbour's height or restyle the band per row; the tile is a fixed shape so a column of them reads as one calendar.
19
+
20
+ ### Anatomy
21
+
22
+ | Part | Required | Description |
23
+ | --- | --- | --- |
24
+ | Band | yes | The short month name on the primary colour. |
25
+ | Day | yes | The day of the month, on the muted surface. |
26
+
27
+ ## Props
28
+
29
+ | Name | Type | Default | Description |
30
+ | --- | --- | --- | --- |
31
+ | date | Date | — | The day to show, read in local time. Rendered as `<time dateTime="YYYY-MM-DD">`. |
32
+ | locale | string | runtime locale | BCP 47 locale for the month name. |
33
+
34
+ ## Tokens used
35
+
36
+ `muted`, `primary`, `primary-foreground`, `foreground`
37
+
38
+ ## Motion
39
+
40
+ - **arrival** · Tile · _no motion_ — A static label; it appears with whatever row or card holds it.
41
+ - **response** · Tile · _no motion_ — Not interactive.
42
+ - **transition** · Tile · _no motion_ — It has no states to move between.
43
+ - **activity** · Tile · _no motion_ — Nothing runs in the background.
44
+
45
+ ## Accessibility
46
+
47
+ - Renders a <time> with a machine-readable `dateTime`, so the day is announced as text ("Oct 14") and parsable as a date.
48
+ - The tile names only the day — state the weekday, time or range in the text beside it, where it is read in order.
49
+
50
+ ## Related
51
+
52
+ - Components: Entity, Card, RelativeTime, Calendar
53
+
54
+ ## Examples
55
+
56
+ ### Event row
57
+
58
+ ```tsx
59
+ import { DateTile, Row, Stack } from "@sableui/react"
60
+
61
+ <Row gap={4} align="start">
62
+ <DateTile date={new Date(2026, 9, 14)} />
63
+ <Stack gap={1}>
64
+ <span className="text-title-lg">Design review</span>
65
+ <span className="text-body-sm text-muted-foreground">Tuesday, 2:00 – 2:45 PM</span>
66
+ </Stack>
67
+ </Row>
68
+ ```
69
+
70
+ ### In an Entity list
71
+
72
+ ```tsx
73
+ import { CardList, DateTile, Entity } from "@sableui/react"
74
+
75
+ <CardList>
76
+ <Entity leading={<DateTile date={new Date(2026, 9, 14)} />} title="Design review" subtitle="Tuesday, 2:00 PM" />
77
+ <Entity leading={<DateTile date={new Date(2026, 9, 21)} />} title="Launch" subtitle="Tuesday, all day" />
78
+ </CardList>
79
+ ```