@sableui/react 0.8.2 → 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 (48) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +83 -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/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 +45 -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 +1 -0
  24. package/dist/components/stepper/stepper.js +7 -2
  25. package/dist/components/stepper/stepper.meta.js +1 -1
  26. package/dist/components/toast/toast.js +2 -0
  27. package/dist/file-hashes.json +28 -27
  28. package/dist/index.d.ts +109 -22
  29. package/dist/index.js +1 -1
  30. package/dist/{meta-CcE_SeFu.d.ts → meta-DVG8ceze.d.ts} +9 -0
  31. package/dist/patterns/index.d.ts +1 -1
  32. package/dist/styles.css +30 -0
  33. package/llms/components/app-shell.md +26 -0
  34. package/llms/components/button.md +2 -1
  35. package/llms/components/card.md +1 -1
  36. package/llms/components/fieldset.md +21 -1
  37. package/llms/components/form.md +11 -2
  38. package/llms/components/media-tile.md +32 -0
  39. package/llms/components/radio-card.md +1 -0
  40. package/llms/components/stepper.md +1 -1
  41. package/llms/tasks/creating-a-preset.md +4 -1
  42. package/llms-full.txt +99 -7
  43. package/llms.txt +2 -2
  44. package/package.json +1 -1
  45. package/registry.json +89 -10
  46. package/scripts/grade.mjs +14 -3
  47. package/scripts/sources.mjs +10 -2
  48. package/skills/sable/SKILL.md +23 -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.
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.
@@ -10015,6 +10018,12 @@ Use AppShell as the outer product frame for authenticated or app-like experience
10015
10018
  | Bottom nav | no | Mobile fixed navigation with reserved bottom space. |
10016
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. |
10017
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
+
10018
10027
  ## Props
10019
10028
 
10020
10029
  | Name | Type | Default | Description |
@@ -10075,6 +10084,26 @@ import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader
10075
10084
  </AppShell>
10076
10085
  ```
10077
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
+
10078
10107
  ### Minimal (sidebar + content only)
10079
10108
 
10080
10109
  ```tsx
@@ -11279,7 +11308,8 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
11279
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. |
11280
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. |
11281
11310
  | 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. |
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). |
11283
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. |
11284
11314
  | loadingLabel | ReactNode | — | Optional text shown in place of children while loading. |
11285
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. |
@@ -11595,7 +11625,7 @@ Use Card for a discrete thing with its own boundary, such as one product, one no
11595
11625
 
11596
11626
  | Name | Type | Default | Description |
11597
11627
  | --- | --- | --- | --- |
11598
- | 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. |
11599
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. |
11600
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:`. |
11601
11631
 
@@ -13834,7 +13864,7 @@ import { Feedback } from "@sableui/react"
13834
13864
 
13835
13865
  **Status:** stable
13836
13866
 
13837
- 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.
13838
13868
 
13839
13869
  **Keywords:** fieldset, form group, legend, fields
13840
13870
 
@@ -13846,6 +13876,7 @@ Use Fieldset to group related controls under a shared label or explanation.
13846
13876
 
13847
13877
  - Do: Use it for grouped form sections such as contact details, preferences, or permissions.
13848
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.
13849
13880
  - Don't: Do not use Fieldset as a generic spacing wrapper outside forms.
13850
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.
13851
13882
 
@@ -13872,6 +13903,7 @@ Use Fieldset to group related controls under a shared label or explanation.
13872
13903
 
13873
13904
  | Name | Type | Default | Description |
13874
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. |
13875
13907
  | disabled | boolean | — | Disables the whole group. Renders <fieldset disabled> (native control disabling) and surfaces data-[disabled] for dimming. |
13876
13908
  | render | ReactElement \| function | — | Base UI render prop to replace or compose the underlying element. |
13877
13909
  | className | string | — | Merged onto each part via cn() for one-off overrides. |
@@ -13937,6 +13969,24 @@ import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } f
13937
13969
  // assistive technology about the form's structure.
13938
13970
  ```
13939
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
+
13940
13990
  ---
13941
13991
 
13942
13992
  # FileDropzone
@@ -14186,13 +14236,14 @@ Use Form and Field as the default wrapper for Sable form controls. Field owns th
14186
14236
 
