@sableui/react 0.8.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +112 -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 +46 -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 +29 -2
  13. package/dist/components/date-tile/date-tile.meta.js +1 -0
  14. package/dist/components/feedback/feedback.meta.js +1 -0
  15. package/dist/components/fieldset/fieldset.js +22 -13
  16. package/dist/components/fieldset/fieldset.meta.js +25 -1
  17. package/dist/components/form/form.js +50 -9
  18. package/dist/components/form/form.meta.js +12 -3
  19. package/dist/components/media-tile/media-tile.js +16 -2
  20. package/dist/components/media-tile/media-tile.meta.js +8 -0
  21. package/dist/components/radio-card/radio-card.js +6 -2
  22. package/dist/components/radio-card/radio-card.meta.js +5 -0
  23. package/dist/components/sheet/sheet.js +16 -1
  24. package/dist/components/stepper/stepper.js +17 -3
  25. package/dist/components/stepper/stepper.meta.js +1 -1
  26. package/dist/components/toast/toast.js +3 -0
  27. package/dist/file-hashes.json +29 -27
  28. package/dist/index.d.ts +109 -22
  29. package/dist/index.js +1 -1
  30. package/dist/lib/is-promise.js +5 -0
  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 +73 -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/fieldset.md +21 -1
  38. package/llms/components/form.md +11 -2
  39. package/llms/components/media-tile.md +32 -0
  40. package/llms/components/radio-card.md +1 -0
  41. package/llms/components/stepper.md +1 -1
  42. package/llms/tasks/building-with-sable.md +4 -0
  43. package/llms/tasks/creating-a-preset.md +4 -1
  44. package/llms-full.txt +103 -7
  45. package/llms.txt +2 -2
  46. package/package.json +1 -1
  47. package/registry.json +89 -10
  48. package/scripts/grade.mjs +14 -3
  49. package/scripts/sources.mjs +13 -2
  50. package/skills/sable/SKILL.md +23 -7
@@ -36,7 +36,10 @@ The item in a media grid or shelf: an album, a playlist, a video, a podcast, a f
36
36
  | --- | --- | --- | --- |
37
37
  | title | ReactNode | — | The item's name. Seeds the fallback art when it is a string. |
38
38
  | subtitle | ReactNode | — | The second line — an artist, a channel, a year. |
39
+ | 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. |
39
40
  | src | string | — | Cover image URL. Without one, or when it fails, the cover is GradientArt seeded by the item. |
41
+ | srcSet | string | — | Responsive cover candidates, passed to the `<img>`. |
42
+ | sizes | string | — | The cover's layout width for `srcSet`, e.g. `"(min-width: 64rem) 25vw, 50vw"`. |
40
43
  | alt | string | "" | Cover alt text. Empty by default: the title below already names the item. |
41
44
  | seed | string \| number | — | Seeds the fallback art. Defaults to `title` when it is a string. |
42
45
  | 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. |
@@ -82,6 +85,35 @@ The item in a media grid or shelf: an album, a playlist, a video, a podcast, a f
82
85
 
83
86
  ## Examples
84
87
 
88
+ ### Product tile
89
+
90
+ ```tsx
91
+ import { Badge, Grid, MediaTile } from "@sableui/react"
92
+ // <Link> = your router's link (react-router, next/link, @tanstack/router, …)
93
+
94
+ <Grid minItemWidth="14rem" gap={4}>
95
+ {products.map((product) => (
96
+ <MediaTile
97
+ key={product.id}
98
+ aspect={4 / 5}
99
+ title={product.name}
100
+ subtitle={product.material}
101
+ src={product.image}
102
+ srcSet={product.imageSet}
103
+ sizes="(min-width: 64rem) 25vw, 50vw"
104
+ render={<Link to={`/products/${product.slug}`} />}
105
+ badge={product.isNew ? <Badge appearance="overlay">New</Badge> : undefined}
106
+ meta={
107
+ <>
108
+ <span className="text-foreground">{formatPrice(product.price)}</span>
109
+ <span>{product.finishes.length} finishes</span>
110
+ </>
111
+ }
112
+ />
113
+ ))}
114
+ </Grid>
115
+ ```
116
+
85
117
  ### Album grid
86
118
 
