@sableui/react 0.17.0 → 0.19.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 (154) hide show
  1. package/AGENTS.md +10 -3
  2. package/CHANGELOG.md +235 -0
  3. package/README.md +11 -4
  4. package/bin/sable.mjs +266 -30
  5. package/dist/{_contract-CDa9-NdB.d.ts → _contract-DlklV0Im.d.ts} +2 -2
  6. package/dist/activity-DT8VK9fx.d.ts +25 -0
  7. package/dist/adapters/github.d.ts +41 -0
  8. package/dist/adapters/github.js +24 -0
  9. package/dist/blocks/index.d.ts +1 -1
  10. package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
  11. package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
  12. package/dist/components/activity-heatmap/index.js +9 -0
  13. package/dist/components/avatar/avatar.js +16 -7
  14. package/dist/components/avatar/avatar.meta.js +43 -10
  15. package/dist/components/avatar/avatar.variants.js +26 -2
  16. package/dist/components/code-block/code-block.js +5 -3
  17. package/dist/components/code-block/code-block.meta.js +6 -0
  18. package/dist/components/collapsible/collapsible.js +6 -5
  19. package/dist/components/collapsible/collapsible.meta.js +14 -5
  20. package/dist/components/data-table/data-table.js +132 -45
  21. package/dist/components/data-table/data-table.meta.js +63 -4
  22. package/dist/components/date-picker/date-picker.js +13 -6
  23. package/dist/components/date-picker/date-picker.meta.js +22 -0
  24. package/dist/components/dialog/dialog.js +25 -5
  25. package/dist/components/dialog/dialog.meta.js +26 -5
  26. package/dist/components/media-image/media-image.js +3 -1
  27. package/dist/components/media-image/media-image.meta.js +15 -1
  28. package/dist/components/media-tile/media-tile.js +13 -1
  29. package/dist/components/media-tile/media-tile.meta.js +10 -0
  30. package/dist/components/menu/menu.js +2 -0
  31. package/dist/components/menu/menu.meta.js +2 -2
  32. package/dist/components/page-header/page-header.js +9 -4
  33. package/dist/components/page-header/page-header.meta.js +26 -1
  34. package/dist/components/panel-group/panel-group.js +17 -1
  35. package/dist/components/panel-group/panel-group.meta.js +43 -3
  36. package/dist/components/pill/pill.meta.js +15 -6
  37. package/dist/components/pill/pill.variants.js +1 -0
  38. package/dist/components/search-input/search-input.js +30 -5
  39. package/dist/components/search-input/search-input.meta.js +29 -4
  40. package/dist/components/section-header/section-header.js +27 -17
  41. package/dist/components/section-header/section-header.meta.js +27 -5
  42. package/dist/components/sheet/sheet.js +15 -3
  43. package/dist/components/sheet/sheet.meta.js +2 -2
  44. package/dist/components/side-nav/side-nav.variants.js +8 -3
  45. package/dist/components/sidebar/index.js +4 -0
  46. package/dist/components/sidebar/sidebar.js +90 -11
  47. package/dist/components/sidebar/sidebar.meta.js +38 -4
  48. package/dist/components/table/table.js +7 -3
  49. package/dist/components/tabs/tabs.js +10 -2
  50. package/dist/components/tabs/tabs.meta.js +4 -0
  51. package/dist/components/workspace/workspace.js +13 -3
  52. package/dist/components/workspace/workspace.meta.js +109 -1
  53. package/dist/file-hashes.json +94 -80
  54. package/dist/foundations/typography.js +13 -0
  55. package/dist/icon/Icon.js +21 -2
  56. package/dist/icon/index.d.ts +36 -1
  57. package/dist/icon/index.js +5 -0
  58. package/dist/icon/names.js +240 -0
  59. package/dist/icon-sets/reicon.js +41 -1
  60. package/dist/index.d.ts +540 -75
  61. package/dist/index.js +18 -1
  62. package/dist/layout/AspectBox.js +2 -5
  63. package/dist/layout/Bleed.js +10 -6
  64. package/dist/layout/Center.js +11 -3
  65. package/dist/layout/Columns.js +6 -3
  66. package/dist/layout/Grid.js +14 -6
  67. package/dist/layout/GridOverlay.js +7 -7
  68. package/dist/layout/Region.js +18 -3
  69. package/dist/layout/Row.js +27 -20
  70. package/dist/layout/Spacer.js +13 -2
  71. package/dist/layout/SplitPane.js +1 -1
  72. package/dist/layout/Stack.js +25 -18
  73. package/dist/layout/index.d.ts +57 -6
  74. package/dist/lib/chart/activity.js +32 -0
  75. package/dist/lib/chart/calendar.js +67 -0
  76. package/dist/lib/chart/chart-tooltip.js +2 -2
  77. package/dist/lib/chart/heat.js +55 -0
  78. package/dist/lib/cn.js +1 -0
  79. package/dist/lib/focus-return.js +53 -0
  80. package/dist/lib/taxonomy.js +1 -0
  81. package/dist/lib/use-media-query.js +41 -0
  82. package/dist/{meta-DX5r5wQL.d.ts → meta-Dfm9AExe.d.ts} +6 -0
  83. package/dist/patterns/dashboard-page.js +23 -17
  84. package/dist/patterns/index.d.ts +7 -2
  85. package/dist/patterns/index.js +5 -1
  86. package/dist/patterns/tracker-page.js +392 -0
  87. package/dist/patterns/tracker-page.meta.js +40 -0
  88. package/dist/presets/index.d.ts +1 -1
  89. package/dist/styles.css +301 -188
  90. package/dist/{theme-provider-BydfM5Z0.d.ts → theme-provider-BUBealk_.d.ts} +1 -1
  91. package/dist/themes/_contract.js +9 -1
  92. package/dist/themes/atelier/index.d.ts +1 -1
  93. package/dist/themes/ember/index.d.ts +1 -1
  94. package/dist/themes/index.d.ts +2 -2
  95. package/dist/themes/meridian/index.d.ts +1 -1
  96. package/dist/themes/poppy/index.d.ts +1 -1
  97. package/dist/themes/prism/index.d.ts +1 -1
  98. package/dist/themes/sable/index.d.ts +1 -1
  99. package/dist/themes/verdant/index.d.ts +1 -1
  100. package/llms/components/activity-heatmap.md +133 -0
  101. package/llms/components/avatar.md +32 -9
  102. package/llms/components/code-block.md +1 -0
  103. package/llms/components/collapsible.md +12 -4
  104. package/llms/components/data-table.md +40 -3
  105. package/llms/components/date-picker.md +15 -1
  106. package/llms/components/dialog.md +34 -3
  107. package/llms/components/media-image.md +12 -0
  108. package/llms/components/media-tile.md +21 -0
  109. package/llms/components/menu.md +1 -1
  110. package/llms/components/page-header.md +16 -1
  111. package/llms/components/panel-group.md +34 -2
  112. package/llms/components/pill.md +24 -5
  113. package/llms/components/search-input.md +37 -4
  114. package/llms/components/section-header.md +28 -4
  115. package/llms/components/sheet.md +2 -2
  116. package/llms/components/sidebar.md +17 -4
  117. package/llms/components/tabs.md +1 -0
  118. package/llms/components/workspace.md +81 -1
  119. package/llms/foundations/typography.md +4 -1
  120. package/llms/layout/aspect-box.md +1 -1
  121. package/llms/layout/bleed.md +1 -1
  122. package/llms/layout/center.md +1 -1
  123. package/llms/layout/columns.md +1 -1
  124. package/llms/layout/grid.md +1 -1
  125. package/llms/layout/region.md +1 -1
  126. package/llms/layout/row.md +9 -1
  127. package/llms/layout/spacer.md +1 -1
  128. package/llms/layout/split-pane.md +1 -1
  129. package/llms/layout/stack.md +1 -1
  130. package/llms/patterns/dashboard-page.md +18 -15
  131. package/llms/patterns/tracker-page.md +450 -0
  132. package/llms/reference/token-intent.md +3 -1
  133. package/llms/reference/utilities.md +4 -1
  134. package/llms/tasks/auditing-an-app.md +35 -1
  135. package/llms/tasks/building-a-page.md +3 -2
  136. package/llms/tasks/building-a-tool.md +101 -5
  137. package/llms/tasks/building-with-sable.md +11 -3
  138. package/llms/tasks/creating-a-preset.md +9 -0
  139. package/llms/tasks/getting-started.md +10 -3
  140. package/llms/tasks/using-presets.md +1 -2
  141. package/llms-full.txt +1215 -89
  142. package/llms.txt +11 -9
  143. package/package.json +7 -1
  144. package/registry.json +1021 -70
  145. package/scripts/adoption.mjs +252 -0
  146. package/scripts/agent-setup.mjs +110 -18
  147. package/scripts/app-css.mjs +129 -0
  148. package/scripts/grade.mjs +178 -2
  149. package/scripts/lint-rules.mjs +126 -0
  150. package/scripts/wiring.mjs +2 -1
  151. package/skills/entry/SKILL.md +1 -1
  152. package/skills/sable/SKILL.md +64 -6
  153. package/skills/sable/agents/openai.yaml +1 -1
  154. package/targets/ios.json +9 -0
