@sableui/react 0.7.1 → 0.8.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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +83 -0
- package/README.md +2 -0
- package/bin/sable.mjs +45 -2
- package/dist/components/breadcrumbs/breadcrumbs.js +10 -9
- package/dist/components/breadcrumbs/breadcrumbs.meta.js +2 -2
- package/dist/components/button/button.meta.js +4 -0
- package/dist/components/chat-composer/chat-composer.js +4 -2
- package/dist/components/chat-composer/chat-composer.meta.js +8 -2
- package/dist/components/command-menu/command-menu.js +9 -2
- package/dist/components/command-menu/command-menu.meta.js +3 -3
- package/dist/components/data-table/data-table.js +117 -64
- package/dist/components/data-table/data-table.meta.js +27 -1
- package/dist/components/form/form.js +30 -9
- package/dist/components/form/form.meta.js +17 -3
- package/dist/components/message-scroller/message-scroller.meta.js +1 -1
- package/dist/components/page-header/page-header.js +1 -0
- package/dist/components/page-header/page-header.meta.js +1 -1
- package/dist/components/settings-menu/settings-menu.js +10 -1
- package/dist/components/settings-menu/settings-menu.meta.js +1 -1
- package/dist/components/sidebar/sidebar.js +1 -1
- package/dist/components/sidebar/sidebar.meta.js +5 -0
- package/dist/components/switch/switch.js +1 -1
- package/dist/components/switch/switch.meta.js +2 -2
- package/dist/components/toggle/toggle.js +2 -1
- package/dist/file-hashes.json +603 -0
- package/dist/icon-sets/reicon.js +13 -1
- package/dist/index.d.ts +80 -13
- package/dist/index.js +1 -1
- package/dist/lib/heading-row.js +3 -2
- package/dist/lib/trim-label.js +17 -4
- package/dist/styles.css +27 -2
- package/llms/components/breadcrumbs.md +1 -1
- package/llms/components/button.md +1 -0
- package/llms/components/chat-composer.md +3 -2
- package/llms/components/command-menu.md +2 -2
- package/llms/components/data-table.md +33 -1
- package/llms/components/form.md +19 -2
- package/llms/components/message-scroller.md +1 -1
- package/llms/components/page-header.md +1 -1
- package/llms/components/settings-menu.md +1 -1
- package/llms/components/sidebar.md +1 -0
- package/llms/components/switch.md +2 -2
- package/llms/tasks/getting-started.md +5 -2
- package/llms/tasks/patching-a-component.md +48 -0
- package/llms/tasks/reviewing-a-design.md +5 -0
- package/llms-full.txt +126 -15
- package/llms.txt +1 -1
- package/package.json +4 -2
- package/registry.json +98 -20
- package/scripts/design-memory.mjs +7 -2
- package/scripts/lint-rules.mjs +32 -1
- package/scripts/patches.mjs +219 -0
- package/scripts/sources.mjs +247 -0
- package/skills/sable/SKILL.md +9 -4
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
Binary on/off toggle for immediate-effect settings. Track fills with the primary tone when checked; the thumb slides on a spring-eased transform (mirrored in RTL).
|
|
5
|
+
Binary on/off toggle for immediate-effect settings. Track fills with the primary tone when checked; the thumb slides on a spring-eased transform (mirrored in RTL). The thumb is white in both states, like every physical toggle, and turns dark only on a near-white track (a dark mode's light primary). Themes can set --theme-switch-thumb-on.
|
|
6
6
|
|
|
7
7
|
**Keywords:** switch, toggle setting, on off, form control
|
|
8
8
|
|
|
@@ -32,7 +32,7 @@ Use Switch for immediate on/off settings that take effect as a persistent prefer
|
|
|
32
32
|
|
|
33
33
|
## Tokens used
|
|
34
34
|
|
|
35
|
-
`input`, `primary`, `
|
|
35
|
+
`input`, `primary`, `ring`
|
|
36
36
|
|
|
37
37
|
## Motion
|
|
38
38
|
|
|
@@ -20,8 +20,11 @@ For `src/styles.css` with `node_modules` at the project root:
|
|
|
20
20
|
@source "../node_modules/@sableui/react/dist/**/*.{js,mjs}";
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Adjust the relative `@source` path to your stylesheet's location.
|
|
24
|
-
|
|
23
|
+
Adjust the relative `@source` path to your stylesheet's location. It makes
|
|
24
|
+
Tailwind generate every Sable component's classes; once the app is built,
|
|
25
|
+
`sable sources --write src/sable-sources.css` lists just the ones it imports, to
|
|
26
|
+
`@import` in its place (`sable check` notes this). Import this stylesheet from
|
|
27
|
+
your app entry. For Vite, preserve your plugins and add
|
|
25
28
|
`resolve: { dedupe: ["react", "react-dom"] }` to its configuration. Load Figtree
|
|
26
29
|
and DM Mono through your font setup, or customize the font tokens.
|
|
27
30
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Task: patching a Sable component
|
|
2
|
+
|
|
3
|
+
A Sable component has a defect that blocks your app, and the fix has not shipped.
|
|
4
|
+
Don't restyle it from the outside with `className` (`component-restyle` flags
|
|
5
|
+
that, and it fights the component's own states), and don't copy the component
|
|
6
|
+
into the app (it imports Sable internals, and the copy drifts from every later
|
|
7
|
+
fix). Patch the installed module and record why. Sable then re-checks the patch
|
|
8
|
+
on every upgrade and tells you when to drop it.
|
|
9
|
+
|
|
10
|
+
## Record it, then patch it
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm exec sable patch Card --reason "Row hover pokes past a flush card's corners" \
|
|
14
|
+
--repro "Hover the last row of the Tickets table at a 20px card radius"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The record goes in `.sable/patches.json`: the component, the files it covers,
|
|
18
|
+
the Sable version, each file's hash as Sable shipped it, your reason and your
|
|
19
|
+
repro. Commit it. The command prints the steps for your package manager:
|
|
20
|
+
|
|
21
|
+
- **pnpm:** `pnpm patch @sableui/react@<version>`, edit the file in the folder
|
|
22
|
+
it prints, then `pnpm patch-commit <folder>`.
|
|
23
|
+
- **Yarn 2+:** `yarn patch @sableui/react`, edit, then `yarn patch-commit -s <folder>`.
|
|
24
|
+
- **npm or Yarn 1:** edit `node_modules/@sableui/react/<file>`, run
|
|
25
|
+
`npx patch-package @sableui/react`, and add `"postinstall": "patch-package"`.
|
|
26
|
+
|
|
27
|
+
A component's patch covers its own modules (`dist/components/card/*.js`). When
|
|
28
|
+
the defect is in a shared module, add it with `--file dist/lib/trim-label.js`.
|
|
29
|
+
Keep the change as small as the defect. Then report it upstream (`--issue` keeps
|
|
30
|
+
the link), so the library fix can replace the patch.
|
|
31
|
+
|
|
32
|
+
## On every upgrade
|
|
33
|
+
|
|
34
|
+
`sable doctor`, and so `sable check`, re-checks each recorded patch against the
|
|
35
|
+
installed Sable:
|
|
36
|
+
|
|
37
|
+
| State | Doctor | What to do |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| Same version, patch applied | passes | nothing |
|
|
40
|
+
| Same version, not applied | warns | make the patch (the printed steps) |
|
|
41
|
+
| Newer Sable changed a patched file | **fails**, citing the changelog entries that name the component | re-run the repro on Sable's own files. Fixed: `sable patch Card --drop` and remove the patch. Still broken: re-patch, then `sable patch Card --keep` |
|
|
42
|
+
| Newer Sable left the file alone, patch carried over | passes | nothing |
|
|
43
|
+
| Newer Sable left the file alone, patch lost | **fails** | re-apply it, then `--keep` |
|
|
44
|
+
|
|
45
|
+
Nothing is kept or dropped silently. pnpm also stops an install whose patch
|
|
46
|
+
names a version that is no longer installed; that is the same alert, earlier.
|
|
47
|
+
|
|
48
|
+
`sable patch --list` shows every patch and its state.
|
|
@@ -10,6 +10,11 @@ Save the surface brief, then capture the rendered route at a desktop width of at
|
|
|
10
10
|
least 1024px and a mobile width of at most 480px. Record the actual viewport,
|
|
11
11
|
route/state, supported color mode, and motion setting with each artifact. Inspect
|
|
12
12
|
the whole route as well as its first viewport. Ensure fonts and media have loaded.
|
|
13
|
+
Charts and other arrival motion draw when they scroll into view, so a full-page
|
|
14
|
+
still taken with full motion shows the ones below the fold empty. Scroll the route
|
|
15
|
+
through first and let the motion finish, or take that capture with
|
|
16
|
+
`prefers-reduced-motion: reduce` emulated (everything then starts drawn). Don't
|
|
17
|
+
report an undrawn chart as a defect without checking which happened.
|
|
13
18
|
Also capture a route that sets a width cap, or goes fluid, at 1920px: a cap applies
|
|
14
19
|
only once the viewport minus app chrome exceeds it, so at 1024–1440px `wide`,
|
|
15
20
|
`full` and fluid usually look the same and the width decision never shows. Add
|
package/llms-full.txt
CHANGED
|
@@ -1807,8 +1807,11 @@ For `src/styles.css` with `node_modules` at the project root:
|
|
|
1807
1807
|
@source "../node_modules/@sableui/react/dist/**/*.{js,mjs}";
|
|
1808
1808
|
```
|
|
1809
1809
|
|
|
1810
|
-
Adjust the relative `@source` path to your stylesheet's location.
|
|
1811
|
-
|
|
1810
|
+
Adjust the relative `@source` path to your stylesheet's location. It makes
|
|
1811
|
+
Tailwind generate every Sable component's classes; once the app is built,
|
|
1812
|
+
`sable sources --write src/sable-sources.css` lists just the ones it imports, to
|
|
1813
|
+
`@import` in its place (`sable check` notes this). Import this stylesheet from
|
|
1814
|
+
your app entry. For Vite, preserve your plugins and add
|
|
1812
1815
|
`resolve: { dedupe: ["react", "react-dom"] }` to its configuration. Load Figtree
|
|
1813
1816
|
and DM Mono through your font setup, or customize the font tokens.
|
|
1814
1817
|
|
|
@@ -1932,6 +1935,57 @@ Only label a record human when it reflects an actual human's stated judgment.
|
|
|
1932
1935
|
|
|
1933
1936
|
---
|
|
1934
1937
|
|
|
1938
|
+
# Task: patching a Sable component
|
|
1939
|
+
|
|
1940
|
+
A Sable component has a defect that blocks your app, and the fix has not shipped.
|
|
1941
|
+
Don't restyle it from the outside with `className` (`component-restyle` flags
|
|
1942
|
+
that, and it fights the component's own states), and don't copy the component
|
|
1943
|
+
into the app (it imports Sable internals, and the copy drifts from every later
|
|
1944
|
+
fix). Patch the installed module and record why. Sable then re-checks the patch
|
|
1945
|
+
on every upgrade and tells you when to drop it.
|
|
1946
|
+
|
|
1947
|
+
## Record it, then patch it
|
|
1948
|
+
|
|
1949
|
+
```bash
|
|
1950
|
+
pnpm exec sable patch Card --reason "Row hover pokes past a flush card's corners" \
|
|
1951
|
+
--repro "Hover the last row of the Tickets table at a 20px card radius"
|
|
1952
|
+
```
|
|
1953
|
+
|
|
1954
|
+
The record goes in `.sable/patches.json`: the component, the files it covers,
|
|
1955
|
+
the Sable version, each file's hash as Sable shipped it, your reason and your
|
|
1956
|
+
repro. Commit it. The command prints the steps for your package manager:
|
|
1957
|
+
|
|
1958
|
+
- **pnpm:** `pnpm patch @sableui/react@<version>`, edit the file in the folder
|
|
1959
|
+
it prints, then `pnpm patch-commit <folder>`.
|
|
1960
|
+
- **Yarn 2+:** `yarn patch @sableui/react`, edit, then `yarn patch-commit -s <folder>`.
|
|
1961
|
+
- **npm or Yarn 1:** edit `node_modules/@sableui/react/<file>`, run
|
|
1962
|
+
`npx patch-package @sableui/react`, and add `"postinstall": "patch-package"`.
|
|
1963
|
+
|
|
1964
|
+
A component's patch covers its own modules (`dist/components/card/*.js`). When
|
|
1965
|
+
the defect is in a shared module, add it with `--file dist/lib/trim-label.js`.
|
|
1966
|
+
Keep the change as small as the defect. Then report it upstream (`--issue` keeps
|
|
1967
|
+
the link), so the library fix can replace the patch.
|
|
1968
|
+
|
|
1969
|
+
## On every upgrade
|
|
1970
|
+
|
|
1971
|
+
`sable doctor`, and so `sable check`, re-checks each recorded patch against the
|
|
1972
|
+
installed Sable:
|
|
1973
|
+
|
|
1974
|
+
| State | Doctor | What to do |
|
|
1975
|
+
|---|---|---|
|
|
1976
|
+
| Same version, patch applied | passes | nothing |
|
|
1977
|
+
| Same version, not applied | warns | make the patch (the printed steps) |
|
|
1978
|
+
| Newer Sable changed a patched file | **fails**, citing the changelog entries that name the component | re-run the repro on Sable's own files. Fixed: `sable patch Card --drop` and remove the patch. Still broken: re-patch, then `sable patch Card --keep` |
|
|
1979
|
+
| Newer Sable left the file alone, patch carried over | passes | nothing |
|
|
1980
|
+
| Newer Sable left the file alone, patch lost | **fails** | re-apply it, then `--keep` |
|
|
1981
|
+
|
|
1982
|
+
Nothing is kept or dropped silently. pnpm also stops an install whose patch
|
|
1983
|
+
names a version that is no longer installed; that is the same alert, earlier.
|
|
1984
|
+
|
|
1985
|
+
`sable patch --list` shows every patch and its state.
|
|
1986
|
+
|
|
1987
|
+
---
|
|
1988
|
+
|
|
1935
1989
|
# Task: reviewing a rendered design
|
|
1936
1990
|
|
|
1937
1991
|
Review has two separate results: code conformance and observed design quality.
|
|
@@ -1944,6 +1998,11 @@ Save the surface brief, then capture the rendered route at a desktop width of at
|
|
|
1944
1998
|
least 1024px and a mobile width of at most 480px. Record the actual viewport,
|
|
1945
1999
|
route/state, supported color mode, and motion setting with each artifact. Inspect
|
|
1946
2000
|
the whole route as well as its first viewport. Ensure fonts and media have loaded.
|
|
2001
|
+
Charts and other arrival motion draw when they scroll into view, so a full-page
|
|
2002
|
+
still taken with full motion shows the ones below the fold empty. Scroll the route
|
|
2003
|
+
through first and let the motion finish, or take that capture with
|
|
2004
|
+
`prefers-reduced-motion: reduce` emulated (everything then starts drawn). Don't
|
|
2005
|
+
report an undrawn chart as a defect without checking which happened.
|
|
1947
2006
|
Also capture a route that sets a width cap, or goes fluid, at 1920px: a cap applies
|
|
1948
2007
|
only once the viewport minus app chrome exceeds it, so at 1024–1440px `wide`,
|
|
1949
2008
|
`full` and fluid usually look the same and the width decision never shows. Add
|
|
@@ -10774,7 +10833,7 @@ Use Breadcrumbs to show the user's location inside a hierarchy and provide light
|
|
|
10774
10833
|
| Name | Type | Description |
|
|
10775
10834
|
| --- | --- | --- |
|
|
10776
10835
|
| BreadcrumbItem | ComponentPropsWithRef<'li'> | A single crumb slot (<li>) holding a link or the current page. |
|
|
10777
|
-
| BreadcrumbLink | ComponentPropsWithRef<'a'> | An interactive ancestor crumb (<a>). Muted by default, brightens to foreground on hover/focus. |
|
|
10836
|
+
| BreadcrumbLink | ComponentPropsWithRef<'a'> & { render?: useRender.RenderProp } | An interactive ancestor crumb (<a>). Muted by default, brightens to foreground on hover/focus. render composes another element with the crumb's styling, e.g. a router link: render={<Link to="/library" />}, so navigating stays in the app. |
|
|
10778
10837
|
| BreadcrumbPage | ComponentPropsWithRef<'span'> | The current-page crumb (<span aria-current="page">). Non-interactive, full-contrast. |
|
|
10779
10838
|
| BreadcrumbSeparator | ComponentPropsWithRef<'li'> + { children? } | Decorative divider (<li aria-hidden>) between crumbs. Defaults to a chevron-right icon; pass children to swap the glyph. |
|
|
10780
10839
|
|
|
@@ -11026,6 +11085,7 @@ Use Button when the user commits an action, submits a form, opens UI, or confirm
|
|
|
11026
11085
|
### Best practices
|
|
11027
11086
|
|
|
11028
11087
|
- Do: Use a single primary button in a view or dense panel so the action hierarchy stays obvious.
|
|
11088
|
+
- Do: For a copy control, pair an icon Button with `useCopy(text, { timeout? })`. It takes the text when the hook is called and returns `{ copy, copied }`; `copy()` takes no argument, so pass the value the button copies to the hook, not to `copy`.
|
|
11029
11089
|
- Do: Write labels as actions, such as "Save changes" or "Delete draft", instead of vague labels like "OK".
|
|
11030
11090
|
- Do: Use loading with loadingLabel for async work so the action cannot double-fire and the state is announced.
|
|
11031
11091
|
- Do: Put icons in iconStart / iconEnd rather than as children, so the button applies its optical padding. Start icons say what the action is (plus, share); end icons say where it goes (chevron-right, external).
|
|
@@ -11554,7 +11614,7 @@ import { Carousel } from "@sableui/react"
|
|
|
11554
11614
|
|
|
11555
11615
|
**Status:** stable
|
|
11556
11616
|
|
|
11557
|
-
The input half of a conversation: one surface holding attachments, an auto-growing field, and its controls. Enter sends, Shift+Enter breaks the line, and send becomes stop while a reply streams.
|
|
11617
|
+
The input half of a conversation: one surface holding attachments, an auto-growing field, and its controls. Enter sends, Shift+Enter breaks the line (sendOn="mod-enter" swaps them for long-form replies), and send becomes stop while a reply streams.
|
|
11558
11618
|
|
|
11559
11619
|
**Keywords:** chat input, composer, message input, prompt input, send message, ai chat, support chat, textarea
|
|
11560
11620
|
|
|
@@ -11571,7 +11631,7 @@ Pairs with MessageScroller to make a working chat. A single surface rather than
|
|
|
11571
11631
|
- Do: Building a docked agent panel inside a tool? Wrap the whole conversation — scroller, messages and composer — in `<Density value="compact">`. The entire set follows the workspace register from there: prose drops a tier, the rhythm tightens, and the controls come off the compact ladder. Do not hand-size it at the call site.
|
|
11572
11632
|
- Don't: Do not clear a controlled composer for the user on send. Only the caller knows whether the send succeeded, and clearing on their behalf loses the message when it did not.
|
|
11573
11633
|
- Don't: Do not add your own Enter-to-send handler on top. The built-in one already guards against firing mid-IME-composition, which is the line hand-rolled chat inputs miss.
|
|
11574
|
-
- Don't: Do not use it as a general multi-line input. That is `Textarea
|
|
11634
|
+
- Don't: Do not use it as a general multi-line input inside a larger form. That is `Textarea`. A standalone reply box that sends multi-paragraph messages (email, support replies) is this one with sendOn="mod-enter".
|
|
11575
11635
|
- Don't: Do not override the field's type utility to make it match your transcript. The field and `Bubble` both read the same tier already, which is what stops a message changing size the moment it is sent — overriding one breaks that pairing.
|
|
11576
11636
|
- Don't: Do not remove the height cap to let the field grow without limit. A pasted essay would push the conversation off the screen; past the cap it scrolls.
|
|
11577
11637
|
|
|
@@ -11593,6 +11653,7 @@ Pairs with MessageScroller to make a working chat. A single surface rather than
|
|
|
11593
11653
|
| onValueChange | (value: string) => void | — | Notified on every keystroke. Required when `value` is supplied. |
|
|
11594
11654
|
| onSend | (value: string) => void | — | Called with the trimmed message. An uncontrolled composer clears itself afterwards; a controlled one is the caller's to clear. |
|
|
11595
11655
|
| placeholder | string | "Send a message…" | Placeholder for the field. |
|
|
11656
|
+
| sendOn | "enter" \| "mod-enter" | "enter" | Which key sends. enter for chat (Shift+Enter breaks the line). mod-enter for long-form replies such as an email or a support answer: Enter breaks the line and Cmd/Ctrl+Enter sends, so a paragraph break never mails a draft. Say so in footer ("Cmd+Enter to send"). |
|
|
11596
11657
|
| busy | boolean | false | A reply is streaming. Disables send, and swaps it for a stop control when `onStop` is supplied. |
|
|
11597
11658
|
| onStop | () => void | — | Interrupt the in-flight reply. Supplying it turns send into stop while busy. |
|
|
11598
11659
|
| disabled | boolean | false | Block sending entirely — over a limit, disconnected, read-only thread. Distinct from `busy`, which is temporary and offers a way out (stop). |
|
|
@@ -12379,7 +12440,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
|
|
|
12379
12440
|
| useCommandMenuShortcut | (onShortcut: (event: KeyboardEvent) => void, key?: string \| false) => void | The page-wide ⌘K / Ctrl+K listener CommandMenuTrigger installs, for a palette with no visible trigger. Ignored while focus is in a text field, textarea, select or contenteditable (Ctrl+K belongs to the field there) except the palette's own input, so the callback may toggle. It calls preventDefault() on a match and skips an event already prevented, so two listeners on one page fire once. key defaults to "k"; false turns it off. Removed on unmount. |
|
|
12380
12441
|
| CommandMenuPanel | { items?: CommandItem[]; query?: string; onQueryChange?: (query: string) => void; filter?: CommandFilter; listClassName?: string; placeholder?: string; emptyMessage?: string; title?: string; scope?: ReactNode; inputProps?: ComponentProps<'input'>; className?: string; children?: ReactNode } | The palette rendered in place: the same search header, filtered and grouped listbox, keyboard model and result-count live region, on a static card (border-border, bg-card, no shadow) instead of in a Dialog. Takes every CommandMenu prop except open, onOpenChange, finalFocus and description; className lands on the card, title names the listbox. Focus is not trapped and the input does not take focus on mount (inputProps.autoFocus opts in). Esc clears the query, and passes through once it is empty, so a Popover around the panel still closes. A chosen row runs its onSelect and the panel stays as it was. The page-wide shortcut does not fire from its input. |
|
|
12381
12442
|
| CommandMenuGroup | { label: string; children } | A labelled group of command rows. |
|
|
12382
|
-
| CommandMenuItem | { onSelect?: () => void; icon?: string; detail?: ReactNode; hint?: ReactNode; disabled?: boolean; id?: string; children } | A selectable row. Self-filters by its text (unless the root was given a filter), runs onSelect, then closes the palette. detail is secondary text after the label; hint is the end-aligned trailing slot — a shortcut, a count, or a disabled row's reason. A disabled row stays listed, stays announced, and stays reachable by arrow key so its reason can be read, but is inert to Enter and to a click. |
|
|
12443
|
+
| CommandMenuItem | { onSelect?: () => void; icon?: string; detail?: ReactNode; stacked?: boolean; hint?: ReactNode; disabled?: boolean; id?: string; children } | A selectable row. Self-filters by its text (unless the root was given a filter), runs onSelect, then closes the palette. detail is secondary text after the label, or under it with stacked; hint is the end-aligned trailing slot — a shortcut, a count, or a disabled row's reason. A disabled row stays listed, stays announced, and stays reachable by arrow key so its reason can be read, but is inert to Enter and to a click. |
|
|
12383
12444
|
|
|
12384
12445
|
## Props
|
|
12385
12446
|
|
|
@@ -12389,7 +12450,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
|
|
|
12389
12450
|
| inputProps | Omit<ComponentProps<'input'>, 'role' \| 'value' \| 'onChange' \| aria-*> | — | Extra props for the search input. `onKeyDown` runs BEFORE the palette's own handling and opts out of a key by calling preventDefault() — which is what makes a scope chip Backspace-dismissable. Running first is deliberate: Backspace happens not to be a key the palette claims today, so a plain passthrough would work until the palette grows a 'Backspace clears the query' affordance, at which point the consumer's behaviour breaks silently. The combobox wiring (role, aria relationships, controlled value) is not overridable; aria-label and className compose. |
|
|
12390
12451
|
| open | boolean | — | Controlled open state of the palette. |
|
|
12391
12452
|
| onOpenChange | (open: boolean) => void | — | Called on open/close (select, Esc, outside click). |
|
|
12392
|
-
| items | CommandItem[] | — | Data-driven commands ({ group, label, onSelect, id?, icon?, detail?, hint?, disabled? }). Ignored when children are provided. id is optional identity: filter is handed these same objects, so it is how a caller that ranks over its own richer model maps the array back onto it — and it becomes the row's DOM id (namespaced to the palette) and React key, so two rows may share a label. |
|
|
12453
|
+
| items | CommandItem[] | — | Data-driven commands ({ group, label, onSelect, id?, icon?, detail?, stacked?, hint?, disabled? }). stacked puts detail on its own line under the label, for results only their detail tells apart (two tickets with one subject); beside a long label an inline detail truncates first. Ignored when children are provided. id is optional identity: filter is handed these same objects, so it is how a caller that ranks over its own richer model maps the array back onto it — and it becomes the row's DOM id (namespaced to the palette) and React key, so two rows may share a label. |
|
|
12393
12454
|
| query | string | — | Controlled query. Leave undefined to let the palette own it; either way it resets on close (a controlled caller gets onQueryChange('')). |
|
|
12394
12455
|
| onQueryChange | (query: string) => void | — | Called on every query change. Required to control query, optional otherwise. |
|
|
12395
12456
|
| filter | ((items: CommandItem[], query: string) => CommandItem[]) \| null | case-insensitive substring match on label | Replaces or disables the built-in match. A function ranks and filters items itself and its order is kept verbatim; null renders items exactly as given. Either also stops declarative CommandMenuItem children self-filtering. The query arrives raw — trimming and case are policy, and this is the prop that hands policy over. |
|
|
@@ -12690,7 +12751,11 @@ Use DataTable when a data array and column definition should render a sortable,
|
|
|
12690
12751
|
| data | Row[] | — | The rows to render. |
|
|
12691
12752
|
| rowKey | (row, index) => string \| number | index | Stable React key per row. |
|
|
12692
12753
|
| onRowClick | (row) => void | — | Row click handler — makes rows pointer-interactive. |
|
|
12693
|
-
| isRowSelected | (row) => boolean | — | Mark a row visually selected (data-selected). Derive it from a stable id (row.id === selectedId), not the row index, so selection survives re-sorts. |
|
|
12754
|
+
| isRowSelected | (row) => boolean | — | Mark a row visually selected (data-selected), e.g. the one open in a detail pane. Derive it from a stable id (row.id === selectedId), not the row index, so selection survives re-sorts. For checking several rows, use selectedKeys. |
|
|
12755
|
+
| selectedKeys | ReadonlySet<string \| number> | — | The checked rows, by rowKey, for bulk actions. With onSelectedKeysChange it adds a checkbox column (each box named by rowSelectionLabel) and a select-all box in the header that acts on the rows shown and goes mixed when some are checked. Checked rows get the selected fill. Controlled: when the rows shown change (a filter, another view), set the keys to keep, usually none. Needs rowKey. |
|
|
12756
|
+
| onSelectedKeysChange | (keys: Set<string \| number>) => void | — | Called with the new set of checked keys. Its presence turns selection on. A click on a box never fires onRowClick. |
|
|
12757
|
+
| rowSelectionLabel | (row) => string | () => "Select row" | A row checkbox's accessible name. Name the row, e.g. (t) => `Select ${t.subject}`, so a screen reader can tell the boxes apart. |
|
|
12758
|
+
| selectAllLabel | (count: number) => string | (count) => `Select all ${count} shown` | The select-all checkbox's accessible name; say what it acts on ("Select all 7 tickets shown"). |
|
|
12694
12759
|
| defaultSort | { key: string; direction: "asc" \| "desc" } | — | Initial sort column + direction. |
|
|
12695
12760
|
| empty | ReactNode | "No results." | Rendered spanning all columns when data is empty. Pass an EmptyState for the full treatment (icon, title, next step). |
|
|
12696
12761
|
| caption | ReactNode | — | Bottom table caption. |
|
|
@@ -12712,6 +12777,34 @@ Use DataTable when a data array and column definition should render a sortable,
|
|
|
12712
12777
|
|
|
12713
12778
|
## Examples
|
|
12714
12779
|
|
|
12780
|
+
### Checkbox selection with a bulk-action bar
|
|
12781
|
+
|
|
12782
|
+
```tsx
|
|
12783
|
+
import { Button, DataTable, Stack, Toolbar } from "@sableui/react"
|
|
12784
|
+
|
|
12785
|
+
const [selected, setSelected] = useState<Set<string | number>>(new Set())
|
|
12786
|
+
|
|
12787
|
+
<Stack gap={2}>
|
|
12788
|
+
{/* Mounted from the start, so a screen reader hears each change. */}
|
|
12789
|
+
<span role="status" className="sr-only">{selected.size ? `${selected.size} of ${tickets.length} selected` : ""}</span>
|
|
12790
|
+
{selected.size ? (
|
|
12791
|
+
<Toolbar aria-label="Bulk actions">
|
|
12792
|
+
<span className="text-label-md">{selected.size} of {tickets.length} selected</span>
|
|
12793
|
+
<Button size="sm" variant="secondary" onClick={() => resolve(selected)}>Resolve</Button>
|
|
12794
|
+
</Toolbar>
|
|
12795
|
+
) : null}
|
|
12796
|
+
<DataTable
|
|
12797
|
+
data={tickets}
|
|
12798
|
+
rowKey={(t) => t.id}
|
|
12799
|
+
columns={columns}
|
|
12800
|
+
selectedKeys={selected}
|
|
12801
|
+
onSelectedKeysChange={setSelected}
|
|
12802
|
+
rowSelectionLabel={(t) => `Select ${t.subject}`}
|
|
12803
|
+
selectAllLabel={(n) => `Select all ${n} tickets shown`}
|
|
12804
|
+
/>
|
|
12805
|
+
</Stack>
|
|
12806
|
+
```
|
|
12807
|
+
|
|
12715
12808
|
### Sortable columns
|
|
12716
12809
|
|
|
12717
12810
|
```tsx
|
|
@@ -13926,16 +14019,17 @@ Use Form and Field as the default wrapper for Sable form controls. Field owns th
|
|
|
13926
14019
|
|
|
13927
14020
|
| Name | Type | Description |
|
|
13928
14021
|
| --- | --- | --- |
|
|
13929
|
-
| Field | { label?: ReactNode; description?: ReactNode; error?: ReactNode; required?: boolean; 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 Base UI's validation message
|
|
14022
|
+
| Field | { label?: ReactNode; description?: ReactNode; error?: ReactNode; required?: boolean; messages?: Partial<Record<"valueMissing" \| "typeMismatch" \| "patternMismatch" \| "tooShort" \| "tooLong" \| "rangeUnderflow" \| "rangeOverflow" \| "stepMismatch" \| "badInput", ReactNode>>; disabled?: boolean; name?: string; validate?: (value, formValues) => string \| string[] \| null \| Promise<…>; validationMode?: "onSubmit" \| "onBlur" \| "onChange"; validationDebounceTime?: number; orientation?: "vertical" \| "horizontal"; className?: string; children: ReactElement } | Wraps a single control element (e.g. <Input />) on Base UI Field and wires label (htmlFor + aria-labelledby), helper text and error (aria-describedby), aria-invalid, and required onto it automatically. Base UI-backed controls (Input, Textarea, Select, Combobox, NumberField, Checkbox, Switch, Slider, OtpField…) register through context; the child is also cloned so plain elements keep the same wiring. error renders a destructive message and takes precedence; without it the slot shows the field's messages entry for the native constraint that failed (messages={{ valueMissing: "Enter a subject." }}), else Base UI's validation message (the browser's wording for a native constraint, validate, or Form errors). required adds the native attribute plus a visual asterisk (defaults to false). disabled disables the control and dims the label. name keys the field in Form errors / onFormSubmit. validationMode overrides the Form's. orientation defaults to "vertical" (label → control); "horizontal" places control then label inline for a single checkbox / switch / radio — the control centres on the label's first line (it stays there when the label wraps) and description/error sit in the label's column. The root, label, description, error and control carry data-invalid / data-valid, data-touched, data-dirty, data-filled, data-focused and data-disabled for state styling. |
|
|
13930
14023
|
|
|
13931
14024
|
## Props
|
|
13932
14025
|
|
|
13933
14026
|
| Name | Type | Default | Description |
|
|
13934
14027
|
| --- | --- | --- | --- |
|
|
13935
|
-
| 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
|
|
14028
|
+
| Form | { validationMode?: "onSubmit" \| "onBlur" \| "onChange"; errors?: Record<string, string \| string[]>; onFormSubmit?: (values, details) => void; actionsRef?: RefObject<Form.Actions> } + ComponentPropsWithRef<"form"> | — | A <form> laid out as a vertical column with comfortable field spacing (override with className, e.g. gap-4). On submit it validates every Field (native constraints plus Field validate), shows each message under its field, focuses the first invalid control, and only then calls onSubmit. validationMode defaults to "onSubmit" (then live re-validation on change). errors shows external (e.g. server) messages keyed by Field name and clears one when its field is edited. onFormSubmit receives the named Field values and prevents the native submit. A control outside any Field is still checked by the browser before onSubmit runs. Validating in your own code (react-hook-form, a schema, state)? Pass noValidate: onSubmit always runs and each Field shows the error you set; without it an empty required Field stops the submit before your handler can set that error. |
|
|
13936
14029
|
| 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. |
|
|
13937
14030
|
| 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. |
|
|
13938
14031
|
| onFormSubmit | (values: Record<string, unknown>, details) => void | — | Called with the named Field values after validation passes; prevents the native submit. |
|
|
14032
|
+
| noValidate | boolean | false | Your code owns validation: nothing is checked before onSubmit (neither the Fields nor the browser), so it always runs and each Field shows the error you pass. required Fields keep their asterisk and aria-required. validationMode, errors, onFormSubmit and actionsRef do nothing with it. |
|
|
13939
14033
|
| actionsRef | RefObject<{ validate(name?: string): void }> | — | Imperative handle to validate every field, or one by name. |
|
|
13940
14034
|
|
|
13941
14035
|
## Tokens used
|
|
@@ -13972,6 +14066,22 @@ import { Field, Input } from "@sableui/react"
|
|
|
13972
14066
|
<Field label="Password" error="Must be at least 8 characters."><Input type="password" /></Field>
|
|
13973
14067
|
```
|
|
13974
14068
|
|
|
14069
|
+
### Your own wording for a missing value
|
|
14070
|
+
|
|
14071
|
+
```tsx
|
|
14072
|
+
import { Button, Field, Form, Input } from "@sableui/react"
|
|
14073
|
+
|
|
14074
|
+
<Form onSubmit={handleSubmit}><Field label="Subject" required messages={{ valueMissing: "Enter a subject." }}><Input name="subject" /></Field><Button type="submit">Send</Button></Form>
|
|
14075
|
+
```
|
|
14076
|
+
|
|
14077
|
+
### Validation in your own code
|
|
14078
|
+
|
|
14079
|
+
```tsx
|
|
14080
|
+
import { Button, Field, Form, Input } from "@sableui/react"
|
|
14081
|
+
|
|
14082
|
+
<Form noValidate onSubmit={handleSubmit}><Field label="Subject" required error={errors.subject}><Input name="subject" /></Field><Button type="submit">Send</Button></Form>
|
|
14083
|
+
```
|
|
14084
|
+
|
|
13975
14085
|
### Validated on submit
|
|
13976
14086
|
|
|
13977
14087
|
```tsx
|
|
@@ -15320,7 +15430,7 @@ The scroll region a conversation lives in. It owns the scrolling and nothing els
|
|
|
15320
15430
|
|
|
15321
15431
|
| Name | Type | Default | Description |
|
|
15322
15432
|
| --- | --- | --- | --- |
|
|
15323
|
-
| ref | Ref<MessageScrollerHandle> | — | Imperative commands, not the DOM node: `scrollToBottom(opts)`, `scrollToTop(opts)`, `scrollToMessage(id, opts)` (returns false when the message is not rendered), `isFollowing()`, and `element` for the scrolling node itself. |
|
|
15433
|
+
| ref | Ref<MessageScrollerHandle> | — | Imperative commands, not the DOM node: `scrollToBottom(opts)`, `scrollToTop(opts)`, `scrollToMessage(id, opts)` (returns false when the message is not rendered), `isFollowing()`, and `element` for the scrolling node itself. `opts` is `ScrollCommandOptions`: `{ animate?: boolean }` (default true, and off whenever motion is), not the DOM's `behavior`. |
|
|
15324
15434
|
| autoFollow | boolean | true | Keep the newest message in view as content arrives, until the reader scrolls away from the bottom. Set false for a transcript that must never move under the reader. |
|
|
15325
15435
|
| followThreshold | number | 64 | Distance from the bottom, in px, that still counts as following. A band rather than a pixel, so a token arriving mid-frame does not flicker the state. |
|
|
15326
15436
|
| onFollowChange | (following: boolean) => void | — | Notified when the viewport pins to, or releases from, the newest message. |
|
|
@@ -16013,7 +16123,7 @@ Use PageHeader to establish a page's title, supporting description, and primary
|
|
|
16013
16123
|
| title | ReactNode | — | The page title (renders an <h1>). |
|
|
16014
16124
|
| description | ReactNode | — | Supporting copy beneath the title. |
|
|
16015
16125
|
| back | ReactNode | — | Leading element before the title — typically a back control on drill-in routes. Keep it a ghost icon Button (size="icon", variant="ghost", aria-label="Back") so it stays quieter than the title. A ghost back control bleeds by its inset at the start, so its glyph lines up with the content edge below (Button bleed="none" opts out). |
|
|
16016
|
-
| actions | ReactNode | — | Trailing actions aligned to the title row's end. They drop under the title when the
|
|
16126
|
+
| actions | ReactNode | — | Trailing actions aligned to the title row's end. They drop under the title when the title would have less than 20rem beside them (on a phone, nearly always), and the title wraps rather than truncating. A ghost or link Button at either end of the group bleeds by its own padding, so its label meets the content edge (the end on the title row, the start once wrapped); filled buttons keep their edge on the line. Direct children only. |
|
|
16017
16127
|
| titleAs | "h1" \| "h2" | "h1" | The title's heading element. Keep h1 — the route's one h1. Use h2 only when something above already owns the h1: a shell that renders the route title, or a page showing a PageHeader as an example. |
|
|
16018
16128
|
|
|
16019
16129
|
## Tokens used
|
|
@@ -18092,7 +18202,7 @@ import { Row, Separator } from "@sableui/react"
|
|
|
18092
18202
|
|
|
18093
18203
|
**Status:** stable
|
|
18094
18204
|
|
|
18095
|
-
A compact trigger that states the current settings, over a menu of rows that change them. Each row shows its own value and opens a submenu.
|
|
18205
|
+
A compact trigger that states the current settings, over a menu of rows that change them. Each row shows its own value and opens a submenu; with one setting, the menu opens straight to its options under the row's label.
|
|
18096
18206
|
|
|
18097
18207
|
**Keywords:** settings, model picker, model switcher, options menu, preferences, submenu, quality settings, ai chat
|
|
18098
18208
|
|
|
@@ -18432,6 +18542,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
|
|
|
18432
18542
|
|
|
18433
18543
|
| Name | Type | Description |
|
|
18434
18544
|
| --- | --- | --- |
|
|
18545
|
+
| SidebarHeader | div props | The top of the rail, at least 64px tall. It holds SidebarBrand and may take a second row under it, such as a search field or a notifications button: give that row basis-full and the header grows to hold it instead of clipping. |
|
|
18435
18546
|
| SidebarBrand | { mark: ReactNode; label: ReactNode; render?: useRender.RenderProp } + div props | The brand row, inside SidebarHeader, on the same grid as SidebarItem: mark in the icons' 24px slot, label on the labels' column, so a logo lines up with the nav below it without copying the rail's inset numbers. Collapsed, it shows the mark alone (named by a string label). render={<Link to="/" />} makes it the home link. |
|
|
18436
18547
|
| SidebarItem | { label: ReactNode; icon?: ReactNode; active?: boolean; badge?: ReactNode; render?: useRender.RenderProp } + button props | A single nav row. label is the row text (inline when expanded, in a hover tooltip when collapsed); icon is the leading icon node (the consumer swaps fill weight for the active state); active marks the current route (accent fill + aria-current="page"); badge is a trailing node (e.g. a count) pushed to the row end and hidden when collapsed; render composes another element, e.g. render={<Link to="/x" />}. |
|
|
18437
18548
|
|
|
@@ -19357,7 +19468,7 @@ import { StreamingText } from "@sableui/react"
|
|
|
19357
19468
|
|
|
19358
19469
|
**Status:** stable
|
|
19359
19470
|
|
|
19360
|
-
Binary on/off toggle for immediate-effect settings. Track fills with the primary tone when checked; the thumb slides on a spring-eased transform (mirrored in RTL).
|
|
19471
|
+
Binary on/off toggle for immediate-effect settings. Track fills with the primary tone when checked; the thumb slides on a spring-eased transform (mirrored in RTL). The thumb is white in both states, like every physical toggle, and turns dark only on a near-white track (a dark mode's light primary). Themes can set --theme-switch-thumb-on.
|
|
19361
19472
|
|
|
19362
19473
|
**Keywords:** switch, toggle setting, on off, form control
|
|
19363
19474
|
|
|
@@ -19387,7 +19498,7 @@ Use Switch for immediate on/off settings that take effect as a persistent prefer
|
|
|
19387
19498
|
|
|
19388
19499
|
## Tokens used
|
|
19389
19500
|
|
|
19390
|
-
`input`, `primary`, `
|
|
19501
|
+
`input`, `primary`, `ring`
|
|
19391
19502
|
|
|
19392
19503
|
## Motion
|
|
19393
19504
|
|
package/llms.txt
CHANGED
|
@@ -447,7 +447,7 @@ import { Button, Card, IconProvider, MotionProvider, SableThemeProvider, useSabl
|
|
|
447
447
|
- **SettingsMenu** — A compact trigger that states the current settings, over a menu of rows that change them. *Parts:* SettingsRow, MenuRadioGroup, MenuRadioItem. → [`llms/components/settings-menu.md`](./llms/components/settings-menu.md)
|
|
448
448
|
- **Sheet** — Edge-anchored drawer over Base UI Drawer — focus trap, scroll lock, Esc/outside dismissal, portal backdrop, plus swipe-to-dismiss on touch. *Parts:* SheetContent, SheetTrigger, SheetClose, SheetHeader, SheetTitle, SheetDescription, SheetList, SheetFooter. → [`llms/components/sheet.md`](./llms/components/sheet.md)
|
|
449
449
|
- **ShowMore** — Collapsible content region that clips children to a collapsed height, fades the cut edge, and reveals the rest with a toggle. → [`llms/components/show-more.md`](./llms/components/show-more.md)
|
|
450
|
-
- **Sidebar** — Desktop side-navigation rail (in-flow sticky, hidden below lg) with SidebarHeader / SidebarBrand / SidebarNav / SidebarItem / SidebarFooter parts. *Parts:* SidebarBrand, SidebarItem. → [`llms/components/sidebar.md`](./llms/components/sidebar.md)
|
|
450
|
+
- **Sidebar** — Desktop side-navigation rail (in-flow sticky, hidden below lg) with SidebarHeader / SidebarBrand / SidebarNav / SidebarItem / SidebarFooter parts. *Parts:* SidebarHeader, SidebarBrand, SidebarItem. → [`llms/components/sidebar.md`](./llms/components/sidebar.md)
|
|
451
451
|
- **SideNav** — A lightweight in-content vertical navigation list - the settings / docs sub-nav, distinct from the app-chrome Sidebar. *Parts:* SideNavItem, SideNavGroup. → [`llms/components/side-nav.md`](./llms/components/side-nav.md)
|
|
452
452
|
- **Skeleton** — Loading placeholder — a muted, shimmering block (rounded-md bg-muted motion-shimmer). → [`llms/components/skeleton.md`](./llms/components/skeleton.md)
|
|
453
453
|
- **Slider** — Slider over Base UI Slider — pointer + keyboard control, ARIA slider wiring, and range support via multiple thumbs. *Parts:* SliderRoot, SliderControl, SliderTrack, SliderIndicator, SliderThumb, SliderValue. → [`llms/components/slider.md`](./llms/components/slider.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sableui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Sable: themeable React components, layout patterns, and tools for coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -124,7 +124,9 @@
|
|
|
124
124
|
"THIRD_PARTY_NOTICES.md",
|
|
125
125
|
"third-party",
|
|
126
126
|
"scripts/agent-setup.mjs",
|
|
127
|
-
"scripts/design-presets.mjs"
|
|
127
|
+
"scripts/design-presets.mjs",
|
|
128
|
+
"scripts/patches.mjs",
|
|
129
|
+
"scripts/sources.mjs"
|
|
128
130
|
],
|
|
129
131
|
"publishConfig": {
|
|
130
132
|
"registry": "https://registry.npmjs.org",
|