87
119
  ```tsx
@@ -37,6 +37,7 @@ Use RadioCard for visual choice cards when users compare a small set of named op
37
37
  | value | string | — | The unique value that pairs this card with the group's selected value. |
38
38
  | title | ReactNode | — | The card's primary label (rendered as text-title-sm). |
39
39
  | description | ReactNode | — | Optional supporting copy beneath the title (text-body-sm, muted). |
40
+ | trailing | ReactNode | — | End-aligned on the title's line: a price, a delivery date, a count. The title takes the rest of the row. |
40
41
  | 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. |
41
42
  | 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. |
42
43
  | disabled | boolean | false | Dims the card and blocks selection. The group skips it during keyboard navigation. |
@@ -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. |
@@ -43,6 +43,10 @@ for each recognizable element with `sable search "<need>" --json`; inspect the
43
43
  matching `component <Name> --props` and `--examples` before implementation.
44
44
  Use Sable's composed anatomy and supported variants. A hand-built button, field,
45
45
  menu, dialog, table, or card that imitates a shipped component violates the contract.
46
+ That binds behaviour and parts, not layouts. Results marked ◇ (patterns, blocks,
47
+ and compositions such as MediaTile) are references: use one when its layout fits
48
+ the brief, otherwise compose your own from Sable's parts. The brief's signature
49
+ surfaces, where the product's identity lives, are designed from its direction.
46
50
 
47
51
  Resolve styling through component variants, documented utilities/recipes, then
48
52
  semantic tokens. App-owned CSS handles uncovered content and composition; it
@@ -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.
package/llms-full.txt CHANGED
@@ -1040,6 +1040,10 @@ for each recognizable element with `sable search "<need>" --json`; inspect the
1040
1040
  matching `component <Name> --props` and `--examples` before implementation.
1041
1041
  Use Sable's composed anatomy and supported variants. A hand-built button, field,
1042
1042
  menu, dialog, table, or card that imitates a shipped component violates the contract.
1043
+ That binds behaviour and parts, not layouts. Results marked ◇ (patterns, blocks,
1044
+ and compositions such as MediaTile) are references: use one when its layout fits
1045
+ the brief, otherwise compose your own from Sable's parts. The brief's signature
1046
+ surfaces, where the product's identity lives, are designed from its direction.
1043
1047
 
1044
1048
  Resolve styling through component variants, documented utilities/recipes, then
1045
1049
  semantic tokens. App-owned CSS handles uncovered content and composition; it
@@ -1350,7 +1354,10 @@ cannot establish animation timing, hidden behavior, or an unseen responsive stat
1350
1354
  Choose an original direction suitable for the user's future product. Map that
1351
1355
  direction onto the installed Sable token vocabulary. Preserve readable semantic
1352
1356
  roles and supported control behavior rather than recoloring every primitive
1353
- independently. Load fonts through the consuming project's font setup. Page widths
1357
+ independently. Load fonts through the consuming project's font setup.
1358
+ `fonts.display` sets every `text-display-*` tier and `text-heading-1` to `-4`,
1359
+ so it reaches PageHeader and SectionHeader titles too, not only a hero.
1360
+ Leave it unset to keep headings in `fonts.sans`. Page widths
1354
1361
  are optional: set `page-inset` and the `region-*` caps (`form`, `feed`, `content`,
1355
1362
  `wide`, `full`) in `tokens.shared` only when the direction calls for a different
1356
1363
  page proportion, and keep each cap's role.
@@ -10015,6 +10022,12 @@ Use AppShell as the outer product frame for authenticated or app-like experience
10015
10022
  | Bottom nav | no | Mobile fixed navigation with reserved bottom space. |
10016
10023
  | 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. |
10017
10024
 
10025
+ ## Subcomponents
10026
+
10027
+ | Name | Type | Description |
10028
+ | --- | --- | --- |
10029
+ | 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". |
10030
+
10018
10031
  ## Props
10019
10032
 
10020
10033
  | Name | Type | Default | Description |
@@ -10075,6 +10088,26 @@ import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader
10075
10088
  </AppShell>
10076
10089
  ```
10077
10090
 
10091
+ ### SkipLink in a layout of your own
10092
+
10093
+ ```tsx
10094
+ import { Button, Icon, PageHeader, Region, SkipLink } from "@sableui/react"
10095
+ // <Link> = your router's link (react-router, next/link, @tanstack/router, …)
10096
+
10097
+ <>
10098
+ <SkipLink />
10099
+ <header className="flex items-center justify-between px-(--page-inset) py-4">
10100
+ <Link to="/">Alder & Lune</Link>
10101
+ <Button variant="ghost" size="icon" aria-label="Search"><Icon name="search" /></Button>
10102
+ </header>
10103
+ <main id="main-content" tabIndex={-1}>
10104
+ <Region max="wide">
10105
+ <PageHeader title="Lighting" />
10106
+ </Region>
10107
+ </main>
10108
+ </>
10109
+ ```
10110
+
10078
10111
  ### Minimal (sidebar + content only)