@@ -15,6 +15,7 @@ Use MediaImage wherever content imagery may be missing or broken: a cover in a g
15
15
  - Do: Give the box the shape: `<AspectBox ratio={1} className="rounded-md"><MediaImage … /></AspectBox>`, or `size-14 rounded-md` on MediaImage itself for a thumbnail. It clips to whatever radius the box has.
16
16
  - Do: Pass `seed` (the title or an id) when `alt` is empty, so the fallback art is the item's own and stays the same in every list it appears in.
17
17
  - Do: Leave `alt` empty when a visible title sits next to the image — a tile, a row — and describe it when the image stands alone. A set `alt` also names the fallback, so the description survives a broken URL.
18
+ - Do: Set `position` when a cover crop would cut off the subject — a face high in a portrait, a product at one edge. Store the focus with the image (`"35% 50%"`) rather than re-cropping the file.
18
19
  - Don't: Do not hand-write `<img onError={() => setBroken(true)}>`. An image that fails before hydration never fires the handler on a prerendered page; MediaImage preloads off the DOM and catches it.
19
20
  - Don't: Do not use it for profile pictures. A person is a circle with initials — Avatar, whose `variant="art"` is the same fallback with the person's initials over it.
20
21
 
@@ -27,6 +28,7 @@ Use MediaImage wherever content imagery may be missing or broken: a cover in a g
27
28
  | seed | string \| number | — | Seeds the fallback art. Defaults to `alt`; pass the title or an id when `alt` is empty. |