14187
14237
  | Name | Type | Description |
14188
14238
  | --- | --- | --- |
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. |
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. |
14190
14240
 
14191
14241
  ## Props
14192
14242
 
14193
14243
  | Name | Type | Default | Description |
14194
14244
  | --- | --- | --- | --- |
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. |
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. |
14196
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. |
14197
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. |
14198
14249
  | onFormSubmit | (values: Record<string, unknown>, details) => void | — | Called with the named Field values after validation passes; prevents the native submit. |
@@ -14233,6 +14284,14 @@ import { Field, Input } from "@sableui/react"
14233
14284
  <Field label="Password" error="Must be at least 8 characters."><Input type="password" /></Field>
14234
14285
  ```
14235
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
+
14236
14295
  ### Your own wording for a missing value
14237
14296
 
14238
14297
  ```tsx
@@ -15210,7 +15269,10 @@ The item in a media grid or shelf: an album, a playlist, a video, a podcast, a f
15210
15269
  | --- | --- | --- | --- |
15211
15270
  | title | ReactNode | — | The item's name. Seeds the fallback art when it is a string. |
15212
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. |
15213
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"`. |
15214
15276
  | alt | string | "" | Cover alt text. Empty by default: the title below already names the item. |
15215
15277
  | seed | string \| number | — | Seeds the fallback art. Defaults to `title` when it is a string. |
15216
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. |
@@ -15256,6 +15318,35 @@ The item in a media grid or shelf: an album, a playlist, a video, a podcast, a f
15256
15318
 
15257
15319
  ## Examples
15258
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
+
15259
15350
  ### Album grid
15260
15351
 
