@sableui/react 0.17.0 → 0.18.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 +10 -3
- package/CHANGELOG.md +106 -0
- package/README.md +11 -4
- package/bin/sable.mjs +160 -18
- package/dist/activity-DT8VK9fx.d.ts +25 -0
- package/dist/adapters/github.d.ts +41 -0
- package/dist/adapters/github.js +24 -0
- package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
- package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
- package/dist/components/activity-heatmap/index.js +9 -0
- package/dist/components/code-block/code-block.js +5 -3
- package/dist/components/code-block/code-block.meta.js +6 -0
- package/dist/components/dialog/dialog.js +25 -5
- package/dist/components/dialog/dialog.meta.js +26 -5
- package/dist/components/media-image/media-image.js +3 -1
- package/dist/components/media-image/media-image.meta.js +15 -1
- package/dist/components/media-tile/media-tile.js +13 -1
- package/dist/components/media-tile/media-tile.meta.js +10 -0
- package/dist/components/page-header/page-header.js +9 -4
- package/dist/components/page-header/page-header.meta.js +26 -1
- package/dist/components/pill/pill.meta.js +15 -6
- package/dist/components/pill/pill.variants.js +1 -0
- package/dist/components/search-input/search-input.js +30 -5
- package/dist/components/search-input/search-input.meta.js +29 -4
- package/dist/components/section-header/section-header.js +27 -17
- package/dist/components/section-header/section-header.meta.js +27 -5
- package/dist/components/sheet/sheet.js +15 -3
- package/dist/components/sheet/sheet.meta.js +2 -2
- package/dist/file-hashes.json +46 -36
- package/dist/icon-sets/reicon.js +23 -1
- package/dist/index.d.ts +270 -27
- package/dist/index.js +8 -1
- package/dist/layout/AspectBox.js +2 -5
- package/dist/layout/Bleed.js +10 -6
- package/dist/layout/Center.js +11 -3
- package/dist/layout/Columns.js +6 -3
- package/dist/layout/Grid.js +14 -6
- package/dist/layout/GridOverlay.js +7 -7
- package/dist/layout/Region.js +18 -3
- package/dist/layout/Row.js +27 -20
- package/dist/layout/Spacer.js +13 -2
- package/dist/layout/SplitPane.js +1 -1
- package/dist/layout/Stack.js +25 -18
- package/dist/layout/index.d.ts +57 -6
- package/dist/lib/chart/activity.js +32 -0
- package/dist/lib/chart/calendar.js +67 -0
- package/dist/lib/chart/chart-tooltip.js +2 -2
- package/dist/lib/chart/heat.js +55 -0
- package/dist/lib/focus-return.js +53 -0
- package/dist/lib/taxonomy.js +1 -0
- package/dist/styles.css +122 -114
- package/llms/components/activity-heatmap.md +133 -0
- package/llms/components/code-block.md +1 -0
- package/llms/components/dialog.md +34 -3
- package/llms/components/media-image.md +12 -0
- package/llms/components/media-tile.md +21 -0
- package/llms/components/page-header.md +16 -1
- package/llms/components/pill.md +24 -5
- package/llms/components/search-input.md +37 -4
- package/llms/components/section-header.md +28 -4
- package/llms/components/sheet.md +2 -2
- package/llms/layout/aspect-box.md +1 -1
- package/llms/layout/bleed.md +1 -1
- package/llms/layout/center.md +1 -1
- package/llms/layout/columns.md +1 -1
- package/llms/layout/grid.md +1 -1
- package/llms/layout/region.md +1 -1
- package/llms/layout/row.md +9 -1
- package/llms/layout/spacer.md +1 -1
- package/llms/layout/split-pane.md +1 -1
- package/llms/layout/stack.md +1 -1
- package/llms/tasks/auditing-an-app.md +31 -0
- package/llms/tasks/getting-started.md +10 -3
- package/llms/tasks/using-presets.md +1 -2
- package/llms-full.txt +372 -35
- package/llms.txt +3 -2
- package/package.json +6 -1
- package/registry.json +532 -36
- package/scripts/adoption.mjs +239 -0
- package/scripts/agent-setup.mjs +110 -18
- package/scripts/grade.mjs +17 -0
- package/scripts/wiring.mjs +2 -1
- package/skills/entry/SKILL.md +1 -1
- package/skills/sable/SKILL.md +33 -4
- package/skills/sable/agents/openai.yaml +1 -1
- package/targets/ios.json +9 -0
package/llms/components/pill.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
A chip someone clicks: a filter, a tag choice, an option in a row. Always a button with aria-pressed. appearance="solid" (filters; selected lifts onto the primary surface) or "outline" (quiet chips that annotate content, like reactions); size md or sm. A static label is a Badge.
|
|
5
|
+
A chip someone clicks: a filter, a tag choice, an option in a row. Always a button with aria-pressed. appearance="solid" (filters; selected lifts onto the primary surface) or "outline" (quiet chips that annotate content, like reactions); size lg, md or sm. A static label is a Badge.
|
|
6
6
|
|
|
7
7
|
**Keywords:** pill, chip, filter, tag, toggle chip, category
|
|
8
8
|
|
|
@@ -14,9 +14,10 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
|
|
|
14
14
|
|
|
15
15
|
- Do: Use short text that names the filter or choice.
|
|
16
16
|
- Do: Reflect the current choice with selected, so the active filter reads at a glance.
|
|
17
|
+
- Do: In a toolbar where filter chips share a row with a search field ("All 8 · Products 5 · Open source 2 … [Find a project]"), pair Pill size="lg" with SearchInput size="sm", and Button size="sm" for any action beside them. All three stand on --control-sm (32px, 28 compact), the rung the spacing doctrine gives toolbar controls, so the row is one height in either density. Pill md (28px) has no text field of its height in the page register, so md beside a field always reads as a mismatch.
|
|
17
18
|
- Don't: Do not use Pill as a static label. A pressable-looking chip that does nothing is the most common cause of dead clicks in a filter row — that is a Badge. → use **Badge**
|
|
18
19
|
- Don't: Do not use Pill for primary actions or important system status.
|
|
19
|
-
- Don't: Do not restyle a Pill with border or background classes to get a quieter chip. That is appearance="outline" (and size="sm" for a small one); an override is a look that exists nowhere else in the system.
|
|
20
|
+
- Don't: Do not restyle a Pill with border or background classes to get a quieter chip. That is appearance="outline" (and size="sm" for a small one); an override is a look that exists nowhere else in the system. Likewise never set h-* on a Pill to match a neighbour: pick the size that shares its rung.
|
|
20
21
|
|
|
21
22
|
## Props
|
|
22
23
|
|
|
@@ -24,14 +25,14 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
|
|
|
24
25
|
| --- | --- | --- | --- |
|
|
25
26
|
| selected | boolean | false | Marks the pill as chosen. Solid lifts onto the primary surface; outline takes a primary tint and soft primary edge. Sets aria-pressed. |
|
|
26
27
|
| appearance | "solid" \| "outline" | "solid" | solid for a filter row; outline for quiet chips that annotate other content (a reaction under a message, a tag on a card), where a filled chip would compete with what it annotates. |
|
|
27
|
-
| size | "md" \| "sm" | "md" |
|
|
28
|
+
| size | "lg" \| "md" \| "sm" | "md" | lg sits on --control-sm (32px, 28 compact), the toolbar rung it shares with SearchInput sm and Button sm, for a filter row beside a search field. md sits on --control-xs (28px, 24 compact), the chip in a row of its own or among content. sm is a fixed 24px (the target floor, which density does not shrink), for chips under or inside other content. |
|
|
28
29
|
| tone | "neutral" \| "red" \| "orange" \| "yellow" \| "green" \| "teal" \| "blue" \| "purple" \| "pink" | "neutral" | neutral is the chip as described. A palette family colours it as a category, like the choices in a tag picker: a wash at rest, the solid family colour when selected. Painted from the theme's generated palette. |
|
|
29
30
|
| icon | ReactNode | — | Optional leading glyph (e.g. <Icon name="lock" />), rendered before the children with the base gap. |
|
|
30
31
|
|
|
31
32
|
## Variants
|
|
32
33
|
|
|
33
34
|
- **appearance**: solid, outline
|
|
34
|
-
- **size**: md, sm
|
|
35
|
+
- **size**: lg, md, sm
|
|
35
36
|
- **selected**: true, false
|
|
36
37
|
- **tone**: neutral, red, orange, yellow, green, teal, blue, purple, pink
|
|
37
38
|
|
|
@@ -54,7 +55,7 @@ Use Pill for a row of filters or choices a person clicks to narrow or toggle wha
|
|
|
54
55
|
|
|
55
56
|
## Related
|
|
56
57
|
|
|
57
|
-
- Components: Badge, ToggleGroup
|
|
58
|
+
- Components: Badge, ToggleGroup, SearchInput
|
|
58
59
|
|
|
59
60
|
## Examples
|
|
60
61
|
|
|
@@ -74,6 +75,24 @@ import { Pill, Row } from "@sableui/react"
|
|
|
74
75
|
<Row gap={1}><Pill selected onClick={() => {}}>All</Pill><Pill onClick={() => {}}>Projects</Pill><Pill onClick={() => {}}>Reports</Pill></Row>
|
|
75
76
|
```
|
|
76
77
|
|
|
78
|
+
### Filters with search
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { Pill, Row, SearchInput } from "@sableui/react"
|
|
82
|
+
|
|
83
|
+
const [filter, setFilter] = useState("all")
|
|
84
|
+
const [q, setQ] = useState("")
|
|
85
|
+
|
|
86
|
+
<Row gap={2} align="center" justify="between" wrap>
|
|
87
|
+
<Row gap={1}>
|
|
88
|
+
<Pill size="lg" selected={filter === "all"} onClick={() => setFilter("all")}>All 8</Pill>
|
|
89
|
+
<Pill size="lg" selected={filter === "products"} onClick={() => setFilter("products")}>Products 5</Pill>
|
|
90
|
+
<Pill size="lg" selected={filter === "oss"} onClick={() => setFilter("oss")}>Open source 2</Pill>
|
|
91
|
+
</Row>
|
|
92
|
+
<SearchInput size="sm" value={q} onValueChange={setQ} placeholder="Find a project…" aria-label="Find a project" className="max-w-64" />
|
|
93
|
+
</Row>
|
|
94
|
+
```
|
|
95
|
+
|
|
77
96
|
### Outline, small (reactions)
|
|
78
97
|
|
|
79
98
|
```tsx
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), the theme's field surface, 2px focus ring; the clear button appears only when there is a value.
|
|
5
|
+
Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), the theme's field surface, 2px focus ring; the clear button appears only when there is a value. size md (form), sm (toolbar, matching Pill lg and Button sm) or xs (inset tool-panel field).
|
|
6
6
|
|
|
7
7
|
**Keywords:** search, input, filter, query
|
|
8
8
|
|
|
@@ -16,6 +16,8 @@ Use SearchInput for text queries that filter or search a known content set.
|
|
|
16
16
|
- Do: Provide a visible clear affordance when query state persists.
|
|
17
17
|
- Do: When a button stands in for search (a header trigger that opens CommandMenu with ⌘K), use CommandMenuTrigger. It wears this field's shell from the same source, so the two cannot drift; a hand-styled button already did once, into a borderless chip that matched neither the field nor a button.
|
|
18
18
|
- Do: Light the query in the results it filters with `useMatchHighlight(query)` — a callback ref for the results container, with `selector` to keep it to titles. It paints on the `highlight` pair without wrapping matches in `<mark>`, the same paint Combobox and CommandMenu use for their rows.
|
|
19
|
+
- Do: In a toolbar beside filter chips, use size="sm" with Pill size="lg" (and Button size="sm" for actions). All three stand on --control-sm (32px, 28 compact), the rung the spacing doctrine gives toolbar controls, so the row is one height in either density. Keep md for a search field in a form or a page header beside md buttons, and xs for a filter inside a workspace panel, where it matches Input xs's inset surface.
|
|
20
|
+
- Don't: Do not match a neighbour's height with h-* on className or inputClassName. The magnifier, padding and clear button would stay sized for the old rung; pick the size that shares the neighbour's rung.
|
|
19
21
|
- Don't: Do not use SearchInput as a command palette; use CommandMenu for global commands, opened from the header by CommandMenuTrigger. A SearchInput that opens a dialog on focus is a text box you cannot type in.
|
|
20
22
|
- Don't: Do not search on every keystroke against an expensive backend without debouncing. The user sees results flicker and reorder under them, and you send a request per character.
|
|
21
23
|
|
|
@@ -23,6 +25,7 @@ Use SearchInput for text queries that filter or search a known content set.
|
|
|
23
25
|
|
|
24
26
|
| Name | Type | Default | Description |
|
|
25
27
|
| --- | --- | --- | --- |
|
|
28
|
+
| size | "md" \| "sm" \| "xs" | "md" | The rung of the control ladder, named as on Input, Select and Combobox. md is the form field (--control-md, 40px / 32 compact). sm is the toolbar field (--control-sm, 32px / 28 compact), the rung Pill lg and Button sm share, so filters + search + actions in one row are one height. xs is the tool-panel field (--control-xs, 28px / 24 compact) on Input xs's inset panel-field surface. The magnifier, inline padding and clear button scale with the rung and stay vertically centred. |
|
|
26
29
|
| value | string | — | Controlled value of the search field. |
|
|
27
30
|
| defaultValue | string | — | Uncontrolled initial value. The field tracks its own text, so the clear button still appears and empties it. |
|
|
28
31
|
| className | string | — | Sizes and places the whole field — magnifier, input and clear button together (e.g. max-w-80). A width cap here keeps the clear button inside the field. |
|
|
@@ -31,11 +34,11 @@ Use SearchInput for text queries that filter or search a known content set.
|
|
|
31
34
|
| onClear | () => void | — | Called when the clear button is pressed. The component also fires onValueChange('') so a controlled value resets. |
|
|
32
35
|
| placeholder | string | "Search" | Placeholder text shown when the field is empty. |
|
|
33
36
|
| disabled | boolean | false | Dims the control and blocks pointer interaction. |
|
|
34
|
-
| ...props | ComponentPropsWithRef<"input"> | — | All native input attributes (name, autoFocus, ref, aria-invalid, …) pass through to the <input> except value/onChange, which are owned by value/onValueChange,
|
|
37
|
+
| ...props | ComponentPropsWithRef<"input"> | — | All native input attributes (name, autoFocus, ref, aria-invalid, …) pass through to the <input> except value/onChange, which are owned by value/onValueChange, className, which sizes the whole field, and the native numeric size attribute, which the size prop replaces. |
|
|
35
38
|
|
|
36
39
|
## Tokens used
|
|
37
40
|
|
|
38
|
-
`field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`
|
|
41
|
+
`field-border`, `field`, `foreground`, `muted-foreground`, `ring`, `destructive`, `panel-field`, `panel-field-hover`
|
|
39
42
|
|
|
40
43
|
## Motion
|
|
41
44
|
|
|
@@ -54,7 +57,7 @@ Use SearchInput for text queries that filter or search a known content set.
|
|
|
54
57
|
|
|
55
58
|
## Related
|
|
56
59
|
|
|
57
|
-
- Components: Input, CommandMenu, Combobox
|
|
60
|
+
- Components: Input, CommandMenu, Combobox, Pill
|
|
58
61
|
|
|
59
62
|
## Examples
|
|
60
63
|
|
|
@@ -100,6 +103,36 @@ const shown = posts.filter((post) => post.title.toLowerCase().includes(q.trim().
|
|
|
100
103
|
</ul>
|
|
101
104
|
```
|
|
102
105
|
|
|
106
|
+
### Filters with search
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { Pill, Row, SearchInput } from "@sableui/react"
|
|
110
|
+
|
|
111
|
+
const [filter, setFilter] = useState("all")
|
|
112
|
+
const [q, setQ] = useState("")
|
|
113
|
+
|
|
114
|
+
<Row gap={2} align="center" justify="between" wrap>
|
|
115
|
+
<Row gap={1}>
|
|
116
|
+
<Pill size="lg" selected={filter === "all"} onClick={() => setFilter("all")}>All 8</Pill>
|
|
117
|
+
<Pill size="lg" selected={filter === "products"} onClick={() => setFilter("products")}>Products 5</Pill>
|
|
118
|
+
<Pill size="lg" selected={filter === "oss"} onClick={() => setFilter("oss")}>Open source 2</Pill>
|
|
119
|
+
</Row>
|
|
120
|
+
<SearchInput size="sm" value={q} onValueChange={setQ} placeholder="Find a project…" aria-label="Find a project" className="max-w-64" />
|
|
121
|
+
</Row>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Sizes
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { SearchInput, Stack } from "@sableui/react"
|
|
128
|
+
|
|
129
|
+
<Stack gap={1}>
|
|
130
|
+
<SearchInput size="md" placeholder="Search posts" aria-label="Search posts" />
|
|
131
|
+
<SearchInput size="sm" placeholder="Find a project…" aria-label="Find a project" />
|
|
132
|
+
<SearchInput size="xs" placeholder="Filter layers" aria-label="Filter layers" />
|
|
133
|
+
</Stack>
|
|
134
|
+
```
|
|
135
|
+
|
|
103
136
|
### Disabled
|
|
104
137
|
|
|
105
138
|
```tsx
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
The heading block for one section of a page: a title (h2 by default), an optional line of description, and actions on the title row — a "View all" link or a couple of buttons — that drop beneath the title only when the row runs out of room.
|
|
6
6
|
|
|
7
|
-
**Keywords:** section header, section heading, section title, shelf, view all, see all, show all, h2, subsection, optical alignment, bleed
|
|
7
|
+
**Keywords:** section header, section heading, section title, shelf, view all, see all, show all, h2, subsection, optical alignment, bleed, editorial section, large section title
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ Use SectionHeader to head each section below the page title — a home-page shel
|
|
|
13
13
|
### Best practices
|
|
14
14
|
|
|
15
15
|
- Do: Wrap the header and its content in a `<section aria-labelledby={titleId}>` (a flex column with a gap, or a Stack inside it) so the section is a named region and the header's title is its name.
|
|
16
|
-
- Do: Pick titleAs by the document outline (h2 under the page h1, h3 inside an h2 section) and size by visual weight: md for a page's sections and shelves, sm for denser groups like settings.
|
|
16
|
+
- Do: Pick titleAs by the document outline (h2 under the page h1, h3 inside an h2 section) and size by visual weight: lg for an editorial page's sections under a PageHeader size="lg", md for a page's sections and shelves, sm for denser groups like settings.
|
|
17
17
|
- Do: Keep actions to one or two `size="sm"` buttons — the title is centred on that height. A shelf's "View all" is a ghost Button rendered as a link (`render={<a href=… />}`), named for what it opens when the label alone is ambiguous (aria-label="View all new releases").
|
|
18
18
|
- Do: Pass a ghost or link action as a plain Button and let the header align it: the last direct child bleeds by its own padding at the end, so "View all" sits on the edge of the grid below, and the first bleeds at the start, which is the edge once the actions wrap beneath the title. Filled buttons keep their edge on the line. Set bleed="none" on the Button to opt one out.
|
|
19
19
|
- Don't: Nudge a View all onto the edge with -me-3, or wrap the actions in your own div: the header aligns direct children only, and only by each button's real padding. If you must wrap them, set bleed on the Button inside instead.
|
|
@@ -38,11 +38,11 @@ Use SectionHeader to head each section below the page title — a home-page shel
|
|
|
38
38
|
| actions | ReactNode | — | Trailing actions on the title row — a View all link or a couple of buttons, size="sm". They stay on the title's row and drop beneath it only when the row runs out of room; the title wraps rather than truncating. A ghost or link Button that ends the group bleeds by its padding at the end (and one that starts it, at the start — the edge once wrapped), so its label lines up with the content below; Button bleed="none" opts out. Direct children only. |
|
|
39
39
|
| titleAs | "h2" \| "h3" \| "h4" | "h2" | The heading element, chosen by the document outline: h2 for a section under the page's h1, h3 for a subsection inside one. |
|
|
40
40
|
| titleId | string | — | The title's id, so the section it heads can point at it: <section aria-labelledby={titleId}>. Generated when omitted. |
|
|
41
|
-
| size | "md" \| "sm" | "md" | Visual weight, independent of the heading level: md (text-heading-3) for a page's sections and shelves; sm (text-heading-4) for denser sets — settings groups, subsections. |
|
|
41
|
+
| size | "lg" \| "md" \| "sm" | "md" | Visual weight, independent of the heading level: lg (text-heading-2 over a text-body-md description) for an editorial page's sections, under a PageHeader size="lg"; md (text-heading-3) for a page's sections and shelves; sm (text-heading-4) for denser sets — settings groups, subsections. lg stops at heading-2: heading-1 is the page title's, one per route. The actions stay centred on the title's first line at every size. |
|
|
42
42
|
|
|
43
43
|
## Variants
|
|
44
44
|
|
|
45
|
-
- **size**: md, sm
|
|
45
|
+
- **size**: lg, md, sm
|
|
46
46
|
|
|
47
47
|
## Tokens used
|
|
48
48
|
|
|
@@ -135,6 +135,30 @@ import { Button, Entity, Icon, SectionHeader, Stack } from "@sableui/react"
|
|
|
135
135
|
</section>
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
### An editorial section (size lg)
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
import { Button, Card, CardDescription, CardHeader, CardTitle, Grid, SectionHeader } from "@sableui/react"
|
|
142
|
+
|
|
143
|
+
<section aria-labelledby="selected-work" className="flex flex-col gap-6">
|
|
144
|
+
<SectionHeader
|
|
145
|
+
titleId="selected-work"
|
|
146
|
+
size="lg"
|
|
147
|
+
title="Selected work"
|
|
148
|
+
description="Five projects from the last two years."
|
|
149
|
+
actions={
|
|
150
|
+
<Button variant="ghost" size="sm" render={<a href="/work" />} aria-label="View all work">
|
|
151
|
+
View all
|
|
152
|
+
</Button>
|
|
153
|
+
}
|
|
154
|
+
/>
|
|
155
|
+
<Grid minItemWidth="16rem" gap={3}>
|
|
156
|
+
<Card><CardHeader><CardTitle>Tidewater</CardTitle><CardDescription>Brand identity</CardDescription></CardHeader></Card>
|
|
157
|
+
<Card><CardHeader><CardTitle>Northlight</CardTitle><CardDescription>Product design</CardDescription></CardHeader></Card>
|
|
158
|
+
</Grid>
|
|
159
|
+
</section>
|
|
160
|
+
```
|
|
161
|
+
|
|
138
162
|
### A subsection inside a section
|
|
139
163
|
|
|
140
164
|
```tsx
|
package/llms/components/sheet.md
CHANGED
|
@@ -48,7 +48,7 @@ Use Sheet for secondary workflows that slide in while preserving context from th
|
|
|
48
48
|
|
|
49
49
|
| Name | Type | Default | Description |
|
|
50
50
|
| --- | --- | --- | --- |
|
|
51
|
-
| Sheet (Root) | Base UI Drawer.Root props (open, defaultOpen, onOpenChange, onOpenChangeComplete, modal, disablePointerDismissal, actionsRef, handle, snapPoints, snapPoint, defaultSnapPoint, onSnapPointChange, snapToSequentialPoints) — minus swipeDirection | — | Controls open state. Uncontrolled by default. The dismiss swipe direction is not a prop: it follows SheetContent's side. onOpenChange reports reason "swipe" when a swipe closed it. |
|
|
51
|
+
| Sheet (Root) | Base UI Drawer.Root props (open, defaultOpen, onOpenChange, onOpenChangeComplete, modal, disablePointerDismissal, actionsRef, handle, snapPoints, snapPoint, defaultSnapPoint, onSnapPointChange, snapToSequentialPoints) — minus swipeDirection | — | Controls open state. Uncontrolled by default. The dismiss swipe direction is not a prop: it follows SheetContent's side. onOpenChange reports reason "swipe" when a swipe closed it. Records the element focused when the sheet opens, trigger or not, and returns focus there on close. |
|
|
52
52
|
| snapPoints | (number \| string)[] | — | Stops a bottom sheet rests at, smallest first: a fraction of the viewport (0.4), pixels (320), or a px/rem string ("20rem"). The panel is the full stop's height and opens at the first stop; a swipe moves between stops and a swipe below the first closes it. side="bottom" only: other sides ignore them, with a development warning. |
|
|
53
53
|
| snapPoint | number \| string \| null | — | The active stop, controlled. Pair with onSnapPointChange; use defaultSnapPoint to open at a different stop uncontrolled. |
|
|
54
54
|
| onSnapPointChange | (snapPoint: number \| string \| null, eventDetails) => void | — | Fires when a swipe (or your code) moves the sheet to another stop. |
|
|
@@ -68,7 +68,7 @@ Use Sheet for secondary workflows that slide in while preserving context from th
|
|
|
68
68
|
- The panel scrolls its own content (thin scrollbar, contained overscroll) when it outgrows its bound — the full height on left/right, 80vh on top/bottom.
|
|
69
69
|
- Esc and click-outside dismiss by default; on touch, a swipe toward the sheet's own edge dismisses too. A short or slow drag springs back.
|
|
70
70
|
- Swipe is an extra path, never the only one: Esc, click-outside and SheetClose all still work.
|
|
71
|
-
- Focus matches Dialog: the first tabbable on mouse/keyboard open, the panel itself on touch (so the virtual keyboard stays down)
|
|
71
|
+
- Focus matches Dialog: the first tabbable on mouse/keyboard open, the panel itself on touch (so the virtual keyboard stays down). On close, focus returns to the element that held it when the sheet opened: the SheetTrigger, or your own button when it drives a controlled open. It is captured before the content mounts, so an autoFocus field inside cannot displace it; an explicit finalFocus on SheetContent wins.
|
|
72
72
|
- Render a SheetClose inside SheetContent so screen-reader users can escape the popup.
|
|
73
73
|
- Slide animation runs via data-[starting-style]/data-[ending-style] attributes on the motion-duration token; while swiping the panel tracks the finger directly, and a released swipe finishes on the same token (scaled by flick speed), so reduced motion makes it instant.
|
|
74
74
|
- A mouse drag that starts on sheet content selects text rather than dragging the panel; mouse dragging only starts from the padding edge.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
Reserves space via aspect-ratio so async media loads with zero layout shift. Wrap images, video, embeds.
|
|
5
|
+
Reserves space via aspect-ratio so async media loads with zero layout shift. Wrap images, video, embeds. The ratio is a class reading a CSS variable, so className reshapes it at a breakpoint (md:aspect-video).
|
|
6
6
|
|
|
7
7
|
**Keywords:** aspect box, aspect ratio, media, layout shift
|
|
8
8
|
|
package/llms/layout/bleed.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
Escape a width-capped or inset ancestor to span the full CONTENT area (container-query units, so it stops at a fixed Sidebar instead of bleeding under it). Hero bands, profile covers, full-bleed media inside a capped Page.
|
|
5
|
+
Escape a width-capped or inset ancestor to span the full CONTENT area (container-query units, so it stops at a fixed Sidebar instead of bleeding under it). Hero bands, profile covers, full-bleed media inside a capped Page. Width and margins are classes, so a breakpoint can turn the bleed off (lg:mx-0 lg:w-auto).
|
|
6
6
|
|
|
7
7
|
**Keywords:** bleed, full bleed, media band, layout
|
|
8
8
|
|
package/llms/layout/center.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
Horizontally center a block, optionally capping its width (commonly "prose" for a reading column inside a wider layout).
|
|
5
|
+
Horizontally center a block, optionally capping its width (commonly "prose" for a reading column inside a wider layout). Centering and cap are classes, so className overrides them at a breakpoint.
|
|
6
6
|
|
|
7
7
|
**Keywords:** center, measure, reading width, layout
|
|
8
8
|
|
package/llms/layout/columns.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
An explicit N-column track (default 12) for proportioned layouts — pair with Col spans (list-detail, asymmetric editorial layouts). For uniform card grids prefer Grid minItemWidth.
|
|
5
|
+
An explicit N-column track (default 12) for proportioned layouts — pair with Col spans (list-detail, asymmetric editorial layouts). For uniform card grids prefer Grid minItemWidth. display is the grid class, so hidden md:grid works; a gap-* utility replaces both gaps and the column-gap cap.
|
|
6
6
|
|
|
7
7
|
**Keywords:** columns, 12 column, span, layout
|
|
8
8
|
|
package/llms/layout/grid.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
CSS Grid within a region. Two modes: fixed/responsive cols count, or minItemWidth so cells breathe with NO breakpoints (the default choice for card grids). A minItemWidth grid keeps its empty columns (track="fill"), so one result stays one cell wide; track="fit" stretches a fixed set across the row.
|
|
5
|
+
CSS Grid within a region. Two modes: fixed/responsive cols count, or minItemWidth so cells breathe with NO breakpoints (the default choice for card grids). A minItemWidth grid keeps its empty columns (track="fill"), so one result stays one cell wide; track="fit" stretches a fixed set across the row. display is the grid class and the gap a CSS variable, so className utilities win at any breakpoint (hidden md:grid, md:gap-6, lg:grid-cols-3).
|
|
6
6
|
|
|
7
7
|
**Keywords:** grid, card grid, auto fit, layout
|
|
8
8
|
|
package/llms/layout/region.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
The page region: fluid, edge-to-edge with the fluid page inset (--page-inset). Opt-in centered max cap picked by ROLE (prose/form/feed/content/wide/full), never a global page max-width. The root of nearly every route.
|
|
5
|
+
The page region: fluid, edge-to-edge with the fluid page inset (--page-inset). Opt-in centered max cap picked by ROLE (prose/form/feed/content/wide/full), never a global page max-width. The root of nearly every route. The inset and cap are classes (px-(--page-inset), max-w-…), so className overrides them at a breakpoint (max-sm:px-0); only safeArea padding is inline.
|
|
6
6
|
|
|
7
7
|
**Keywords:** region, page width, container, layout
|
|
8
8
|
|
package/llms/layout/row.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
Horizontal flex flow — inline rows of controls, toolbars, action clusters. Defaults to center-aligned, no wrap.
|
|
5
|
+
Horizontal flex flow — inline rows of controls, toolbars, action clusters. Defaults to center-aligned, no wrap. Layout lives in classes (flex flex-row items-* justify-*) and the gap in a CSS variable, never inline style, so className utilities override any prop at a breakpoint (hidden md:flex, max-md:flex-col, md:gap-6).
|
|
6
6
|
|
|
7
7
|
**Keywords:** row, toolbar, horizontal, layout
|
|
8
8
|
|
|
@@ -38,3 +38,11 @@ import { Button, Row } from "@sableui/react"
|
|
|
38
38
|
|
|
39
39
|
<Row gap={2} justify="end"><Button variant="ghost">Cancel</Button><Button>Save changes</Button></Row>
|
|
40
40
|
```
|
|
41
|
+
|
|
42
|
+
### Responsive through className
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { Button, Row } from "@sableui/react"
|
|
46
|
+
|
|
47
|
+
<Row justify="between"><span className="text-title-sm">Acme</span><Row gap={1} className="max-md:hidden"><Button variant="ghost" size="sm">Docs</Button><Button variant="ghost" size="sm">Pricing</Button></Row></Row>
|
|
48
|
+
```
|
package/llms/layout/spacer.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
Explicit space on the rhythm, or a flexible grow pusher between siblings (e.g. pushing a footer item to the end of a Row).
|
|
5
|
+
Explicit space on the rhythm, or a flexible grow pusher between siblings (e.g. pushing a footer item to the end of a Row). The size is a class reading a CSS variable, so className resizes or hides it at a breakpoint.
|
|
6
6
|
|
|
7
7
|
**Keywords:** spacer, spacing, grow, layout
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
Responsive two-pane layout: side-by-side at/above a breakpoint, stacked (primary first) below. List-detail, media + metadata.
|
|
5
|
+
Responsive two-pane layout: side-by-side at/above a breakpoint, stacked (primary first) below. List-detail, media + metadata. display is the grid class and the breakpoint rules are layered, so className utilities (hidden lg:grid, gap-8) win.
|
|
6
6
|
|
|
7
7
|
**Keywords:** split pane, two pane, list detail, responsive
|
|
8
8
|
|
package/llms/layout/stack.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** stable
|
|
4
4
|
|
|
5
|
-
Vertical flow on the 8px rhythm — the default container for stacked content. Gap by role: tight within a group (1.5–2), looser between groups (3–4), generous between sections (6+).
|
|
5
|
+
Vertical flow on the 8px rhythm — the default container for stacked content. Gap by role: tight within a group (1.5–2), looser between groups (3–4), generous between sections (6+). Layout lives in classes (flex flex-col items-* justify-*) and the gap in a CSS variable, never inline style, so className utilities override any prop at a breakpoint (max-lg:hidden, md:flex-row, md:gap-8).
|
|
6
6
|
|
|
7
7
|
**Keywords:** stack, vertical rhythm, layout, spacing
|
|
8
8
|
|
|
@@ -58,6 +58,37 @@ names the nearest pair that reads. Report it with that pair as a recommendation.
|
|
|
58
58
|
A reasoned `design-lint-disable-next-line label-contrast` is the app's recorded
|
|
59
59
|
choice: list it as intentional, not as a defect.
|
|
60
60
|
|
|
61
|
+
## Adoption: an app wired for Sable that barely uses it
|
|
62
|
+
|
|
63
|
+
The commonest drift is not a wrong prop. It is an app where an agent never
|
|
64
|
+
loaded this skill: the stylesheet and tokens are wired, and every control is the
|
|
65
|
+
app's own (a `Modal`, a `PrimaryButton`, a `Tabs` written from scratch). Wiring
|
|
66
|
+
checks pass, and lint only sees the raw elements. `sable check` (and `sable
|
|
67
|
+
grade --json` → `adoption`) measures it: how many UI files compose Sable, which
|
|
68
|
+
app-defined components are named for something Sable ships and do the behaviour
|
|
69
|
+
by hand, and what the app's foundation is. `sable doctor` warns when adoption is
|
|
70
|
+
`none` or `low`. It is never a failure: whether working screens move is the
|
|
71
|
+
user's call.
|
|
72
|
+
|
|
73
|
+
Put the choice to the user before changing anything. Ask, don't migrate silently
|
|
74
|
+
and don't leave it unmentioned. Show the swaps, behaviour first (overlays,
|
|
75
|
+
controls, tabs, menus, anything with focus or keyboard handling), with what each
|
|
76
|
+
one gains ("your Modal has no focus trap or Esc; Sable's Dialog has both").
|
|
77
|
+
Layout and look stay the app's. How strongly to recommend it depends on the
|
|
78
|
+
`foundation` the report names:
|
|
79
|
+
|
|
80
|
+
| Foundation | What to say |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `ad-hoc`: no system besides Sable | Recommend the move plainly. There is nothing to preserve but one-off components, and every one is a behaviour bug waiting. Offer to do it a screen at a time. |
|
|
83
|
+
| `own`: a folder of the app's own primitives | Offer the swaps and say what each gains. Keep the app's own where the user prefers them; suggest building them on Sable's behaviour (a `Button` wrapper around Sable's `Button`) instead. |
|
|
84
|
+
| `library`: another component library (MUI, Radix, shadcn-style `components/ui`) | Ask whether Sable should replace it or only cover new work. Don't mix two control libraries on one screen. |
|
|
85
|
+
| `sable`: Sable already carries the UI | Mention the few swaps listed when those screens are next touched. |
|
|
86
|
+
|
|
87
|
+
Use the question tool the host offers when there is one. Record the answer in
|
|
88
|
+
the project's instructions, so the next task doesn't ask again. On a yes, move
|
|
89
|
+
one screen or component at a time with `fix`/`polish`, running `sable check` and
|
|
90
|
+
looking at the rendered states after each.
|
|
91
|
+
|
|
61
92
|
## Deliver evidence-backed findings
|
|
62
93
|
|
|
63
94
|
Save the audit under `design/<surface>/audit/` or the existing review location.
|
|
@@ -79,15 +79,22 @@ In a monorepo where Sable belongs to one workspace package, run setup in that
|
|
|
79
79
|
package with `--root` naming the repository root (for example
|
|
80
80
|
`pnpm --filter web exec sable setup --agent --root ../..`); the entries go
|
|
81
81
|
there and resolve Sable through the package.
|
|
82
|
+
Setup also adds a short managed block to the project's `AGENTS.md` and
|
|
83
|
+
`CLAUDE.md`, the instructions an agent reads on every task, so a UI request
|
|
84
|
+
goes through the skill even when it doesn't mention Sable. Only the text between
|
|
85
|
+
its `<!-- sable:begin -->` and `<!-- sable:end -->` markers is Sable's;
|
|
86
|
+
`--no-instructions` skips it.
|
|
82
87
|
Each entry resolves `skills/sable/SKILL.md` from the installed package, so package
|
|
83
88
|
upgrades also update the workflow. Other agents can read that file directly.
|
|
84
89
|
Use `$sable build <idea>`, `inspire <references>`, `audit <target>`, or
|
|
85
90
|
`polish <target>` in Codex; use `/sable` with the same modes in Claude Code or
|
|
86
91
|
Cursor. `create` is a build alias. These are agent modes, not shell commands.
|
|
87
92
|
|
|
88
|
-
Upgrade with `pnpm
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
Upgrade with `pnpm exec sable upgrade`: it updates the package, then runs the
|
|
94
|
+
new version's `init`, which refreshes the skill entries and the instructions
|
|
95
|
+
block and checks the wiring. From 0.17 or earlier, which has no `upgrade`, run
|
|
96
|
+
`pnpm update @sableui/react --latest` and then `pnpm exec sable init` once.
|
|
97
|
+
Setup only replaces managed files that have not been locally edited; `--dry-run` checks without writing. Keep
|
|
91
98
|
project preferences in project instructions. Package installation alone does not
|
|
92
99
|
change agent settings. See [building-with-sable.md](./building-with-sable.md),
|
|
93
100
|
[creating-a-preset.md](./creating-a-preset.md), and
|
|
@@ -102,8 +102,7 @@ saved file. Replace, don't add beside:
|
|
|
102
102
|
## Upgrade the package and skill
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
|
-
pnpm
|
|
106
|
-
pnpm exec sable setup --agent --update
|
|
105
|
+
pnpm exec sable upgrade
|
|
107
106
|
pnpm exec sable preset --check .sable/presets/clay.json --json
|
|
108
107
|
pnpm exec sable doctor
|
|
109
108
|
```
|