28
29
  | fallback | ReactNode | — | Replaces the GradientArt fallback — a glyph or monogram. Fills the frame under the same clipping. |
29
30
  | fit | "cover" \| "contain" | "cover" | `cover` crops to fill (artwork, photos); `contain` letterboxes on the muted surface (a logo, a product that must not be cropped). |
31
+ | position | CSSProperties["objectPosition"] | "center" | Where the image sits in its frame — CSS `object-position`, the sibling of `fit`. Under `cover` it is the part the crop keeps (`"50% 30%"` for a face, `"bottom"` for a horizon); under `contain` it is the side the letterbox gives way to. |
30
32
  | srcSet | string | — | Responsive sources, passed to the `<img>`. |
31
33
  | sizes | string | — | The `sizes` attribute for `srcSet`. |
32
34
  | loading | "lazy" \| "eager" | "lazy" | Native lazy loading. Use `eager` for the first row of a grid above the fold. |
@@ -96,3 +98,13 @@ import { AspectBox, MediaImage } from "@sableui/react"
96
98
  <MediaImage src={brand.logo} alt={brand.name} fit="contain" />
97
99
  </AspectBox>
98
100
  ```
101
+
102
+ ### Crop focus per image
103
+
104
+ ```tsx
105
+ import { AspectBox, MediaImage } from "@sableui/react"
106
+
107
+ <AspectBox ratio={1} className="rounded-md">
108
+ <MediaImage src={project.thumbnail} seed={project.title} position={project.focus ?? "center"} />
109
+ </AspectBox>
110
+ ```
@@ -42,6 +42,7 @@ The item in a media grid or shelf: an album, a playlist, a video, a podcast, a f
42
42
  | sizes | string | — | The cover's layout width for `srcSet`, e.g. `"(min-width: 64rem) 25vw, 50vw"`. |
43
43
  | alt | string | "" | Cover alt text. Empty by default: the title below already names the item. |
44
44
  | seed | string \| number | — | Seeds the fallback art. Defaults to `title` when it is a string. |
45
+ | position | CSSProperties["objectPosition"] | "center" | Which part of the cover the crop keeps — CSS `object-position`, forwarded to the cover's MediaImage: `"50% 30%"` for a face, `"top"` for a poster's title. Store it with the image when each one frames differently. |
45
46
  | aspect | "square" \| "video" \| "poster" \| number | "square" | The cover's shape: square (albums, podcasts), video (16:9), poster (2:3 — films, books), or any width / height ratio. |
46
47
  | size | "sm" \| "md" \| "lg" | "md" | Moves the type tier, the cover-to-text gap, the corner and the overlay inset together. |
47
48
  | href | string | — | Where the tile goes. The title becomes the link, stretched over the whole tile. |
@@ -166,6 +167,26 @@ import { Grid, MediaTile } from "@sableui/react"
166
167
  </Grid>
167
168
  ```
