@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,111 @@
1
+ # Form
2
+
3
+ **Status:** stable
4
+
5
+ A styled <form> plus Field — the form orchestration layer, built on Base UI Form and Field. Field wires a label, control, helper text, and error message together with correct a11y (htmlFor, aria-labelledby, aria-describedby, aria-invalid, required) automatically, around any Sable control. Validation-library-agnostic: pass each Field an error string, or let Form check native constraints and Field validate functions on submit.
6
+
7
+ **Keywords:** form, field, label, validation, error, required, settings, input
8
+
9
+ ## Usage
10
+
11
+ Use Form and Field as the default wrapper for Sable form controls. Field owns the label, description, required state, and error wiring so agents do not hand-roll inconsistent label/input relationships.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Wrap each control in Field unless a specialized component already owns its label and error semantics.
16
+ - Do: Use Field error for validation messages so aria-invalid and aria-describedby are wired together.
17
+ - Do: For built-in checks, put required / type="email" / minLength on the control (or a validate function on Field) inside a Form: on submit the message appears under the field and the first invalid control is focused. Pass server errors to Form errors, keyed by Field name.
18
+ - Do: Use horizontal Field orientation only for one-line boolean controls like Checkbox, Switch, or Radio.
19
+ - Don't: Use placeholder text as the only label. A visible label should identify the field.
20
+ - Don't: Wrap multiple controls in one Field. Field expects a single child control and wires that one control (an Input or Textarea nested in a wrapper is still found through context).
21
+
22
+ ### Anatomy
23
+
24
+ | Part | Required | Description |
25
+ | --- | --- | --- |
26
+ | Form | yes | The native form element (Base UI Form) with vertical field spacing; validates its Fields on submit. |
27
+ | Field | yes | The label/control/helper/error wrapper for one control. |
28
+ | Control | yes | The single Sable control. Base UI-backed controls register with Field through context; the child is also cloned with the same wiring. |
29
+ | Error | no | Validation copy associated with the control: the error prop when set, otherwise the Base UI validation message (native constraint, validate function, or Form errors). |
30
+
31
+ ## Subcomponents
32
+
33
+ | Name | Type | Description |
34
+ | --- | --- | --- |
35
+ | Field | { label?: ReactNode; description?: ReactNode; error?: ReactNode; required?: boolean; disabled?: boolean; name?: string; validate?: (value, formValues) => string \| string[] \| null \| Promise<…>; validationMode?: "onSubmit" \| "onBlur" \| "onChange"; validationDebounceTime?: number; orientation?: "vertical" \| "horizontal"; className?: string; children: ReactElement } | Wraps a single control element (e.g. <Input />) on Base UI Field and wires label (htmlFor + aria-labelledby), helper text and error (aria-describedby), aria-invalid, and required onto it automatically. Base UI-backed controls (Input, Textarea, Select, Combobox, NumberField, Checkbox, Switch, Slider, OtpField…) register through context; the child is also cloned so plain elements keep the same wiring. error renders a destructive message and takes precedence; without it the slot shows Base UI's validation message when the field fails (native constraint, validate, or Form errors). required adds the native attribute plus a visual asterisk (defaults to false). disabled disables the control and dims the label. name keys the field in Form errors / onFormSubmit. validationMode overrides the Form's. orientation defaults to "vertical" (label → control); "horizontal" places control then label inline for a single checkbox / switch / radio — the control centres on the label's first line (it stays there when the label wraps) and description/error sit in the label's column. The root, label, description, error and control carry data-invalid / data-valid, data-touched, data-dirty, data-filled, data-focused and data-disabled for state styling. |
36
+
37
+ ## Props
38
+
39
+ | Name | Type | Default | Description |
40
+ | --- | --- | --- | --- |
41
+ | Form | { validationMode?: "onSubmit" \| "onBlur" \| "onChange"; errors?: Record<string, string \| string[]>; onFormSubmit?: (values, details) => void; actionsRef?: RefObject<Form.Actions> } + ComponentPropsWithRef<"form"> | — | A <form> laid out as a vertical column with comfortable field spacing (override with className, e.g. gap-4). On submit it validates every Field (native constraints plus Field validate), shows each message under its field, focuses the first invalid control, and only then calls onSubmit. validationMode defaults to "onSubmit" (then live re-validation on change). errors shows external (e.g. server) messages keyed by Field name and clears one when its field is edited. onFormSubmit receives the named Field values and prevents the native submit. A control outside any Field is still checked by the browser before onSubmit runs; pass noValidate to skip that. |
42
+ | validationMode | "onSubmit" \| "onBlur" \| "onChange" | "onSubmit" | When Fields validate. After the first submit, fields re-check as the user edits. A Field's own validationMode wins. |
43
+ | errors | Record<string, string \| string[]> | — | External (e.g. server) messages keyed by Field name, shown under that field. Each clears when its field is edited. |
44
+ | onFormSubmit | (values: Record<string, unknown>, details) => void | — | Called with the named Field values after validation passes; prevents the native submit. |
45
+ | actionsRef | RefObject<{ validate(name?: string): void }> | — | Imperative handle to validate every field, or one by name. |
46
+
47
+ ## Tokens used
48
+
49
+ `muted-foreground`, `destructive`
50
+
51
+ ## Accessibility
52
+
53
+ - Field auto-wires label htmlFor ↔ control id, aria-describedby (description + error), and aria-invalid — so the label, helper, and error are all announced for the control.
54
+ - required adds the native `required` attribute (not just a visual asterisk); the asterisk is aria-hidden.
55
+ - Error text is associated via aria-describedby and reinforced by the destructive color, not color alone.
56
+ - Inside Form, a failed submit focuses the first invalid control and shows its message inline instead of a browser bubble.
57
+
58
+ ## Related
59
+
60
+ - Components: Input, Textarea, Select, Checkbox, Switch, Button
61
+ - Patterns: SettingsPage, AuthPage
62
+
63
+ ## Examples
64
+
65
+ ### Text field
66
+
67
+ ```tsx
68
+ import { Field, Input } from "@sableui/react"
69
+
70
+ <Field label="Email" description="We'll never share it." required><Input type="email" /></Field>
71
+ ```
72
+
73
+ ### Field with an error
74
+
75
+ ```tsx
76
+ import { Field, Input } from "@sableui/react"
77
+
78
+ <Field label="Password" error="Must be at least 8 characters."><Input type="password" /></Field>
79
+ ```
80
+
81
+ ### Validated on submit
82
+
83
+ ```tsx
84
+ import { Button, Field, Form, Input } from "@sableui/react"
85
+
86
+ <Form onSubmit={handleSubmit}><Field label="Handle" required validate={(value) => String(value).length < 3 ? "At least 3 characters." : null}><Input name="handle" /></Field><Button type="submit">Claim</Button></Form>
87
+ ```
88
+
89
+ ### Server errors
90
+
91
+ ```tsx
92
+ import { Field, Form, Input } from "@sableui/react"
93
+
94
+ <Form errors={serverErrors}><Field label="Email" name="email"><Input type="email" /></Field></Form>
95
+ ```
96
+
97
+ ### Form
98
+
99
+ ```tsx
100
+ import { Button, Checkbox, Field, Form, Input } from "@sableui/react"
101
+
102
+ <Form onSubmit={handleSubmit}><Field label="Name"><Input /></Field><Field label="Subscribe" orientation="horizontal"><Checkbox /></Field><Button type="submit">Save</Button></Form>
103
+ ```
104
+
105
+ ### Settings form
106
+
107
+ ```tsx
108
+ import { Button, Field, Form, Input, Select, SelectContent, SelectItem, SelectTrigger } from "@sableui/react"
109
+
110
+ <Form onSubmit={handleSubmit}><Field label="Display name" description="Shown on your profile."><Input defaultValue="Nova Reed" /></Field><Field label="Visibility"><Select defaultValue="public"><SelectTrigger /><SelectContent><SelectItem value="public">Public</SelectItem><SelectItem value="private">Private</SelectItem></SelectContent></Select></Field><Button type="submit">Save changes</Button></Form>
111
+ ```
@@ -0,0 +1,84 @@
1
+ # GlidingHighlight
2
+
3
+ **Status:** stable
4
+
5
+ One highlight that travels to the active item, instead of each item toggling its own background. Measured, not assumed, so rows of unequal height stay exact.
6
+
7
+ **Keywords:** highlight, active, selection, indicator, menu, list, glide, slide, thumb, hover
8
+
9
+ ## Usage
10
+
11
+ Use GlidingHighlight for a set of items where one is active at a time and the pointer or arrow keys move between them — a menu, an option list, a command palette, a segmented control. It replaces per-item `hover:bg-*` with a single element that moves.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Pass `hovered ?? selected`. The fallback is what makes the pointer leaving a menu return the highlight to the current choice instead of dropping it, so the reader's place is never lost.
16
+ - Do: Reach for it wherever a list is keyboard-navigable. The gain is largest for arrow keys: a background that teleports between rows makes the eye re-find the highlight at every step, where one that moves can be tracked while it travels — which is what keeps a long list readable in motion rather than only at rest.
17
+ - Do: Keep selection semantics on the items — `aria-selected`, `aria-current`, `data-selected`. The highlight is `aria-hidden` decoration over whatever the children already announce.
18
+ - Don't: Do not use it for items that are not mutually exclusive. A travelling highlight says 'one of these, and it is this one'; over a multi-select list it contradicts the checkboxes beside it, and only one of the two can be right.
19
+ - Don't: Do not put non-item chrome among the direct children — a group heading, a separator, a footer hint. Every direct child is a measurable row, so `active` counts them, and the highlight lands one row off from the index the caller is reasoning about. Wrap the items in their own GlidingHighlight, or move the chrome outside.
20
+ - Don't: Do not drive it from a scroll position or a timer. It is an affordance for a set with a discrete active member; a highlight that moves without an item becoming active reads as the interface selecting things on its own.
21
+
22
+ ### Anatomy
23
+
24
+ | Part | Required | Description |
25
+ | --- | --- | --- |
26
+ | Container | yes | Positioning context for the highlight. Each direct child is measured as one item. |
27
+ | Highlight | yes | The travelling element. `aria-hidden`, absolutely positioned, invisible until something is active. |
28
+
29
+ ## Props
30
+
31
+ | Name | Type | Default | Description |
32
+ | --- | --- | --- | --- |
33
+ | active | number \| null | — | Index of the item the highlight sits behind, or `null` for none. Pass `hovered ?? selected`. |
34
+ | orientation | "vertical" \| "horizontal" | — | Deprecated and ignored: the highlight measures both axes, so a horizontal row needs no flag. |
35
+ | onPointerLeave | PointerEventHandler (and every other div prop) | — | The container is a plain div, so its props pass through. For a hover-following list, reset the hovered index here so the highlight returns to the selected row when the pointer leaves. |
36
+ | highlightClassName | string | — | Class for the travelling highlight itself, for changing its fill or radius. The radius defaults to `sable-frame-inner`, so inside a `sable-frame` container it nests concentrically; in any other rounded, inset container pass `rounded-concentric-<outer>/<inset>` here and on the rows. The container's own `className` styles the list. |
37
+
38
+ ## Tokens used
39
+
40
+ `accent`
41
+
42
+ ## Motion
43
+
44
+ - **arrival** · Highlight · _no motion_ — It deliberately does not animate in. Fading it up at rest position — usually row 0 — makes a menu look like it opened with the first item pre-selected, which is a claim about state that is not true. It stays invisible until something is genuinely active.
45
+ - **response** · Container · _no motion_ — The container is not the control; the items are, and they own their own pointer states. This draws the result.
46
+ - **transition** · Highlight · `motion-interactive` — The travel IS the component — a highlight that jumps is just a background toggle with extra machinery. Position, size and opacity ride one declaration so a tall row's height cannot catch up after its position has, which would read as the highlight stretching into place rather than moving.
47
+ - **activity** · Highlight · _no motion_ — Nothing here is indeterminate. A list still loading its options shows a Skeleton in place of the rows.
48
+
49
+ ## Accessibility
50
+
51
+ - The highlight is `aria-hidden` and carries no semantics — it renders selection, it does not declare it. Keep `aria-selected` / `aria-current` on the items themselves.
52
+ - Pointer-free by construction (`pointer-events-none`), so it can never intercept a click meant for the item beneath it.
53
+ - Works with roving-tabindex and `aria-activedescendant` lists alike: it reads an index the caller already owns, and does not manage focus.
54
+ - Under reduced motion or with MotionProvider off, the highlight moves instantly rather than gliding — the active item is still shown, which is the part that carries meaning.
55
+
56
+ ## Related
57
+
58
+ - Components: Menu, Select, Combobox, CommandMenu, SideNav
59
+
60
+ ## Examples
61
+
62
+ ### A hoverable menu, returning home on leave
63
+
64
+ ```tsx
65
+ import { GlidingHighlight } from "@sableui/react"
66
+
67
+ <GlidingHighlight active={hovered ?? selected} className="sable-frame sable-surface sable-surface-popover sable-frame-popover" onPointerLeave={() => setHovered(null)}>{items.map((item, i) => (<button key={item.id} type="button" className="sable-frame-inner px-2 py-1.5 text-start" onPointerEnter={() => setHovered(i)} onClick={() => setSelected(i)}>{item.label}</button>))}</GlidingHighlight>
68
+ ```
69
+
70
+ ### Horizontal, as a segmented control
71
+
72
+ ```tsx
73
+ import { GlidingHighlight } from "@sableui/react"
74
+
75
+ <GlidingHighlight active={tab} className="sable-well flex gap-1 rounded-md p-1" highlightClassName="rounded-concentric-md/1">{tabs.map((t, i) => (<button key={t} type="button" className="rounded-concentric-md/1 px-3 py-1" onClick={() => setTab(i)}>{t}</button>))}</GlidingHighlight>
76
+ ```
77
+
78
+ ### Keyboard navigation — the case it exists for
79
+
80
+ ```tsx
81
+ import { GlidingHighlight } from "@sableui/react"
82
+
83
+ <GlidingHighlight active={cursor} onKeyDown={(e) => { if (e.key === "ArrowDown") setCursor((c) => (c + 1) % rows.length) }}>{rows.map((r) => (<div key={r.id} role="option" aria-selected={false}>{r.label}</div>))}</GlidingHighlight>
84
+ ```
@@ -0,0 +1,62 @@
1
+ # InlineStatus
2
+
3
+ **Status:** stable
4
+
5
+ A borderless single-line status message — a leading semantic icon + one line of text over the canonical tone scale (neutral/info/success/warning/destructive). The non-boxed sibling of Note for inline affirmations, errors, and progress; pass a custom icon (e.g. a Spinner) for a pending row.
6
+
7
+ **Keywords:** inline status, state, message, availability
8
+
9
+ ## Usage
10
+
11
+ Use InlineStatus for short state messages placed near the thing they describe.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use it beside inputs, rows, actions, or settings that need local status.
16
+ - Do: Keep the message brief and pair color with text.
17
+ - Don't: Do not use InlineStatus for global announcements or page-level alerts.
18
+ - Don't: Do not use tone as the only signal. Colour alone fails for colour-blind users — the leading icon is what carries the meaning, so never hide it.
19
+
20
+ ## Props
21
+
22
+ | Name | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | tone | "neutral" \| "info" \| "success" \| "warning" \| "destructive" | "neutral" | Severity tone — drives the leading icon (name + color) and the text color. The shared status-tone vocabulary (STATUS_TONES). |
25
+ | size | "sm" \| "md" | "sm" | Text size: sm (text-body-sm) or md (text-body-md). sm is the default for inline placement under a control; md matches surrounding body copy. |
26
+ | icon | ReactNode | — | Override the leading glyph (e.g. a <Spinner />); otherwise the tone selects it. |
27
+
28
+ ## Variants
29
+
30
+ - **tone**: neutral, info, success, warning, destructive
31
+ - **size**: sm, md
32
+
33
+ ## Tokens used
34
+
35
+ `tone-info`, `success`, `tone-warning`, `destructive`, `muted-foreground`
36
+
37
+ ## Accessibility
38
+
39
+ - Color reinforces, never replaces, meaning — the leading icon and the message text carry the status.
40
+ - For a live status (e.g. a form submit result), render it in an aria-live region or pair it with the field's error wiring so the change is announced.
41
+
42
+ ## Related
43
+
44
+ - Components: StatusDot, Badge, Banner, Note
45
+
46
+ ## Examples
47
+
48
+ ### Tones
49
+
50
+ ```tsx
51
+ import { InlineStatus, Stack } from "@sableui/react"
52
+
53
+ <Stack gap={1}><InlineStatus tone="success">Top-up confirmed.</InlineStatus><InlineStatus tone="destructive">Insufficient balance.</InlineStatus></Stack>
54
+ ```
55
+
56
+ ### Pending (custom icon)
57
+
58
+ ```tsx
59
+ import { InlineStatus, Spinner } from "@sableui/react"
60
+
61
+ <InlineStatus tone="neutral" icon={<Spinner size="sm" />}>Preparing transaction…</InlineStatus>
62
+ ```
@@ -0,0 +1,127 @@
1
+ # Input
2
+
3
+ **Status:** stable
4
+
5
+ Styled native text input. Soft-square (rounded-sm), card surface, 2px focus ring; aria-invalid raises the destructive error treatment.
6
+
7
+ **Keywords:** input, text input, field, form, email, search, file, invalid, validation
8
+
9
+ ## Usage
10
+
11
+ Use Input for short freeform text, search, email, password, file, and native input types. In forms, wrap Input in Field so label, description, required state, and validation messaging stay accessible and consistent.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Pair every Input with a visible label through Field or an explicit label/htmlFor connection.
16
+ - Do: Use type, inputMode, autoComplete, and placeholder as progressive hints, not as the only label.
17
+ - Do: Set aria-invalid or use Field error copy when validation fails.
18
+ - Do: Keep a focusable field at 16px or larger on mobile. iOS Safari zooms the whole page when a focused input computes under 16px and never zooms back out. Input handles this itself — below the sm breakpoint it renders text-body-lg (17px) and returns to text-body-md at sm and up. Build a custom field the same way: a second paired tier under a max-sm: variant, never a hand-set font-size.
19
+ - Don't: Override an Input's type with a page tier below 16px (text-body-md, text-body-sm, text-label-*) without a >=16px mobile arm — that reintroduces the iOS focus zoom.
20
+ - Don't: Use placeholder text as the label; it disappears once the user types and is not enough context.
21
+ - Don't: Use Input for long body copy. Use Textarea for multi-line content.
22
+
23
+ ### Anatomy
24
+
25
+ | Part | Required | Description |
26
+ | --- | --- | --- |
27
+ | Input | yes | The native input element with Sable surface, disabled, focus, and invalid styling. |
28
+ | Label | yes | Usually provided by Field or a separate Label. It gives the input its visible purpose. |
29
+ | Description | no | Optional helper copy connected through aria-describedby when using Field. |
30
+ | Error | no | Validation message paired with aria-invalid and destructive styling. |
31
+
32
+ ## Props
33
+
34
+ | Name | Type | Default | Description |
35
+ | --- | --- | --- | --- |
36
+ | size | "md" \| "xs" | "md" | md is the form field. xs is the dense inset field of a tool panel (an inset canvas fill, no outline, tight padding) — the same ladder as Select and Combobox. Use it instead of overriding the height with className. |
37
+ | type | string | "text" | Native input type (text, email, password, search, number, file, …). type="file" themes the native selector button via file: token utilities. |
38
+ | disabled | boolean | false | Dims the control and blocks pointer interaction. |
39
+ | aria-invalid | boolean | — | Surfaces the destructive border + ring. Base UI form fields set this automatically. |
40
+ | ...props | ComponentPropsWithRef<"input"> | — | All native input attributes (placeholder, value, onChange, ref, …) pass through. |
41
+
42
+ ## Variants
43
+
44
+ - **size**: md, xs
45
+
46
+ ## Tokens used
47
+
48
+ `input`, `card`, `foreground`, `muted-foreground`, `ring`, `destructive`
49
+
50
+ ## Accessibility
51
+
52
+ - Renders a native <input> — labels, keyboard, and form semantics come for free.
53
+ - Renders through Base UI's Input (Field.Control): inside a Field it picks up the label, description, error and validity wiring from context, even when nested in a wrapper.
54
+ - Always associate a <label htmlFor> (or wrap) — placeholder text is not a label.
55
+ - Focus-visible ring uses the --ring token at 40% opacity.
56
+ - Set aria-invalid='true' on error so the destructive treatment is conveyed, not color-only.
57
+
58
+ ## Related
59
+
60
+ - Components: Field, Form, Textarea, SearchInput, NumberField
61
+ - Patterns: SettingsPage, AuthPage
62
+
63
+ ## Examples
64
+
65
+ ### Default
66
+
67
+ ```tsx
68
+ import { Input } from "@sableui/react"
69
+
70
+ <Input placeholder="Search posts" />
71
+ ```
72
+
73
+ ### With a label
74
+
75
+ ```tsx
76
+ import { Field, Input } from "@sableui/react"
77
+
78
+ <Field label="Email"><Input type="email" placeholder="you@example.com" /></Field>
79
+ ```
80
+
81
+ ### Field with helper text
82
+
83
+ ```tsx
84
+ import { Field, Input } from "@sableui/react"
85
+
86
+ <Field label="Display name" description="Shown on your public profile."><Input placeholder="Nova Reed" /></Field>
87
+ ```
88
+
89
+ ### Field error
90
+
91
+ ```tsx
92
+ import { Field, Input } from "@sableui/react"
93
+
94
+ <Field label="Email" error="Enter a valid email address."><Input type="email" defaultValue="not-an-email" /></Field>
95
+ ```
96
+
97
+ ### Search input
98
+
99
+ ```tsx
100
+ import { Field, Input } from "@sableui/react"
101
+
102
+ <Field label="Search posts"><Input type="search" placeholder="Search by title, creator, or tag" /></Field>
103
+ ```
104
+
105
+ ### Invalid
106
+
107
+ ```tsx
108
+ import { Input } from "@sableui/react"
109
+
110
+ <Input aria-invalid defaultValue="not-an-email" />
111
+ ```
112
+
113
+ ### File
114
+
115
+ ```tsx
116
+ import { Input } from "@sableui/react"
117
+
118
+ <Input type="file" accept="image/*" />
119
+ ```
120
+
121
+ ### Disabled
122
+
123
+ ```tsx
124
+ import { Input } from "@sableui/react"
125
+
126
+ <Input disabled placeholder="Disabled" />
127
+ ```
@@ -0,0 +1,57 @@
1
+ # Kbd
2
+
3
+ **Status:** stable
4
+
5
+ A keyboard key indicator. Renders a styled <kbd> key cap in monospace; compose several with a + between to show a shortcut combo. `mod` renders the platform modifier (⌘ or Ctrl).
6
+
7
+ **Keywords:** keyboard, shortcut, key, kbd
8
+
9
+ ## Usage
10
+
11
+ Use Kbd to display keyboard keys or shortcuts in help text, menus, and docs.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use it only for literal keyboard input such as Cmd, Enter, or Esc.
16
+ - Do: Keep multi-key shortcuts readable by separating individual keys.
17
+ - Don't: Do not use Kbd as a decorative badge or button replacement.
18
+ - Don't: Do not hardcode ⌘ or Ctrl. The modifier differs by platform; use `<Kbd mod />`, which renders ⌘ on macOS/iOS and Ctrl elsewhere, or Windows users read Mac keys.
19
+
20
+ ## Props
21
+
22
+ | Name | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | mod | boolean | false | Render the platform's shortcut modifier in place of children: ⌘ on macOS and iOS, Ctrl everywhere else. Server-rendered as Ctrl and corrected on hydration. |
25
+ | className | string | — | Merge additional classes onto the key cap. |
26
+ | ...props | ComponentPropsWithRef<"kbd"> | — | All native <kbd> attributes (children, ref, etc.). |
27
+
28
+ ## Tokens used
29
+
30
+ `border`, `muted`, `muted-foreground`
31
+
32
+ ## Accessibility
33
+
34
+ - Uses the semantic <kbd> element so assistive tech announces keyboard input.
35
+ - Monospace key text keeps single keys and symbols legible at small sizes.
36
+
37
+ ## Related
38
+
39
+ - Components: Code, Menu, CommandMenu
40
+
41
+ ## Examples
42
+
43
+ ### Single keys
44
+
45
+ ```tsx
46
+ import { Kbd } from "@sableui/react"
47
+
48
+ <Kbd>Esc</Kbd>
49
+ ```
50
+
51
+ ### Combo
52
+
53
+ ```tsx
54
+ import { Kbd, Row } from "@sableui/react"
55
+
56
+ <Row gap={1}><Kbd mod /><span>+</span><Kbd>K</Kbd></Row>
57
+ ```
@@ -0,0 +1,73 @@
1
+ # Label
2
+
3
+ **Status:** stable
4
+
5
+ Styled native <label>. Paired label typography via a size step (xs / sm / md / lg, all sentence case) or an uppercase eyebrow (eyebrow), non-selectable, dims when its peer control is disabled. Optional hint adds a dotted-underline tooltip affordance.
6
+
7
+ **Keywords:** label, form label, control, accessibility
8
+
9
+ ## Usage
10
+
11
+ Use Label to name a form control and create a clear accessible relationship with that control.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use explicit, persistent labels for inputs, selects, textareas, and grouped controls.
16
+ - Do: Place helper text separately so the label stays short and direct.
17
+ - Don't: Do not rely on placeholder text as the only label for a control.
18
+ - Don't: Do not use it as a bare styled span. A Label without htmlFor or a wrapped control looks like a label and does nothing — clicking it will not focus the field, which is the entire point.
19
+
20
+ ## Props
21
+
22
+ | Name | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | htmlFor | string | — | Associates the label with a control by its id (native <label for>). |
25
+ | size | "xs" \| "sm" \| "md" \| "lg" | "lg" | Paired label typography step, all sentence case. xs is simply the smallest; for an uppercase eyebrow use eyebrow. |
26
+ | eyebrow | boolean | false | Set the label as an eyebrow: the uppercase, letter-spaced text-overline tier (themeable to sentence case). Replaces size. For a kicker above a group, not an ordinary field label. |
27
+ | hint | ReactNode | — | When set, the label gets a dotted-underline affordance and reveals this content in a tooltip on hover/focus. |
28
+ | className | string | — | Merge or override classes via cn(). |
29
+
30
+ ## Variants
31
+
32
+ - **size**: xs, sm, md, lg
33
+
34
+ ## Tokens used
35
+
36
+ `foreground`, `muted-foreground`
37
+
38
+ ## Accessibility
39
+
40
+ - Renders a native <label> — clicking it focuses/activates the associated control.
41
+ - Associate with a control via htmlFor (matching the control's id) or by wrapping the control.
42
+ - In a form, use Field: it renders this Label, wires htmlFor/id, and dims the label when the field is disabled. Hand-wired outside a Field, the label only dims via peer-disabled when the control carries `peer` AND precedes the Label in the DOM (Tailwind peer-* only matches a later sibling).
43
+ - With `hint`, the label becomes focusable and the help text is announced via the tooltip's aria-describedby — so the affordance works for keyboard and screen-reader users, not just hover.
44
+
45
+ ## Related
46
+
47
+ - Components: Input, Textarea, Fieldset, Form, Field
48
+
49
+ ## Examples
50
+
51
+ ### With control
52
+
53
+ ```tsx
54
+ import { Input, Label, Stack } from "@sableui/react"
55
+
56
+ <Stack gap={1}><Label htmlFor="name">Display name</Label><Input id="name" /></Stack>
57
+ ```
58
+
59
+ ### In a form
60
+
61
+ ```tsx
62
+ import { Field, Input } from "@sableui/react"
63
+
64
+ <Field label="Display name" disabled><Input /></Field>
65
+ ```
66
+
67
+ ### With hint
68
+
69
+ ```tsx
70
+ import { Label } from "@sableui/react"
71
+
72
+ <Label htmlFor="royalty" hint="A percentage of every resale, paid to you.">Royalty</Label>
73
+ ```
@@ -0,0 +1,99 @@
1
+ # Marker
2
+
3
+ **Status:** stable
4
+
5
+ A non-message row in a conversation: streaming status, system note, bordered notice, or a labelled rule for date breaks. Quieter than the message tier by design.
6
+
7
+ **Keywords:** chat status, typing indicator, thinking, streaming, tool call, system message, date separator, date break, conversation divider
8
+
9
+ ## Usage
10
+
11
+ Everything in a conversation that is not a message. Date breaks, "Carla joined", "Thinking…", "Ran search — 3 results", "Message failed to send". One component because they share one job: annotate the stream without competing with it.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use `status` with `icon="spinner" spin` for work in flight, and add `shimmer` while text is actually streaming. The shimmer is scale-aware, so it freezes with the rest of the system under reduced motion.
16
+ - Do: Use `variant="separator"` for date breaks. The label sits between two rules, which reads as a division of the transcript rather than as another thing someone said.
17
+ - Do: Replace a status marker's text as the work progresses — "Thinking…" → "Searching the docs" → "Writing the answer". It is a live region, so each change is announced.
18
+ - Do: Reach for `bordered` only when a note needs containing — an error, an escalation, a retention notice. Boxing routine system events makes a transcript look like a list of alerts.
19
+ - Don't: Do not style a Marker up to message weight. Its whole value is being below that tier; a marker as loud as a message competes with the conversation it is annotating.
20
+ - Don't: Do not leave a status marker mounted after the work finishes. A permanent "Thinking…" is a live region that never resolves, which is worse than no status at all.
21
+ - Don't: Do not use it for a message from a person or an assistant. That is `Message` + `Bubble` — a marker has no author and no side.
22
+ - Don't: Do not set `live` on a `separator` or on a run of `note` markers. Announcing every date break and join event turns the live region into noise and buries the status that mattered.
23
+
24
+ ## Props
25
+
26
+ | Name | Type | Default | Description |
27
+ | --- | --- | --- | --- |
28
+ | variant | "status" \| "note" \| "bordered" \| "separator" | "status" | `status` for live activity, `note` for a system event, `bordered` for a note that needs containing, `separator` for a labelled rule. |
29
+ | icon | string | — | Semantic icon name shown before the label, resolved through IconProvider. |
30
+ | spin | boolean | false | Spin the icon — pair with `icon="spinner"` for work in flight. |
31
+ | shimmer | boolean | false | Sweep the label while something is streaming, via the scale-aware `motion-text-shimmer` recipe (freezes under reduced motion). |
32
+ | live | boolean | true for `status`, false otherwise | Announce the label to assistive tech as it changes. On for `status`, because a screen-reader user otherwise gets silence for the whole generation and no way to tell a slow answer from a dead one. |
33
+ | ...div props | ComponentPropsWithRef<"div"> | — | Spread onto the row. |
34
+
35
+ ## Variants
36
+
37
+ - **variant**: status, note, bordered, separator
38
+
39
+ ## Tokens used
40
+
41
+ `muted-foreground`, `muted`, `border`, `foreground`
42
+
43
+ ## Accessibility
44
+
45
+ - `status` markers render as `role="status"` with `aria-live="polite"`, so a change of activity is announced without interrupting whatever is being read.
46
+ - The other variants are silent by default and can opt in with `live` — announcing every date break and join event would bury the status that mattered.
47
+ - The separator's rules are `aria-hidden`, leaving only the label in the accessibility tree; the shimmer is decoration over readable text, not a replacement for it.
48
+ - `motion-text-shimmer` keeps the label opaque through every frame — the gradient runs muted-foreground → foreground rather than fading to transparent, so the text never becomes unreadable mid-animation, and `--motion-scale` freezes it entirely under reduced motion.
49
+
50
+ ## Related
51
+
52
+ - Components: Message, Bubble, MessageScroller, InlineStatus, Separator
53
+
54
+ ## Examples
55
+
56
+ ### Streaming status
57
+
58
+ ```tsx
59
+ import { Marker } from "@sableui/react"
60
+
61
+ <Marker icon="spinner" spin shimmer>Thinking…</Marker>
62
+ ```
63
+
64
+ ### Date break
65
+
66
+ ```tsx
67
+ import { Marker } from "@sableui/react"
68
+
69
+ <Marker variant="separator">Yesterday</Marker>
70
+ ```
71
+
72
+ ### System note
73
+
74
+ ```tsx
75
+ import { Marker } from "@sableui/react"
76
+
77
+ <Marker variant="note" icon="user">Carla joined the conversation</Marker>
78
+ ```
79
+
80
+ ### A notice that needs containing
81
+
82
+ ```tsx
83
+ import { Marker } from "@sableui/react"
84
+
85
+ <Marker variant="bordered" icon="warning">This conversation will be archived in 7 days.</Marker>
86
+ ```
87
+
88
+ ### In a transcript
89
+
90
+ ```tsx
91
+ import { Bubble, Marker, Message, MessageScroller } from "@sableui/react"
92
+
93
+ <MessageScroller className="h-96">
94
+ <Marker variant="separator">Today</Marker>
95
+ <Message messageId="m1"><Bubble>Can you summarise the release?</Bubble></Message>
96
+ <Marker variant="note" icon="sparkles">Ran changelog — 12 entries</Marker>
97
+ <Marker icon="spinner" spin shimmer>Writing the answer</Marker>
98
+ </MessageScroller>
99
+ ```