15261
15352
  ```tsx
@@ -17536,6 +17627,7 @@ Use RadioCard for visual choice cards when users compare a small set of named op
17536
17627
  | value | string | — | The unique value that pairs this card with the group's selected value. |
17537
17628
  | title | ReactNode | — | The card's primary label (rendered as text-title-sm). |
17538
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. |
17539
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. |
17540
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. |
17541
17633
  | disabled | boolean | false | Dims the card and blocks selection. The group skips it during keyboard navigation. |
@@ -19454,7 +19546,7 @@ Use Stepper above or beside a flow that is split into a known, small number of n
19454
19546
  | Name | Type | Default | Description |
19455
19547
  | --- | --- | --- | --- |
19456
19548
  | 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. |
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. |
19458
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. |
19459
19551
  | statusLabels | Partial<Record<StepStatus, string>> | — | Screen-reader status words appended to each title (Completed, Current step, Not started, Needs attention). Override to localize. |
19460
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.2",
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": [
package/registry.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://sable.design/registry.schema.json",
3
3
  "name": "@sableui/react",
4
- "version": "0.8.2",
4
+ "version": "0.9.0",
5
5
  "description": "Themeable React components, semantic tokens, layout patterns, presets, and package-owned agent skills. Default theme: Sable.",
6
6
  "engine": {
7
7
  "behavior": "@base-ui/react",
@@ -2957,6 +2957,13 @@
2957
2957
  "description": "Floating overlays — a message launcher, FAB, or modal mount points."
2958
2958
  }
2959
2959
  ],
2960
+ "subcomponents": [
2961
+ {
2962
+ "name": "SkipLink",
2963
+ "type": "ComponentProps<\"a\">",
2964
+ "description": "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\"."
2965
+ }
2966
+ ],
2960
2967
  "tokensUsed": [
2961
2968
  "background",
2962
2969
  "foreground",
@@ -2975,6 +2982,11 @@
2975
2982
  "code": "<AppShell\n banners={<Banner tone=\"warning\">Network congested — mints may be slow.</Banner>}\n header={<AppHeader title=\"Studio\" />}\n sidebar={\n <Sidebar>\n <SidebarNav>\n <SidebarItem icon={<Icon name=\"home\" />} label=\"Home\" active render={<Link to=\"/\" />} />\n <SidebarItem icon={<Icon name=\"bell\" />} label=\"Notifications\" render={<Link to=\"/notifications\" />} />\n <SidebarItem icon={<Icon name=\"gear\" />} label=\"Settings\" render={<Link to=\"/settings\" />} />\n </SidebarNav>\n </Sidebar>\n }\n bottomNav={\n <BottomNav>\n <BottomNavItem icon={<Icon name=\"home\" />} label=\"Home\" active render={<Link to=\"/\" />} />\n <BottomNavItem icon={<Icon name=\"bell\" />} label=\"Alerts\" render={<Link to=\"/notifications\" />} />\n </BottomNav>\n }\n>\n <Region max=\"feed\">\n <PageHeader title=\"Home\" description=\"Latest posts from artists you follow.\" />\n </Region>\n</AppShell>",
2976
2983
  "imports": "import { AppHeader, AppShell, Banner, BottomNav, BottomNavItem, Icon, PageHeader, Region, Sidebar, SidebarItem, SidebarNav } from \"@sableui/react\"\n// <Link> = your router's link (react-router, next/link, @tanstack/router, …)"
2977
2984
  },
2985
+ {
2986
+ "label": "SkipLink in a layout of your own",
2987
+ "code": "<>\n <SkipLink />\n <header className=\"flex items-center justify-between px-(--page-inset) py-4\">\n <Link to=\"/\">Alder & Lune</Link>\n <Button variant=\"ghost\" size=\"icon\" aria-label=\"Search\"><Icon name=\"search\" /></Button>\n </header>\n <main id=\"main-content\" tabIndex={-1}>\n <Region max=\"wide\">\n <PageHeader title=\"Lighting\" />\n </Region>\n </main>\n</>",
2988
+ "imports": "import { Button, Icon, PageHeader, Region, SkipLink } from \"@sableui/react\"\n// <Link> = your router's link (react-router, next/link, @tanstack/router, …)"
2989
+ },
2978
2990
  {
2979
2991
  "label": "Minimal (sidebar + content only)",
2980
2992
  "code": "<AppShell\n sidebar={\n <Sidebar>\n <SidebarNav>\n <SidebarItem icon={<Icon name=\"home\" />} label=\"Dashboard\" active render={<Link to=\"/\" />} />\n </SidebarNav>\n </Sidebar>\n }\n>\n <Region max=\"wide\">\n <PageHeader title=\"Dashboard\" />\n </Region>\n</AppShell>",
@@ -4922,7 +4934,12 @@
4922
4934
  "name": "loading",
4923
4935
  "type": "boolean",
4924
4936
  "default": "false",
4925
- "description": "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."
4937
+ "description": "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."
4938
+ },
4939
+ {
4940
+ "name": "onClick",
4941
+ "type": "(event: MouseEvent<HTMLButtonElement>) => unknown",
4942
+ "description": "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)."
4926
4943
  },
4927
4944
  {
4928
4945
  "name": "badge",
@@ -5354,7 +5371,7 @@
5354
5371
  "name": "variant",
5355
5372
  "type": "\"default\" | \"flat\"",
5356
5373
  "default": "\"default\"",
5357
- "description": "\"default\" raises with bg-card + border + soft shadow; \"flat\" is transparent."
5374
+ "description": "\"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."
5358
5375
  },
5359
5376
  {
5360
5377
  "name": "density",
@@ -7800,6 +7817,7 @@
7800
7817
  {
7801
7818
  "name": "DateTile",
7802
7819
  "status": "stable",
7820
+ "composition": "a <time> element with the type tiers, on a surface utility",
7803
7821
  "group": "Data display",
7804
7822
  "register": "page",
7805
7823
  "path": "components/date-tile",
@@ -8859,6 +8877,7 @@
8859
8877
  {
8860
8878
  "name": "Feedback",
8861
8879
  "status": "stable",
8880
+ "composition": "Popover, Textarea, ToggleGroup, Button",
8862
8881
  "group": "Overlays",
8863
8882
  "register": "page",
8864
8883
  "path": "components/feedback",
@@ -8972,7 +8991,7 @@
8972
8991
  "group": "Forms",
8973
8992
  "register": "either",
8974
8993
  "path": "components/fieldset",
8975
- "summary": "Form section panel (Card-like bordered surface) over Base UI Fieldset, with Legend/Description/Content/Footer parts.",
8994
+ "summary": "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.",
8976
8995
  "keywords": [
8977
8996
  "fieldset",
8978
8997
  "form group",
@@ -8994,6 +9013,10 @@
8994
9013
  "guidance": "dont",
8995
9014
  "description": "Do not use Fieldset as a generic spacing wrapper outside forms."
8996
9015
  },
9016
+ {
9017
+ "guidance": "do",
9018
+ "description": "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."
9019
+ },
8997
9020
  {
8998
9021
  "guidance": "dont",
8999
9022
  "description": "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."
@@ -9034,6 +9057,12 @@
9034
9057
  ]
9035
9058
  },
9036
9059
  "props": [
9060
+ {
9061
+ "name": "variant",
9062
+ "type": "\"card\" | \"plain\"",
9063
+ "default": "\"card\"",
9064
+ "description": "`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."
9065
+ },
9037
9066
  {
9038
9067
  "name": "disabled",
9039
9068
  "type": "boolean",
@@ -9095,6 +9124,11 @@
9095
9124
  "label": "Why a fieldset, and when it is wrong",
9096
9125
  "code": "// Fieldset groups inputs that are ONE question with multiple parts, and gives\n// the group a single accessible name — which is the part you cannot do with a\n// div and a heading.\n\n<Fieldset>\n <FieldsetLegend>Digest frequency</FieldsetLegend>\n <FieldsetContent>\n <RadioGroup defaultValue=\"weekly\">\n <Field label=\"Daily\" orientation=\"horizontal\"><Radio value=\"daily\" /></Field>\n <Field label=\"Weekly\" orientation=\"horizontal\"><Radio value=\"weekly\" /></Field>\n </RadioGroup>\n </FieldsetContent>\n</Fieldset>\n\n// ✓ Required for radio groups: without a fieldset the options have no shared\n// name, so a screen reader announces \"Weekly, radio\" with no question.\n// ✗ Not for visual grouping of unrelated fields. Use PanelSection on a panel or\n// a Card section on a page; a legend that is really a heading lies to\n// assistive technology about the form's structure.",
9097
9126
  "imports": "import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } from \"@sableui/react\""
9127
+ },
9128
+ {
9129
+ "label": "A radio group's name, without a second box",
9130
+ "code": "<Card>\n <CardContent>\n <Fieldset variant=\"plain\">\n <FieldsetLegend>Delivery</FieldsetLegend>\n <RadioGroup defaultValue=\"standard\">\n <RadioCard value=\"standard\" title=\"Standard\" description=\"3–5 working days\" trailing=\"Free\" />\n <RadioCard value=\"express\" title=\"Express\" description=\"Next working day\" trailing=\"$12.00\" />\n </RadioGroup>\n </Fieldset>\n </CardContent>\n</Card>",
9131
+ "imports": "import { Card, CardContent, Fieldset, FieldsetLegend, RadioCard, RadioGroup } from \"@sableui/react\""
9098
9132
  }