168
169
 
170
+ ### Portfolio grid with crop focus
171
+
172
+ ```tsx
173
+ import { Grid, MediaTile } from "@sableui/react"
174
+
175
+ <Grid minItemWidth="16rem" gap={4}>
176
+ {projects.map((project) => (
177
+ <MediaTile
178
+ key={project.slug}
179
+ aspect="video"
180
+ title={project.title}
181
+ subtitle={project.client}
182
+ src={project.thumbnail}
183
+ position={project.focus}
184
+ href={`/work/${project.slug}`}
185
+ />
186
+ ))}
187
+ </Grid>
188
+ ```
189
+
169
190
  ### Poster shelf
170
191
 
171
192
  ```tsx
@@ -39,7 +39,7 @@ Use Menu for a compact list of actions opened from a visible trigger.
39
39
  | MenuLinkItem | Base UI Menu.LinkItem props (href, target, closeOnClick) + className + icon?: ReactNode + description?: ReactNode | A row that navigates: a real <a>, so it opens in a new tab, middle-clicks and copies as a link. With target="_blank" it adds rel="noopener noreferrer" and a trailing ↗. Takes icon and description like MenuItem. |
40
40
  | MenuSeparator | Base UI Separator props + className | Hairline rule between item runs. |
41
41
  | MenuSubmenu | Base UI Menu.SubmenuRoot props | A nested menu. Wraps a MenuSubmenuTrigger and its own MenuContent — pass side="inline-end" to that content so the submenu opens away from its parent in RTL as well as LTR. |
42
- | MenuSubmenuTrigger | Base UI Menu.SubmenuTrigger props + value: ReactNode + className | The row that opens a submenu: item styling, a trailing chevron, and an optional `value` showing the current choice so the row reads "Effort High ›" rather than making the reader open it to find out. |
42
+ | MenuSubmenuTrigger | Base UI Menu.SubmenuTrigger props + value: ReactNode + icon: ReactNode + className | The row that opens a submenu: item styling, a trailing chevron, and an optional `value` showing the current choice so the row reads "Effort High ›" rather than making the reader open it to find out. `icon` leads the row as MenuItem's does; give it one whenever the items around it have icons, or its label starts a column to their left. |
43
43
  | MenuRadioGroup | Base UI Menu.RadioGroup props (value, defaultValue, onValueChange) | A single-choice group inside a menu. Controlled via value/onValueChange, uncontrolled via defaultValue. |
44
44
  | MenuRadioItem | Base UI Menu.RadioItem props + className (value, disabled) | One option in a MenuRadioGroup, rendered as menuitemradio with a real checked state. The check sits in a reserved leading column so labels never shift sideways as the highlight moves. |
45
45
  | MenuCheckboxItem | Base UI Menu.CheckboxItem props + className + { checked?: boolean; defaultChecked?: boolean; onCheckedChange?: (checked: boolean) => void; disabled?: boolean } | A toggling row, with the same reserved leading column as MenuRadioItem so a menu mixing the two keeps one text margin. |
@@ -4,7 +4,7 @@
4
4
 
5
5
  In-flow page heading block: a title row (optional back control + actions) over an optional description, with room for children (filters, tabs) below.
6
6
 
7
- **Keywords:** page header, title, lede, page actions
7
+ **Keywords:** page header, title, lede, page actions, page title size, display title, editorial header, hero title
8
8
 
9
9
  ## Usage
10
10
 
@@ -14,6 +14,8 @@ Use PageHeader to establish a page's title, supporting description, and primary
14
14
 
15
15
  - Do: Use one PageHeader per major page or route.
16
16
  - Do: Keep the title literal and put explanation in the lede or nearby content.
17
+ - Do: Use size="lg" for an editorial page (a personal site, a marketing page, an article index): a display-tier title over a body-lg lede. Pair it with SectionHeader size="lg" for the sections below, so the steps stay display → heading-2 → body.
18
+ - Don't: Use size="lg" on product routes (settings, lists, dashboards). Display type is never product chrome: there it competes with the content it frames. Keep md, and do not restyle the title with a display class through className either.
17
19
  - Don't: Do not use PageHeader inside repeated cards, dialogs, or compact panels.
18
20
  - Don't: Do not use a second PageHeader for a section of the page. It is the route's one h1; each section below it takes a SectionHeader (an h2 with the same title row, description and actions).
19
21
  - Don't: Do not put it outside the Region that holds the page content. It then sits at a different measure from what it introduces, and the page reads as two documents.
@@ -27,6 +29,11 @@ Use PageHeader to establish a page's title, supporting description, and primary
27
29
  | 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
30
  | 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
31
  | 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. |
32
+ | size | "md" \| "lg" | "md" | The title's scale. md: text-heading-2 over a text-body-sm description, for product pages. lg: text-display-lg over a text-body-lg lede capped at the prose measure, with more space between and below, for an editorial page (personal site, marketing page, article index). Beside actions or a back control, a title line taller than the controls lifts by half the difference, so its first line stays centred on them. |
33
+
34
+ ## Variants
35
+
36
+ - **size**: md, lg
30
37
 
31
38
  ## Tokens used
32
39
 
@@ -51,6 +58,14 @@ import { Button, PageHeader } from "@sableui/react"
51
58
  <PageHeader title="Storage credits" description="Fund permanent storage." actions={<Button>Top up</Button>} />
52
59
  ```
