@sableui/react 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/AGENTS.md +157 -0
  2. package/CHANGELOG.md +55 -0
  3. package/CONTRIBUTING.md +221 -0
  4. package/DESIGN.md +548 -0
  5. package/LICENSE +21 -0
  6. package/README.md +194 -2
  7. package/THIRD_PARTY_NOTICES.md +44 -0
  8. package/bin/sable-lint.mjs +0 -0
  9. package/bin/sable.mjs +1223 -0
  10. package/dist/_contract-CYScOdf5.d.ts +150 -0
  11. package/dist/chunk-3IWZYBGH.js +398 -0
  12. package/dist/chunk-5Z5KLOAU.js +167 -0
  13. package/dist/chunk-7MG4SHYX.js +15 -0
  14. package/dist/chunk-ADLLDWLW.js +15 -0
  15. package/dist/chunk-EP55KSYA.js +14 -0
  16. package/dist/chunk-J23ELQQZ.js +9396 -0
  17. package/dist/chunk-JI7WJP2L.js +286 -0
  18. package/dist/chunk-K7RUHUGC.js +14 -0
  19. package/dist/chunk-KO3NAEBF.js +15 -0
  20. package/dist/chunk-L4IEIQQA.js +14 -0
  21. package/dist/chunk-L6Y46C62.js +270 -0
  22. package/dist/chunk-NEQNXI2G.js +25 -0
  23. package/dist/chunk-PAOZBZME.js +40 -0
  24. package/dist/chunk-RWP4XBSK.js +139 -0
  25. package/dist/chunk-SRQWQUTW.js +63 -0
  26. package/dist/chunk-YXBOYWBN.js +15 -0
  27. package/dist/foundations/index.d.ts +88 -0
  28. package/dist/foundations/index.js +792 -0
  29. package/dist/icon/index.d.ts +60 -0
  30. package/dist/icon/index.js +2 -0
  31. package/dist/icon-sets/brands.d.ts +44 -0
  32. package/dist/icon-sets/brands.js +147 -0
  33. package/dist/icon-sets/reicon.d.ts +8 -0
  34. package/dist/icon-sets/reicon.js +1 -0
  35. package/dist/index.d.ts +7013 -0
  36. package/dist/index.js +15462 -0
  37. package/dist/ios/chrome.css +83 -0
  38. package/dist/ios/frame.css +5 -0
  39. package/dist/ios/index.d.ts +70 -0
  40. package/dist/ios/index.js +313 -0
  41. package/dist/ios/preview.d.ts +64 -0
  42. package/dist/ios/preview.js +133 -0
  43. package/dist/ios/styles.css +77 -0
  44. package/dist/layout/index.d.ts +280 -0
  45. package/dist/layout/index.js +2 -0
  46. package/dist/meta-6dUzgC6M.d.ts +205 -0
  47. package/dist/patterns/index.d.ts +36 -0
  48. package/dist/patterns/index.js +934 -0
  49. package/dist/presets/index.d.ts +37 -0
  50. package/dist/presets/index.js +117 -0
  51. package/dist/registry-CyL28OzJ.d.ts +28 -0
  52. package/dist/styles.css +1893 -0
  53. package/dist/theme-provider-BWcizhAt.d.ts +105 -0
  54. package/dist/themes/atelier/atelier.css +213 -0
  55. package/dist/themes/atelier/index.d.ts +9 -0
  56. package/dist/themes/atelier/index.js +1 -0
  57. package/dist/themes/base/base.css +443 -0
  58. package/dist/themes/base/index.d.ts +9 -0
  59. package/dist/themes/base/index.js +1 -0
  60. package/dist/themes/ember/ember.css +270 -0
  61. package/dist/themes/ember/index.d.ts +9 -0
  62. package/dist/themes/ember/index.js +1 -0
  63. package/dist/themes/index.d.ts +10 -0
  64. package/dist/themes/index.js +8 -0
  65. package/dist/themes/meridian/index.d.ts +9 -0
  66. package/dist/themes/meridian/index.js +1 -0
  67. package/dist/themes/meridian/meridian.css +255 -0
  68. package/dist/themes/poppy/index.d.ts +6 -0
  69. package/dist/themes/poppy/index.js +1 -0
  70. package/dist/themes/poppy/poppy.css +174 -0
  71. package/dist/themes/prism/index.d.ts +6 -0
  72. package/dist/themes/prism/index.js +1 -0
  73. package/dist/themes/prism/prism.css +183 -0
  74. package/dist/themes/verdant/index.d.ts +9 -0
  75. package/dist/themes/verdant/index.js +1 -0
  76. package/dist/themes/verdant/verdant.css +210 -0
  77. package/dist/themes.css +30 -0
  78. package/llms/components/accordion.md +93 -0
  79. package/llms/components/app-header.md +70 -0
  80. package/llms/components/app-shell.md +117 -0
  81. package/llms/components/attachment.md +105 -0
  82. package/llms/components/avatar.md +141 -0
  83. package/llms/components/badge.md +117 -0
  84. package/llms/components/banner.md +119 -0
  85. package/llms/components/bottom-nav.md +80 -0
  86. package/llms/components/breadcrumbs.md +75 -0
  87. package/llms/components/browser.md +67 -0
  88. package/llms/components/bubble.md +119 -0
  89. package/llms/components/button.md +199 -0
  90. package/llms/components/calendar.md +75 -0
  91. package/llms/components/card.md +135 -0
  92. package/llms/components/carousel.md +87 -0
  93. package/llms/components/chat-composer.md +126 -0
  94. package/llms/components/checkbox-card.md +98 -0
  95. package/llms/components/checkbox.md +90 -0
  96. package/llms/components/code-block.md +81 -0
  97. package/llms/components/code.md +62 -0
  98. package/llms/components/collapsible.md +83 -0
  99. package/llms/components/color-field.md +100 -0
  100. package/llms/components/combobox.md +119 -0
  101. package/llms/components/command-menu.md +158 -0
  102. package/llms/components/context-card.md +73 -0
  103. package/llms/components/context-menu.md +79 -0
  104. package/llms/components/data-table.md +99 -0
  105. package/llms/components/date-picker.md +97 -0
  106. package/llms/components/description.md +96 -0
  107. package/llms/components/destructive-action-modal.md +76 -0
  108. package/llms/components/dialog.md +127 -0
  109. package/llms/components/dots-menu.md +64 -0
  110. package/llms/components/empty-state.md +113 -0
  111. package/llms/components/entity.md +67 -0
  112. package/llms/components/feedback.md +63 -0
  113. package/llms/components/fieldset.md +106 -0
  114. package/llms/components/file-dropzone.md +103 -0
  115. package/llms/components/form.md +111 -0
  116. package/llms/components/gliding-highlight.md +84 -0
  117. package/llms/components/inline-status.md +62 -0
  118. package/llms/components/input.md +127 -0
  119. package/llms/components/kbd.md +57 -0
  120. package/llms/components/label.md +73 -0
  121. package/llms/components/marker.md +99 -0
  122. package/llms/components/marquee.md +66 -0
  123. package/llms/components/menu.md +122 -0
  124. package/llms/components/message-scroller.md +110 -0
  125. package/llms/components/message.md +114 -0
  126. package/llms/components/meter.md +124 -0
  127. package/llms/components/middle-truncate.md +62 -0
  128. package/llms/components/note.md +81 -0
  129. package/llms/components/notification-badge.md +67 -0
  130. package/llms/components/number-field.md +70 -0
  131. package/llms/components/otp-field.md +101 -0
  132. package/llms/components/page-header.md +76 -0
  133. package/llms/components/page-loader.md +67 -0
  134. package/llms/components/pagination.md +92 -0
  135. package/llms/components/panel-group.md +120 -0
  136. package/llms/components/panel-section.md +122 -0
  137. package/llms/components/phone.md +70 -0
  138. package/llms/components/pill.md +73 -0
  139. package/llms/components/popover.md +79 -0
  140. package/llms/components/progress.md +98 -0
  141. package/llms/components/radio-card.md +90 -0
  142. package/llms/components/radio.md +86 -0
  143. package/llms/components/relative-time.md +60 -0
  144. package/llms/components/scroller.md +88 -0
  145. package/llms/components/scrub-field.md +97 -0
  146. package/llms/components/search-input.md +84 -0
  147. package/llms/components/select.md +106 -0
  148. package/llms/components/separator.md +70 -0
  149. package/llms/components/settings-menu.md +139 -0
  150. package/llms/components/sheet.md +97 -0
  151. package/llms/components/show-more.md +59 -0
  152. package/llms/components/side-nav.md +106 -0
  153. package/llms/components/sidebar.md +87 -0
  154. package/llms/components/skeleton.md +62 -0
  155. package/llms/components/slider.md +114 -0
  156. package/llms/components/spinner.md +87 -0
  157. package/llms/components/status-bar.md +91 -0
  158. package/llms/components/status-dot.md +70 -0
  159. package/llms/components/stepper.md +125 -0
  160. package/llms/components/streaming-text.md +86 -0
  161. package/llms/components/switch.md +87 -0
  162. package/llms/components/table.md +112 -0
  163. package/llms/components/tabs.md +84 -0
  164. package/llms/components/textarea.md +88 -0
  165. package/llms/components/theme-selector.md +95 -0
  166. package/llms/components/theme-switcher.md +71 -0
  167. package/llms/components/thinking-orb.md +91 -0
  168. package/llms/components/toast.md +130 -0
  169. package/llms/components/toggle.md +95 -0
  170. package/llms/components/toolbar.md +168 -0
  171. package/llms/components/tooltip.md +93 -0
  172. package/llms/components/tree.md +227 -0
  173. package/llms/components/video.md +66 -0
  174. package/llms/components/workspace.md +138 -0
  175. package/llms/foundations/accessibility.md +60 -0
  176. package/llms/foundations/motion.md +96 -0
  177. package/llms/foundations/scrolling.md +41 -0
  178. package/llms/foundations/shape.md +73 -0
  179. package/llms/foundations/spacing.md +82 -0
  180. package/llms/foundations/surfaces.md +69 -0
  181. package/llms/foundations/typography.md +120 -0
  182. package/llms/layout/aspect-box.md +37 -0
  183. package/llms/layout/bleed.md +31 -0
  184. package/llms/layout/center.md +37 -0
  185. package/llms/layout/columns.md +51 -0
  186. package/llms/layout/grid-overlay.md +40 -0
  187. package/llms/layout/grid.md +47 -0
  188. package/llms/layout/page.md +39 -0
  189. package/llms/layout/region.md +49 -0
  190. package/llms/layout/row.md +40 -0
  191. package/llms/layout/safe-area.md +37 -0
  192. package/llms/layout/spacer.md +39 -0
  193. package/llms/layout/split-pane.md +41 -0
  194. package/llms/layout/stack.md +39 -0
  195. package/llms/patterns/auth-page.md +68 -0
  196. package/llms/patterns/brand-landing-page.md +423 -0
  197. package/llms/patterns/chat-panel.md +210 -0
  198. package/llms/patterns/dashboard-page.md +170 -0
  199. package/llms/patterns/feed-page.md +103 -0
  200. package/llms/patterns/landing-page.md +122 -0
  201. package/llms/patterns/list-detail-page.md +100 -0
  202. package/llms/patterns/settings-page.md +121 -0
  203. package/llms/reference/token-intent.md +324 -0
  204. package/llms/reference/utilities.md +129 -0
  205. package/llms/tasks/adding-a-component.md +113 -0
  206. package/llms/tasks/auditing-an-app.md +216 -0
  207. package/llms/tasks/building-a-page.md +109 -0
  208. package/llms/tasks/building-a-tool.md +141 -0
  209. package/llms/tasks/building-an-ios-prototype.md +105 -0
  210. package/llms/tasks/building-with-sable.md +73 -0
  211. package/llms/tasks/choosing-a-component.md +109 -0
  212. package/llms/tasks/converting-an-app.md +139 -0
  213. package/llms/tasks/creating-a-preset.md +43 -0
  214. package/llms/tasks/creating-design-assets.md +96 -0
  215. package/llms/tasks/designing-a-surface.md +115 -0
  216. package/llms/tasks/developing-taste.md +157 -0
  217. package/llms/tasks/getting-started.md +63 -0
  218. package/llms/tasks/learning-from-builds.md +74 -0
  219. package/llms/tasks/reviewing-a-design.md +114 -0
  220. package/llms/tasks/reviewing-a-journey.md +84 -0
  221. package/llms/tasks/reviewing-motion.md +92 -0
  222. package/llms/tasks/using-presets.md +87 -0
  223. package/llms/taste/asset-plan.template.json +53 -0
  224. package/llms/taste/composition-follows-content.md +20 -0
  225. package/llms/taste/hierarchy-is-a-relationship.md +22 -0
  226. package/llms/taste/lessons.json +57 -0
  227. package/llms/taste/motion-needs-a-reason.md +21 -0
  228. package/llms/taste/surface-states-travel-together.md +20 -0
  229. package/llms-full.txt +14906 -0
  230. package/llms.txt +405 -0
  231. package/package.json +185 -3
  232. package/registry.json +18812 -0
  233. package/scripts/agent-setup.mjs +54 -0
  234. package/scripts/design-atlas.mjs +221 -0
  235. package/scripts/design-directions.mjs +123 -0
  236. package/scripts/design-memory.mjs +166 -0
  237. package/scripts/design-preferences.mjs +227 -0
  238. package/scripts/design-presets.mjs +48 -0
  239. package/scripts/grade.mjs +509 -0
  240. package/scripts/lint-rules.mjs +1065 -0
  241. package/scripts/review-evidence.mjs +120 -0
  242. package/scripts/skeleton.mjs +248 -0
  243. package/skills/entry/SKILL.md +34 -0
  244. package/skills/sable/SKILL.md +187 -0
  245. package/skills/sable/agents/openai.yaml +6 -0
  246. package/targets/ios.json +982 -0
  247. package/third-party/reicon-LICENSE.txt +20 -0