9099
9133
  ]
9100
9134
  },
@@ -9575,7 +9609,12 @@
9575
9609
  {
9576
9610
  "name": "Form",
9577
9611
  "type": "{ validationMode?: \"onSubmit\" | \"onBlur\" | \"onChange\"; errors?: Record<string, string | string[]>; onFormSubmit?: (values, details) => void; actionsRef?: RefObject<Form.Actions> } + ComponentPropsWithRef<\"form\">",
9578
- "description": "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."
9612
+ "description": "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."
9613
+ },
9614
+ {
9615
+ "name": "onSubmit",
9616
+ "type": "(event: FormEvent<HTMLFormElement>) => unknown",
9617
+ "description": "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."
9579
9618
  },
9580
9619
  {
9581
9620
  "name": "validationMode",
@@ -9608,8 +9647,8 @@
9608
9647
  "subcomponents": [
9609
9648
  {
9610
9649
  "name": "Field",
9611
- "type": "{ 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 }",
9612
- "description": "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."
9650
+ "type": "{ 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",
9651
+ "description": "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."
9613
9652
  }
9614
9653
  ],
9615
9654
  "tokensUsed": [
@@ -9633,6 +9672,11 @@
9633
9672
  "code": "<Field label=\"Password\" error=\"Must be at least 8 characters.\"><Input type=\"password\" /></Field>",
9634
9673
  "imports": "import { Field, Input } from \"@sableui/react\""
9635
9674
  },
9675
+ {
9676
+ "label": "An input with its action",
9677
+ "code": "<Field label=\"Promo code\" error={promoError} trailing={<Button variant=\"secondary\" onClick={applyPromo}>Apply</Button>}><Input name=\"promo\" autoCapitalize=\"characters\" /></Field>",
9678
+ "imports": "import { Button, Field, Input } from \"@sableui/react\""
9679
+ },
9636
9680
  {
9637
9681
  "label": "Your own wording for a missing value",
9638
9682
  "code": "<Form onSubmit={handleSubmit}><Field label=\"Subject\" required messages={{ valueMissing: \"Enter a subject.\" }}><Input name=\"subject\" /></Field><Button type=\"submit\">Send</Button></Form>",
@@ -11107,6 +11151,7 @@
11107
11151
  {
11108
11152
  "name": "MediaTile",
11109
11153
  "status": "beta",
11154
+ "composition": "AspectBox, MediaImage, a stretched link (or Card render), Badge appearance=\"overlay\", Button",
11110
11155
  "group": "Media & mockups",
11111
11156
  "register": "page",
11112
11157
  "path": "components/media-tile",
@@ -11208,11 +11253,26 @@
11208
11253
  "type": "ReactNode",
11209
11254
  "description": "The second line — an artist, a channel, a year."
11210
11255
  },
11256
+ {
11257
+ "name": "meta",
11258
+ "type": "ReactNode",
11259
+ "description": "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."
11260
+ },
11211
11261
  {
11212
11262
  "name": "src",
11213
11263
  "type": "string",
11214
11264
  "description": "Cover image URL. Without one, or when it fails, the cover is GradientArt seeded by the item."
11215
11265
  },
11266
+ {
11267
+ "name": "srcSet",
11268
+ "type": "string",
11269
+ "description": "Responsive cover candidates, passed to the `<img>`."
11270
+ },
11271
+ {
11272
+ "name": "sizes",
11273
+ "type": "string",
11274
+ "description": "The cover's layout width for `srcSet`, e.g. `\"(min-width: 64rem) 25vw, 50vw\"`."
11275
+ },
11216
11276
  {
11217
11277
  "name": "alt",
11218
11278
  "type": "string",
@@ -11338,6 +11398,11 @@
11338
11398
  "Truncated titles keep their full text in the link's accessible name and in a `title` tooltip."
11339
11399
  ],
11340
11400
  "examples": [
11401
+ {
11402
+ "label": "Product tile",
11403
+ "code": "<Grid minItemWidth=\"14rem\" gap={4}>\n\t{products.map((product) => (\n\t\t<MediaTile\n\t\t\tkey={product.id}\n\t\t\taspect={4 / 5}\n\t\t\ttitle={product.name}\n\t\t\tsubtitle={product.material}\n\t\t\tsrc={product.image}\n\t\t\tsrcSet={product.imageSet}\n\t\t\tsizes=\"(min-width: 64rem) 25vw, 50vw\"\n\t\t\trender={<Link to={`/products/${product.slug}`} />}\n\t\t\tbadge={product.isNew ? <Badge appearance=\"overlay\">New</Badge> : undefined}\n\t\t\tmeta={\n\t\t\t\t<>\n\t\t\t\t\t<span className=\"text-foreground\">{formatPrice(product.price)}</span>\n\t\t\t\t\t<span>{product.finishes.length} finishes</span>\n\t\t\t\t</>\n\t\t\t}\n\t\t/>\n\t))}\n</Grid>",
11404
+ "imports": "import { Badge, Grid, MediaTile } from \"@sableui/react\"\n// <Link> = your router's link (react-router, next/link, @tanstack/router, …)"
11405
+ },
11341
11406
  {
11342
11407
  "label": "Album grid",
11343
11408
  "code": "<Grid minItemWidth=\"10rem\" gap={3}>\n\t{albums.map((album) => (\n\t\t<MediaTile\n\t\t\tkey={album.id}\n\t\t\ttitle={album.title}\n\t\t\tsubtitle={album.artist}\n\t\t\tsrc={album.cover}\n\t\t\trender={<Link to={`/albums/${album.id}`} />}\n\t\t\tsubtitleRender={<Link to={`/artists/${album.artistId}`} />}\n\t\t\taction={\n\t\t\t\t<Button size=\"icon\" aria-label={`Play ${album.title}`} onClick={() => play(album)}>\n\t\t\t\t\t<Icon name=\"play\" variant=\"solid\" />\n\t\t\t\t</Button>\n\t\t\t}\n\t\t/>\n\t))}\n</Grid>",
@@ -14801,6 +14866,11 @@
14801
14866
  "type": "ReactNode",
14802
14867
  "description": "Optional supporting copy beneath the title (text-body-sm, muted)."
14803
14868
  },
14869
+ {
14870
+ "name": "trailing",
14871
+ "type": "ReactNode",
14872
+ "description": "End-aligned on the title's line: a price, a delivery date, a count. The title takes the rest of the row."
14873
+ },
14804
14874
  {
14805
14875
  "name": "icon",
14806
14876
  "type": "ReactNode",
@@ -17687,7 +17757,7 @@
17687
17757
  "name": "orientation",
17688
17758
  "type": "\"horizontal\" | \"vertical\"",
17689
17759
  "default": "\"horizontal\"",
17690
- "description": "Horizontal for a short flow across the top of a form; vertical for long flows, narrow columns or steps with descriptions."
17760
+ "description": "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."
17691
17761
  },
17692
17762
  {
17693
17763
  "name": "indicator",
@@ -23874,6 +23944,7 @@
23874
23944
  "iconStart",
23875
23945
  "loading",
23876
23946
  "loadingLabel",
23947
+ "onClick",
23877
23948
  "render",
23878
23949
  "size",
23879
23950
  "variant"
@@ -24289,6 +24360,7 @@
24289
24360
  "disabled",
24290
24361
  "hint",
24291
24362
  "icon",
24363
+ "leading",
24292
24364
  "onSelect",
24293
24365
  "stacked"
24294
24366
  ],
@@ -24621,6 +24693,7 @@
24621
24693
  "name",
24622
24694
  "orientation",
24623
24695
  "required",
24696
+ "trailing",
24624
24697
  "validate",
24625
24698
  "validationDebounceTime",
24626
24699
  "validationMode"
@@ -24628,7 +24701,8 @@
24628
24701
  "Fieldset": [
24629
24702
  "className",
24630
24703
  "render",
24631
- "style"
24704
+ "style",
24705
+ "variant"
24632
24706
  ],
24633
24707
  "FieldsetLegend": [
24634
24708
  "className",
@@ -24676,6 +24750,7 @@
24676
24750
  "errors",
24677
24751
  "noValidate",
24678
24752
  "onFormSubmit",
24753
+ "onSubmit",
24679
24754
  "validationMode"
24680
24755
  ],
24681
24756
  "GlidingHighlight": [
@@ -24790,10 +24865,13 @@
24790
24865
  "badge",
24791
24866
  "href",
24792
24867
  "loading",
24868
+ "meta",
24793
24869
  "render",
24794
24870
  "seed",
24795
24871
  "size",
24872
+ "sizes",
24796
24873
  "src",
24874
+ "srcSet",
24797
24875
  "subtitle",
24798
24876
  "subtitleHref",
24799
24877
  "subtitleRender",
@@ -25352,6 +25430,7 @@
25352
25430
  "title",
25353
25431
  "titleClassName",
25354
25432
  "tone",
25433
+ "trailing",
25355
25434
  "value"
25356
25435
  ],
25357
25436
  "RadioGroup": [
@@ -26377,7 +26456,7 @@
26377
26456
  "name": "themeStorageKey",
26378
26457
  "type": "string | null",
26379
26458
  "default": "SABLE_THEME_STORAGE_KEY",
26380
- "description": "localStorage key for the design theme. null disables persistence."
26459
+ "description": "localStorage key for the design theme. null disables persistence. The default key is shared by every Sable app on an origin (several apps on localhost:5173 in development), and a stored id another app set wins over `defaultThemeId` when this app knows it too. An app with one fixed preset passes `themeId` (or `themeStorageKey={null}`); an app that lets users switch themes passes its own key."
26381
26460
  },
26382
26461
  {
26383
26462
  "name": "target",