53
60
 
61
+ ### Editorial page title (size lg)
62
+
63
+ ```tsx
64
+ import { Button, PageHeader } from "@sableui/react"
65
+
66
+ <PageHeader size="lg" title="Field notes" description="Essays on making things slowly, from a studio by the sea." actions={<Button variant="outline">Subscribe</Button>} />
67
+ ```
68
+
54
69
  ### One per page, inside the region
55
70
 
56
71
  ```tsx
@@ -4,7 +4,7 @@
4
4
 
5
5
  A row of docked chrome panels around a flexible centre, with draggable and keyboard-resizable edges and optional persisted widths.
6
6
 
7
- **Keywords:** panel, panels, resize, resizable, splitter, docked, workspace, tool, chrome, inspector, layout
7
+ **Keywords:** panel, panels, resize, resizable, splitter, docked, workspace, tool, chrome, inspector, layout, mobile, responsive, sheet, drawer, narrow, detail pane
8
8
 
9
9
  ## Usage
10
10
 
@@ -15,6 +15,8 @@ Use PanelGroup for the body of a tool workspace: fixed-width Panels docked left
15
15
  - Do: Give every docked Panel a fixed width and leave the centre Panel width-less so it absorbs the remaining space.
16
16
  - Do: Drive the width with usePanelWidth and pass a storageKey, so a user's layout survives a reload. Spread its bounds onto the PanelResizer rather than repeating min and max by hand.
17
17
  - Do: Label every Panel — they are landmarks, and 'Layers' or 'Properties' is what a screen-reader user navigates by.
18
+ - Do: Decide what each docked Panel becomes on a phone with `narrow`: "sheet" for an inspector or a navigation rail the user opens on demand, "hidden" for chrome a phone does without. Drive a sheet's `open` from the same state that selects the record, so selecting a row opens the inspector at every width.
19
+ - Don't: Render the same inspector twice, once in a Panel and once in a Sheet behind a hand-rolled viewport check. `narrow="sheet"` renders the content once, in whichever place applies.
18
20
  - Don't: Let the group scroll. Only panels and the canvas scroll; a scrolling group means the shell lost its height constraint.
19
21
  - Don't: Use it for page content columns — that's Grid or Columns. PanelGroup is chrome, and it sits outside the content grid.
20
22
  - Don't: Omit PanelResizer to save effort and then let users live with a fixed panel. If a panel holds a tree or a property grid, its width is a preference.
@@ -31,7 +33,7 @@ Use PanelGroup for the body of a tool workspace: fixed-width Panels docked left
31
33
 
32
34
  | Name | Type | Description |
33
35
  | --- | --- | --- |
34
- | Panel | { label: string; width?: number; scroll?: boolean } | A docked or flexible column. `label` names the landmark; omit `width` for the flexible centre. `scroll` defaults to true — set it false for a header + scrolling body + pinned footer, so the footer doesn't scroll away with the list. When you own the layout, give the header and footer shrink-0 and the middle min-h-0 flex-1 overflow-y-auto. overflow-y-auto only activates if every flex ancestor can shrink: a single missing min-h-0 anywhere up the chain makes the panel grow instead of scroll, and nothing reports it. |
36
+ | Panel | { label: string; width?: number; scroll?: boolean; narrow?: "dock" \| "sheet" \| "hidden"; open?: boolean; onOpenChange?: (open: boolean) => void; side?: "left" \| "right"; below?: "sm" \| "md" \| "lg" \| "xl" \| "2xl" } | A docked or flexible column. Below `below` (default lg, where Sidebar and BottomNav swap), `narrow` decides what it becomes: "dock" (default) stays docked, "sheet" renders the same content once inside a Sheet from `side` (default "right"; a panel before the canvas is "left"), titled by `label`, open while `open` is true and closed through `onOpenChange` (Esc, scrim, swipe), and "hidden" drops it. `open` defaults to true docked (false removes the column, so the canvas takes its width) and false as a sheet. Crossing the breakpoint remounts the content, so keep state that must survive it in the app. `label` names the landmark; omit `width` for the flexible centre. `scroll` defaults to true — set it false for a header + scrolling body + pinned footer, so the footer doesn't scroll away with the list. When you own the layout, give the header and footer shrink-0 and the middle min-h-0 flex-1 overflow-y-auto. overflow-y-auto only activates if every flex ancestor can shrink: a single missing min-h-0 anywhere up the chain makes the panel grow instead of scroll, and nothing reports it. |
35
37
  | PanelResizer | { side: 'left' \| 'right'; width: number; onWidthChange: (n: number) => void; label: string; min?: number; max?: number; step?: number } | The draggable edge. `side` says which side the resized panel is on, so the drag delta's sign is right. |
36
38
  | usePanelWidth | (defaultWidth: number, options?: { storageKey?: string; min?: number; max?: number }) => readonly [number, (n: number) => void, { min: number; max: number }] | Controlled width for a Panel, clamped and optionally persisted to localStorage (only when `storageKey` is passed). Read through useSyncExternalStore, so there is no server/client hydration mismatch and no setState in an effect; do not hand-roll a localStorage read during render or a write from an effect. Without `storageKey` the width is kept in memory per hook instance. The third element is the `{ min, max }` it clamps to: spread it onto the paired PanelResizer so the separator's aria-valuemin/max and Home/End match the real clamp. |
37
39
 
@@ -110,6 +112,36 @@ function Body() {
110
112
  }
111
113
  ```
