@sableui/react 0.7.0 → 0.8.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.
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +115 -0
- package/README.md +2 -0
- package/bin/sable.mjs +34 -0
- package/dist/blocks/traffic-sources-block.js +1 -1
- package/dist/blocks/traffic-trend-block.js +3 -3
- 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/card/card.js +7 -4
- 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/pie-chart/pie-chart.js +3 -3
- package/dist/components/pie-chart/pie-chart.meta.js +3 -3
- package/dist/components/radial-chart/radial-chart.js +3 -3
- package/dist/components/radial-chart/radial-chart.meta.js +2 -2
- package/dist/components/settings-menu/settings-menu.js +10 -1
- package/dist/components/settings-menu/settings-menu.meta.js +1 -1
- package/dist/components/side-nav/side-nav.variants.js +3 -2
- 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/table/table.js +1 -1
- 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 +88 -15
- package/dist/index.js +1 -1
- package/dist/layout/index.d.ts +5 -2
- package/dist/lib/chart/format.js +5 -0
- package/dist/lib/component-guidance.js +7 -2
- package/dist/lib/heading-row.js +3 -2
- package/dist/lib/trim-label.js +17 -4
- package/dist/presets/index.d.ts +7 -1
- package/dist/presets/index.js +10 -0
- package/dist/styles.css +27 -2
- package/llms/blocks/traffic-sources-block.md +1 -1
- package/llms/blocks/traffic-trend-block.md +5 -3
- 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/pie-chart.md +2 -2
- package/llms/components/radial-chart.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/layout/region.md +16 -3
- package/llms/tasks/building-a-page.md +34 -3
- package/llms/tasks/creating-a-preset.md +4 -1
- package/llms/tasks/patching-a-component.md +48 -0
- package/llms/tasks/reviewing-a-design.md +9 -0
- package/llms/tasks/using-presets.md +19 -0
- package/llms-full.txt +207 -27
- package/llms.txt +1 -1
- package/package.json +4 -2
- package/registry.json +132 -30
- package/scripts/design-memory.mjs +7 -2
- package/scripts/design-presets.mjs +15 -4
- package/scripts/lint-rules.mjs +32 -1
- package/scripts/patches.mjs +219 -0
- package/scripts/sources.mjs +247 -0
- package/skills/sable/SKILL.md +16 -4
|
@@ -44,7 +44,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
|
|
|
44
44
|
| 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. |
|
|
45
45
|
| 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. |
|
|
46
46
|
| CommandMenuGroup | { label: string; children } | A labelled group of command rows. |
|
|
47
|
-
| 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. |
|
|
47
|
+
| 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. |
|
|
48
48
|
|
|
49
49
|
## Props
|
|
50
50
|
|
|
@@ -54,7 +54,7 @@ Use CommandMenu for global or contextual keyboard-driven actions: navigation, cr
|
|
|
54
54
|
| 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. |
|
|
55
55
|
| open | boolean | — | Controlled open state of the palette. |
|
|
56
56
|
| onOpenChange | (open: boolean) => void | — | Called on open/close (select, Esc, outside click). |
|
|
57
|
-
| 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. |
|
|
57
|
+
| 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. |
|
|
58
58
|
| query | string | — | Controlled query. Leave undefined to let the palette own it; either way it resets on close (a controlled caller gets onQueryChange('')). |
|
|
59
59
|
| onQueryChange | (query: string) => void | — | Called on every query change. Required to control query, optional otherwise. |
|
|
60
60
|
| 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. |
|
|
@@ -36,7 +36,11 @@ Use DataTable when a data array and column definition should render a sortable,
|
|
|
36
36
|
| data | Row[] | — | The rows to render. |
|
|
37
37
|
| rowKey | (row, index) => string \| number | index | Stable React key per row. |
|
|
38
38
|
| onRowClick | (row) => void | — | Row click handler — makes rows pointer-interactive. |
|
|
39
|
-
| 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. |
|
|
39
|
+
| 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. |
|
|
40
|
+
| 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. |
|
|
41
|
+
| 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. |
|
|
42
|
+
| 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. |
|
|
43
|
+
| 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"). |
|
|
40
44
|
| defaultSort | { key: string; direction: "asc" \| "desc" } | — | Initial sort column + direction. |
|
|
41
45
|
| empty | ReactNode | "No results." | Rendered spanning all columns when data is empty. Pass an EmptyState for the full treatment (icon, title, next step). |
|
|
42
46
|
| caption | ReactNode | — | Bottom table caption. |
|
|
@@ -58,6 +62,34 @@ Use DataTable when a data array and column definition should render a sortable,
|
|
|
58
62
|
|
|
59
63
|
## Examples
|
|
60
64
|
|
|
65
|
+
### Checkbox selection with a bulk-action bar
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Button, DataTable, Stack, Toolbar } from "@sableui/react"
|
|
69
|
+
|
|
70
|
+
const [selected, setSelected] = useState<Set<string | number>>(new Set())
|
|
71
|
+
|
|
72
|
+
<Stack gap={2}>
|
|
73
|
+
{/* Mounted from the start, so a screen reader hears each change. */}
|
|
74
|
+
<span role="status" className="sr-only">{selected.size ? `${selected.size} of ${tickets.length} selected` : ""}</span>
|
|
75
|
+
{selected.size ? (
|
|
76
|
+
<Toolbar aria-label="Bulk actions">
|
|
77
|
+
<span className="text-label-md">{selected.size} of {tickets.length} selected</span>
|
|
78
|
+
<Button size="sm" variant="secondary" onClick={() => resolve(selected)}>Resolve</Button>
|
|
79
|
+
</Toolbar>
|
|
80
|
+
) : null}
|
|
81
|
+
<DataTable
|
|
82
|
+
data={tickets}
|
|
83
|
+
rowKey={(t) => t.id}
|
|
84
|
+
columns={columns}
|
|
85
|
+
selectedKeys={selected}
|
|
86
|
+
onSelectedKeysChange={setSelected}
|
|
87
|
+
rowSelectionLabel={(t) => `Select ${t.subject}`}
|
|
88
|
+
selectAllLabel={(n) => `Select all ${n} tickets shown`}
|
|
89
|
+
/>
|
|
90
|
+
</Stack>
|
|
91
|
+
```
|
|
92
|
+
|
|
61
93
|
### Sortable columns
|
|
62
94
|
|
|
63
95
|
```tsx
|
package/llms/components/form.md
CHANGED
|
@@ -32,16 +32,17 @@ Use Form and Field as the default wrapper for Sable form controls. Field owns th
|
|
|
32
32
|
|
|
33
33
|
| Name | Type | Description |
|
|
34
34
|
| --- | --- | --- |
|
|
35
|
-
| 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
|
|
35
|
+
| 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. |
|
|
36
36
|
|
|
37
37
|
## Props
|
|
38
38
|
|
|
39
39
|
| Name | Type | Default | Description |
|
|
40
40
|
| --- | --- | --- | --- |
|
|
41
|
-
| 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
|
|
41
|
+
| 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. |
|
|
42
42
|
| 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. |
|
|
43
43
|
| 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. |
|
|
44
44
|
| onFormSubmit | (values: Record<string, unknown>, details) => void | — | Called with the named Field values after validation passes; prevents the native submit. |
|
|
45
|
+
| 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. |
|
|
45
46
|
| actionsRef | RefObject<{ validate(name?: string): void }> | — | Imperative handle to validate every field, or one by name. |
|
|
46
47
|
|
|
47
48
|
## Tokens used
|
|
@@ -78,6 +79,22 @@ import { Field, Input } from "@sableui/react"
|
|
|
78
79
|
<Field label="Password" error="Must be at least 8 characters."><Input type="password" /></Field>
|
|
79
80
|
```
|
|
80
81
|
|
|
82
|
+
### Your own wording for a missing value
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { Button, Field, Form, Input } from "@sableui/react"
|
|
86
|
+
|
|
87
|
+
<Form onSubmit={handleSubmit}><Field label="Subject" required messages={{ valueMissing: "Enter a subject." }}><Input name="subject" /></Field><Button type="submit">Send</Button></Form>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Validation in your own code
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { Button, Field, Form, Input } from "@sableui/react"
|
|
94
|
+
|
|
95
|
+
<Form noValidate onSubmit={handleSubmit}><Field label="Subject" required error={errors.subject}><Input name="subject" /></Field><Button type="submit">Send</Button></Form>
|
|
96
|
+
```
|
|
97
|
+
|
|
81
98
|
### Validated on submit
|
|
82
99
|
|
|
83
100
|
```tsx
|
|
@@ -35,7 +35,7 @@ The scroll region a conversation lives in. It owns the scrolling and nothing els
|
|
|
35
35
|
|
|
36
36
|
| Name | Type | Default | Description |
|
|
37
37
|
| --- | --- | --- | --- |
|
|
38
|
-
| 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. |
|
|
38
|
+
| 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`. |
|
|
39
39
|
| 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. |
|
|
40
40
|
| 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. |
|
|
41
41
|
| onFollowChange | (following: boolean) => void | — | Notified when the viewport pins to, or releases from, the newest message. |
|
|
@@ -25,7 +25,7 @@ Use PageHeader to establish a page's title, supporting description, and primary
|
|
|
25
25
|
| title | ReactNode | — | The page title (renders an <h1>). |
|
|
26
26
|
| description | ReactNode | — | Supporting copy beneath the title. |
|
|
27
27
|
| 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). |
|
|
28
|
-
| actions | ReactNode | — | Trailing actions aligned to the title row's end. They drop under the title when the
|
|
28
|
+
| 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. |
|
|
29
29
|
| 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. |
|
|
30
30
|
|
|
31
31
|
## Tokens used
|
|
@@ -28,7 +28,7 @@ Use PieChart for a share of one total across two to six categories — traffic s
|
|
|
28
28
|
| categories | Record<string, { label?: string; tone?: ChartTone; color?: string; icon?: string }> | — | Labels, tones and icons per slice, keyed by the slice's name. Unlisted slices take the tones in order. |
|
|
29
29
|
| className | string | "h-64" | Sizes the chart. Set a height. |
|
|
30
30
|
| donut | boolean \| number | false | Cut out the middle: `true` for a ring at 60% of the radius, or a ratio (0–0.9). |
|
|
31
|
-
| centerLabel | { value: string; label?: string } | — | A value and caption in the hole of a donut. |
|
|
31
|
+
| centerLabel | { value: string \| number; label?: string } | — | A value and caption in the hole of a donut. The value holds the centre and the caption hangs below it. A number is formatted for you: in full up to 9,999, compact from 10,000 (10.2K, 102.1K, 1.2M); pass a string for units. |
|
|
32
32
|
| separator | boolean | true | A hairline in the surface colour between slices. |
|
|
33
33
|
| labels | "none" \| "outside" \| "inside" | "none" | Label each slice outside the pie or inside it. |
|
|
34
34
|
| labelContent | "value" \| "percent" \| "name" | "value" | What a slice label says. |
|
|
@@ -82,7 +82,7 @@ const sources = [
|
|
|
82
82
|
index="source"
|
|
83
83
|
valueKey="visitors"
|
|
84
84
|
donut
|
|
85
|
-
centerLabel={{ value:
|
|
85
|
+
centerLabel={{ value: 752, label: "Visitors" }}
|
|
86
86
|
/>
|
|
87
87
|
```
|
|
88
88
|
|
|
@@ -32,7 +32,7 @@ Use RadialChart for progress toward goals (several rings), a single score or usa
|
|
|
32
32
|
| stacked | boolean | false | Split ONE ring between the rows instead of a ring each. |
|
|
33
33
|
| track | boolean | true | Draw each ring's full track behind it. |
|
|
34
34
|
| labels | boolean | false | Name each ring at its start. |
|
|
35
|
-
| centerLabel | { value: string; label?: string } | — | A value and caption in the middle. |
|
|
35
|
+
| centerLabel | { value: string \| number; label?: string } | — | A value and caption in the middle. On a full ring the value holds the centre and the caption hangs below it. A number is formatted for you: in full up to 9,999, compact from 10,000 (10.2K); pass a string for units ("7.2 GB", "72%"). |
|
|
36
36
|
| legend | boolean | true unless rings are labelled | Show the legend. |
|
|
37
37
|
| tooltip | boolean \| { indicator?: "dot" \| "line" \| "dashed" \| "none"; hideLabel?: boolean; labelFormatter?: (label, datum) => ReactNode; showTotal?: boolean } | true | Show a tooltip on hover and keyboard focus, or configure it: the row marker, hiding the heading, rewriting it, adding a Total row. |
|
|
38
38
|
| valueFormatter | (value: number) => string | — | Format values everywhere (axis, labels, tooltip, table) — units like "$12,480". Default: compact on the axis, full elsewhere. |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
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.
|
|
5
|
+
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.
|
|
6
6
|
|
|
7
7
|
**Keywords:** settings, model picker, model switcher, options menu, preferences, submenu, quality settings, ai chat
|
|
8
8
|
|
|
@@ -28,6 +28,7 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
|
|
|
28
28
|
|
|
29
29
|
| Name | Type | Description |
|
|
30
30
|
| --- | --- | --- |
|
|
31
|
+
| 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. |
|
|
31
32
|
| 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. |
|
|
32
33
|
| 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" />}. |
|
|
33
34
|
|
|
@@ -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
|
|
package/llms/layout/region.md
CHANGED
|
@@ -12,9 +12,14 @@ Use Region as the page-width primitive that applies fluid page inset and an opti
|
|
|
12
12
|
|
|
13
13
|
### Best practices
|
|
14
14
|
|
|
15
|
-
- Do: Choose the max value by
|
|
15
|
+
- Do: Choose the max value by what the person does on the page: read (prose), fill in a form (form), scroll one stream (feed), review one record or settings (content), scan and compare rows or cards (wide).
|
|
16
|
+
- Do: Omit max (fluid) for work across panes, a board, an editor or a map, where a flexible part uses the extra width; cap text inside it with --measure-prose.
|
|
17
|
+
- Do: Give sibling routes in one nav the same width so the header does not jump between them.
|
|
18
|
+
- Do: Review a capped or fluid route at 1920px and 2560px: a cap is a ceiling, so below the cap plus app chrome every choice looks the same.
|
|
16
19
|
- Do: Use Region near the route or section boundary so inner layout stays intentional.
|
|
17
20
|
- Don't: Do not use Region as a global wrapper that forces every page into the same max width.
|
|
21
|
+
- Don't: Do not go fluid when the extra width only widens the gaps between fixed columns; cap it.
|
|
22
|
+
- Don't: Do not read full as edge to edge: it is the largest cap and keeps the page inset. Use bleed for edge to edge.
|
|
18
23
|
|
|
19
24
|
## Props
|
|
20
25
|
|
|
@@ -22,7 +27,7 @@ Use Region as the page-width primitive that applies fluid page inset and an opti
|
|
|
22
27
|
| --- | --- | --- | --- |
|
|
23
28
|
| as | ElementType | "div" | Semantic element to render (e.g. "main", "section", "nav"). |
|
|
24
29
|
| inset | boolean | true | Apply the fluid horizontal page inset (--page-inset). |
|
|
25
|
-
| max | "prose" \| "form" \| "feed" \| "content" \| "wide" \| "full" \| CSS length | — | Opt-in centered max-width cap, purpose-named: prose 65ch (long-form reading) · form 480px (auth, narrow single-column forms) · feed 640px (single-column timeline) · content 896px (settings/admin panes) · wide 1280px (dashboards, tables, card grids) · full 1536px (
|
|
30
|
+
| max | "prose" \| "form" \| "feed" \| "content" \| "wide" \| "full" \| CSS length | — | Opt-in centered max-width cap, purpose-named: prose 65ch (long-form reading) · form 480px (auth, narrow single-column forms) · feed 640px (single-column timeline) · content 896px (settings/admin panes) · wide 1280px (dashboards, tables, card grids) · full 1536px (the largest cap; keeps the inset, so not edge to edge — that is bleed). Omit for fluid: work views whose panes, board or canvas use the extra width. A cap is a ceiling: it applies only once the viewport minus app chrome exceeds it, so judge it at 1920px and up. |
|
|
26
31
|
| bleed | boolean | false | Full-bleed: edge to edge with no inset. Overrides inset. |
|
|
27
32
|
| safeArea | boolean \| ("top"\|"bottom"\|"left"\|"right")[] | false | env(safe-area-inset-*) padding for fixed or native-shell surfaces. true pads top+bottom; pass an edge array to target edges. |
|
|
28
33
|
|
|
@@ -40,10 +45,18 @@ import { PageHeader, Region } from "@sableui/react"
|
|
|
40
45
|
<Region as="main" max="content"><PageHeader title="Settings" /></Region>
|
|
41
46
|
```
|
|
42
47
|
|
|
43
|
-
###
|
|
48
|
+
### Dashboard region (capped at wide)
|
|
44
49
|
|
|
45
50
|
```tsx
|
|
46
51
|
import { Card, CardHeader, CardTitle, Grid, Region } from "@sableui/react"
|
|
47
52
|
|
|
48
53
|
<Region max="wide"><Grid minItemWidth="16rem"><Card><CardHeader><CardTitle>Revenue</CardTitle></CardHeader></Card><Card><CardHeader><CardTitle>Collects</CardTitle></CardHeader></Card></Grid></Region>
|
|
49
54
|
```
|
|
55
|
+
|
|
56
|
+
### Fluid workspace: the queue's table takes the extra width
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { Card, CardContent, Col, Columns, DataTable, Region, SideNav, SideNavItem } from "@sableui/react"
|
|
60
|
+
|
|
61
|
+
<Region as="main"><Columns count={{ base: 1, lg: 12 }}><Col span={{ base: 1, lg: 3 }}><SideNav aria-label="Views"><SideNavItem label="Open" active /><SideNavItem label="Pending" /></SideNav></Col><Col span={{ base: 1, lg: 9 }}><Card><CardContent flush><DataTable columns={columns} data={tickets} /></CardContent></Card></Col></Columns></Region>
|
|
62
|
+
```
|
|
@@ -30,9 +30,9 @@ designs. Record the reason for the chosen composition.
|
|
|
30
30
|
</AppShell>
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
- **Never a global `max-width`.** Width is
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
- **Never a global `max-width`.** Width is chosen per route by what the person
|
|
34
|
+
does there (below). A form region is narrow because a 700px text input is
|
|
35
|
+
harder to use, not more generous.
|
|
36
36
|
- **Measure ≠ width.** Cap prose line length in `ch` (`--measure-prose`)
|
|
37
37
|
independently of the region's layout width.
|
|
38
38
|
- **The 12-column grid lives *inside* a region**, for genuinely multi-column
|
|
@@ -40,6 +40,37 @@ designs. Record the reason for the chosen composition.
|
|
|
40
40
|
- **Chrome is off the grid.** `Sidebar` and rails are fixed-width, outside the
|
|
41
41
|
content grid.
|
|
42
42
|
|
|
43
|
+
## Choosing a width
|
|
44
|
+
|
|
45
|
+
Pick each route's `max` by what the person mainly does on it:
|
|
46
|
+
|
|
47
|
+
| They mainly… | `max` | Why |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| read long text | `prose` on the text, inside `content` | line length sets readability: cap the text, not the page |
|
|
50
|
+
| fill in a form | `form` | narrow fields are faster to scan and fill |
|
|
51
|
+
| scroll one stream | `feed` | one column, one eye path |
|
|
52
|
+
| review one record, or settings | `content` | labels stay close to their values |
|
|
53
|
+
| scan and compare rows, cards or charts | `wide` | room for columns without stretching the gaps between them |
|
|
54
|
+
| work across panes, a board, an editor or a map | omit it (fluid), or `full` | the extra width goes to the parts that use it |
|
|
55
|
+
|
|
56
|
+
- **Go fluid only when a flexible part uses the space**: a table's main column,
|
|
57
|
+
a canvas, a board, a reading pane. If extra width only widens the gaps between
|
|
58
|
+
fixed columns (a date drifting away from its subject), cap it.
|
|
59
|
+
- **In a fluid page, cap text by line length** (`--measure-prose`, or
|
|
60
|
+
`Center max="prose"`), not by page width.
|
|
61
|
+
- **Sibling routes in one nav share a width**, so the header doesn't jump as
|
|
62
|
+
people move between them. A detail view may differ.
|
|
63
|
+
- **`full` is the largest cap, not edge to edge.** It keeps the page inset;
|
|
64
|
+
`bleed` is edge to edge.
|
|
65
|
+
- **A cap is a ceiling.** It applies only once the viewport minus app chrome is
|
|
66
|
+
wider than it: beside a 256px sidebar, `wide` (1280px) starts at about 1536px,
|
|
67
|
+
so at 1440px `wide`, `full` and fluid look the same. Judge widths at 1920px and
|
|
68
|
+
2560px, and record each route's width and reason in the brief.
|
|
69
|
+
- **A product's widths belong in its theme.** A preset may set `page-inset` and
|
|
70
|
+
`region-*` (`form`, `feed`, `content`, `wide`, `full`) in `tokens.shared`
|
|
71
|
+
(a copy in `light` or `dark` wins over it); keep the role names and change
|
|
72
|
+
the values.
|
|
73
|
+
|
|
43
74
|
## Typography: pick the tier by ROLE, not by size
|
|
44
75
|
|
|
45
76
|
| tier | for | not |
|
|
@@ -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.
|
|
16
|
+
independently. Load fonts through the consuming project's font setup. Page widths
|
|
17
|
+
are optional: set `page-inset` and the `region-*` caps (`form`, `feed`, `content`,
|
|
18
|
+
`wide`, `full`) in `tokens.shared` only when the direction calls for a different
|
|
19
|
+
page proportion, and keep each cap's role.
|
|
17
20
|
|
|
18
21
|
```bash
|
|
19
22
|
pnpm exec sable preset --template
|
|
@@ -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,15 @@ 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.
|
|
18
|
+
Also capture a route that sets a width cap, or goes fluid, at 1920px: a cap applies
|
|
19
|
+
only once the viewport minus app chrome exceeds it, so at 1024–1440px `wide`,
|
|
20
|
+
`full` and fluid usually look the same and the width decision never shows. Add
|
|
21
|
+
2560px for a fluid route.
|
|
13
22
|
Use a recording or an observed interaction report for behavior that a still cannot
|
|
14
23
|
show. Include reduced motion and every color mode the surface promises to support.
|
|
15
24
|
|
|
@@ -76,6 +76,25 @@ Use a unique preset ID so it cannot override a bundled theme or another record.
|
|
|
76
76
|
Record any intentional adaptation and run the validation and rendered review in
|
|
77
77
|
[building-with-sable.md](./building-with-sable.md).
|
|
78
78
|
|
|
79
|
+
## Apply to an existing app
|
|
80
|
+
|
|
81
|
+
A preset pasted into an agent for a running app (Canvas's “Copy for agent”) is a
|
|
82
|
+
restyle: the job ends with the app built and rendered in the preset, not with a
|
|
83
|
+
saved file. Replace, don't add beside:
|
|
84
|
+
|
|
85
|
+
1. Save it to `.sable/presets/<id>.json`, overwriting an earlier revision of the
|
|
86
|
+
same ID, and run `sable preset --check`.
|
|
87
|
+
2. Regenerate its CSS with `sable preset --css` and import it after Sable's
|
|
88
|
+
stylesheet. Remove the stylesheet of a theme it replaces when nothing else uses it.
|
|
89
|
+
3. Load the families its `fonts` stacks name through the app's existing font
|
|
90
|
+
setup, and drop the old theme's font loading when no other code uses it.
|
|
91
|
+
An unloaded family falls back to a system face without any error.
|
|
92
|
+
4. Point SableThemeProvider's `defaultThemeId` at the preset ID and
|
|
93
|
+
`defaultPreference` at its `defaultMode`, unless the user asked for another
|
|
94
|
+
mode. A bundled theme left as the default keeps the app on the old look.
|
|
95
|
+
5. Run the app's build and type check, then `sable check`, and review the result
|
|
96
|
+
in both modes.
|
|
97
|
+
|
|
79
98
|
## Upgrade the package and skill
|
|
80
99
|
|
|
81
100
|
```bash
|