@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.
Files changed (86) hide show
  1. package/AGENTS.md +10 -3
  2. package/CHANGELOG.md +106 -0
  3. package/README.md +11 -4
  4. package/bin/sable.mjs +160 -18
  5. package/dist/activity-DT8VK9fx.d.ts +25 -0
  6. package/dist/adapters/github.d.ts +41 -0
  7. package/dist/adapters/github.js +24 -0
  8. package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
  9. package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
  10. package/dist/components/activity-heatmap/index.js +9 -0
  11. package/dist/components/code-block/code-block.js +5 -3
  12. package/dist/components/code-block/code-block.meta.js +6 -0
  13. package/dist/components/dialog/dialog.js +25 -5
  14. package/dist/components/dialog/dialog.meta.js +26 -5
  15. package/dist/components/media-image/media-image.js +3 -1
  16. package/dist/components/media-image/media-image.meta.js +15 -1
  17. package/dist/components/media-tile/media-tile.js +13 -1
  18. package/dist/components/media-tile/media-tile.meta.js +10 -0
  19. package/dist/components/page-header/page-header.js +9 -4
  20. package/dist/components/page-header/page-header.meta.js +26 -1
  21. package/dist/components/pill/pill.meta.js +15 -6
  22. package/dist/components/pill/pill.variants.js +1 -0
  23. package/dist/components/search-input/search-input.js +30 -5
  24. package/dist/components/search-input/search-input.meta.js +29 -4
  25. package/dist/components/section-header/section-header.js +27 -17
  26. package/dist/components/section-header/section-header.meta.js +27 -5
  27. package/dist/components/sheet/sheet.js +15 -3
  28. package/dist/components/sheet/sheet.meta.js +2 -2
  29. package/dist/file-hashes.json +46 -36
  30. package/dist/icon-sets/reicon.js +23 -1
  31. package/dist/index.d.ts +270 -27
  32. package/dist/index.js +8 -1
  33. package/dist/layout/AspectBox.js +2 -5
  34. package/dist/layout/Bleed.js +10 -6
  35. package/dist/layout/Center.js +11 -3
  36. package/dist/layout/Columns.js +6 -3
  37. package/dist/layout/Grid.js +14 -6
  38. package/dist/layout/GridOverlay.js +7 -7
  39. package/dist/layout/Region.js +18 -3
  40. package/dist/layout/Row.js +27 -20
  41. package/dist/layout/Spacer.js +13 -2
  42. package/dist/layout/SplitPane.js +1 -1
  43. package/dist/layout/Stack.js +25 -18
  44. package/dist/layout/index.d.ts +57 -6
  45. package/dist/lib/chart/activity.js +32 -0
  46. package/dist/lib/chart/calendar.js +67 -0
  47. package/dist/lib/chart/chart-tooltip.js +2 -2
  48. package/dist/lib/chart/heat.js +55 -0
  49. package/dist/lib/focus-return.js +53 -0
  50. package/dist/lib/taxonomy.js +1 -0
  51. package/dist/styles.css +122 -114
  52. package/llms/components/activity-heatmap.md +133 -0
  53. package/llms/components/code-block.md +1 -0
  54. package/llms/components/dialog.md +34 -3
  55. package/llms/components/media-image.md +12 -0
  56. package/llms/components/media-tile.md +21 -0
  57. package/llms/components/page-header.md +16 -1
  58. package/llms/components/pill.md +24 -5
  59. package/llms/components/search-input.md +37 -4
  60. package/llms/components/section-header.md +28 -4
  61. package/llms/components/sheet.md +2 -2
  62. package/llms/layout/aspect-box.md +1 -1
  63. package/llms/layout/bleed.md +1 -1
  64. package/llms/layout/center.md +1 -1
  65. package/llms/layout/columns.md +1 -1
  66. package/llms/layout/grid.md +1 -1
  67. package/llms/layout/region.md +1 -1
  68. package/llms/layout/row.md +9 -1
  69. package/llms/layout/spacer.md +1 -1
  70. package/llms/layout/split-pane.md +1 -1
  71. package/llms/layout/stack.md +1 -1
  72. package/llms/tasks/auditing-an-app.md +31 -0
  73. package/llms/tasks/getting-started.md +10 -3
  74. package/llms/tasks/using-presets.md +1 -2
  75. package/llms-full.txt +372 -35
  76. package/llms.txt +3 -2
  77. package/package.json +6 -1
  78. package/registry.json +532 -36
  79. package/scripts/adoption.mjs +239 -0
  80. package/scripts/agent-setup.mjs +110 -18
  81. package/scripts/grade.mjs +17 -0
  82. package/scripts/wiring.mjs +2 -1
  83. package/skills/entry/SKILL.md +1 -1
  84. package/skills/sable/SKILL.md +33 -4
  85. package/skills/sable/agents/openai.yaml +1 -1
  86. package/targets/ios.json +9 -0
@@ -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" | md sits on the density ladder's xs control height (28px); sm is 24px, for chips under or inside other content. |
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, and className, which sizes the whole field. |
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
@@ -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); focus returns to the trigger on close.
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
 
@@ -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
 
@@ -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
 
@@ -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
 
@@ -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
 
@@ -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
 
@@ -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
+ ```
@@ -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
 
@@ -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 update @sableui/react --latest`, then
89
- `pnpm exec sable setup --agent --update`. Setup only replaces managed files
90
- that have not been locally edited; `--dry-run` checks without writing. Keep
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 update @sableui/react --latest
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
  ```