112
114
 
115
+ ### Inspector that becomes a sheet on phones
116
+
117
+ ```tsx
118
+ import { Button, Input, Panel, PanelGroup, PanelSection, PropertyRow } from "@sableui/react"
119
+
120
+ function Body() {
121
+ const [selected, setSelected] = useState<string | null>(null)
122
+ return (
123
+ <PanelGroup>
124
+ <Panel label="Issues">
125
+ <div className="p-2">
126
+ <Button size="xs" variant="ghost" onClick={() => setSelected("AES-104")}>AES-104 Refine product page</Button>
127
+ </div>
128
+ </Panel>
129
+ <Panel
130
+ label="Issue details"
131
+ width={320}
132
+ narrow="sheet"
133
+ open={selected !== null}
134
+ onOpenChange={(open) => !open && setSelected(null)}
135
+ >
136
+ <PanelSection title={selected ?? ""}>
137
+ <PropertyRow label="Status"><Input defaultValue="In progress" /></PropertyRow>
138
+ </PanelSection>
139
+ </Panel>
140
+ </PanelGroup>
141
+ )
142
+ }
143
+ ```
144
+
113
145
  ### Fixed panel, no resizer
114
146
 
115
147
  ```tsx
@@ -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,9 +2,9 @@
2
2
 
3
3
  **Status:** stable
4
4
 
5
- Desktop side-navigation rail (in-flow sticky, hidden below lg) with SidebarHeader / SidebarBrand / SidebarNav / SidebarItem / SidebarFooter parts. Items are router-agnostic via the `render` prop; an optional collapsed icon-rail mode reveals labels in tooltips.
5
+ Desktop side-navigation rail (in-flow sticky, hidden below lg) with SidebarHeader / SidebarBrand / SidebarNav / SidebarGroup / SidebarItem / SidebarFooter / SidebarAccount parts, all on one set of columns. Items are router-agnostic via the `render` prop; an optional collapsed icon-rail mode reveals labels in tooltips.
6
6
 
7
- **Keywords:** sidebar, navigation, app chrome, rail
7
+ **Keywords:** sidebar, navigation, app chrome, rail, nav group, section label, account, user menu, profile, account switcher, workspace switcher, sidebar footer
8
8
 
9
9
  ## Usage
10
10
 
@@ -16,6 +16,9 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
16
16
  - Do: Keep item labels predictable and active state obvious.
17
17
  - Do: Set surface="page" when a full-width top bar sits above the rail. A theme's tinted or dark rail is for a full-height rail that frames the app.
18
18
  - Do: Keep surface="rail" inside AppShell frame="inset": the rail's tint becomes the ground the main panel floats on. Do not add a border-r class back — the gap is the divider.
19
+ - Do: Group items with SidebarGroup and end the rail with one SidebarAccount that opens the account menu (settings, appearance, sign out). Every part sits on the same icon and label columns, at either density.
20
+ - Don't: Do not put SideNav, an Entity or a stack of ghost Buttons in the rail. Each brings its own insets, so the icons and labels land on four different columns. SideNav is the in-page section list; the rail has its own parts.
21
+ - Don't: Do not put a Badge or pill in the account row. Its one quiet detail line carries the plan, role or 'Demo'.
19
22
  - Don't: Do not use Sidebar for page-local settings, filters, or temporary action groups.
20
23
  - Don't: Do not put page content in it. It is fixed-width chrome OUTSIDE the content grid, so anything in it is exempt from the page's measure and rhythm.
21
24
 
@@ -30,9 +33,11 @@ Use Sidebar for persistent desktop navigation or application chrome outside the
30
33
 
31
34
  | Name | Type | Description |
32
35
  | --- | --- | --- |
33
- | 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. |
36
+ | SidebarHeader | div props | The top of the rail, at least 64px tall (the chrome bar's height inside a compact Workspace, so it lines up with the WorkspaceBar beside it). 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. |
34
37
  | 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. |
35
- | 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" />}. |
38
+ | SidebarItem | { label: ReactNode; icon?: ReactNode; active?: boolean; badge?: ReactNode; render?: useRender.RenderProp } + button props | A single nav row, 44px on a page and 32px inside a compact scope such as a Workspace (the rail reads the density; nothing to set). 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" />}. |
39
+ | SidebarGroup | { label?: ReactNode } + div props | A labelled run of SidebarItems inside SidebarNav ("Workspace", "Favorites"). The label starts on the items' icon column; collapsed, a hairline stands in for it and the label stays as the group's accessible name. |
40
+ | SidebarAccount | { avatar: ReactNode; label: ReactNode; detail?: ReactNode; indicator?: ReactNode \| false; render?: useRender.RenderProp } + button props | The account row at the foot of the rail, inside SidebarFooter: the avatar in the icon slot (Avatar xs on a page rail, 2xs in a compact one), the label on the label column, one muted detail line, and an up-down chevron. Make it the trigger of the account Menu (render it through MenuTrigger render) and put Settings, appearance and sign-out there; it holds its fill while the menu is open. Collapsed, the avatar alone, with the label in a tooltip. |
36
41
 
37
42
  ## Props
38
43
 
@@ -92,6 +97,14 @@ const [collapsed, setCollapsed] = useState(false)
92
97
  <Sidebar collapsed={collapsed}><SidebarNav><SidebarItem icon={<Icon name="home" />} label="Home" active render={<Link to="/" />} /></SidebarNav><SidebarFooter><Button variant="ghost" size="icon" onClick={() => setCollapsed((c) => !c)} aria-label="Toggle sidebar"><Icon name="chevron-left" /></Button></SidebarFooter></Sidebar>
93
98
  ```
