@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,66 @@
1
+ # Marquee
2
+
3
+ **Status:** stable
4
+
5
+ Seamless, constant-speed infinite loop (logos, tags, testimonials). Zero-dependency: duplicates children to fill the track and advances exactly one group + gap per iteration so it never seams. Pauses on hover/focus; freezes to a scroll region when motion is off/reduced.
6
+
7
+ **Keywords:** marquee, ticker, scrolling, ambient
8
+
9
+ ## Usage
10
+
11
+ Use Marquee for decorative or ambient continuous content where motion supports the surface.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use it for low-priority repeated content such as logos, tags, or lightweight highlights.
16
+ - Do: Respect reduced motion and keep the content understandable without relying on animation.
17
+ - Don't: Do not put critical messages, controls, or required reading inside Marquee.
18
+ - Don't: Do not put content that must be read in a Marquee. It moves continuously, so anything longer than a logo or a short label cannot be read reliably — and it must stop for prefers-reduced-motion.
19
+
20
+ ## Props
21
+
22
+ | Name | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | direction | "left" \| "right" \| "up" \| "down" | "left" | Scroll direction; up/down switch to a vertical loop, which needs a bounded height (e.g. className="h-56"). |
25
+ | speed | number | 40 | Constant speed in px/s — independent of content length. |
26
+ | gap | string | "calc(var(--spacing) * 8)" | Gap between items (any CSS length). Use a spacing-scale value (calc(var(--spacing) * n)) so it moves with density. |
27
+ | pauseOnHover | boolean | true | Pause while hovered or keyboard-focused (WCAG 2.2.2). |
28
+ | controls | boolean | false | Render a visible pause/play control — recommended for WCAG 2.2.2 when content auto-scrolls. |
29
+ | fade | boolean | false | Fade the leading/trailing edges with a mask. |
30
+ | label | string | — | Accessible label for the scrolling region. |
31
+
32
+ ## Accessibility
33
+
34
+ - Duplicate groups are aria-hidden, so screen readers read the content once.
35
+ - Pauses on hover and focus-within; under prefers-reduced-motion / MotionProvider off it stops and becomes a normal scroll region.
36
+ - Decorative by default — pass a label only when the row conveys meaning.
37
+
38
+ ## Related
39
+
40
+ - Components: Scroller, Carousel, Badge
41
+
42
+ ## Examples
43
+
44
+ ### Logo row
45
+
46
+ ```tsx
47
+ import { Marquee } from "@sableui/react"
48
+
49
+ <Marquee>{logos.map((l) => <img key={l.id} src={l.src} alt={l.name} className="h-8" />)}</Marquee>
50
+ ```
51
+
52
+ ### Reverse, faster
53
+
54
+ ```tsx
55
+ import { Badge, Marquee } from "@sableui/react"
56
+
57
+ <Marquee direction="right" speed={70}>{tags.map((t) => <Badge key={t} appearance="outline">{t}</Badge>)}</Marquee>
58
+ ```
59
+
60
+ ### Vertical
61
+
62
+ ```tsx
63
+ import { Marquee } from "@sableui/react"
64
+
65
+ <Marquee direction="up" className="h-64">{items}</Marquee>
66
+ ```
@@ -0,0 +1,122 @@
1
+ # Menu
2
+
3
+ **Status:** stable
4
+
5
+ Dropdown menu over Base UI Menu — roving focus, typeahead, ARIA menu/menuitem wiring, collision-aware positioning, Esc/outside dismissal. Trigger + portal-rendered popup with items, groups, and separators.
6
+
7
+ **Keywords:** menu, dropdown, actions, commands
8
+
9
+ ## Usage
10
+
11
+ Use Menu for a compact list of actions opened from a visible trigger.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use it for secondary or grouped commands that do not need to be visible all the time.
16
+ - Do: Keep item labels action-oriented and easy to scan.
17
+ - Do: Inside a <Workspace> a Menu adapts automatically — its popup re-applies the compact density across the portal, so items render on the chrome type tier and the control ladder resolves to workspace sizes. Do NOT override the size per call site; that was necessary before v0.41 and is now drift.
18
+ - Don't: Do not use Menu for complex forms, persistent navigation, or arbitrary rich content.
19
+ - Don't: Do not build a Select out of it. A menu fires actions and has no value; a Select holds one and reports changes. Swapping them loses form semantics and keyboard type-ahead.
20
+
21
+ ### Anatomy
22
+
23
+ | Part | Required | Description |
24
+ | --- | --- | --- |
25
+ | MenuTrigger | yes | The control that opens the menu. |
26
+ | MenuContent | yes | The floating command surface. |
27
+ | MenuItem | yes | One selectable command. |
28
+ | MenuSeparator | no | A divider between groups. |
29
+ | MenuGroup | no | A grouped set of menu items. |
30
+ | MenuGroupLabel | no | A label for a group of items. |
31
+
32
+ ## Subcomponents
33
+
34
+ | Name | Type | Description |
35
+ | --- | --- | --- |
36
+ | MenuTrigger | Base UI Menu.Trigger props + render | The button that opens the menu. Compose with render={<Button … />}. |
37
+ | MenuContent | Base UI Popup props + side: 'top' \| 'right' \| 'bottom' \| 'left' + align: 'start' \| 'center' \| 'end' + sideOffset: number + className | Portal + Positioner + Popup. side picks which side of the trigger the popup renders against (auto-flips on collision; defaults to 'bottom'); align sets alignment along that side (defaults to 'start'); sideOffset is the gap in px between trigger and popup (defaults to 6). |
38
+ | MenuItem | Base UI Menu.Item props + className (disabled, closeOnClick, onClick) + variant?: "default" \| "destructive" | An interactive row. Highlights via data-[highlighted]; dims via data-[disabled]. variant="destructive" for a delete or irreversible action: it keeps the destructive ink when highlighted (never className="text-destructive", which the highlight repaints). |
39
+ | MenuSeparator | Base UI Separator props + className | Hairline rule between item runs. |
40
+ | MenuSubmenu | Base UI Menu.SubmenuRoot props | A nested menu. Wraps a MenuSubmenuTrigger and its own MenuContent — pass side="inline-end" to that content so the submenu opens away from its parent in RTL as well as LTR. |
41
+ | MenuSubmenuTrigger | Base UI Menu.SubmenuTrigger props + value: ReactNode + className | The row that opens a submenu: item styling, a trailing chevron, and an optional `value` showing the current choice so the row reads "Effort High ›" rather than making the reader open it to find out. |
42
+ | MenuRadioGroup | Base UI Menu.RadioGroup props (value, defaultValue, onValueChange) | A single-choice group inside a menu. Controlled via value/onValueChange, uncontrolled via defaultValue. |
43
+ | MenuRadioItem | Base UI Menu.RadioItem props + className (value, disabled) | One option in a MenuRadioGroup, rendered as menuitemradio with a real checked state. The check sits in a reserved leading column so labels never shift sideways as the highlight moves. |
44
+ | MenuCheckboxItem | Base UI Menu.CheckboxItem props + className + { checked?: boolean; defaultChecked?: boolean; onCheckedChange?: (checked: boolean) => void; disabled?: boolean } | A toggling row, with the same reserved leading column as MenuRadioItem so a menu mixing the two keeps one text margin. |
45
+ | MenuGroup | Base UI Menu.Group props + className | Groups related items; pair with MenuGroupLabel for an accessible group name. |
46
+ | MenuGroupLabel | Base UI Menu.GroupLabel props + className | The label for a MenuGroup — auto-associated with the group for screen readers. |
47
+
48
+ ## Props
49
+
50
+ | Name | Type | Default | Description |
51
+ | --- | --- | --- | --- |
52
+ | Menu (Root) | Base UI Menu.Root props (open, defaultOpen, onOpenChange, modal, orientation, loopFocus) | — | Groups all parts and controls open state. Uncontrolled by default. |
53
+
54
+ ## Tokens used
55
+
56
+ `popover`, `popover-foreground`, `border`, `accent`, `accent-foreground`, `muted-foreground`
57
+
58
+ ## Accessibility
59
+
60
+ - Base UI provides role=menu/menuitem, roving tabindex, arrow-key navigation, and typeahead.
61
+ - Esc and outside-press dismiss; focus returns to the trigger on close.
62
+ - Animate open/close via data-[starting-style]/data-[ending-style] attributes.
63
+ - MenuGroupLabel is auto-associated with its MenuGroup for screen readers.
64
+ - A disabled MenuItem stays in the arrow-key order — Base UI keeps it focusable with aria-disabled so screen-reader users learn the command exists — but it cannot be activated, and it ignores the pointer.
65
+
66
+ ## Related
67
+
68
+ - Components: ContextMenu, DotsMenu
69
+
70
+ ## Examples
71
+
72
+ ### Basic
73
+
74
+ ```tsx
75
+ import { Button, Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from "@sableui/react"
76
+
77
+ <Menu><MenuTrigger render={<Button variant="secondary">Options</Button>} /><MenuContent><MenuItem>Edit</MenuItem><MenuItem>Duplicate</MenuItem><MenuSeparator /><MenuItem variant="destructive">Delete</MenuItem></MenuContent></Menu>
78
+ ```
79
+
80
+ ### Grouped
81
+
82
+ ```tsx
83
+ import { Button, Menu, MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuSeparator, MenuTrigger } from "@sableui/react"
84
+
85
+ <Menu><MenuTrigger render={<Button variant="secondary">Account</Button>} /><MenuContent><MenuGroup><MenuGroupLabel>Signed in</MenuGroupLabel><MenuItem>Profile</MenuItem><MenuItem>Settings</MenuItem></MenuGroup><MenuSeparator /><MenuItem>Sign out</MenuItem></MenuContent></Menu>
86
+ ```
87
+
88
+ ### Nested, with a single-choice submenu
89
+
90
+ ```tsx
91
+ import { Button, Menu, MenuContent, MenuItem, MenuRadioGroup, MenuRadioItem, MenuSubmenu, MenuSubmenuTrigger, MenuTrigger } from "@sableui/react"
92
+
93
+ <Menu>
94
+ <MenuTrigger render={<Button variant="secondary">View</Button>} />
95
+ <MenuContent>
96
+ <MenuItem>Reload</MenuItem>
97
+ <MenuSubmenu>
98
+ <MenuSubmenuTrigger value={density}>Density</MenuSubmenuTrigger>
99
+ <MenuContent side="inline-end" align="start" sideOffset={4}>
100
+ <MenuRadioGroup value={density} onValueChange={setDensity}>
101
+ <MenuRadioItem value="Comfortable">Comfortable</MenuRadioItem>
102
+ <MenuRadioItem value="Compact">Compact</MenuRadioItem>
103
+ </MenuRadioGroup>
104
+ </MenuContent>
105
+ </MenuSubmenu>
106
+ </MenuContent>
107
+ </Menu>
108
+ ```
109
+
110
+ ### Toggling rows
111
+
112
+ ```tsx
113
+ import { Button, Menu, MenuCheckboxItem, MenuContent, MenuTrigger } from "@sableui/react"
114
+
115
+ <Menu>
116
+ <MenuTrigger render={<Button variant="secondary">Columns</Button>} />
117
+ <MenuContent>
118
+ <MenuCheckboxItem checked={cols.status} onCheckedChange={(v) => setCol("status", v)}>Status</MenuCheckboxItem>
119
+ <MenuCheckboxItem checked={cols.updated} onCheckedChange={(v) => setCol("updated", v)}>Updated</MenuCheckboxItem>
120
+ </MenuContent>
121
+ </Menu>
122
+ ```
@@ -0,0 +1,110 @@
1
+ # MessageScroller
2
+
3
+ **Status:** stable
4
+
5
+ Scroll container for a conversation. Auto-follows new messages until the reader scrolls away, holds position when older history is prepended, and exposes scroll commands and visibility tracking through its ref.
6
+
7
+ **Keywords:** chat, conversation, messages, thread, transcript, stick to bottom, auto scroll, streaming, infinite scroll, inbox, ai chat, support chat
8
+
9
+ ## Usage
10
+
11
+ The scroll region a conversation lives in. It owns the scrolling and nothing else — not your messages, not your AI state, not your transport or persistence — so you bring the content renderer and it handles the behaviors that make streaming chat feel right: pinning to the newest message, releasing when the reader scrolls up, keeping the view still when older history loads above, jumping to a specific message, restoring a saved read position, and reporting what is actually on screen.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Bound it the way you would bound any scroll box — `flex-1 min-h-0` inside a flex column, or `max-h-96`. `className` lands on the element that actually scrolls, so the obvious bound is the correct one (same contract as Scroller).
16
+ - Do: Give every Message a `messageId`. It is the handle for `scrollToMessage`, `initialScroll`, and `onVisibleMessagesChange` — a message without one is invisible to all three.
17
+ - Do: Prepend older history directly into `children` when the reader reaches the top. The scroller anchors on the first rendered message and re-adds the height that appeared above it, so the view does not jump.
18
+ - Do: Set the rhythm between messages with the `gap` prop, not with a flex class on `className` — the messages are laid out in a column inside the scroll region, not directly on it.
19
+ - Do: Restore a reopened thread with `initialScroll={{ messageId: lastRead }}` instead of dropping the reader at the bottom of a conversation they have not finished.
20
+ - Do: Wrap a docked agent panel in `<Density value="compact">`. The scroller tightens its rhythm and every message inside it drops to the workspace type tier — one attribute for the whole conversation, no per-component sizing.
21
+ - Don't: Do not call `scrollToBottom` on every incoming token. Auto-follow already does it, and doing it yourself fights the reader who scrolled up to read something.
22
+ - Don't: Do not expect `ref` to be the DOM node — it is a command handle (`scrollToBottom`, `scrollToMessage`, `isFollowing`). Reach for `ref.current.element` when you genuinely need the element.
23
+ - Don't: Do not wrap it in a positioned div to place a jump-to-latest button. It ships one, stuck to the bottom edge from inside the scroll region, which is why `className` gets to stay on the scrolling element.
24
+ - Don't: Do not use it for an ordinary long list. That is `Scroller` — this one's follow-the-bottom behavior is wrong for content the reader is browsing rather than receiving.
25
+
26
+ ### Anatomy
27
+
28
+ | Part | Required | Description |
29
+ | --- | --- | --- |
30
+ | Scroll region | yes | The element that scrolls. Carries `className`: sizing, padding, and background. |
31
+ | Message column | yes | The flex column holding the children, spaced by `gap`. Publishes `--message-gap` so `<Message grouped>` can close it. |
32
+ | Jump to latest | no | A control stuck to the bottom edge, shown only while the reader is scrolled away from the newest message. |
33
+
34
+ ## Props
35
+
36
+ | Name | Type | Default | Description |
37
+ | --- | --- | --- | --- |
38
+ | ref | Ref<MessageScrollerHandle> | — | Imperative commands, not the DOM node: `scrollToBottom(opts)`, `scrollToTop(opts)`, `scrollToMessage(id, opts)` (returns false when the message is not rendered), `isFollowing()`, and `element` for the scrolling node itself. |
39
+ | autoFollow | boolean | true | Keep the newest message in view as content arrives, until the reader scrolls away from the bottom. Set false for a transcript that must never move under the reader. |
40
+ | followThreshold | number | 64 | Distance from the bottom, in px, that still counts as following. A band rather than a pixel, so a token arriving mid-frame does not flicker the state. |
41
+ | onFollowChange | (following: boolean) => void | — | Notified when the viewport pins to, or releases from, the newest message. |
42
+ | initialScroll | "bottom" \| "top" \| { messageId: string } | "bottom" | Where a freshly mounted thread lands. Pass a messageId to restore a saved read position. Applied once, on mount — changing it later will not yank a reader mid-conversation. |
43
+ | jumpToLatest | boolean | true | Show the jump-to-latest control while the reader is scrolled away from the newest message. |
44
+ | jumpToLatestLabel | string | "Jump to latest" | Accessible label and tooltip for that control. |
45
+ | gap | "sm" \| "md" \| "lg" | "md" | Vertical rhythm between messages. Also published as `--message-gap`, which `<Message grouped>` reads to collapse the gap within a run. |
46
+ | onVisibleMessagesChange | (ids: string[]) => void | — | Ids of the messages currently intersecting the viewport, in DOM order. The hook for read receipts, mark-as-seen, and unread counters. |
47
+ | ...div props | Omit<ComponentPropsWithRef<"div">, "ref"> | — | Spread onto the scrolling element. `className` bounds and pads the scroll region; `onScroll` composes with the component's own. |
48
+
49
+ ## Variants
50
+
51
+ - **gap**: sm, md, lg
52
+
53
+ ## Tokens used
54
+
55
+ `border`, `secondary`, `secondary-foreground`, `ring`
56
+
57
+ ## Accessibility
58
+
59
+ - Native overflow, so keyboard scrolling (arrows, Page Up/Down, Home/End) and the OS scrollbar work unchanged; the scrollbar is styled thin, not replaced.
60
+ - Overscroll is contained, so a wheel gesture that runs out of conversation does not start scrolling the page behind it.
61
+ - The jump-to-latest control is a real button with an accessible label, reachable by keyboard, and only present when there is somewhere to jump to.
62
+ - Smooth scrolling is used only when motion is enabled — `prefers-reduced-motion` and <MotionProvider> off both collapse every scroll command to an instant jump.
63
+
64
+ ## Related
65
+
66
+ - Components: Message, Bubble, Marker, Attachment, Scroller
67
+
68
+ ## Examples
69
+
70
+ ### Streaming conversation
71
+
72
+ ```tsx
73
+ import { Bubble, Marker, Message, MessageScroller } from "@sableui/react"
74
+
75
+ <MessageScroller className="flex-1 min-h-0 px-4 py-6">{messages.map((m) => (<Message key={m.id} messageId={m.id} align={m.mine ? "end" : "start"}><Bubble variant={m.mine ? "primary" : "muted"} align={m.mine ? "end" : "start"}>{m.body}</Bubble></Message>))}{pending ? <Marker icon="spinner" spin shimmer>Thinking…</Marker> : null}</MessageScroller>
76
+ ```
77
+
78
+ ### Scroll to a message on demand
79
+
80
+ ```tsx
81
+ import { Bubble, Button, Message, MessageScroller } from "@sableui/react"
82
+
83
+ function Thread({ messages, replyTo }) {
84
+ const thread = useRef(null)
85
+ return (
86
+ <>
87
+ <Button onClick={() => thread.current?.scrollToMessage(replyTo)}>Show original</Button>
88
+ <MessageScroller ref={thread} className="max-h-96">
89
+ {messages.map((m) => (<Message key={m.id} messageId={m.id}><Bubble>{m.body}</Bubble></Message>))}
90
+ </MessageScroller>
91
+ </>
92
+ )
93
+ }
94
+ ```
95
+
96
+ ### Restore a saved read position and track receipts
97
+
98
+ ```tsx
99
+ import { Bubble, Message, MessageScroller } from "@sableui/react"
100
+
101
+ <MessageScroller className="h-96" initialScroll={{ messageId: lastRead }} onVisibleMessagesChange={(ids) => markSeen(ids)}>{messages.map((m) => (<Message key={m.id} messageId={m.id}><Bubble>{m.body}</Bubble></Message>))}</MessageScroller>
102
+ ```
103
+
104
+ ### Prepend older history without moving the view
105
+
106
+ ```tsx
107
+ import { Bubble, Button, Message, MessageScroller } from "@sableui/react"
108
+
109
+ <MessageScroller className="h-96" gap="sm">{hasMore ? <Button variant="secondary" loadingLabel="Loading…" onClick={loadOlder}>Load earlier messages</Button> : null}{messages.map((m) => (<Message key={m.id} messageId={m.id}><Bubble>{m.body}</Bubble></Message>))}</MessageScroller>
110
+ ```
@@ -0,0 +1,114 @@
1
+ # Message
2
+
3
+ **Status:** stable
4
+
5
+ One row in a conversation: avatar gutter, side, and stacked header / content / footer. Layout only — the surface is Bubble, passed as children.
6
+
7
+ **Keywords:** chat, message, conversation row, turn, thread, messaging, grouped messages, ai chat
8
+
9
+ ## Usage
10
+
11
+ The row a single turn occupies. It owns the geometry — which side, the avatar gutter, where the name and timestamp go, where reactions and receipts go — and nothing about how the content looks. That separation is the point: an assistant's markdown answer, a one-line reply, an attachment, and a tool-call card are all the same row with different children.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Set `messageId`. It is what MessageScroller reads for `scrollToMessage`, `initialScroll`, and visibility tracking — a message without one cannot be jumped to or marked as read.
16
+ - Do: Pass the same `align` to Message and to the Bubble inside it, so the flattened tail corner lands on the speaker's side.
17
+ - Do: Use `grouped` for consecutive messages from one author. It tightens the spacing and hides the avatar while keeping its column reserved, so the run reads as one block instead of stepping left. Pass `tail={false}` to the Bubble in each continuation too, so only the first message in the run carries a tail.
18
+ - Do: Match `avatarSize` to the `size` of the Avatar you pass (sm/md/lg line up 1:1). The gutter is declared, not measured, because it has to hold its width when `grouped` hides the avatar.
19
+ - Don't: Do not justify the content to move a message to the right. `align="end"` already mirrors the whole row, gutter included; adding your own justification fights it.
20
+ - Don't: Do not put the timestamp inside the Bubble. It belongs in `header` or `footer`, which stay outside the surface so `variant="plain"` content still has one.
21
+ - Don't: Do not reach for Message to lay out a system note or a date break. That is `Marker`, which is deliberately quieter than the message tier.
22
+
23
+ ### Anatomy
24
+
25
+ | Part | Required | Description |
26
+ | --- | --- | --- |
27
+ | Avatar gutter | no | Fixed-width column for the author's Avatar. Kept (but emptied) when `grouped`, which is what keeps a run aligned. |
28
+ | Header | no | Author name and timestamp, above the content. |
29
+ | Content | yes | The children — usually a Bubble, but equally an Attachment, a tool card, or plain rendered markdown. |
30
+ | Footer | no | Reactions, delivery receipts, retry actions. |
31
+
32
+ ## Props
33
+
34
+ | Name | Type | Default | Description |
35
+ | --- | --- | --- | --- |
36
+ | messageId | string | — | Stable id, published as `data-message-id`. The handle MessageScroller uses for scroll commands and visibility tracking. |
37
+ | align | "start" \| "end" | "start" | Which side of the conversation. "start" is the other party or the assistant; "end" is the person reading. Mirrors the whole row, gutter included. |
38
+ | avatar | ReactNode | — | Avatar node for the gutter. Omit for a message with no author identity. |
39
+ | avatarSize | "sm" \| "md" \| "lg" | "sm" | Gutter width, matching Avatar's own size scale 1:1 (32 / 40 / 48px). Declared rather than measured so it survives `grouped`. |
40
+ | header | ReactNode | — | Author name, timestamp — anything above the content. |
41
+ | footer | ReactNode | — | Reactions, receipts, retry actions — anything below the content. |
42
+ | grouped | boolean | false | This message continues a run from the same author: tightens the spacing (via `--message-gap`) and drops the avatar while reserving its column. |
43
+ | ...div props | Omit<ComponentPropsWithRef<"div">, "id"> | — | Spread onto the row. Use `messageId` rather than `id` — the scroller matches on `data-message-id`. |
44
+
45
+ ## Variants
46
+
47
+ - **align**: start, end
48
+ - **avatarSize**: sm, md, lg
49
+
50
+ ## Tokens used
51
+
52
+ `foreground`, `muted-foreground`
53
+
54
+ ## Accessibility
55
+
56
+ - A plain container with no imposed role — the conversation's semantics belong to the list or log element you wrap it in, not to each row.
57
+ - `align` reverses the visual row without reordering the DOM, so reading order in assistive tech stays chronological regardless of which side a message is on.
58
+ - The header and footer render at the secondary type step in muted ink, keeping both below the message tier in the visual hierarchy without dropping either below the AA contrast floor.
59
+
60
+ ## Related
61
+
62
+ - Components: Bubble, MessageScroller, Marker, Attachment, Avatar
63
+
64
+ ## Examples
65
+
66
+ ### Incoming and outgoing
67
+
68
+ ```tsx
69
+ import { Avatar, Bubble, Message, RelativeTime } from "@sableui/react"
70
+
71
+ <>
72
+ <Message messageId="m1" avatar={<Avatar size="sm" fallback="CA" />} header={<><strong className="text-foreground">Carla</strong><RelativeTime date={sentAt} /></>}>
73
+ <Bubble>Did the deploy finish?</Bubble>
74
+ </Message>
75
+ <Message messageId="m2" align="end">
76
+ <Bubble variant="primary" align="end">Green across the board.</Bubble>
77
+ </Message>
78
+ </>
79
+ ```
80
+
81
+ ### A grouped run
82
+
83
+ ```tsx
84
+ import { Avatar, Bubble, Message } from "@sableui/react"
85
+
86
+ <>
87
+ <Message messageId="m1" avatar={<Avatar size="sm" fallback="CA" />} header={<strong className="text-foreground">Carla</strong>}>
88
+ <Bubble>One more thing.</Bubble>
89
+ </Message>
90
+ <Message messageId="m2" grouped>
91
+ <Bubble tail={false}>The staging cert expires Friday.</Bubble>
92
+ </Message>
93
+ </>
94
+ ```
95
+
96
+ ### With reactions in the footer
97
+
98
+ ```tsx
99
+ import { Avatar, Bubble, BubbleReaction, BubbleReactions, Message } from "@sableui/react"
100
+
101
+ <Message messageId="m3" avatar={<Avatar size="sm" fallback="JR" />} footer={<BubbleReactions><BubbleReaction emoji="🎉" count={3} active /><BubbleReaction emoji="👀" /></BubbleReactions>}>
102
+ <Bubble>Shipped 0.45.0.</Bubble>
103
+ </Message>
104
+ ```
105
+
106
+ ### An attachment as the content
107
+
108
+ ```tsx
109
+ import { Attachment, Message } from "@sableui/react"
110
+
111
+ <Message messageId="m4" align="end">
112
+ <Attachment name="proposal.pdf" meta="2.4 MB · PDF" />
113
+ </Message>
114
+ ```
@@ -0,0 +1,124 @@
1
+ # Meter
2
+
3
+ **Status:** stable
4
+
5
+ A role=meter reading over Base UI Meter, drawn as a bar or a ring — how full something is within a known range (storage, quota, strength), with a tone for good/caution/bad. Not task progress.
6
+
7
+ **Keywords:** meter, usage bar, storage, quota, capacity, limit, password strength, level
8
+
9
+ ## Usage
10
+
11
+ Use Meter to show a static measurement against a known maximum — storage used, seats taken, API quota, password strength. It is announced as a meter, not a progress bar, so a screen-reader user hears a level rather than a task underway.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Label it and show the reading in units people use ("7.2 of 10 GB"), with MeterLabel and MeterValue. The bar shows proportion; the number is what gets acted on.
16
+ - Do: Pick `tone` from thresholds you own and apply them consistently across the page — warning near the limit, destructive at or over it. The component does not guess where good becomes bad.
17
+ - Do: Use shape="ring" when the reading is a score that belongs in a small, glanceable spot on a card. It is the same measurement drawn as a circular gauge, not a different component.
18
+ - Don't: Do not use Meter for a task in flight — an upload, an install, a step count. That is Progress; Meter tells assistive tech nothing is happening.
19
+ - Don't: Do not use it for a value without a meaningful maximum (followers, total spend). A bar implies a full scale; an unbounded count is a stat.
20
+
21
+ ### Anatomy
22
+
23
+ | Part | Required | Description |
24
+ | --- | --- | --- |
25
+ | MeterTrack | yes | The rail that represents the full range. |
26
+ | MeterIndicator | yes | The fill, sized from value and coloured by tone. |
27
+ | MeterLabel | no | The visible name; also names the meter for assistive tech. |
28
+ | MeterValue | no | The formatted reading. |
29
+
30
+ ## Subcomponents
31
+
32
+ | Name | Type | Description |
33
+ | --- | --- | --- |
34
+ | MeterTrack | Base UI Track props + className + size | The rail. Rendered automatically when Meter has no children; renders a MeterIndicator by default. |
35
+ | MeterIndicator | Base UI Indicator props + className + tone | The fill. Inherits the Meter's tone unless given its own. |
36
+ | MeterLabel | Base UI Label props + className | Visible label; wires aria-labelledby on the root. |
37
+ | MeterValue | Base UI Value props + className | Formatted reading. Accepts a `(formatted, value) => ReactNode` child. |
38
+
39
+ ## Props
40
+
41
+ | Name | Type | Default | Description |
42
+ | --- | --- | --- | --- |
43
+ | value | number | — | The current reading, between min and max. Required. |
44
+ | min | number | 0 | The bottom of the range. |
45
+ | max | number | 100 | The top of the range. |
46
+ | tone | "primary" \| "standard" \| "success" \| "warning" \| "destructive" | "primary" | Fill colour, mapped to a semantic token. Choose it from your own thresholds. |
47
+ | shape | "bar" \| "ring" | "bar" | bar is a horizontal track; ring is a circular gauge for a small, glanceable reading — the same value, min, max and tones. |
48
+ | size | "sm" \| "md" \| "lg" | "md" | Ring diameter (40 / 64 / 96px at the default spacing; the centre value steps with it), or bar thickness on the same sm / md / lg rails as Progress. |
49
+ | showValue | boolean | — | Print the value in the ring's centre (formatted like MeterValue). |
50
+ | strokeWidth | number | 6 | Ring stroke width, in viewBox units of 100. |
51
+ | format | Intl.NumberFormatOptions | — | Formatting for MeterValue and aria-valuetext (percent, unit, currency). |
52
+ | getAriaValueText | (formatted: string, value: number) => string | — | Human-readable reading for assistive tech ("7.2 of 10 gigabytes used"). |
53
+ | locale | Intl.LocalesArgument | — | Locale for formatting. Defaults to the runtime locale. |
54
+
55
+ ## Variants
56
+
57
+ - **tone**: primary, standard, success, warning, destructive
58
+ - **shape**: bar, ring
59
+ - **size**: sm, md, lg
60
+
61
+ ## Tokens used
62
+
63
+ `secondary`, `primary`, `success`, `tone-warning`, `destructive`, `foreground`, `muted-foreground`
64
+
65
+ ## Motion
66
+
67
+ - **arrival** · Meter · _no motion_ — A meter is a reading, present with the page that reports it. Animating the fill up from zero on load would act out a change that did not happen.
68
+ - **response** · Meter · _no motion_ — It reports and accepts no input. A bar that reacted to the pointer would read as a Slider the user could drag.
69
+ - **transition** · MeterIndicator · _custom (outside the recipe layer)_ — Width and colour ease on the motion tokens (no recipe animates width) when the reading changes — freeing space after a delete visibly shrinks the bar, so the user sees the cause and effect rather than a different bar.
70
+ - **activity** · Meter · _no motion_ — A meter has no indeterminate state; a reading is either known or not shown. Unknown-but-working is Progress with no value.
71
+
72
+ ## Accessibility
73
+
74
+ - Base UI renders role=meter with aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext — distinct from Progress's role=progressbar.
75
+ - Name it with MeterLabel or aria-label; an unnamed meter is announced as a bare number.
76
+ - Tone is colour only. Say the state in text too ("Almost full") when it matters, and use getAriaValueText to give units.
77
+
78
+ ## Related
79
+
80
+ - Components: Progress, Badge
81
+
82
+ ## Examples
83
+
84
+ ### Ring
85
+
86
+ ```tsx
87
+ import { Meter } from "@sableui/react"
88
+
89
+ <Meter shape="ring" value={72} showValue aria-label="Storage used" />
90
+ ```
91
+
92
+ ### Bare
93
+
94
+ ```tsx
95
+ import { Meter } from "@sableui/react"
96
+
97
+ <Meter value={64} aria-label="Storage used" />
98
+ ```
99
+
100
+ ### Storage with label and reading
101
+
102
+ ```tsx
103
+ import { Meter, MeterLabel, MeterTrack, MeterValue } from "@sableui/react"
104
+
105
+ <Meter value={7.2} max={10} tone="warning" getAriaValueText={(_, v) => `${v} of 10 gigabytes used`}>
106
+ <div className="mb-2 flex flex-wrap items-center justify-between gap-x-3 gap-y-1">
107
+ <MeterLabel>Storage</MeterLabel>
108
+ <MeterValue>{(_, value) => `${value} of 10 GB`}</MeterValue>
109
+ </div>
110
+ <MeterTrack />
111
+ </Meter>
112
+ ```
113
+
114
+ ### Tones
115
+
116
+ ```tsx
117
+ import { Meter } from "@sableui/react"
118
+
119
+ <div className="flex flex-col gap-3">
120
+ <Meter value={30} tone="success" aria-label="Seats used" />
121
+ <Meter value={82} tone="warning" aria-label="API quota used" />
122
+ <Meter value={100} tone="destructive" aria-label="Storage used" />
123
+ </div>
124
+ ```
@@ -0,0 +1,62 @@
1
+ # MiddleTruncate
2
+
3
+ **Status:** stable
4
+
5
+ Middle-truncates a long string (hash, address, signature) to `5J7s…8xQp`, keeping the start and end legible while preserving the full value on hover.
6
+
7
+ **Keywords:** truncate, middle truncate, hash, identifier
8
+
9
+ ## Usage
10
+
11
+ Use MiddleTruncate when both the beginning and end of a long identifier must remain visible.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Use it for hashes, addresses, long filenames, URLs, or generated IDs.
16
+ - Do: Provide the full value through title text, copy action, or surrounding detail when needed.
17
+ - Don't: Do not use MiddleTruncate for human prose where the middle words carry meaning.
18
+ - Don't: Do not middle-truncate a value with no way to obtain it in full. Truncation removes characters from the page, so the value needs a copy affordance beside it: an icon Button with useCopy, as in the Copyable value example.
19
+
20
+ ## Props
21
+
22
+ | Name | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | text | string | — | The full string to render. When it is truncated, the full value is also set as the title attribute (a pointer tooltip). |
25
+ | startChars | number | 6 | How many leading characters to keep before the ellipsis. |
26
+ | endChars | number | 6 | How many trailing characters to keep after the ellipsis. |
27
+
28
+ ## Accessibility
29
+
30
+ - When truncated, the visible abbreviation is aria-hidden and a visually hidden copy carries the full value, so screen readers announce the whole string. The title attribute holds it too, for a pointer tooltip only.
31
+ - Renders the string verbatim when it is too short to benefit from truncation, so no value is hidden needlessly.
32
+
33
+ ## Related
34
+
35
+ - Components: Code, Button
36
+
37
+ ## Examples
38
+
39
+ ### Solana address
40
+
41
+ ```tsx
42
+ import { MiddleTruncate } from "@sableui/react"
43
+
44
+ <MiddleTruncate text="5J7sX2QpHt9vK4mNbR8wYcZ3dF6gH1jL0pA8xQpVnTu" />
45
+ ```
46
+
47
+ ### Custom start/end counts
48
+
49
+ ```tsx
50
+ import { MiddleTruncate } from "@sableui/react"
51
+
52
+ <MiddleTruncate text="0xAbC1234567890dEf1234567890aBcDeF12345678" startChars={4} endChars={4} />
53
+ ```
54
+
55
+ ### Copyable value
56
+
57
+ ```tsx
58
+ import { Button, Icon, MiddleTruncate, Row, useCopy } from "@sableui/react"
59
+
60
+ const { copy, copied } = useCopy(address)
61
+ <Row gap={1}><MiddleTruncate text={address} /><Button variant="ghost" size="icon-xs" aria-label="Copy address" onClick={copy}><Icon name={copied ? "check" : "copy"} /></Button></Row>
62
+ ```