@@ -0,0 +1,86 @@
1
+ # Radio
2
+
3
+ **Status:** stable
4
+
5
+ Single-select control over Base UI RadioGroup + Radio (roving focus, arrow-key nav, ARIA radiogroup wiring, form integration). RadioGroup + Radio.
6
+
7
+ **Keywords:** radio, radio group, single choice, form control
8
+
9
+ ## Usage
10
+
11
+ Use Radio when a user must choose exactly one option from a visible set.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use it when all options should be visible for comparison.
16
+ - Do: Keep option labels mutually exclusive and easy to understand.
17
+ - Don't: Do not use Radio for independent toggles or multiple selection; use Checkbox or Switch.
18
+ - Don't: Do not use a radio group with only one option, and do not omit the Fieldset. Without the fieldset the options share no accessible name, so a screen reader reads the choices with no question attached.
19
+
20
+ ### Anatomy
21
+
22
+ | Part | Required | Description |
23
+ | --- | --- | --- |
24
+ | RadioGroup | yes | The grouped set that manages one selected value. |
25
+ | Radio | yes | One selectable option in the group. |
26
+
27
+ ## Subcomponents
28
+
29
+ | Name | Type | Description |
30
+ | --- | --- | --- |
31
+ | RadioGroup | Base UI RadioGroup props (value, defaultValue, onValueChange, name, disabled, readOnly, required) + className | The group container that wraps Radio items and shares selection state across them. Controlled via value + onValueChange; uncontrolled via defaultValue. Lays its Radio items out in a vertical stack. |
32
+
33
+ ## Props
34
+
35
+ | Name | Type | Default | Description |
36
+ | --- | --- | --- | --- |
37
+ | Radio | Base UI Radio.Root props (value, disabled, readOnly, required) + className | — | A single radio button. value pairs it with the group's selected value. Renders a native button with radio semantics, the ring and an inner Indicator dot. A custom non-button render must also set nativeButton={false}. |
38
+
39
+ ## Tokens used
40
+
41
+ `input`, `card`, `primary`, `ring`
42
+
43
+ ## Accessibility
44
+
45
+ - Base UI provides the radiogroup/radio roles, aria-checked, hidden native inputs, and roving tabindex.
46
+ - Arrow keys move and select between items; Space selects the focused item; Tab enters/leaves the group.
47
+ - Checked state styling reads from the data-[checked] attribute; disabled items from the disabled state.
48
+ - Focus-visible draws a 2px ring on the focused item. Wrap each Radio in <Field label orientation="horizontal"> so the label names it, is part of the hit target, sits on the first line, and dims with it when disabled — do not hand-build a <label> row.
49
+
50
+ ## Related
51
+
52
+ - Components: Checkbox, RadioCard, Field, Fieldset, Select
53
+
54
+ ## Examples
55
+
56
+ ### Basic
57
+
58
+ ```tsx
59
+ import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } from "@sableui/react"
60
+
61
+ <Fieldset>
62
+ <FieldsetLegend>Post type</FieldsetLegend>
63
+ <FieldsetContent>
64
+ <RadioGroup defaultValue="standard">
65
+ <Field label="Standard" orientation="horizontal"><Radio value="standard" /></Field>
66
+ <Field label="Edition" orientation="horizontal"><Radio value="edition" /></Field>
67
+ </RadioGroup>
68
+ </FieldsetContent>
69
+ </Fieldset>
70
+ ```
71
+
72
+ ### With a disabled option
73
+
74
+ ```tsx
75
+ import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } from "@sableui/react"
76
+
77
+ <Fieldset>
78
+ <FieldsetLegend>Post type</FieldsetLegend>
79
+ <FieldsetContent>
80
+ <RadioGroup defaultValue="standard">
81
+ <Field label="Standard" orientation="horizontal"><Radio value="standard" /></Field>
82
+ <Field label="Edition" orientation="horizontal" disabled><Radio value="edition" /></Field>
83
+ </RadioGroup>
84
+ </FieldsetContent>
85
+ </Fieldset>
86
+ ```
@@ -0,0 +1,60 @@
1
+ # RelativeTime
2
+
3
+ **Status:** stable
4
+
5
+ A human relative timestamp (e.g. "3 minutes ago") that reveals the full absolute date/time on hover or focus. Renders a native <time> element wrapped in our Tooltip; the absolute time is formatted with date-fns format(date, 'PPpp') (e.g. "Apr 29, 2025, 4:05:12 PM").
6
+
7
+ **Keywords:** relative time, timestamp, time ago, date
8
+
9
+ ## Usage
10
+
11
+ Use RelativeTime to show recent activity or timestamps in conversational relative language.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use it for feeds, audit summaries, comments, and activity streams.
16
+ - Do: Expose the exact timestamp when precision matters.
17
+ - Don't: Do not use RelativeTime as the only date format for legal, billing, or audit-critical records.
18
+ - Don't: Do not render it during SSR without a stable absolute fallback. "2 minutes ago" computed on the server differs from the client's value, which is a hydration mismatch — and it silently drifts as the page sits open.
19
+
20
+ ## Props
21
+
22
+ | Name | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | date | Date \| string \| number | — | The instant to render. Accepts a Date, an ISO/parseable string, or an epoch number (ms). Coerced to a Date for date-fns. |
25
+ | className | string | — | Merged onto the <time> trigger element (the visible relative label). |
26
+ | ...time props | Omit<ComponentPropsWithRef<"time">, "dateTime" \| "children"> | — | Remaining native <time> attributes pass through to the trigger. dateTime is set automatically from the parsed ISO value. |
27
+
28
+ ## Tokens used
29
+
30
+ `popover`, `popover-foreground`, `border`, `ring`
31
+
32
+ ## Accessibility
33
+
34
+ - Renders a semantic <time> element with a machine-readable dateTime (ISO) attribute.
35
+ - The <time> carries tabIndex=0 (it is not focusable natively), so the absolute time — exposed via Base UI's Tooltip, which opens on keyboard focus as well as hover and wires aria-describedby — is reachable from the keyboard.
36
+ - It inherits the type size of the surrounding text rather than setting its own, so it sits inside a meta row or message header without changing tier.
37
+ - Wrap the page or a group in <TooltipProvider> to share open/close delays.
38
+ - Relative phrasing is supplementary; the dateTime attribute remains the canonical machine value.
39
+
40
+ ## Related
41
+
42
+ - Components: InlineStatus, Table
43
+
44
+ ## Examples
45
+
46
+ ### Basic
47
+
48
+ ```tsx
49
+ import { RelativeTime, TooltipProvider } from "@sableui/react"
50
+
51
+ <TooltipProvider><RelativeTime date={new Date()} /></TooltipProvider>
52
+ ```
53
+
54
+ ### From an ISO string
55
+
56
+ ```tsx
57
+ import { RelativeTime, TooltipProvider } from "@sableui/react"
58
+
59
+ <TooltipProvider><RelativeTime date="2025-04-29T16:05:12Z" /></TooltipProvider>
60
+ ```
@@ -0,0 +1,88 @@
1
+ # Scroller
2
+
3
+ **Status:** stable
4
+
5
+ Scroll container that fades its leading/trailing edges when content overflows. Tracks scroll position to toggle the fades.
6
+
7
+ **Keywords:** scroller, overflow, horizontal scroll, carousel
8
+
9
+ ## Usage
10
+
11
+ Use Scroller to create a contained overflow region for content that naturally extends beyond the viewport.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use it for chip rows, media strips, or comparison sets where horizontal browsing is expected.
16
+ - Do: Keep scroll affordances visible enough that overflow is discoverable.
17
+ - Do: Put everything on `className` — sizing (`max-h-80`, `h-64`, `flex-1 min-h-0`) and chrome (`rounded-lg border`, `bg-card`) alike. It styles the Scroller's outer box, and the scrolling element inside it is `flex-1 min-h-0`, so a cap on the Scroller is still a cap on what scrolls.
18
+ - Do: Pass `label` on any Scroller with real content. It is a focusable region, and an unnamed one announces as a bare group — "Tags" or "Recent covers" costs a word and is the difference between navigable and not.
19
+ - Don't: Do not use Scroller to hide primary page layout or required controls.
20
+ - Don't: Do not add your own wheel handler to make a horizontal strip mouse-scrollable — it already is. A second handler double-scrolls and usually loses the end-release, which traps the page.
21
+ - Don't: Do not reach past it with a child selector (`[&>div]:snap-x`). The scrolling element is addressable by name: `viewportClassName`.
22
+ - Don't: Do not use it to hide overflow you should have laid out. It exists to make an intentionally scrolling strip legible, not to contain content that overflowed by accident.
23
+
24
+ ## Props
25
+
26
+ | Name | Type | Default | Description |
27
+ | --- | --- | --- | --- |
28
+ | orientation | "vertical" \| "horizontal" | "vertical" | "vertical" scrolls and fades top/bottom; "horizontal" scrolls and fades left/right. |
29
+ | wheelScroll | boolean | true | On a horizontal Scroller, let a vertical mouse wheel scroll it sideways. Ignored when vertical. Turn it off only if the region is inside something that owns the wheel itself (a map, a canvas). |
30
+ | label | string | — | Accessible name for the scroll region. The region takes focus, so it should say what it contains; without one a screen reader announces an unnamed group. |
31
+ | viewportClassName | string | — | Classes for the inner element that actually scrolls. Narrow on purpose — `className` is what you want almost always. This is for the few utilities that only mean something on the scrolling box: `snap-x`, `scroll-smooth`, `scroll-p-*`. |
32
+ | ...div props | ComponentPropsWithRef<'div'> | — | Everything except `className`/`style` spreads onto the SCROLLING element — `role`, `id`, `aria-*`, `data-*` and event handlers all describe the thing that scrolls, and CommandMenu relies on it to make this component its listbox. `ref` and `onScroll` likewise address the scroller, and compose with the component's own. |
33
+
34
+ ## Variants
35
+
36
+ - **orientation**: vertical, horizontal
37
+ - **wheelScroll**: true, false
38
+
39
+ ## Tokens used
40
+
41
+ `ring`
42
+
43
+ ## Accessibility
44
+
45
+ - The edge fades are a CSS mask-image on the scrolling element, not overlay elements — there is nothing in the DOM to intercept a pointer or be read by assistive tech, and the content under a fade stays fully interactive.
46
+ - Two boxes: a chrome box carrying `className` (border, background, radius, sizing) and, inside it, the element that actually scrolls and carries the fade mask. They are split because a `mask-image` applies to its element's own border — so with both on one box, a bordered Scroller watched its frame dissolve at the scrolling edge. The axis is published as `data-orientation`, not `aria-orientation`, which is invalid on `role="group"`.
47
+ - The scroll region uses native overflow and contains overscroll, so a gesture that runs out of content does not scroll the page behind it.
48
+ - It is focusable (`tabIndex=0`, `role="group"`) and handles arrows, Home/End and PageUp/PageDown along its own axis. Chrome 128+ and Firefox make scrollable regions focusable on their own; Safari does not, so a strip with a hidden scrollbar and no focusable children is unreachable there. Declaring it makes the behaviour a property of the component, not of the reader's browser. Pass `label` to name it — an unnamed focusable region announces as a bare group.
49
+ - On a horizontal Scroller, a vertical mouse wheel scrolls it sideways, because most mice have no horizontal wheel and the content past the fold is otherwise unreachable on them. The strip releases the wheel at both ends so it never traps the page, and leaves trackpad `deltaX` gestures and `shift+wheel` to the browser.
50
+ - Cross-axis arrow keys are deliberately not captured, and a consumer can take the arrows entirely by calling `preventDefault()` in their own `onKeyDown` — a strip of interactive chips usually wants them for roving focus.
51
+
52
+ ## Related
53
+
54
+ - Components: Carousel, Marquee
55
+
56
+ ## Examples
57
+
58
+ ### Vertical with overflow
59
+
60
+ ```tsx
61
+ import { Scroller } from "@sableui/react"
62
+
63
+ <Scroller label="Editions" className="h-64"><ul className="space-y-2 p-4">{items.map((item) => (<li key={item.id} className="text-body-sm">{item.title}</li>))}</ul></Scroller>
64
+ ```
65
+
66
+ ### Capped, scrolls past the cap
67
+
68
+ ```tsx
69
+ import { Scroller } from "@sableui/react"
70
+
71
+ <Scroller label="Versions" className="max-h-80 rounded-md border border-border"><ul className="divide-y divide-border">{versions.map((v) => (<li key={v.id} className="px-3 py-2 text-body-sm">{v.label}</li>))}</ul></Scroller>
72
+ ```
73
+
74
+ ### Horizontal
75
+
76
+ ```tsx
77
+ import { Scroller } from "@sableui/react"
78
+
79
+ <Scroller orientation="horizontal" label="Recent covers" className="w-full"><div className="flex gap-3 p-4">{covers.map((src) => (<img key={src} src={src} alt="" className="h-32 w-32 rounded-lg object-cover" />))}</div></Scroller>
80
+ ```
81
+
82
+ ### Snap points on the scrolling box
83
+
84
+ ```tsx
85
+ import { Scroller } from "@sableui/react"
86
+
87
+ <Scroller orientation="horizontal" label="Covers" className="w-full rounded-lg border border-border" viewportClassName="snap-x snap-mandatory scroll-smooth"><div className="flex gap-3 p-4">{covers.map((src) => (<img key={src} src={src} alt="" className="h-32 w-32 snap-start rounded-lg object-cover" />))}</div></Scroller>
88
+ ```
@@ -0,0 +1,97 @@
1
+ # ScrubField
2
+
3
+ **Status:** stable
4
+
5
+ The compact numeric field a property grid is built from: a leading glyph inside the field that doubles as a drag-to-scrub handle, the value, and an optional unit.
6
+
7
+ **Keywords:** scrub, numeric, number, field, property, inspector, drag, workspace, tool, unit
8
+
9
+ ## Usage
10
+
11
+ Use ScrubField for numeric properties in tool chrome — an inspector, a property grid, a panel. The leading icon or letter says what the number is and is also the thing you drag, which is why a value like opacity needs no slider. Reach for NumberField instead in page-register forms: it has steppers and locale formatting, and it renders at page-register height. Those two steppers put a ~112px floor on a control that needs to be about 44px in a property grid, which is why ScrubField exists rather than a NumberField size variant. The input is sized in ch, so each field is as wide as its longest value and a grid of them lines up without per-field tuning.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Give the field an icon or prefix when it stands alone, so the row has a drag affordance and the value has an identity.
16
+ - Do: Pass the unit as `suffix` rather than baking it into the label — it renders against the number the way reference tools show 100%.
17
+ - Do: Bracket undo with onGestureStart/onGestureEnd. A scrub fires per frame, so without it one undo rewinds a single pixel of the drag.
18
+ - Do: Set `step` to the real precision (0.01 for a normalised threshold). Display precision follows it, so a seed shows 1234 and never 1,234.
19
+ - Don't: Add a separate drag gripper beside the glyph. The icon or prefix already is the scrub handle; a second one is the same affordance twice and costs width a 240px panel does not have.
20
+ - Don't: Pair it with a Slider AND give it a glyph. The slider is already the drag affordance; two on one row is the same gesture twice. Omit icon/prefix when there's a slider.
21
+ - Don't: Use it in a page form. It is chrome-sized and has no label, description, or error slots — that's Field plus Input or NumberField.
22
+
23
+ ## Props
24
+
25
+ | Name | Type | Default | Description |
26
+ | --- | --- | --- | --- |
27
+ | value | number | — | The current value (controlled). |
28
+ | onValueChange | (next: number) => void | — | Fires on commit, arrow-key step, and every frame of a scrub. |
29
+ | min | number | — | Lower clamp, applied to typing and scrubbing. |
30
+ | max | number | — | Upper clamp, applied to typing and scrubbing. |
31
+ | step | number | 1 | Arrow-key and scrub increment, and the source of display precision. Shift multiplies arrows by 10 and divides scrubbing by 10. |
32
+ | label | string | — | Accessible name for the input. Required — the visible glyph is decorative. |
33
+ | icon | string | — | Semantic icon name rendered inside the field; doubles as the scrub handle. Any name the app's IconProvider resolves works — image tools typically supply their own checkerboard glyph for opacity, which is a domain icon rather than one Sable's contract carries. |
34
+ | prefix | ReactNode | — | Short text used instead of an icon (e.g. "X" / "Y"), with the same scrub role. |
35
+ | suffix | ReactNode | — | Unit rendered after the value inside the field (e.g. "%", "px"). |
36
+ | onGestureStart | () => void | — | Fires once when a scrub drag begins — open an undo transaction here, and drop expensive downstream work (e.g. a GPU renderer) to a cheap preview quality; without it a pipeline re-renders at full quality on every pointer move and the drag stutters. |
37
+ | onGestureEnd | () => void | — | Fires when a scrub drag ends — close the undo transaction here and restore full render quality. |
38
+ | disabled | boolean | — | Disables typing and scrubbing. |
39
+ | id | string | — | Input id, for a PropertyRow's htmlFor. |
40
+
41
+ ## Tokens used
42
+
43
+ `foreground`, `muted-foreground`, `ring`, `canvas`, `panel-raised`
44
+
45
+ ## Accessibility
46
+
47
+ - The input carries the accessible name via `label`; the glyph is aria-hidden because it is decorative.
48
+ - Every scrub gesture is reachable from the keyboard: ArrowUp/ArrowDown step by `step`, Shift+Arrow by ten steps.
49
+ - Enter commits, Escape reverts to the last committed value, and blur commits — so a partial edit is never silently kept.
50
+
51
+ ## Related
52
+
53
+ - Components: NumberField, Slider, PanelSection, Workspace, Input
54
+
55
+ ## Examples
56
+
57
+ ### Opacity with a glyph handle
58
+
59
+ ```tsx
60
+ import { ScrubField } from "@sableui/react"
61
+
62
+ <ScrubField label="Opacity" icon="grid" suffix="%" value={60} min={0} max={100} onValueChange={(v) => setOpacity(v)} />
63
+ ```
64
+
65
+ ### X/Y offset pair (PropertyPair puts two fields on one scan line)
66
+
67
+ ```tsx
68
+ import { PropertyPair, ScrubField } from "@sableui/react"
69
+
70
+ <PropertyPair labels={["Offset X", "Offset Y"]}>
71
+ <ScrubField label="Offset X" prefix="X" value={x} min={-128} max={128} onValueChange={setX} />
72
+ <ScrubField label="Offset Y" prefix="Y" value={y} min={-128} max={128} onValueChange={setY} />
73
+ </PropertyPair>
74
+ ```
75
+
76
+ ### No glyph — the property name is the handle
77
+
78
+ ```tsx
79
+ import { PropertyRow, ScrubField } from "@sableui/react"
80
+
81
+ <PropertyRow label="Block size" scrub={{ value: size, onValueChange: setSize, min: 2, max: 256 }}>
82
+ <ScrubField label="Block size" value={size} min={2} max={256} suffix="px" onValueChange={setSize} />
83
+ </PropertyRow>
84
+ ```
85
+
86
+ ### Paired with a slider (no glyph — one drag affordance per row)
87
+
88
+ ```tsx
89
+ import { PropertyRow, ScrubField, Slider } from "@sableui/react"
90
+
91
+ <PropertyRow label="Block size" stacked>
92
+ <div className="flex items-center gap-2">
93
+ <Slider value={32} min={4} max={256} className="min-w-0 flex-1" />
94
+ <ScrubField label="Block size" value={32} min={4} max={256} step={1} suffix="px" className="w-16 shrink-0" onValueChange={(v) => setBlockSize(v)} />
95
+ </div>
96
+ </PropertyRow>
97
+ ```
@@ -0,0 +1,84 @@
1
+ # SearchInput
2
+
3
+ **Status:** stable
4
+
5
+ Styled search field with a leading magnifier and a clearable X. Soft-square (rounded-sm), card surface, 2px focus ring; the clear button appears only when there is a value.
6
+
7
+ **Keywords:** search, input, filter, query
8
+
9
+ ## Usage
10
+
11
+ Use SearchInput for text queries that filter or search a known content set.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use a clear placeholder that names the content being searched.
16
+ - Do: Provide a visible clear affordance when query state persists.
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
+ - 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.
19
+ - 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.
20
+
21
+ ## Props
22
+
23
+ | Name | Type | Default | Description |
24
+ | --- | --- | --- | --- |
25
+ | value | string | — | Controlled value of the search field. |
26
+ | defaultValue | string | — | Uncontrolled initial value. The field tracks its own text, so the clear button still appears and empties it. |
27
+ | 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. |
28
+ | inputClassName | string | — | Classes for the <input> element alone, for the rare input-only override. |
29
+ | onValueChange | (value: string) => void | — | Called with the next string on every keystroke. |
30
+ | onClear | () => void | — | Called when the clear button is pressed. The component also fires onValueChange('') so a controlled value resets. |
31
+ | placeholder | string | "Search" | Placeholder text shown when the field is empty. |
32
+ | disabled | boolean | false | Dims the control and blocks pointer interaction. |
33
+ | ...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. |
34
+
35
+ ## Tokens used
36
+
37
+ `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
38
+
39
+ ## Accessibility
40
+
41
+ - Renders a native <input type="search"> — keyboard and form semantics come for free.
42
+ - Associate a <label htmlFor> (or wrap) — the magnifier glyph is decorative, not a label.
43
+ - The leading search Icon is pointer-events-none and purely decorative.
44
+ - The clear button is a real <button> (Sable's ghost icon Button) with aria-label='Clear search' and a focus-visible ring. It is hidden while the field is disabled.
45
+ - Focus-visible draws a 2px ring in the --ring token; aria-invalid raises the destructive border and ring.
46
+
47
+ ## Related
48
+
49
+ - Components: Input, CommandMenu, Combobox
50
+
51
+ ## Examples
52
+
53
+ ### Controlled
54
+
55
+ ```tsx
56
+ import { SearchInput } from "@sableui/react"
57
+
58
+ const [q, setQ] = useState("")
59
+ <SearchInput value={q} onValueChange={setQ} placeholder="Search posts" />
60
+ ```
61
+
62
+ ### With onClear
63
+
64
+ ```tsx
65
+ import { SearchInput } from "@sableui/react"
66
+
67
+ <SearchInput value={q} onValueChange={setQ} onClear={() => console.log("cleared")} />
68
+ ```
69
+
70
+ ### Uncontrolled, sized as a whole
71
+
72
+ ```tsx
73
+ import { Field, SearchInput } from "@sableui/react"
74
+
75
+ <Field label="Search posts"><SearchInput defaultValue="editions" className="max-w-80" /></Field>
76
+ ```
77
+
78
+ ### Disabled
79
+
80
+ ```tsx
81
+ import { SearchInput } from "@sableui/react"
82
+
83
+ <SearchInput disabled placeholder="Search" />
84
+ ```
@@ -0,0 +1,106 @@
1
+ # Select
2
+
3
+ **Status:** stable
4
+
5
+ Listbox over Base UI Select — typeahead, roving focus, collision-aware popup, ARIA option wiring, and form integration. Trigger shows the selected value; the popup portals to <body>.
6
+
7
+ **Keywords:** select, listbox, dropdown, picker, option, choice, typeahead, form
8
+
9
+ ## Usage
10
+
11
+ Use Select when users choose one value from a known, finite set. Use Combobox for searchable or user-entered values, Combobox `multiple` for several choices, and Radio when all options should remain visible.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use clear item labels and pass label explicitly when children are richer than plain text.
16
+ - Do: Group long option sets with SelectGroup and SelectGroupLabel when categories improve scanning.
17
+ - Do: Wrap in Field when the select belongs to a form so label, helper text, and errors are wired automatically.
18
+ - Do: Leave the trigger's type alone on mobile. Below the sm breakpoint the md trigger renders text-body-lg (17px) so it matches the Input and Textarea beside it, which carry that tier because iOS Safari zooms the page when a focused field computes under 16px. A custom field in the same row should follow: a second paired tier under a max-sm: variant, never a hand-set font-size.
19
+ - Do: Inside a <Workspace> a Select adapts automatically — trigger and popup both pick up the compact density, including across the portal. Do NOT hand-size it per call site; that was necessary before v0.41 and is now drift.
20
+ - Don't: Use Select for very large or fuzzy lists. Use Combobox when typing is the primary way to find an option.
21
+ - Don't: Use Select for binary on/off settings. Use Switch or Checkbox instead.
22
+
23
+ ### Anatomy
24
+
25
+ | Part | Required | Description |
26
+ | --- | --- | --- |
27
+ | Root | yes | Owns value, open state, form integration, and item collection. |
28
+ | Trigger | yes | The button that shows the selected value and opens the listbox. |
29
+ | Content | yes | The positioned popup containing options and optional groups. |
30
+ | Item | yes | A selectable option with selected and highlighted states. |
31
+
32
+ ## Subcomponents
33
+
34
+ | Name | Type | Description |
35
+ | --- | --- | --- |
36
+ | SelectTrigger | Base UI Trigger props + placeholder?: ReactNode + size?: "md" \| "xs" + className | The button. Composes Trigger + Value + a chevron Icon that flips on open (data-[popup-open]). `size="xs"` is the property-grid rung — height, padding, radius AND surface move together, which is why it is a prop and not a className: the xs trigger is an inset `bg-canvas` field with no border, matching ScrubField on the same row. A consumer overriding only the height gets an md-register 8px radius on a 24px box and a raised card sitting among inset fields; one inspector panel had eleven of those, and both tells are visible at a glance. |
37
+ | SelectContent | Base UI Popup props + side, align, sideOffset + className | Portal + Positioner + Popup + List. side="bottom", align="start", sideOffset=6 by default; matches trigger width. |
38
+ | SelectItem | { value: string; disabled?: boolean; label?: string } + className | An option. Composes Item + ItemText + a check ItemIndicator for the selected row. String children double as the item's `label`, so the closed trigger shows the display text — pass `label` explicitly only when children are richer than a string. |
39
+ | SelectGroup | Base UI Group props + className | Optional grouping of related options. |
40
+ | SelectGroupLabel | Base UI GroupLabel props + className | The heading associated with a SelectGroup for screen readers. |
41
+ | SelectValue | Base UI Value props (children render fn) + placeholder | Renders the selected option's label with a placeholder fallback. Already composed inside SelectTrigger; exported for advanced layouts that render the value outside the default trigger. |
42
+ | SelectSeparator | Base UI Separator props + className | A thin themed divider between groups of items. Decorative — Base UI marks it aria-hidden. |
43
+
44
+ ## Props
45
+
46
+ | Name | Type | Default | Description |
47
+ | --- | --- | --- | --- |
48
+ | items | Record<string, ReactNode> \| { value: string; label: ReactNode }[] | — | value → label map (a record, or an array of { value, label }). Derived automatically from literal SelectItem children; pass it explicitly when the items are generated in a loop, or a closed trigger renders the raw value instead of the label. |
49
+ | Select (Root) | Base UI Select.Root props (value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, disabled, required, name, multiple, items) | — | Controls the selected value and open state. Uncontrolled by default. When `items` isn't passed, it is derived from the SelectItem children so the closed trigger shows display text instead of the raw value; pass `items` explicitly when item children aren't plain strings. |
50
+
51
+ ## Tokens used
52
+
53
+ `input`, `card`, `foreground`, `ring`, `border`, `popover`, `popover-foreground`, `accent`, `accent-foreground`, `muted-foreground`
54
+
55
+ ## Motion
56
+
57
+ - **arrival** · SelectContent · `motion-pop` — The popup's transform-origin follows Base UI's anchor, so it grows out of the trigger it belongs to. On a dense form with several selects in a column, that origin is the only thing identifying which field the open list is for.
58
+ - **response** · SelectItem · `motion-interactive` — Highlight follows both pointer and keyboard. Easing it means arrow-keying down a list is continuous motion rather than a background teleporting between rows, which is what makes a long list readable while moving.
59
+ - **transition** · SelectContent · `motion-pop` — Closing collapses back toward the trigger, so the reader's eye is returned to the field that now holds the value they just picked.
60
+ - **activity** · SelectContent · _no motion_ — Select takes a resident list of options. Anything that has to be fetched or filtered as you type is Combobox, which owns that state.
61
+
62
+ ## Accessibility
63
+
64
+ - Base UI provides listbox/option roles, aria-expanded, typeahead, roving focus, and a hidden form input.
65
+ - Esc and click-outside dismiss; arrow keys and Home/End navigate the list.
66
+ - Selected option carries data-[selected]; the highlighted option carries data-[highlighted].
67
+ - Animate the popup via data-[starting-style] / data-[ending-style].
68
+
69
+ ## Related
70
+
71
+ - Components: Combobox, Radio, Field, Input
72
+ - Patterns: SettingsPage, AuthPage
73
+
74
+ ## Examples
75
+
76
+ ### Basic (controlled)
77
+
78
+ ```tsx
79
+ import { Select, SelectContent, SelectItem, SelectTrigger } from "@sableui/react"
80
+
81
+ <Select value={value} onValueChange={setValue}><SelectTrigger placeholder="Choose a font" /><SelectContent><SelectItem value="sans">Sans-serif</SelectItem><SelectItem value="serif">Serif</SelectItem><SelectItem value="mono">Monospace</SelectItem></SelectContent></Select>
82
+ ```
83
+
84
+ ### Grouped
85
+
86
+ ```tsx
87
+ import { SelectContent, SelectGroup, SelectGroupLabel, SelectItem } from "@sableui/react"
88
+
89
+ <SelectContent><SelectGroup><SelectGroupLabel>Weights</SelectGroupLabel><SelectItem value="regular">Regular</SelectItem><SelectItem value="bold">Bold</SelectItem></SelectGroup></SelectContent>
90
+ ```
91
+
92
+ ### Inside Field
93
+
94
+ ```tsx
95
+ import { Field, Select, SelectContent, SelectItem, SelectTrigger } from "@sableui/react"
96
+
97
+ <Field label="Default visibility" description="Controls how new editions appear."><Select defaultValue="public"><SelectTrigger placeholder="Choose visibility" /><SelectContent><SelectItem value="public">Public</SelectItem><SelectItem value="private">Private</SelectItem><SelectItem value="unlisted">Unlisted</SelectItem></SelectContent></Select></Field>
98
+ ```
99
+
100
+ ### Rich item labels
101
+
102
+ ```tsx
103
+ import { Badge, Select, SelectContent, SelectItem, SelectTrigger } from "@sableui/react"
104
+
105
+ <Select defaultValue="edition" items={[{ value: "edition", label: "Edition" }, { value: "collectible", label: "Collectible" }]}><SelectTrigger /><SelectContent><SelectItem value="edition" label="Edition"><span className="flex items-center gap-2"><Badge tone="edition">Edition</Badge><span>Edition drop</span></span></SelectItem><SelectItem value="collectible" label="Collectible"><span className="flex items-center gap-2"><Badge tone="collectible">Collectible</Badge><span>Collectible</span></span></SelectItem></SelectContent></Select>
106
+ ```
@@ -0,0 +1,70 @@
1
+ # Separator
2
+
3
+ **Status:** stable
4
+
5
+ A hairline divider. Horizontal full-width rule or vertical 1px column, tinted with the border token. Optional centered label (horizontal) for a captioned section divider.
6
+
7
+ **Keywords:** separator, divider, section break, rule
8
+
9
+ ## Usage
10
+
11
+ Use Separator to divide related groups where spacing alone does not provide enough structure.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use it sparingly between sections, menu groups, or dense rows.
16
+ - Do: Choose orientation based on the layout direction it separates.
17
+ - Don't: Do not use Separator as decoration when hierarchy, spacing, or headings would explain the layout better.
18
+ - Don't: Do not use it where space would do. A page whose every section is divided by a rule reads as a form; the rule should mark a real change of subject.
19
+
20
+ ## Props
21
+
22
+ | Name | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | orientation | "horizontal" \| "vertical" | "horizontal" | Axis of the rule. Vertical stretches to its flex parent's height. |
25
+ | label | ReactNode | — | When set (horizontal only), centers a muted caption between two hairline rules. |
26
+ | render | ReactElement \| (props, state) => ReactElement | — | Compose with another element/component through the Base UI render prop. Ignored on the labeled variant. |
27
+
28
+ ## Variants
29
+
30
+ - **orientation**: horizontal, vertical
31
+
32
+ ## Tokens used
33
+
34
+ `border`, `muted-foreground`
35
+
36
+ ## Accessibility
37
+
38
+ - Base UI sets role=separator and aria-orientation for assistive tech.
39
+ - The labeled variant renders a div with explicit role=separator + aria-orientation (the caption is the visible context).
40
+ - Purely decorative dividers can carry aria-hidden when adjacent content already conveys the grouping.
41
+
42
+ ## Related
43
+
44
+ - Components: Menu, ContextMenu, Card
45
+
46
+ ## Examples
47
+
48
+ ### Horizontal
49
+
50
+ ```tsx
51
+ import { Separator } from "@sableui/react"
52
+
53
+ <Separator />
54
+ ```
55
+
56
+ ### Labeled section divider
57
+
58
+ ```tsx
59
+ import { Separator } from "@sableui/react"
60
+
61
+ <Separator label="Other holdings" />
62
+ ```
63
+
64
+ ### Vertical (inside a flex row)
65
+
66
+ ```tsx
67
+ import { Row, Separator } from "@sableui/react"
68
+
69
+ <Row gap={3} className="h-6 items-center"><span>Edit</span><Separator orientation="vertical" /><span>Delete</span></Row>
70
+ ```