94
99
 
100
+ ### Grouped nav with an account menu
101
+
102
+ ```tsx
103
+ import { Avatar, Icon, Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger, Sidebar, SidebarAccount, SidebarBrand, SidebarFooter, SidebarGroup, SidebarHeader, SidebarItem, SidebarNav } from "@sableui/react"
104
+
105
+ <Sidebar><SidebarHeader><SidebarBrand mark={<Avatar shape="square" size="xs" fallback="F" alt="" />} label="Forma Studio" /></SidebarHeader><SidebarNav><SidebarItem icon={<Icon name="inbox" />} label="Inbox" badge={<span className="text-ui-num">4</span>} /><SidebarGroup label="Workspace"><SidebarItem icon={<Icon name="folder" />} label="Projects" active /><SidebarItem icon={<Icon name="users" />} label="Clients" /></SidebarGroup></SidebarNav><SidebarFooter><Menu><MenuTrigger render={<SidebarAccount avatar={<Avatar size="xs" fallback="EL" alt="" />} label="Emma Lee" detail="Forma Studio" />} /><MenuContent side="top" align="start"><MenuItem>Settings</MenuItem><MenuSeparator /><MenuItem>Sign out</MenuItem></MenuContent></Menu></SidebarFooter></Sidebar>
106
+ ```
107
+
95
108
  ### Inline (inside a bounded frame)
96
109
 
97
110
  ```tsx
@@ -13,6 +13,7 @@ Use Tabs to switch between related panels within the same page context.
13
13
  ### Best practices
14
14
 
15
15
  - Do: Use tabs when each panel is peer content and users may compare or switch often.
16
+ - Do: In tool chrome (a WorkspaceBar, a compact panel header) use variant="pill" for a view switch such as Overview / Issues / Updates: it reads as a control beside the bar's other controls. Underline is the page-section idiom; inside a WorkspaceBar it sits on the bar's hairline rather than drawing its own.
16
17
  - Do: Keep tab labels short and make the active tab unmistakable.
17
18
  - Do: Use variant="pill" for a short switcher inside a card or toolbar (Preview / Code, List / Grid); keep the default underline for sections of a page.
18
19
  - Don't: Do not use Tabs for unrelated pages or primary navigation that needs distinct URLs. → use **SideNav**, **NavigationMenu**