@sableui/react 0.8.1 → 0.9.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 (52) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +97 -0
  3. package/bin/sable.mjs +26 -11
  4. package/dist/blocks/index.d.ts +1 -1
  5. package/dist/components/app-shell/app-shell.js +2 -12
  6. package/dist/components/app-shell/app-shell.meta.js +22 -0
  7. package/dist/components/app-shell/index.js +2 -0
  8. package/dist/components/app-shell/skip-link.js +28 -0
  9. package/dist/components/button/button.js +35 -8
  10. package/dist/components/button/button.meta.js +6 -1
  11. package/dist/components/card/card.meta.js +1 -1
  12. package/dist/components/command-menu/command-menu.js +27 -2
  13. package/dist/components/data-table/data-table.meta.js +4 -0
  14. package/dist/components/date-tile/date-tile.meta.js +1 -0
  15. package/dist/components/feedback/feedback.meta.js +1 -0
  16. package/dist/components/fieldset/fieldset.js +22 -13
  17. package/dist/components/fieldset/fieldset.meta.js +25 -1
  18. package/dist/components/form/form.js +45 -9
  19. package/dist/components/form/form.meta.js +12 -3
  20. package/dist/components/media-tile/media-tile.js +16 -2
  21. package/dist/components/media-tile/media-tile.meta.js +8 -0
  22. package/dist/components/radio-card/radio-card.js +6 -2
  23. package/dist/components/radio-card/radio-card.meta.js +5 -0
  24. package/dist/components/sheet/sheet.js +1 -0
  25. package/dist/components/stepper/stepper.js +7 -2
  26. package/dist/components/stepper/stepper.meta.js +1 -1
  27. package/dist/components/toast/toast.js +2 -0
  28. package/dist/file-hashes.json +29 -28
  29. package/dist/index.d.ts +109 -22
  30. package/dist/index.js +1 -1
  31. package/dist/{meta-CcE_SeFu.d.ts → meta-DVG8ceze.d.ts} +9 -0
  32. package/dist/patterns/index.d.ts +1 -1
  33. package/dist/styles.css +30 -0
  34. package/llms/components/app-shell.md +26 -0
  35. package/llms/components/button.md +2 -1
  36. package/llms/components/card.md +1 -1
  37. package/llms/components/data-table.md +1 -0
  38. package/llms/components/fieldset.md +21 -1
  39. package/llms/components/form.md +11 -2
  40. package/llms/components/media-tile.md +32 -0
  41. package/llms/components/radio-card.md +1 -0
  42. package/llms/components/stepper.md +1 -1
  43. package/llms/tasks/creating-a-preset.md +4 -1
  44. package/llms/tasks/getting-started.md +6 -0
  45. package/llms/tasks/tables-with-tanstack.md +157 -0
  46. package/llms-full.txt +266 -7
  47. package/llms.txt +2 -2
  48. package/package.json +1 -1
  49. package/registry.json +94 -10
  50. package/scripts/grade.mjs +14 -3
  51. package/scripts/sources.mjs +10 -2
  52. package/skills/sable/SKILL.md +32 -7
@@ -38,7 +38,7 @@ Use Stepper above or beside a flow that is split into a known, small number of n
38
38
  | Name | Type | Default | Description |
39
39
  | --- | --- | --- | --- |
40
40
  | current | number | 0 | Zero-based index of the current step. Earlier steps derive complete, later ones upcoming. |