10079
10112
 
10080
10113
  ```tsx
@@ -11279,7 +11312,8 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
11279
11312
  | 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. |
11280
11313
  | 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. |
11281
11314
  | render | ReactElement \| (props, state) => ReactElement | — | Compose with another element/component through the Base UI render prop. |
11282
- | 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. |
11315
+ | 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. |
11316
+ | 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). |
11283
11317
  | 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. |
11284
11318
  | loadingLabel | ReactNode | — | Optional text shown in place of children while loading. |
11285
11319
  | 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. |
@@ -11595,7 +11629,7 @@ Use Card for a discrete thing with its own boundary, such as one product, one no
11595
11629
 
11596
11630
  | Name | Type | Default | Description |
11597
11631
  | --- | --- | --- | --- |
11598
- | variant | "default" \| "flat" | "default" | "default" raises with bg-card + border + soft shadow; "flat" is transparent. |
11632
+ | 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. |
11599
11633
  | 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. |
11600
11634
  | 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:`. |
11601
11635
 
@@ -13834,7 +13868,7 @@ import { Feedback } from "@sableui/react"
13834
13868
 
13835
13869
  **Status:** stable
13836
13870
 
13837
- Form section panel (Card-like bordered surface) over Base UI Fieldset, with Legend/Description/Content/Footer parts.
13871
+ 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.
13838
13872
 
13839
13873
  **Keywords:** fieldset, form group, legend, fields
13840
13874
 
@@ -13846,6 +13880,7 @@ Use Fieldset to group related controls under a shared label or explanation.
13846
13880
 
13847
13881
  - Do: Use it for grouped form sections such as contact details, preferences, or permissions.
13848
13882
  - Do: Keep the legend meaningful so assistive tech and agents understand the group purpose.
13883
+ - 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.
13849
13884
  - Don't: Do not use Fieldset as a generic spacing wrapper outside forms.
13850
13885
  - 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.
13851
13886
 
@@ -13872,6 +13907,7 @@ Use Fieldset to group related controls under a shared label or explanation.
13872
13907
 
13873
13908
  | Name | Type | Default | Description |
13874
13909
  | --- | --- | --- | --- |
13910
+ | 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. |
13875
13911
  | disabled | boolean | — | Disables the whole group. Renders <fieldset disabled> (native control disabling) and surfaces data-[disabled] for dimming. |
13876
13912
  | render | ReactElement \| function | — | Base UI render prop to replace or compose the underlying element. |
13877
13913
  | className | string | — | Merged onto each part via cn() for one-off overrides. |
@@ -13937,6 +13973,24 @@ import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } f
13937
13973
  // assistive technology about the form's structure.
13938
13974
  ```
13939
13975
 
13976
+ ### A radio group's name, without a second box
13977
+
13978
+ ```tsx
13979
+ import { Card, CardContent, Fieldset, FieldsetLegend, RadioCard, RadioGroup } from "@sableui/react"
13980
+
13981
+ <Card>
13982
+ <CardContent>
13983
+ <Fieldset variant="plain">
13984
+ <FieldsetLegend>Delivery</FieldsetLegend>
13985
+ <RadioGroup defaultValue="standard">
13986
+ <RadioCard value="standard" title="Standard" description="3–5 working days" trailing="Free" />
13987
+ <RadioCard value="express" title="Express" description="Next working day" trailing="$12.00" />
13988
+ </RadioGroup>
13989
+ </Fieldset>
13990
+ </CardContent>
13991
+ </Card>
13992
+ ```
13993
+
13940
13994
  ---
13941
13995
 
13942
13996
  # FileDropzone
@@ -14186,13 +14240,14 @@ Use Form and Field as the default wrapper for Sable form controls. Field owns th
14186
14240
 
14187
14241
  | Name | Type | Description |
14188
14242
  | --- | --- | --- |
14189
- | 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. |
14243
+ | 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. |
14190
14244
 
14191
14245
  ## Props
14192
14246
 
14193
14247
  | Name | Type | Default | Description |
14194
14248
  | --- | --- | --- | --- |
14195
- | 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. |
14249
+ | 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. |
14250
+ | 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. |
14196
14251
  | 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. |
14197
14252
  | 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. |
14198
14253
  | onFormSubmit | (values: Record<string, unknown>, details) => void | — | Called with the named Field values after validation passes; prevents the native submit. |
@@ -14233,6 +14288,14 @@ import { Field, Input } from "@sableui/react"
14233
14288
  <Field label="Password" error="Must be at least 8 characters."><Input type="password" /></Field>
14234
14289
  ```
