@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.
- package/AGENTS.md +157 -0
- package/CHANGELOG.md +55 -0
- package/CONTRIBUTING.md +221 -0
- package/DESIGN.md +548 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/THIRD_PARTY_NOTICES.md +44 -0
- package/bin/sable-lint.mjs +0 -0
- package/bin/sable.mjs +1223 -0
- package/dist/_contract-CYScOdf5.d.ts +150 -0
- package/dist/chunk-3IWZYBGH.js +398 -0
- package/dist/chunk-5Z5KLOAU.js +167 -0
- package/dist/chunk-7MG4SHYX.js +15 -0
- package/dist/chunk-ADLLDWLW.js +15 -0
- package/dist/chunk-EP55KSYA.js +14 -0
- package/dist/chunk-J23ELQQZ.js +9396 -0
- package/dist/chunk-JI7WJP2L.js +286 -0
- package/dist/chunk-K7RUHUGC.js +14 -0
- package/dist/chunk-KO3NAEBF.js +15 -0
- package/dist/chunk-L4IEIQQA.js +14 -0
- package/dist/chunk-L6Y46C62.js +270 -0
- package/dist/chunk-NEQNXI2G.js +25 -0
- package/dist/chunk-PAOZBZME.js +40 -0
- package/dist/chunk-RWP4XBSK.js +139 -0
- package/dist/chunk-SRQWQUTW.js +63 -0
- package/dist/chunk-YXBOYWBN.js +15 -0
- package/dist/foundations/index.d.ts +88 -0
- package/dist/foundations/index.js +792 -0
- package/dist/icon/index.d.ts +60 -0
- package/dist/icon/index.js +2 -0
- package/dist/icon-sets/brands.d.ts +44 -0
- package/dist/icon-sets/brands.js +147 -0
- package/dist/icon-sets/reicon.d.ts +8 -0
- package/dist/icon-sets/reicon.js +1 -0
- package/dist/index.d.ts +7013 -0
- package/dist/index.js +15462 -0
- package/dist/ios/chrome.css +83 -0
- package/dist/ios/frame.css +5 -0
- package/dist/ios/index.d.ts +70 -0
- package/dist/ios/index.js +313 -0
- package/dist/ios/preview.d.ts +64 -0
- package/dist/ios/preview.js +133 -0
- package/dist/ios/styles.css +77 -0
- package/dist/layout/index.d.ts +280 -0
- package/dist/layout/index.js +2 -0
- package/dist/meta-6dUzgC6M.d.ts +205 -0
- package/dist/patterns/index.d.ts +36 -0
- package/dist/patterns/index.js +934 -0
- package/dist/presets/index.d.ts +37 -0
- package/dist/presets/index.js +117 -0
- package/dist/registry-CyL28OzJ.d.ts +28 -0
- package/dist/styles.css +1893 -0
- package/dist/theme-provider-BWcizhAt.d.ts +105 -0
- package/dist/themes/atelier/atelier.css +213 -0
- package/dist/themes/atelier/index.d.ts +9 -0
- package/dist/themes/atelier/index.js +1 -0
- package/dist/themes/base/base.css +443 -0
- package/dist/themes/base/index.d.ts +9 -0
- package/dist/themes/base/index.js +1 -0
- package/dist/themes/ember/ember.css +270 -0
- package/dist/themes/ember/index.d.ts +9 -0
- package/dist/themes/ember/index.js +1 -0
- package/dist/themes/index.d.ts +10 -0
- package/dist/themes/index.js +8 -0
- package/dist/themes/meridian/index.d.ts +9 -0
- package/dist/themes/meridian/index.js +1 -0
- package/dist/themes/meridian/meridian.css +255 -0
- package/dist/themes/poppy/index.d.ts +6 -0
- package/dist/themes/poppy/index.js +1 -0
- package/dist/themes/poppy/poppy.css +174 -0
- package/dist/themes/prism/index.d.ts +6 -0
- package/dist/themes/prism/index.js +1 -0
- package/dist/themes/prism/prism.css +183 -0
- package/dist/themes/verdant/index.d.ts +9 -0
- package/dist/themes/verdant/index.js +1 -0
- package/dist/themes/verdant/verdant.css +210 -0
- package/dist/themes.css +30 -0
- package/llms/components/accordion.md +93 -0
- package/llms/components/app-header.md +70 -0
- package/llms/components/app-shell.md +117 -0
- package/llms/components/attachment.md +105 -0
- package/llms/components/avatar.md +141 -0
- package/llms/components/badge.md +117 -0
- package/llms/components/banner.md +119 -0
- package/llms/components/bottom-nav.md +80 -0
- package/llms/components/breadcrumbs.md +75 -0
- package/llms/components/browser.md +67 -0
- package/llms/components/bubble.md +119 -0
- package/llms/components/button.md +199 -0
- package/llms/components/calendar.md +75 -0
- package/llms/components/card.md +135 -0
- package/llms/components/carousel.md +87 -0
- package/llms/components/chat-composer.md +126 -0
- package/llms/components/checkbox-card.md +98 -0
- package/llms/components/checkbox.md +90 -0
- package/llms/components/code-block.md +81 -0
- package/llms/components/code.md +62 -0
- package/llms/components/collapsible.md +83 -0
- package/llms/components/color-field.md +100 -0
- package/llms/components/combobox.md +119 -0
- package/llms/components/command-menu.md +158 -0
- package/llms/components/context-card.md +73 -0
- package/llms/components/context-menu.md +79 -0
- package/llms/components/data-table.md +99 -0
- package/llms/components/date-picker.md +97 -0
- package/llms/components/description.md +96 -0
- package/llms/components/destructive-action-modal.md +76 -0
- package/llms/components/dialog.md +127 -0
- package/llms/components/dots-menu.md +64 -0
- package/llms/components/empty-state.md +113 -0
- package/llms/components/entity.md +67 -0
- package/llms/components/feedback.md +63 -0
- package/llms/components/fieldset.md +106 -0
- package/llms/components/file-dropzone.md +103 -0
- package/llms/components/form.md +111 -0
- package/llms/components/gliding-highlight.md +84 -0
- package/llms/components/inline-status.md +62 -0
- package/llms/components/input.md +127 -0
- package/llms/components/kbd.md +57 -0
- package/llms/components/label.md +73 -0
- package/llms/components/marker.md +99 -0
- package/llms/components/marquee.md +66 -0
- package/llms/components/menu.md +122 -0
- package/llms/components/message-scroller.md +110 -0
- package/llms/components/message.md +114 -0
- package/llms/components/meter.md +124 -0
- package/llms/components/middle-truncate.md +62 -0
- package/llms/components/note.md +81 -0
- package/llms/components/notification-badge.md +67 -0
- package/llms/components/number-field.md +70 -0
- package/llms/components/otp-field.md +101 -0
- package/llms/components/page-header.md +76 -0
- package/llms/components/page-loader.md +67 -0
- package/llms/components/pagination.md +92 -0
- package/llms/components/panel-group.md +120 -0
- package/llms/components/panel-section.md +122 -0
- package/llms/components/phone.md +70 -0
- package/llms/components/pill.md +73 -0
- package/llms/components/popover.md +79 -0
- package/llms/components/progress.md +98 -0
- package/llms/components/radio-card.md +90 -0
- package/llms/components/radio.md +86 -0
- package/llms/components/relative-time.md +60 -0
- package/llms/components/scroller.md +88 -0
- package/llms/components/scrub-field.md +97 -0
- package/llms/components/search-input.md +84 -0
- package/llms/components/select.md +106 -0
- package/llms/components/separator.md +70 -0
- package/llms/components/settings-menu.md +139 -0
- package/llms/components/sheet.md +97 -0
- package/llms/components/show-more.md +59 -0
- package/llms/components/side-nav.md +106 -0
- package/llms/components/sidebar.md +87 -0
- package/llms/components/skeleton.md +62 -0
- package/llms/components/slider.md +114 -0
- package/llms/components/spinner.md +87 -0
- package/llms/components/status-bar.md +91 -0
- package/llms/components/status-dot.md +70 -0
- package/llms/components/stepper.md +125 -0
- package/llms/components/streaming-text.md +86 -0
- package/llms/components/switch.md +87 -0
- package/llms/components/table.md +112 -0
- package/llms/components/tabs.md +84 -0
- package/llms/components/textarea.md +88 -0
- package/llms/components/theme-selector.md +95 -0
- package/llms/components/theme-switcher.md +71 -0
- package/llms/components/thinking-orb.md +91 -0
- package/llms/components/toast.md +130 -0
- package/llms/components/toggle.md +95 -0
- package/llms/components/toolbar.md +168 -0
- package/llms/components/tooltip.md +93 -0
- package/llms/components/tree.md +227 -0
- package/llms/components/video.md +66 -0
- package/llms/components/workspace.md +138 -0
- package/llms/foundations/accessibility.md +60 -0
- package/llms/foundations/motion.md +96 -0
- package/llms/foundations/scrolling.md +41 -0
- package/llms/foundations/shape.md +73 -0
- package/llms/foundations/spacing.md +82 -0
- package/llms/foundations/surfaces.md +69 -0
- package/llms/foundations/typography.md +120 -0
- package/llms/layout/aspect-box.md +37 -0
- package/llms/layout/bleed.md +31 -0
- package/llms/layout/center.md +37 -0
- package/llms/layout/columns.md +51 -0
- package/llms/layout/grid-overlay.md +40 -0
- package/llms/layout/grid.md +47 -0
- package/llms/layout/page.md +39 -0
- package/llms/layout/region.md +49 -0
- package/llms/layout/row.md +40 -0
- package/llms/layout/safe-area.md +37 -0
- package/llms/layout/spacer.md +39 -0
- package/llms/layout/split-pane.md +41 -0
- package/llms/layout/stack.md +39 -0
- package/llms/patterns/auth-page.md +68 -0
- package/llms/patterns/brand-landing-page.md +423 -0
- package/llms/patterns/chat-panel.md +210 -0
- package/llms/patterns/dashboard-page.md +170 -0
- package/llms/patterns/feed-page.md +103 -0
- package/llms/patterns/landing-page.md +122 -0
- package/llms/patterns/list-detail-page.md +100 -0
- package/llms/patterns/settings-page.md +121 -0
- package/llms/reference/token-intent.md +324 -0
- package/llms/reference/utilities.md +129 -0
- package/llms/tasks/adding-a-component.md +113 -0
- package/llms/tasks/auditing-an-app.md +216 -0
- package/llms/tasks/building-a-page.md +109 -0
- package/llms/tasks/building-a-tool.md +141 -0
- package/llms/tasks/building-an-ios-prototype.md +105 -0
- package/llms/tasks/building-with-sable.md +73 -0
- package/llms/tasks/choosing-a-component.md +109 -0
- package/llms/tasks/converting-an-app.md +139 -0
- package/llms/tasks/creating-a-preset.md +43 -0
- package/llms/tasks/creating-design-assets.md +96 -0
- package/llms/tasks/designing-a-surface.md +115 -0
- package/llms/tasks/developing-taste.md +157 -0
- package/llms/tasks/getting-started.md +63 -0
- package/llms/tasks/learning-from-builds.md +74 -0
- package/llms/tasks/reviewing-a-design.md +114 -0
- package/llms/tasks/reviewing-a-journey.md +84 -0
- package/llms/tasks/reviewing-motion.md +92 -0
- package/llms/tasks/using-presets.md +87 -0
- package/llms/taste/asset-plan.template.json +53 -0
- package/llms/taste/composition-follows-content.md +20 -0
- package/llms/taste/hierarchy-is-a-relationship.md +22 -0
- package/llms/taste/lessons.json +57 -0
- package/llms/taste/motion-needs-a-reason.md +21 -0
- package/llms/taste/surface-states-travel-together.md +20 -0
- package/llms-full.txt +14906 -0
- package/llms.txt +405 -0
- package/package.json +185 -3
- package/registry.json +18812 -0
- package/scripts/agent-setup.mjs +54 -0
- package/scripts/design-atlas.mjs +221 -0
- package/scripts/design-directions.mjs +123 -0
- package/scripts/design-memory.mjs +166 -0
- package/scripts/design-preferences.mjs +227 -0
- package/scripts/design-presets.mjs +48 -0
- package/scripts/grade.mjs +509 -0
- package/scripts/lint-rules.mjs +1065 -0
- package/scripts/review-evidence.mjs +120 -0
- package/scripts/skeleton.mjs +248 -0
- package/skills/entry/SKILL.md +34 -0
- package/skills/sable/SKILL.md +187 -0
- package/skills/sable/agents/openai.yaml +6 -0
- package/targets/ios.json +982 -0
- 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
|
+
```
|