41
- | orientation | "horizontal" \| "vertical" | "horizontal" | Horizontal for a short flow across the top of a form; vertical for long flows, narrow columns or steps with descriptions. |
41
+ | orientation | "horizontal" \| "vertical" | "horizontal" | Horizontal for a short flow across the top of a form; vertical for long flows, narrow columns or steps with descriptions. A horizontal stepper narrower than 28rem (a phone) compacts itself: every disc stays, only the current step's title shows, and the other titles stay for screen readers. No breakpoint code needed. |
42
42
  | indicator | "number" \| "dot" | "number" | `number` for a flow the user moves through; `dot` for a timeline of events (delivery tracking, a document's history) where the position number adds nothing. The dot centres on the title's first line and scales with its type at every density. |
43
43
  | statusLabels | Partial<Record<StepStatus, string>> | — | Screen-reader status words appended to each title (Completed, Current step, Not started, Needs attention). Override to localize. |
44
44
  | aria-label | string | — | Names the flow ("Checkout progress"). Every Stepper needs one. |
@@ -13,7 +13,10 @@ cannot establish animation timing, hidden behavior, or an unseen responsive stat
13
13
  Choose an original direction suitable for the user's future product. Map that
14
14
  direction onto the installed Sable token vocabulary. Preserve readable semantic
15
15
  roles and supported control behavior rather than recoloring every primitive
16
- independently. Load fonts through the consuming project's font setup. Page widths
16
+ independently. Load fonts through the consuming project's font setup.
17
+ `fonts.display` sets every `text-display-*` tier and `text-heading-1` to `-4`,
18
+ so it reaches PageHeader and SectionHeader titles too, not only a hero.
19
+ Leave it unset to keep headings in `fonts.sans`. Page widths
17
20
  are optional: set `page-inset` and the `region-*` caps (`form`, `feed`, `content`,
18
21
  `wide`, `full`) in `tokens.shared` only when the direction calls for a different
19
22
  page proportion, and keep each cap's role.
@@ -4,6 +4,12 @@ Sable is on npm as `@sableui/react`. The installed version's `registry.json`
4
4
  consume block has the exact recipe. Requirements: React 19, React DOM 19, and
5
5
  Tailwind CSS 4 integrated into your build.
6
6
 
7
+ Sable works in any React 19 app and depends on no router, data or form
8
+ library. Starting from a blank slate, its suggested stack is React with
9
+ TypeScript on Vite, TanStack Router, TanStack Query, TanStack Form for forms with
10
+ validation logic, and TanStack Table once a table outgrows `DataTable`
11
+ (`sable docs tables-with-tanstack`). An existing app keeps its own.
12
+
7
13
  ## Install
8
14
 
9
15
  ```bash
@@ -0,0 +1,157 @@
1
+ # Task: tables with TanStack Table
2
+
3
+ `DataTable` is Sable's own table: a column list and a data array, with
4
+ client-side sorting, row click, checkbox selection and an empty state, and no
5
+ dependency. Use it until the table needs what it doesn't do:
6
+
7
+ - server-side sorting, filtering or paging over a large data set;
8
+ - column visibility, ordering, resizing or pinning;
9
+ - grouping, expanding rows, faceted filters;
10
+ - thousands of rows that need virtualising.
11
+
12
+ Then use **TanStack Table** (`@tanstack/react-table`) for the table's logic and
13
+ render it with Sable's `Table` parts, so it still looks and behaves like every
14
+ other Sable table: the same row hover, selected fill, edge insets in a flush
15
+ `Card`, and numeric columns. Sable doesn't depend on TanStack; this is the
16
+ pairing it suggests, and any headless table library renders the same way.
17
+
18
+ ## The pairing
19
+
20
+ TanStack owns state (sorting, selection, pagination); Sable owns markup. Map them
21
+ one to one:
22
+
23
+ | TanStack | Sable |
24
+ |---|---|
25
+ | `getHeaderGroups()` → header row | `TableHeader` › `TableRow` › `TableHead` |
26
+ | `getRowModel().rows` | `TableBody` › `TableRow` › `TableCell` |
27
+ | `row.getIsSelected()` | `data-selected` on the `TableRow` (the selected fill) |
28
+ | `column.getToggleSortingHandler()` | a ghost `Button size="xs"` in the head, `aria-sort` on the `TableHead` |
29
+ | selection column | `Checkbox` per row (named after the row) and in the head (`indeterminate` when some are checked) |
30
+ | `getPageCount()`, `setPageIndex()` | `Pagination` (1-based) |
31
+
32
+ ```tsx
33
+ import {
34
+ type ColumnDef,
35
+ type RowSelectionState,
36
+ type SortingState,
37
+ flexRender,
38
+ getCoreRowModel,
39
+ getPaginationRowModel,
40
+ getSortedRowModel,
41
+ useReactTable,
42
+ } from "@tanstack/react-table"
43
+ import { Button, Checkbox, Icon, Pagination, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@sableui/react"
44
+ import { useState } from "react"
45
+
46
+ type Ticket = { id: string; subject: string; customer: string; updated: Date }
47
+
48
+ const columns: ColumnDef<Ticket>[] = [
49
+ {
50
+ id: "select",
51
+ enableSorting: false,
52
+ header: ({ table }) => (
53
+ <Checkbox
54
+ aria-label={`Select all ${table.getRowModel().rows.length} tickets shown`}
55
+ checked={table.getIsAllPageRowsSelected()}
56
+ indeterminate={table.getIsSomePageRowsSelected()}
57
+ onCheckedChange={(checked) => table.toggleAllPageRowsSelected(checked)}
58
+ />
59
+ ),
60
+ cell: ({ row }) => (
61
+ <Checkbox
62
+ aria-label={`Select ${row.original.subject}`}
63
+ checked={row.getIsSelected()}
64
+ onCheckedChange={(checked) => row.toggleSelected(checked)}
65
+ />
66
+ ),
67
+ },
68
+ { accessorKey: "subject", header: "Subject" },
69
+ { accessorKey: "customer", header: "Customer" },
70
+ { accessorKey: "updated", header: "Updated", cell: ({ getValue }) => getValue<Date>().toLocaleDateString() },
71
+ ]
72
+
73
+ export function TicketTable({ data }: { data: Ticket[] }) {
74
+ const [sorting, setSorting] = useState<SortingState>([])
75
+ const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
76
+ const table = useReactTable({
77
+ data,
78
+ columns,
79
+ getRowId: (ticket) => ticket.id,
80
+ state: { sorting, rowSelection },
81
+ onSortingChange: setSorting,
82
+ onRowSelectionChange: setRowSelection,
83
+ getCoreRowModel: getCoreRowModel(),
84
+ getSortedRowModel: getSortedRowModel(),
85
+ getPaginationRowModel: getPaginationRowModel(),
86
+ })
87
+
88
+ return (
89
+ <>
90
+ <Table>
91
+ <TableHeader>
92
+ {table.getHeaderGroups().map((group) => (
93
+ <TableRow key={group.id}>
94
+ {group.headers.map((header) => {
95
+ const sorted = header.column.getIsSorted()
96
+ const label = flexRender(header.column.columnDef.header, header.getContext())
97
+ return (
98
+ <TableHead
99
+ key={header.id}
100
+ className={header.column.id === "select" ? "w-0" : undefined}
101
+ aria-sort={sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : undefined}
102
+ >
103
+ {header.column.getCanSort() ? (
104
+ <Button variant="ghost" size="xs" bleed="start" onClick={header.column.getToggleSortingHandler()}>
105
+ {label}
106
+ <Icon name={sorted === "desc" ? "chevron-down" : "chevron-up"} className={sorted ? undefined : "opacity-60"} />
107
+ </Button>
108
+ ) : (
109
+ label
110
+ )}
111
+ </TableHead>
112
+ )
113
+ })}
114
+ </TableRow>
115
+ ))}
116
+ </TableHeader>
117
+ <TableBody>
118
+ {table.getRowModel().rows.map((row) => (
119
+ <TableRow key={row.id} data-selected={row.getIsSelected() ? "" : undefined}>
120
+ {row.getVisibleCells().map((cell) => (
121
+ <TableCell key={cell.id} className={cell.column.id === "select" ? "w-0" : undefined}>
122
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
123
+ </TableCell>
124
+ ))}
125
+ </TableRow>
126
+ ))}
127
+ </TableBody>
128
+ </Table>
129
+ <Pagination
130
+ page={table.getState().pagination.pageIndex + 1}
131
+ total={table.getPageCount()}
132
+ onPageChange={(page) => table.setPageIndex(page - 1)}
133
+ />
134
+ </>
135
+ )
136
+ }
137
+ ```
138
+
139
+ Inside a card, put the `Table` in `<CardContent flush>`, as with `DataTable`.
140
+ Mark a numeric column with `numeric` on its `TableHead` and `TableCell`s.
141
+
142
+ ## Server-side data
143
+
144
+ Set `manualSorting`, `manualPagination` (and `manualFiltering`) with
145
+ `pageCount` or `rowCount`, and fetch with the table's state as the query key.
146
+ In a blank-slate app that is TanStack Query:
147
+ `useQuery({ queryKey: ["tickets", sorting, pagination], queryFn })`, passing
148
+ `data ?? []` to the table. Show `Skeleton` rows while the first page loads and
149
+ keep the previous page on screen while the next one does.
150
+
151
+ ## Selection and bulk actions
152
+
153
+ Selection belongs to the rows shown: clear `rowSelection` when the filter or
154
+ view changes the data set. Above the table, render the bulk actions only while
155
+ something is selected, and keep a `role="status"` element mounted from the start
156
+ that says "3 of 25 selected", so a screen reader hears each change. The
157
+ `DataTable` docs show the same bar (`sable component DataTable --examples`).
package/llms-full.txt CHANGED
@@ -1350,7 +1350,10 @@ cannot establish animation timing, hidden behavior, or an unseen responsive stat
1350
1350
  Choose an original direction suitable for the user's future product. Map that
1351
1351
  direction onto the installed Sable token vocabulary. Preserve readable semantic
1352
1352
  roles and supported control behavior rather than recoloring every primitive
1353
- independently. Load fonts through the consuming project's font setup. Page widths
1353
+ independently. Load fonts through the consuming project's font setup.
1354
+ `fonts.display` sets every `text-display-*` tier and `text-heading-1` to `-4`,
1355
+ so it reaches PageHeader and SectionHeader titles too, not only a hero.
1356
+ Leave it unset to keep headings in `fonts.sans`. Page widths
1354
1357
  are optional: set `page-inset` and the `region-*` caps (`form`, `feed`, `content`,
1355
1358
  `wide`, `full`) in `tokens.shared` only when the direction calls for a different
1356
1359
  page proportion, and keep each cap's role.
@@ -1791,6 +1794,12 @@ Sable is on npm as `@sableui/react`. The installed version's `registry.json`
1791
1794
  consume block has the exact recipe. Requirements: React 19, React DOM 19, and
1792
1795
  Tailwind CSS 4 integrated into your build.
1793
1796
 
1797
+ Sable works in any React 19 app and depends on no router, data or form
1798
+ library. Starting from a blank slate, its suggested stack is React with
1799
+ TypeScript on Vite, TanStack Router, TanStack Query, TanStack Form for forms with
1800
+ validation logic, and TanStack Table once a table outgrows `DataTable`
1801
+ (`sable docs tables-with-tanstack`). An existing app keeps its own.
1802
+
1794
1803
  ## Install
1795
1804
 
1796
1805
  ```bash
@@ -2318,6 +2327,166 @@ lesson only within the contexts the observation supports.
2318
2327
 
2319
2328
  ---
2320
2329
 
2330
+ # Task: tables with TanStack Table
2331
+
2332
+ `DataTable` is Sable's own table: a column list and a data array, with
2333
+ client-side sorting, row click, checkbox selection and an empty state, and no
2334
+ dependency. Use it until the table needs what it doesn't do:
2335
+
2336
+ - server-side sorting, filtering or paging over a large data set;
2337
+ - column visibility, ordering, resizing or pinning;
2338
+ - grouping, expanding rows, faceted filters;
2339
+ - thousands of rows that need virtualising.
2340
+
2341
+ Then use **TanStack Table** (`@tanstack/react-table`) for the table's logic and
2342
+ render it with Sable's `Table` parts, so it still looks and behaves like every
2343
+ other Sable table: the same row hover, selected fill, edge insets in a flush
2344
+ `Card`, and numeric columns. Sable doesn't depend on TanStack; this is the
2345
+ pairing it suggests, and any headless table library renders the same way.
2346
+
2347
+ ## The pairing
2348
+
2349
+ TanStack owns state (sorting, selection, pagination); Sable owns markup. Map them
2350
+ one to one:
2351
+
2352
+ | TanStack | Sable |
2353
+ |---|---|
2354
+ | `getHeaderGroups()` → header row | `TableHeader` › `TableRow` › `TableHead` |
2355
+ | `getRowModel().rows` | `TableBody` › `TableRow` › `TableCell` |
2356
+ | `row.getIsSelected()` | `data-selected` on the `TableRow` (the selected fill) |
2357
+ | `column.getToggleSortingHandler()` | a ghost `Button size="xs"` in the head, `aria-sort` on the `TableHead` |
2358
+ | selection column | `Checkbox` per row (named after the row) and in the head (`indeterminate` when some are checked) |
2359
+ | `getPageCount()`, `setPageIndex()` | `Pagination` (1-based) |
2360
+
2361
+ ```tsx
2362
+ import {
2363
+ type ColumnDef,
2364
+ type RowSelectionState,
2365
+ type SortingState,
2366
+ flexRender,
2367
+ getCoreRowModel,
2368
+ getPaginationRowModel,
2369
+ getSortedRowModel,
2370
+ useReactTable,
2371
+ } from "@tanstack/react-table"
2372
+ import { Button, Checkbox, Icon, Pagination, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@sableui/react"
2373
+ import { useState } from "react"
2374
+
2375
+ type Ticket = { id: string; subject: string; customer: string; updated: Date }
2376
+
2377
+ const columns: ColumnDef<Ticket>[] = [
2378
+ {
2379
+ id: "select",
2380
+ enableSorting: false,
2381
+ header: ({ table }) => (
2382
+ <Checkbox
2383
+ aria-label={`Select all ${table.getRowModel().rows.length} tickets shown`}
2384
+ checked={table.getIsAllPageRowsSelected()}
2385
+ indeterminate={table.getIsSomePageRowsSelected()}
2386
+ onCheckedChange={(checked) => table.toggleAllPageRowsSelected(checked)}
2387
+ />
2388
+ ),
2389
+ cell: ({ row }) => (
2390
+ <Checkbox
2391
+ aria-label={`Select ${row.original.subject}`}
2392
+ checked={row.getIsSelected()}
2393
+ onCheckedChange={(checked) => row.toggleSelected(checked)}
2394
+ />
2395
+ ),
2396
+ },
2397
+ { accessorKey: "subject", header: "Subject" },
2398
+ { accessorKey: "customer", header: "Customer" },
2399
+ { accessorKey: "updated", header: "Updated", cell: ({ getValue }) => getValue<Date>().toLocaleDateString() },
2400
+ ]
2401
+
2402
+ export function TicketTable({ data }: { data: Ticket[] }) {
2403
+ const [sorting, setSorting] = useState<SortingState>([])
2404
+ const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
2405
+ const table = useReactTable({
2406
+ data,
2407
+ columns,
2408
+ getRowId: (ticket) => ticket.id,
2409
+ state: { sorting, rowSelection },
2410
+ onSortingChange: setSorting,
2411
+ onRowSelectionChange: setRowSelection,
2412
+ getCoreRowModel: getCoreRowModel(),
2413
+ getSortedRowModel: getSortedRowModel(),
2414
+ getPaginationRowModel: getPaginationRowModel(),
2415
+ })
2416
+
2417
+ return (
2418
+ <>
2419
+ <Table>
2420
+ <TableHeader>
2421
+ {table.getHeaderGroups().map((group) => (
2422
+ <TableRow key={group.id}>
2423
+ {group.headers.map((header) => {
2424
+ const sorted = header.column.getIsSorted()
2425
+ const label = flexRender(header.column.columnDef.header, header.getContext())
2426
+ return (
2427
+ <TableHead
2428
+ key={header.id}
2429
+ className={header.column.id === "select" ? "w-0" : undefined}
2430
+ aria-sort={sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : undefined}
2431
+ >
2432
+ {header.column.getCanSort() ? (
2433
+ <Button variant="ghost" size="xs" bleed="start" onClick={header.column.getToggleSortingHandler()}>
2434
+ {label}
2435
+ <Icon name={sorted === "desc" ? "chevron-down" : "chevron-up"} className={sorted ? undefined : "opacity-60"} />
2436
+ </Button>
2437
+ ) : (
2438
+ label
2439
+ )}
2440
+ </TableHead>
2441
+ )
2442
+ })}
2443
+ </TableRow>
2444
+ ))}
2445
+ </TableHeader>
2446
+ <TableBody>
2447
+ {table.getRowModel().rows.map((row) => (
2448
+ <TableRow key={row.id} data-selected={row.getIsSelected() ? "" : undefined}>
2449
+ {row.getVisibleCells().map((cell) => (
2450
+ <TableCell key={cell.id} className={cell.column.id === "select" ? "w-0" : undefined}>
2451
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
2452
+ </TableCell>
2453
+ ))}
2454
+ </TableRow>
2455
+ ))}
2456
+ </TableBody>
2457
+ </Table>
2458
+ <Pagination
2459
+ page={table.getState().pagination.pageIndex + 1}
2460
+ total={table.getPageCount()}
2461
+ onPageChange={(page) => table.setPageIndex(page - 1)}
2462
+ />
2463
+ </>
2464
+ )
2465
+ }
2466
+ ```
2467
+
2468
+ Inside a card, put the `Table` in `<CardContent flush>`, as with `DataTable`.
2469
+ Mark a numeric column with `numeric` on its `TableHead` and `TableCell`s.
2470
+
2471
+ ## Server-side data
2472
+
2473
+ Set `manualSorting`, `manualPagination` (and `manualFiltering`) with
2474
+ `pageCount` or `rowCount`, and fetch with the table's state as the query key.
2475
+ In a blank-slate app that is TanStack Query:
2476
+ `useQuery({ queryKey: ["tickets", sorting, pagination], queryFn })`, passing
2477
+ `data ?? []` to the table. Show `Skeleton` rows while the first page loads and
2478
+ keep the previous page on screen while the next one does.
2479
+
2480
+ ## Selection and bulk actions
2481
+
2482
+ Selection belongs to the rows shown: clear `rowSelection` when the filter or
2483
+ view changes the data set. Above the table, render the bulk actions only while
2484
+ something is selected, and keep a `role="status"` element mounted from the start
2485
+ that says "3 of 25 selected", so a screen reader hears each change. The
2486
+ `DataTable` docs show the same bar (`sable component DataTable --examples`).
2487
+
2488
+ ---
2489
+
2321
2490
  # Task: use a portable preset
2322
2491
 
2323
2492
  Bundled presets are maintained with Sable. Personal presets are app-owned JSON
@@ -9849,6 +10018,12 @@ Use AppShell as the outer product frame for authenticated or app-like experience
9849
10018
  | Bottom nav | no | Mobile fixed navigation with reserved bottom space. |
9850
10019
  | Dock | no | Persistent bottom chrome that outlives routes — a now-playing bar, a call bar. Sticky above the bottom nav; the desktop Sidebar shortens to clear it. |
9851
10020
 
10021
+ ## Subcomponents
10022
+
10023
+ | Name | Type | Description |
10024
+ | --- | --- | --- |
10025
+ | SkipLink | ComponentProps<"a"> | AppShell's skip link on its own, for a layout that is not an AppShell (a storefront with a top header). Put it first in the body and give the page's <main> the matching id: `<SkipLink />` then `<main id="main-content" tabIndex={-1}>`. `href` defaults to #main-content, the label to "Skip to content". |
10026
+
9852
10027
  ## Props
9853
10028
 
9854
10029
  | Name | Type | Default | Description |
@@ -9909,6 +10084,26 @@ import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader
9909
10084
  </AppShell>
9910
10085
  ```
9911
10086
 
10087
+ ### SkipLink in a layout of your own
10088
+
10089
+ ```tsx
10090
+ import { Button, Icon, PageHeader, Region, SkipLink } from "@sableui/react"
10091
+ // <Link> = your router's link (react-router, next/link, @tanstack/router, …)
10092
+
10093
+ <>
10094
+ <SkipLink />
10095
+ <header className="flex items-center justify-between px-(--page-inset) py-4">
10096
+ <Link to="/">Alder & Lune</Link>
10097
+ <Button variant="ghost" size="icon" aria-label="Search"><Icon name="search" /></Button>
10098
+ </header>
10099
+ <main id="main-content" tabIndex={-1}>
10100
+ <Region max="wide">
10101
+ <PageHeader title="Lighting" />
10102
+ </Region>
10103
+ </main>
10104
+ </>
10105
+ ```
10106
+
9912
10107
  ### Minimal (sidebar + content only)
9913
10108
 
9914
10109
  ```tsx
@@ -11113,7 +11308,8 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
11113
11308
  | variant | "primary" \| "secondary" \| "ghost" \| "outline" \| "destructive" \| "link" \| "floating" \| "field" \| "on-color" \| "on-color-outline" \| "on-color-ghost" | "primary" | Visual emphasis tier. One primary per screen. The on-color tiers are for buttons ON a coloured fill: they paint with the inherited text colour (the fill's ink) — on-color is solid (ink background, fill-coloured label via the optional --on-color-fill var, else black/white), on-color-outline and on-color-ghost are transparent with an ink label. Never use them on page or card surfaces. floating is for a control that sits OVER content (carousel arrows, a marquee pause, a jump-to-latest over a conversation): its own edge, lift and translucent popover fill, so it reads on arbitrary content beneath. field is for a button docked in a workspace panel among xs fields: the inset panel-field fill and the field's corner, so a picker trigger or an add action reads as one of the row's controls; pair it with size xs or icon-xs. |
11114
11309
  | size | "xs" \| "sm" \| "md" \| "cta" \| "icon" \| "icon-sm" \| "icon-xs" | "md" | Reads the density ladder, so every value differs by register: page 28/32/40/44, workspace 24/28/32/36. xs and icon-xs were added in 0.31.0 for the workspace control floor and are the sizes tool chrome wants; md is the default everywhere. icon/icon-sm/icon-xs are square. |
11115
11310
  | render | ReactElement \| (props, state) => ReactElement | — | Compose with another element/component through the Base UI render prop. |
11116
- | loading | boolean | false | Disable the button and show a leading Spinner while an action is in flight. Sets aria-busy; a native button is disabled, a render element (e.g. an <a>) gets aria-disabled. |
11311
+ | loading | boolean | false | Show a leading Spinner while an action is in flight and refuse presses. Sets aria-busy and aria-disabled, never the native disabled: disabling the focused button sent focus to <body>, so a sheet the action opened had nowhere to return it. |
11312
+ | onClick | (event: MouseEvent<HTMLButtonElement>) => unknown | — | May return a promise: the button shows `loading` until it settles and ignores further presses meanwhile, from the first click on. Prefer it to a `busy` flag in state, which takes effect a render late (three fast clicks ran three times). |
11117
11313
  | badge | ReactNode | — | A corner indicator (usually <NotificationBadge dot /> or a count) overlaid on the top-end corner of the glyph. For icon-only sizes; never hand-position a badge over a Button with absolute offsets. |
11118
11314
  | loadingLabel | ReactNode | — | Optional text shown in place of children while loading. |
11119
11315
  | iconStart | ReactNode | — | Icon before the label. The button takes one 2px step off its start padding so icon + label read as centred. Replaced by the spinner while loading. |
@@ -11429,7 +11625,7 @@ Use Card for a discrete thing with its own boundary, such as one product, one no
11429
11625
 
11430
11626
  | Name | Type | Default | Description |
11431
11627
  | --- | --- | --- | --- |
11432
- | variant | "default" \| "flat" | "default" | "default" raises with bg-card + border + soft shadow; "flat" is transparent. |
11628
+ | variant | "default" \| "flat" | "default" | "default" raises with bg-card + border + soft shadow; "flat" is transparent, for a card inside a section that already frames it. A flat card made a link or button shows its shape only on hover (the muted fill), so over a photo with square bottom corners it reads as a cropped image at rest: give a linked media card the default variant. |
11433
11629
  | density | "comfortable" \| "compact" | "comfortable" | Padding of the Header/Content/Footer parts: comfortable = p-6 (default), compact = p-4 for denser section panels (pair compact with the flat variant for a settings / form section panel). Threaded to the parts via the inherited --card-p custom property. Card sets --card-p via inline style, which beats a plain (non-!important) stylesheet override of the same property on the same element — use this prop rather than a className override; if an external override is unavoidable, it needs !important. |
11434
11630
  | render | useRender.RenderProp | — | Replace the rendered element through the Base UI render prop — most often `render={<a href="/x" />}` (or a router Link) to make the whole card one link. When the rendered element is a link (a[href]) or a button, the card gains a hover state (stronger border and shadow; a muted fill when flat), a full-card focus-visible ring and motion-interactive easing. Any other element (article, li) stays a static surface. Children can answer the hover with `group-hover/card:`. |
11435
11631
 
@@ -12731,6 +12927,7 @@ Use DataTable when a data array and column definition should render a sortable,
12731
12927
  - Do: Provide stable rowKey values for dynamic data so selection and sorting do not jump.
12732
12928
  - Do: Use sortable only on columns where ordering is obvious and useful.
12733
12929
  - Do: Pair server-side datasets with Pagination and pass already-sorted/paged rows.
12930
+ - Do: Move to TanStack Table, rendered with Sable's Table parts, once the table needs server-side sorting or filtering, column visibility or resizing, grouping, or virtualised rows. DataTable stays dependency-free and does not grow into a data grid; `sable docs tables-with-tanstack` maps one onto the other.
12734
12931
  - Don't: Make every row clickable and also fill it with competing buttons. Choose a clear row interaction model.
12735
12932
  - Don't: Use DataTable for heterogeneous cards or timeline content. A list or card layout will scan better.
12736
12933
 
@@ -13667,7 +13864,7 @@ import { Feedback } from "@sableui/react"
13667
13864
 
13668
13865
  **Status:** stable
13669
13866
 
13670
- Form section panel (Card-like bordered surface) over Base UI Fieldset, with Legend/Description/Content/Footer parts.
13867
+ Form section panel (Card-like bordered surface) over Base UI Fieldset, with Legend/Description/Content/Footer parts; `variant="plain"` is the bare group, for a radio group's shared name.
13671
13868
 
13672
13869
  **Keywords:** fieldset, form group, legend, fields
13673
13870
 
@@ -13679,6 +13876,7 @@ Use Fieldset to group related controls under a shared label or explanation.
13679
13876
 
13680
13877
  - Do: Use it for grouped form sections such as contact details, preferences, or permissions.
13681
13878
  - Do: Keep the legend meaningful so assistive tech and agents understand the group purpose.
13879
+ - Do: Use `variant="plain"` to name a radio group or a run of checkboxes inside a form or card that is already framed: the legend reads as a field label and nothing draws a second box. Do not strip the card variant with classes instead.
13682
13880
  - Don't: Do not use Fieldset as a generic spacing wrapper outside forms.
13683
13881
  - Don't: Do not use a Legend that is really a heading. A legend names the group's question for assistive technology; using it as a section title puts a question mark where the outline should be.
13684
13882
 
@@ -13705,6 +13903,7 @@ Use Fieldset to group related controls under a shared label or explanation.
13705
13903
 
13706
13904
  | Name | Type | Default | Description |
13707
13905
  | --- | --- | --- | --- |
13906
+ | variant | "card" \| "plain" | "card" | `card` is a bordered, raised section with padded parts. `plain` is the grouping alone (no border, surface or inset) with the legend in the field-label style: the shared name a radio group needs, inside a form that is already framed. |
13708
13907
  | disabled | boolean | — | Disables the whole group. Renders <fieldset disabled> (native control disabling) and surfaces data-[disabled] for dimming. |
13709
13908
  | render | ReactElement \| function | — | Base UI render prop to replace or compose the underlying element. |
13710
13909
  | className | string | — | Merged onto each part via cn() for one-off overrides. |
@@ -13770,6 +13969,24 @@ import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } f
13770
13969
  // assistive technology about the form's structure.
13771
13970
  ```
13772
13971
 
13972
+ ### A radio group's name, without a second box
13973
+
13974
+ ```tsx
13975
+ import { Card, CardContent, Fieldset, FieldsetLegend, RadioCard, RadioGroup } from "@sableui/react"
13976
+
13977
+ <Card>
13978
+ <CardContent>
13979
+ <Fieldset variant="plain">
13980
+ <FieldsetLegend>Delivery</FieldsetLegend>
13981
+ <RadioGroup defaultValue="standard">
13982
+ <RadioCard value="standard" title="Standard" description="3–5 working days" trailing="Free" />
13983
+ <RadioCard value="express" title="Express" description="Next working day" trailing="$12.00" />
13984
+ </RadioGroup>
13985
+ </Fieldset>
13986
+ </CardContent>
13987
+ </Card>
13988
+ ```
13989
+
13773
13990
  ---
13774
13991
 
13775
13992
  # FileDropzone
@@ -14019,13 +14236,14 @@ Use Form and Field as the default wrapper for Sable form controls. Field owns th
14019
14236
 
14020
14237
  | Name | Type | Description |
14021
14238
  | --- | --- | --- |
14022
- | Field | { label?: ReactNode; description?: ReactNode; error?: ReactNode; required?: boolean; messages?: Partial<Record<"valueMissing" \| "typeMismatch" \| "patternMismatch" \| "tooShort" \| "tooLong" \| "rangeUnderflow" \| "rangeOverflow" \| "stepMismatch" \| "badInput", ReactNode>>; disabled?: boolean; name?: string; validate?: (value, formValues) => string \| string[] \| null \| Promise<…>; validationMode?: "onSubmit" \| "onBlur" \| "onChange"; validationDebounceTime?: number; orientation?: "vertical" \| "horizontal"; className?: string; children: ReactElement } | Wraps a single control element (e.g. <Input />) on Base UI Field and wires label (htmlFor + aria-labelledby), helper text and error (aria-describedby), aria-invalid, and required onto it automatically. Base UI-backed controls (Input, Textarea, Select, Combobox, NumberField, Checkbox, Switch, Slider, OtpField…) register through context; the child is also cloned so plain elements keep the same wiring. error renders a destructive message and takes precedence; without it the slot shows the field's messages entry for the native constraint that failed (messages={{ valueMissing: "Enter a subject." }}), else Base UI's validation message (the browser's wording for a native constraint, validate, or Form errors). required adds the native attribute plus a visual asterisk (defaults to false). disabled disables the control and dims the label. name keys the field in Form errors / onFormSubmit. validationMode overrides the Form's. orientation defaults to "vertical" (label → control); "horizontal" places control then label inline for a single checkbox / switch / radio — the control centres on the label's first line (it stays there when the label wraps) and description/error sit in the label's column. The root, label, description, error and control carry data-invalid / data-valid, data-touched, data-dirty, data-filled, data-focused and data-disabled for state styling. |
14239
+ | Field | { label?: ReactNode; description?: ReactNode; error?: ReactNode; required?: boolean; messages?: Partial<Record<"valueMissing" \| "typeMismatch" \| "patternMismatch" \| "tooShort" \| "tooLong" \| "rangeUnderflow" \| "rangeOverflow" \| "stepMismatch" \| "badInput", ReactNode>>; disabled?: boolean; name?: string; validate?: (value, formValues) => string \| string[] \| null \| Promise<…>; validationMode?: "onSubmit" \| "onBlur" \| "onChange"; validationDebounceTime?: number; orientation?: "vertical" \| "horizontal"; trailing?: ReactNode; className?: string; children: ReactElement } + other div props on the root | Wraps a single control element (e.g. <Input />) on Base UI Field and wires label (htmlFor + aria-labelledby), helper text and error (aria-describedby), aria-invalid, and required onto it automatically. Base UI-backed controls (Input, Textarea, Select, Combobox, NumberField, Checkbox, Switch, Slider, OtpField…) register through context; the child is also cloned so plain elements keep the same wiring. error renders a destructive message and takes precedence; without it the slot shows the field's messages entry for the native constraint that failed (messages={{ valueMissing: "Enter a subject." }}), else Base UI's validation message (the browser's wording for a native constraint, validate, or Form errors). required adds the native attribute plus a visual asterisk (defaults to false). disabled disables the control and dims the label. name keys the field in Form errors / onFormSubmit. validationMode overrides the Form's. orientation defaults to "vertical" (label → control); "horizontal" places control then label inline for a single checkbox / switch / radio — the control centres on the label's first line (it stays there when the label wraps) and description/error sit in the label's column. trailing puts something at the end of the control's row: vertically, beside the control (a promo code's Apply button, the control taking the rest); horizontally, at the end of the label's first line (a facet count). Other props (data-*, id, aria-*) land on the root. The root, label, description, error and control carry data-invalid / data-valid, data-touched, data-dirty, data-filled, data-focused and data-disabled for state styling. |
14023
14240
 
14024
14241
  ## Props
14025
14242
 
14026
14243
  | Name | Type | Default | Description |
14027
14244
  | --- | --- | --- | --- |
14028
- | Form | { validationMode?: "onSubmit" \| "onBlur" \| "onChange"; errors?: Record<string, string \| string[]>; onFormSubmit?: (values, details) => void; actionsRef?: RefObject<Form.Actions> } + ComponentPropsWithRef<"form"> | — | A <form> laid out as a vertical column with comfortable field spacing (override with className, e.g. gap-4). On submit it validates every Field (native constraints plus Field validate), shows each message under its field, focuses the first invalid control, and only then calls onSubmit. validationMode defaults to "onSubmit" (then live re-validation on change). errors shows external (e.g. server) messages keyed by Field name and clears one when its field is edited. onFormSubmit receives the named Field values and prevents the native submit. A control outside any Field is still checked by the browser before onSubmit runs. Validating in your own code (react-hook-form, a schema, state)? Pass noValidate: onSubmit always runs and each Field shows the error you set; without it an empty required Field stops the submit before your handler can set that error. |
14245
+ | Form | { validationMode?: "onSubmit" \| "onBlur" \| "onChange"; errors?: Record<string, string \| string[]>; onFormSubmit?: (values, details) => void; actionsRef?: RefObject<Form.Actions> } + ComponentPropsWithRef<"form"> | — | A <form> laid out as a vertical column with comfortable field spacing (override with className, e.g. gap-4). On submit it validates every Field (native constraints plus Field validate), shows each message under its field, focuses the first invalid control, and only then calls onSubmit. validationMode defaults to "onSubmit" (then live re-validation on change). errors shows external (e.g. server) messages keyed by Field name and clears one when its field is edited. onFormSubmit receives the named Field values and prevents the native submit. A control outside any Field is still checked by the browser before onSubmit runs. Validating in your own code (react-hook-form, a schema, state)? Pass noValidate: onSubmit always runs and each Field shows the error you set; without it an empty required Field stops the submit before your handler can set that error. Either way, once onSubmit returns (or the promise it returns settles), the first control a Field marks invalid takes focus. |
14246
+ | onSubmit | (event: FormEvent<HTMLFormElement>) => unknown | — | The native submit handler. It may return a promise. When it returns, or its promise settles, the first control a Field marks invalid takes focus, so errors your own code sets get the same treatment as Sable's validation. |
14029
14247
  | validationMode | "onSubmit" \| "onBlur" \| "onChange" | "onSubmit" | When Fields validate. After the first submit, fields re-check as the user edits. A Field's own validationMode wins. |
14030
14248
  | errors | Record<string, string \| string[]> | — | External (e.g. server) messages keyed by Field name, shown under that field. Each clears when its field is edited. |
14031
14249
  | onFormSubmit | (values: Record<string, unknown>, details) => void | — | Called with the named Field values after validation passes; prevents the native submit. |
@@ -14066,6 +14284,14 @@ import { Field, Input } from "@sableui/react"
14066
14284
  <Field label="Password" error="Must be at least 8 characters."><Input type="password" /></Field>
14067
14285
  ```
14068
14286
 
14287
+ ### An input with its action
14288
+
14289
+ ```tsx
14290
+ import { Button, Field, Input } from "@sableui/react"
14291
+
14292
+ <Field label="Promo code" error={promoError} trailing={<Button variant="secondary" onClick={applyPromo}>Apply</Button>}><Input name="promo" autoCapitalize="characters" /></Field>
14293
+ ```
14294
+
14069
14295
  ### Your own wording for a missing value
14070
14296
 
14071
14297
  ```tsx
@@ -15043,7 +15269,10 @@ The item in a media grid or shelf: an album, a playlist, a video, a podcast, a f
15043
15269
  | --- | --- | --- | --- |
15044
15270
  | title | ReactNode | — | The item's name. Seeds the fallback art when it is a string. |
15045
15271
  | subtitle | ReactNode | — | The second line — an artist, a channel, a year. |
15272
+ | meta | ReactNode | — | Lines under the subtitle, in its muted type: a price beside its sale price, a count of finishes, a status Badge. A wrapping block, where the subtitle is one truncated line. Use it for a product tile instead of building one. |
15046
15273
  | src | string | — | Cover image URL. Without one, or when it fails, the cover is GradientArt seeded by the item. |
15274
+ | srcSet | string | — | Responsive cover candidates, passed to the `<img>`. |
15275
+ | sizes | string | — | The cover's layout width for `srcSet`, e.g. `"(min-width: 64rem) 25vw, 50vw"`. |
15047
15276
  | alt | string | "" | Cover alt text. Empty by default: the title below already names the item. |
15048
15277
  | seed | string \| number | — | Seeds the fallback art. Defaults to `title` when it is a string. |
15049
15278
  | aspect | "square" \| "video" \| "poster" \| number | "square" | The cover's shape: square (albums, podcasts), video (16:9), poster (2:3 — films, books), or any width / height ratio. |
@@ -15089,6 +15318,35 @@ The item in a media grid or shelf: an album, a playlist, a video, a podcast, a f
15089
15318
 
15090
15319
  ## Examples
15091
15320
 
15321
+ ### Product tile
15322
+
15323
+ ```tsx
15324
+ import { Badge, Grid, MediaTile } from "@sableui/react"
15325
+ // <Link> = your router's link (react-router, next/link, @tanstack/router, …)
15326
+
15327
+ <Grid minItemWidth="14rem" gap={4}>
15328
+ {products.map((product) => (
15329
+ <MediaTile
15330
+ key={product.id}
15331
+ aspect={4 / 5}
15332
+ title={product.name}
15333
+ subtitle={product.material}
15334
+ src={product.image}
15335
+ srcSet={product.imageSet}
15336
+ sizes="(min-width: 64rem) 25vw, 50vw"
15337
+ render={<Link to={`/products/${product.slug}`} />}
15338
+ badge={product.isNew ? <Badge appearance="overlay">New</Badge> : undefined}
15339
+ meta={
15340
+ <>
15341
+ <span className="text-foreground">{formatPrice(product.price)}</span>
15342
+ <span>{product.finishes.length} finishes</span>
15343
+ </>
15344
+ }
15345
+ />
15346
+ ))}
15347
+ </Grid>
15348
+ ```
15349
+
15092
15350
  ### Album grid
15093
15351
 
15094
15352
  ```tsx
@@ -17369,6 +17627,7 @@ Use RadioCard for visual choice cards when users compare a small set of named op
17369
17627
  | value | string | — | The unique value that pairs this card with the group's selected value. |
17370
17628
  | title | ReactNode | — | The card's primary label (rendered as text-title-sm). |
17371
17629
  | description | ReactNode | — | Optional supporting copy beneath the title (text-body-sm, muted). |
17630
+ | trailing | ReactNode | — | End-aligned on the title's line: a price, a delivery date, a count. The title takes the rest of the row. |
17372
17631
  | icon | ReactNode | — | Leading icon (e.g. a solid <Icon>). Its presence switches to the two-dimensional card — icon top-left, indicator top-right, copy below — and the glyph is rendered as a solid-fill icon in the tone colour. |
17373
17632
  | tone | "neutral" \| "standard" \| "collectible" \| "edition" \| "info" \| "warning" | "neutral" | Semantic colour the selected state carries across the border, radio, and icon glyph. Neutral uses the primary ring with a muted wash. |
17374
17633
  | disabled | boolean | false | Dims the card and blocks selection. The group skips it during keyboard navigation. |
@@ -19287,7 +19546,7 @@ Use Stepper above or beside a flow that is split into a known, small number of n
19287
19546
  | Name | Type | Default | Description |
19288
19547
  | --- | --- | --- | --- |
19289
19548
  | current | number | 0 | Zero-based index of the current step. Earlier steps derive complete, later ones upcoming. |
19290
- | orientation | "horizontal" \| "vertical" | "horizontal" | Horizontal for a short flow across the top of a form; vertical for long flows, narrow columns or steps with descriptions. |
19549
+ | orientation | "horizontal" \| "vertical" | "horizontal" | Horizontal for a short flow across the top of a form; vertical for long flows, narrow columns or steps with descriptions. A horizontal stepper narrower than 28rem (a phone) compacts itself: every disc stays, only the current step's title shows, and the other titles stay for screen readers. No breakpoint code needed. |
19291
19550
  | indicator | "number" \| "dot" | "number" | `number` for a flow the user moves through; `dot` for a timeline of events (delivery tracking, a document's history) where the position number adds nothing. The dot centres on the title's first line and scales with its type at every density. |
19292
19551
  | statusLabels | Partial<Record<StepStatus, string>> | — | Screen-reader status words appended to each title (Completed, Current step, Not started, Needs attention). Override to localize. |
19293
19552
  | aria-label | string | — | Names the flow ("Checkout progress"). Every Stepper needs one. |
package/llms.txt CHANGED
@@ -362,7 +362,7 @@ import { Button, Card, IconProvider, MotionProvider, SableThemeProvider, useSabl
362
362
 
363
363
  - **Accordion** — Stacked, collapsible disclosure sections over Base UI Accordion — roving focus, arrow-key navigation, ARIA region/heading/button wiring, and an animated height transition. *Parts:* AccordionItem, AccordionTrigger, AccordionPanel. → [`llms/components/accordion.md`](./llms/components/accordion.md)
364
364
  - **AppHeader** — Mobile top bar (sticky, hidden at lg) with leading / title / actions slots on a 3-column grid that keeps the title optically centered. → [`llms/components/app-header.md`](./llms/components/app-header.md)
365
- - **AppShell** — The application frame: slots for status banners, a mobile header, a desktop sidebar, main content, a mobile bottom nav, a persistent dock (a now-playing or call bar), and floating overlays — plus a built-in skip link and #main-content target. → [`llms/components/app-shell.md`](./llms/components/app-shell.md)
365
+ - **AppShell** — The application frame: slots for status banners, a mobile header, a desktop sidebar, main content, a mobile bottom nav, a persistent dock (a now-playing or call bar), and floating overlays — plus a built-in skip link and #main-content target. *Parts:* SkipLink. → [`llms/components/app-shell.md`](./llms/components/app-shell.md)
366
366
  - **AreaChart** — **BETA.** 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. → [`llms/components/area-chart.md`](./llms/components/area-chart.md)
367
367
  - **Attachment** — A file or image attached to a message: media or icon lead, name and metadata, upload and error states, an actions slot, and a full-card trigger that keeps those actions separately clickable. → [`llms/components/attachment.md`](./llms/components/attachment.md)
368
368
  - **Avatar** — Circular profile image over Base UI Avatar — automatic image-load tracking with an initials/glyph fallback. *Parts:* AvatarRoot, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount. → [`llms/components/avatar.md`](./llms/components/avatar.md)
@@ -397,7 +397,7 @@ import { Button, Card, IconProvider, MotionProvider, SableThemeProvider, useSabl
397
397
  - **EmptyState** — The centered empty / error block: optional icon or error code, title, description and an action. → [`llms/components/empty-state.md`](./llms/components/empty-state.md)
398
398
  - **Entity** — A list-row record: leading visual + title/subtitle column + trailing action, with truncating text. → [`llms/components/entity.md`](./llms/components/entity.md)
399
399
  - **Feedback** — A compact feedback popover. → [`llms/components/feedback.md`](./llms/components/feedback.md)
400
- - **Fieldset** — Form section panel (Card-like bordered surface) over Base UI Fieldset, with Legend/Description/Content/Footer parts. *Parts:* FieldsetLegend, FieldsetDescription, FieldsetContent, FieldsetFooter. → [`llms/components/fieldset.md`](./llms/components/fieldset.md)
400
+ - **Fieldset** — Form section panel (Card-like bordered surface) over Base UI Fieldset, with Legend/Description/Content/Footer parts; `variant="plain"` is the bare group, for a radio group's shared name. *Parts:* FieldsetLegend, FieldsetDescription, FieldsetContent, FieldsetFooter. → [`llms/components/fieldset.md`](./llms/components/fieldset.md)
401
401
  - **FileDropzone** — Drag-and-drop target and file picker in one keyboard-operable button. → [`llms/components/file-dropzone.md`](./llms/components/file-dropzone.md)
402
402
  - **FontPicker** — A DisclosurePicker for a typeface: the row names the chosen family in its own face and opens in place into a searchable list of families, each drawn in itself. → [`llms/components/font-picker.md`](./llms/components/font-picker.md)
403
403
  - **Form** — A styled <form> plus Field — the form orchestration layer, built on Base UI Form and Field. *Parts:* Field. → [`llms/components/form.md`](./llms/components/form.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sableui/react",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Sable: themeable React components, layout patterns, and tools for coding agents.",
5
5
  "type": "module",
6
6
  "sideEffects": [