14235
14290
 
14291
+ ### An input with its action
14292
+
14293
+ ```tsx
14294
+ import { Button, Field, Input } from "@sableui/react"
14295
+
14296
+ <Field label="Promo code" error={promoError} trailing={<Button variant="secondary" onClick={applyPromo}>Apply</Button>}><Input name="promo" autoCapitalize="characters" /></Field>
14297
+ ```
14298
+
14236
14299
  ### Your own wording for a missing value
14237
14300
 
14238
14301
  ```tsx
@@ -15210,7 +15273,10 @@ The item in a media grid or shelf: an album, a playlist, a video, a podcast, a f
15210
15273
  | --- | --- | --- | --- |
15211
15274
  | title | ReactNode | — | The item's name. Seeds the fallback art when it is a string. |
15212
15275
  | subtitle | ReactNode | — | The second line — an artist, a channel, a year. |
15276
+ | 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. |
15213
15277
  | src | string | — | Cover image URL. Without one, or when it fails, the cover is GradientArt seeded by the item. |
15278
+ | srcSet | string | — | Responsive cover candidates, passed to the `<img>`. |
15279
+ | sizes | string | — | The cover's layout width for `srcSet`, e.g. `"(min-width: 64rem) 25vw, 50vw"`. |
15214
15280
  | alt | string | "" | Cover alt text. Empty by default: the title below already names the item. |
15215
15281
  | seed | string \| number | — | Seeds the fallback art. Defaults to `title` when it is a string. |
15216
15282
  | 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. |
@@ -15256,6 +15322,35 @@ The item in a media grid or shelf: an album, a playlist, a video, a podcast, a f
15256
15322
 
15257
15323
  ## Examples
15258
15324
 
15325
+ ### Product tile
15326
+
15327
+ ```tsx
15328
+ import { Badge, Grid, MediaTile } from "@sableui/react"
15329
+ // <Link> = your router's link (react-router, next/link, @tanstack/router, …)
15330
+
15331
+ <Grid minItemWidth="14rem" gap={4}>
15332
+ {products.map((product) => (
15333
+ <MediaTile
15334
+ key={product.id}
15335
+ aspect={4 / 5}
15336
+ title={product.name}
15337
+ subtitle={product.material}
15338
+ src={product.image}
15339
+ srcSet={product.imageSet}
15340
+ sizes="(min-width: 64rem) 25vw, 50vw"
15341
+ render={<Link to={`/products/${product.slug}`} />}
15342
+ badge={product.isNew ? <Badge appearance="overlay">New</Badge> : undefined}
15343
+ meta={
15344
+ <>
15345
+ <span className="text-foreground">{formatPrice(product.price)}</span>
15346
+ <span>{product.finishes.length} finishes</span>
15347
+ </>
15348
+ }
15349
+ />
15350
+ ))}
15351
+ </Grid>
15352
+ ```
15353
+
15259
15354
  ### Album grid
15260
15355
 
15261
15356
  ```tsx
@@ -17536,6 +17631,7 @@ Use RadioCard for visual choice cards when users compare a small set of named op
17536
17631
  | value | string | — | The unique value that pairs this card with the group's selected value. |
17537
17632
  | title | ReactNode | — | The card's primary label (rendered as text-title-sm). |
17538
17633
  | description | ReactNode | — | Optional supporting copy beneath the title (text-body-sm, muted). |
17634
+ | trailing | ReactNode | — | End-aligned on the title's line: a price, a delivery date, a count. The title takes the rest of the row. |
17539
17635
  | 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. |
17540
17636
  | 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. |
17541
17637
  | disabled | boolean | false | Dims the card and blocks selection. The group skips it during keyboard navigation. |
@@ -19454,7 +19550,7 @@ Use Stepper above or beside a flow that is split into a known, small number of n
19454
19550
  | Name | Type | Default | Description |
19455
19551
  | --- | --- | --- | --- |
19456
19552
  | current | number | 0 | Zero-based index of the current step. Earlier steps derive complete, later ones upcoming. |
19457
- | 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. |
19553
+ | 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. |
19458
19554
  | 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. |
19459
19555
  | statusLabels | Partial<Record<StepStatus, string>> | — | Screen-reader status words appended to each title (Completed, Current step, Not started, Needs attention). Override to localize. |
19460
19556
  | 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.2",
3
+ "version": "0.9.1",
4
4
  "description": "Sable: themeable React components, layout patterns, and tools for coding agents.",
5
5
  "type": "module",
6
6
  "sideEffects": [