@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,127 @@
|
|
|
1
|
+
# Dialog
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Modal surface over Base UI Dialog (focus trap, scroll lock, Esc/outside dismissal). Centered on desktop; max-width 500px. size="fullscreen" is the full-bleed media viewer (image, video, Carousel) over a near-opaque scrim.
|
|
6
|
+
|
|
7
|
+
**Keywords:** dialog, modal, overlay, confirmation, alert, form, focus trap, sheet, lightbox, fullscreen, image viewer, media overlay, gallery
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Dialog for focused decisions, short modal forms, confirmations, and content that temporarily blocks the underlying page. Prefer Sheet for side-panel workflows, Popover for lightweight anchored content, and inline sections for content that should remain part of the page.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Always include DialogTitle and usually DialogDescription so the modal has an accessible name and context.
|
|
16
|
+
- Do: Keep the footer actions explicit: a safe escape path plus the primary or destructive action.
|
|
17
|
+
- Do: Use showCloseButton for dismissible informational dialogs; keep destructive confirmations action-led.
|
|
18
|
+
- Do: In a destructive confirmation, state the consequence in DialogDescription ("This is permanent.") and reserve the destructive button variant for actions that cannot be undone.
|
|
19
|
+
- Do: Use size="fullscreen" when an image, video, or gallery needs a larger view than the layout can give it. Give the popup an accessible name (aria-label on DialogContent, or a visually hidden DialogTitle) and alt text on the media.
|
|
20
|
+
- Do: Use the AlertDialog family for a confirmation that must be answered (discard changes, leave without saving). It looks identical but is role="alertdialog" and a backdrop click does not dismiss it — only Cancel, Esc, or the action. For irreversible deletes, use DestructiveActionModal, which is built on it.
|
|
21
|
+
- Don't: Put large multipage workflows in a centered dialog. Use Sheet or a page route when the task needs space.
|
|
22
|
+
- Don't: Use size="fullscreen" for forms, confirmations, or anything that isn't media. A dialog that happens to need room is a Sheet or a page; the fullscreen affordances (dark scrim, corner close, centred content) all assume something to look at.
|
|
23
|
+
- Don't: Hide the close button at size="fullscreen" without another visible way out. A full-bleed viewer that only Esc dismisses is a dead end for anyone who doesn't know the key.
|
|
24
|
+
- Don't: Open a dialog for passive status updates. Use Toast, Banner, or InlineStatus instead.
|
|
25
|
+
|
|
26
|
+
### Anatomy
|
|
27
|
+
|
|
28
|
+
| Part | Required | Description |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| Trigger | no | The element that opens the dialog when using the composed trigger pattern. |
|
|
31
|
+
| Overlay | yes | The dimming backdrop behind the modal content. |
|
|
32
|
+
| Content | yes | The modal surface that receives focus trap, labelled-by, and described-by wiring. |
|
|
33
|
+
| Header | yes | Title and optional description that explain the decision or task. |
|
|
34
|
+
| Footer | no | Action row for cancel, confirm, submit, or destructive choices. |
|
|
35
|
+
|
|
36
|
+
## Subcomponents
|
|
37
|
+
|
|
38
|
+
| Name | Type | Description |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| DialogContent | Base UI Popup props + size?: "default" \| "fullscreen" + showCloseButton?: boolean + scroll?: boolean + className | Backdrop + popup; portals to <body>. Composes DialogOverlay by default. size="fullscreen" is the media viewer: a near-opaque scrim (bg-overlay/95), an edge-to-edge popup that centres its children with no card chrome, motion-fade instead of motion-pop, and a safe-area-aware floating close button (Button variant="floating"); at that size showCloseButton defaults to true and scroll to false, and the media fits itself with max-h-full max-w-full object-contain. At the default size, showCloseButton (defaults to false) renders a built-in close (X) button in the top-end corner, and the header leaves room for it (Icon name="xmark"). The popup caps at the viewport height less a 1rem margin and wraps its children in a scrolling region, so a tall dialog scrolls instead of overflowing a viewport whose own scroll is locked; pass scroll={false} when the dialog owns its internal layout (a pinned header over a scrolling body) and supplies its own min-h-0 scroll region. |
|
|
41
|
+
| DialogOverlay | Base UI Backdrop props + size?: "default" \| "fullscreen" + className | The styleable scrim/backdrop — bg-overlay/50 by default, near-opaque bg-overlay/95 at size="fullscreen". Composed into DialogContent (at the matching size) by default; exported for manual styling. |
|
|
42
|
+
| DialogTrigger | Base UI Dialog.Trigger props (render) | Element that opens the dialog. Compose via render prop. |
|
|
43
|
+
| DialogClose | Base UI Dialog.Close props (render) | Element that dismisses the dialog. Compose via render prop. |
|
|
44
|
+
| DialogHeader | ComponentProps<"div"> | Top section holding DialogTitle/DialogDescription. |
|
|
45
|
+
| DialogTitle | Base UI Dialog.Title props + className | The dialog's accessible name — wires aria-labelledby on the popup. |
|
|
46
|
+
| DialogDescription | Base UI Dialog.Description props + className | Muted supporting line; wires aria-describedby. |
|
|
47
|
+
| DialogFooter | ComponentProps<"div"> | Bottom action row (right-aligned buttons). |
|
|
48
|
+
| AlertDialog (Root) | Base UI AlertDialog.Root props (open, defaultOpen, onOpenChange) | Root for a confirmation that needs an answer. Always modal; the popup is role="alertdialog" and a click outside does not dismiss — only an AlertDialogClose, an action that sets open to false, or Esc. |
|
|
49
|
+
| AlertDialogTrigger | Base UI AlertDialog.Trigger props (render) | Opens the alert dialog. |
|
|
50
|
+
| AlertDialogContent / AlertDialogOverlay / AlertDialogTitle / AlertDialogDescription / AlertDialogHeader / AlertDialogFooter / AlertDialogClose | Same props as the matching Dialog part | The Dialog parts under AlertDialog names — Base UI shares the popup, backdrop, title, description and close parts between the two, so the surface is identical. Pass initialFocus to AlertDialogContent (a ref to the Cancel button) to open focus on the safe action. |
|
|
51
|
+
|
|
52
|
+
## Props
|
|
53
|
+
|
|
54
|
+
| Name | Type | Default | Description |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| Dialog (Root) | Base UI Dialog.Root props (open, defaultOpen, onOpenChange, modal) | — | Controls open state. Uncontrolled by default. |
|
|
57
|
+
|
|
58
|
+
## Variants
|
|
59
|
+
|
|
60
|
+
- **size**: default, fullscreen
|
|
61
|
+
|
|
62
|
+
## Tokens used
|
|
63
|
+
|
|
64
|
+
`popover`, `popover-foreground`, `border`, `muted-foreground`, `overlay`, `overlay-foreground`, `accent`, `accent-foreground`, `ring`
|
|
65
|
+
|
|
66
|
+
## Motion
|
|
67
|
+
|
|
68
|
+
- **arrival** · DialogOverlay · `motion-fade` — The scrim is what tells the reader the page behind is no longer live. Appearing at full opacity in one frame reads as a page navigation — the surrounding context vanishes instead of receding.
|
|
69
|
+
- **arrival** · DialogContent · `motion-pop` — The panel scales up as the scrim darkens, so the two read as one event. Overlay and panel arriving on separate timings is the most common way a modal feels cheap.
|
|
70
|
+
- **response** · DialogClose · `motion-interactive` — The close affordance is a small icon in a corner and is often the only control a reader looks for. Its hover state has to be findable, not just present.
|
|
71
|
+
- **transition** · DialogContent · `motion-pop` — Exit runs the same recipe in reverse so dismissal is legibly the inverse of opening. A modal that fades out having scaled in leaves the reader unsure whether it closed or navigated away.
|
|
72
|
+
- **activity** · DialogContent · _no motion_ — The dialog is a container. Work happening inside it is the content's to report — a dialog-level indicator would claim the whole surface is busy when usually one field is.
|
|
73
|
+
|
|
74
|
+
## Accessibility
|
|
75
|
+
|
|
76
|
+
- Base UI provides focus trapping, scroll lock, aria-modal, and labelled-by wiring (DialogTitle).
|
|
77
|
+
- Content taller than the viewport scrolls inside the dialog (thin scrollbar, contained overscroll) rather than overflowing off screen — the page behind is scroll-locked, so anything past the edge would be unreachable.
|
|
78
|
+
- Esc and click-outside dismiss by default. AlertDialog keeps Esc but ignores click-outside, and its popup is role="alertdialog".
|
|
79
|
+
- Animate via data-[starting-style]/data-[ending-style] attributes.
|
|
80
|
+
- size="fullscreen" has no title chrome, so name the popup yourself: aria-label on DialogContent (or a visually hidden DialogTitle), plus alt text on the media — the zoomed image is a separate image to assistive technology. Its built-in close button carries aria-label="Close" and clears the safe-area insets (notch, home indicator).
|
|
81
|
+
|
|
82
|
+
## Related
|
|
83
|
+
|
|
84
|
+
- Components: Sheet, Popover, Toast, Button, Form, Note, Carousel, Video
|
|
85
|
+
- Patterns: SettingsPage, AuthPage
|
|
86
|
+
|
|
87
|
+
## Examples
|
|
88
|
+
|
|
89
|
+
### Fullscreen media
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { Button, Dialog, DialogContent, DialogTrigger } from "@sableui/react"
|
|
93
|
+
|
|
94
|
+
<Dialog><DialogTrigger render={<Button variant="secondary">View full size</Button>} /><DialogContent size="fullscreen" aria-label="Studio, 2026"><img src={url} alt="Studio, 2026" className="max-h-full max-w-full object-contain p-8" /></DialogContent></Dialog>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Fullscreen gallery
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { Button, Carousel, Dialog, DialogContent, DialogTrigger } from "@sableui/react"
|
|
101
|
+
|
|
102
|
+
<Dialog><DialogTrigger render={<Button>Open gallery</Button>} /><DialogContent size="fullscreen" aria-label="Gallery"><Carousel loop dots counter label="Gallery" className="w-full max-w-4xl px-8">{images.map((src) => <img key={src} src={src} alt="" className="size-full object-contain" />)}</Carousel></DialogContent></Dialog>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Basic
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@sableui/react"
|
|
109
|
+
|
|
110
|
+
<Dialog><DialogTrigger render={<Button>Open</Button>} /><DialogContent><DialogHeader><DialogTitle>Edition details</DialogTitle><DialogDescription>A limited run of 100, stored permanently on Arweave.</DialogDescription></DialogHeader><DialogFooter><DialogClose render={<Button variant="secondary">Close</Button>} /><DialogClose render={<Button>Collect</Button>} /></DialogFooter></DialogContent></Dialog>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Settings form
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { Button, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, Field, Form, Input } from "@sableui/react"
|
|
117
|
+
|
|
118
|
+
<Dialog><DialogTrigger render={<Button variant="secondary">Rename collection</Button>} /><DialogContent showCloseButton><DialogHeader><DialogTitle>Rename collection</DialogTitle><DialogDescription>Update the display name shown to collectors.</DialogDescription></DialogHeader><Form><Field label="Collection name"><Input defaultValue="Midnight Archive" /></Field><DialogFooter><DialogClose render={<Button variant="secondary">Cancel</Button>} /><Button type="submit">Save changes</Button></DialogFooter></Form></DialogContent></Dialog>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Destructive confirmation
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, Button } from "@sableui/react"
|
|
125
|
+
|
|
126
|
+
<AlertDialog><AlertDialogTrigger render={<Button variant="destructive">Delete edition</Button>} /><AlertDialogContent><AlertDialogHeader><AlertDialogTitle>Delete edition?</AlertDialogTitle><AlertDialogDescription>This removes the edition and cannot be undone.</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogClose render={<Button variant="secondary">Cancel</Button>} /><Button variant="destructive" onClick={deleteEdition}>Delete edition</Button></AlertDialogFooter></AlertDialogContent></AlertDialog>
|
|
127
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# DotsMenu
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
An overflow ellipsis icon button that opens a Menu. Composes our Menu + Button + Icon — pass MenuItems as children and it wires the ghost icon trigger, accessible label, and portal-rendered content. All keyboard, focus, and dismissal behavior comes through Menu.
|
|
6
|
+
|
|
7
|
+
**Keywords:** dots menu, more actions, overflow, kebab
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use DotsMenu for compact overflow actions when the primary action is already visible elsewhere.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use it for secondary commands that belong to a row, card, or toolbar item.
|
|
16
|
+
- Do: Keep labels explicit so the icon-only trigger does not obscure the command meaning.
|
|
17
|
+
- Don't: Do not hide the most important or most frequent action inside DotsMenu.
|
|
18
|
+
- Don't: Do not use it as the only affordance on a touch surface. It is revealed on hover in most layouts, and hover does not exist on touch — pair it with a visible action or make it always-visible there.
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Name | Type | Default | Description |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| children | ReactNode | — | The menu rows — MenuItem / MenuSeparator / MenuGroup, etc. Rendered inside MenuContent. |
|
|
25
|
+
| align | 'start' \| 'center' \| 'end' | end | Alignment of the popup along its side. Overflow menus typically right-align under the trigger; 'start' anchors the popup's left edge to the trigger instead. |
|
|
26
|
+
| label | string | More actions | Accessible label (aria-label) for the icon-only trigger button. |
|
|
27
|
+
| icon | string | ellipsis | Semantic icon name rendered in the trigger. |
|
|
28
|
+
| size | 'icon' \| 'icon-sm' \| 'icon-xs' | icon | The trigger's rung on the icon-button ladder. 'icon' for a card or page header; 'icon-sm' or 'icon-xs' in a list row or dense toolbar, so the trigger does not set the row height. |
|
|
29
|
+
|
|
30
|
+
## Variants
|
|
31
|
+
|
|
32
|
+
- **size**: icon, icon-sm, icon-xs
|
|
33
|
+
|
|
34
|
+
## Tokens used
|
|
35
|
+
|
|
36
|
+
`popover`, `popover-foreground`, `border`, `accent`, `accent-foreground`, `muted-foreground`
|
|
37
|
+
|
|
38
|
+
## Accessibility
|
|
39
|
+
|
|
40
|
+
- The icon-only trigger is labeled via aria-label (label prop, default 'More actions').
|
|
41
|
+
- Menu provides role=menu/menuitem, roving tabindex, arrow-key navigation, and typeahead via Base UI.
|
|
42
|
+
- Esc and outside-press dismiss; focus returns to the trigger on close.
|
|
43
|
+
|
|
44
|
+
## Related
|
|
45
|
+
|
|
46
|
+
- Components: Menu, ContextMenu
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
### Basic
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { DotsMenu, MenuItem, MenuSeparator } from "@sableui/react"
|
|
54
|
+
|
|
55
|
+
<DotsMenu label="Post actions"><MenuItem>Edit</MenuItem><MenuItem>Share</MenuItem><MenuSeparator /><MenuItem variant="destructive">Delete</MenuItem></DotsMenu>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### With leading icons
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { DotsMenu, Icon, MenuItem, MenuSeparator } from "@sableui/react"
|
|
62
|
+
|
|
63
|
+
<DotsMenu><MenuItem><Icon name="check" className="text-sm" />Mark as read</MenuItem><MenuSeparator /><MenuItem variant="destructive"><Icon name="xmark" className="text-sm" />Delete</MenuItem></DotsMenu>
|
|
64
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# EmptyState
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
The centered empty / error block: optional icon disc or error code, title, description and an action. tone="destructive" makes it an announced error, size="lg" a page-level state, surface="card" a self-contained card.
|
|
6
|
+
|
|
7
|
+
**Keywords:** empty, zero state, blank slate, no results, placeholder, onboarding, no data
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use EmptyState when a content area has no items, no search results, or no first-time setup yet. The title should name the empty condition and the description/action should give the user a next step.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Explain what is empty in plain language, such as "No posts yet" or "No matching collectors".
|
|
16
|
+
- Do: Offer one next step when the user can recover, create, clear filters, or start setup.
|
|
17
|
+
- Do: Keep the icon decorative and make the title carry the meaning.
|
|
18
|
+
- Do: For a search or filter result, name the query back to the user and offer the way out, such as "No results for 'quarterly'" with a Clear filters action. An empty result that does not say what was searched leaves the user unsure whether the query or the data was wrong.
|
|
19
|
+
- Don't: Use generic copy like "No data"; it gives users and agents no useful path forward.
|
|
20
|
+
- Don't: Park anything the user needs permanently here, such as setup instructions, billing notices, or keyboard shortcuts. An empty state is the one surface guaranteed to disappear the moment content exists, so information that matters after the first item is lost exactly when the user has learned enough to want it.
|
|
21
|
+
- Don't: Use it for a field's validation error — that is Field error messaging. Reserve tone="destructive" for a view or block that failed to load.
|
|
22
|
+
|
|
23
|
+
### Anatomy
|
|
24
|
+
|
|
25
|
+
| Part | Required | Description |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Icon | no | A decorative cue that reinforces the empty condition. |
|
|
28
|
+
| Title | yes | The short message naming what is empty. |
|
|
29
|
+
| Description | no | Supporting copy that explains why the state exists or what to try next. |
|
|
30
|
+
| Action | no | One clear button or link that moves the user forward. |
|
|
31
|
+
|
|
32
|
+
## Props
|
|
33
|
+
|
|
34
|
+
| Name | Type | Default | Description |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| icon | ReactNode | — | Optional leading glyph — pass an `<Icon />`. Rendered in a soft muted disc. |
|
|
37
|
+
| title | ReactNode | — | The headline. Short and direct. |
|
|
38
|
+
| description | ReactNode | — | Optional supporting line beneath the title. |
|
|
39
|
+
| action | ReactNode | — | Optional action below the copy — typically a `<Button />`. |
|
|
40
|
+
| code | ReactNode | — | A large figure above the title, such as an error code (404). |
|
|
41
|
+
| tone | "neutral" \| "destructive" | "neutral" | destructive is an error state: a destructive icon disc (a warning by default) and role="alert" so it is announced. |
|
|
42
|
+
| size | "md" \| "lg" | "md" | lg is page-level: the title is an h2 and the block takes more room. md sits in a panel, list or card. |
|
|
43
|
+
| surface | "none" \| "card" | "none" | card draws it as a Card, for an error or empty block standing on its own in a page. |
|
|
44
|
+
|
|
45
|
+
## Variants
|
|
46
|
+
|
|
47
|
+
- **tone**: neutral, destructive
|
|
48
|
+
- **size**: md, lg
|
|
49
|
+
- **surface**: none, card
|
|
50
|
+
|
|
51
|
+
## Tokens used
|
|
52
|
+
|
|
53
|
+
`muted`, `muted-foreground`, `foreground`, `destructive`
|
|
54
|
+
|
|
55
|
+
## Accessibility
|
|
56
|
+
|
|
57
|
+
- Decorative icon discs should not carry meaning the title doesn't already convey.
|
|
58
|
+
- Pair with an actionable next step so the zero-state is a path forward, not a dead end.
|
|
59
|
+
|
|
60
|
+
## Related
|
|
61
|
+
|
|
62
|
+
- Components: Button, Icon, Card, Banner, DataTable
|
|
63
|
+
- Patterns: FeedPage, ListDetailPage, DashboardPage
|
|
64
|
+
|
|
65
|
+
## Examples
|
|
66
|
+
|
|
67
|
+
### With icon and action
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { Button, EmptyState, Icon } from "@sableui/react"
|
|
71
|
+
|
|
72
|
+
<EmptyState icon={<Icon name="info" />} title="No posts yet" description="Create your first." action={<Button>New post</Button>} />
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### No search results
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { Button, EmptyState, Icon } from "@sableui/react"
|
|
79
|
+
|
|
80
|
+
<EmptyState icon={<Icon name="search" />} title="No matching posts" description="Try a different keyword or clear your filters." action={<Button variant="secondary">Clear filters</Button>} />
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### First-run setup
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { Button, EmptyState, Icon, Row } from "@sableui/react"
|
|
87
|
+
|
|
88
|
+
<EmptyState surface="card" icon={<Icon name="sparkles" />} title="Start your collection" description="Create a first edition, import an existing drop, or invite collaborators." action={<Row gap={1}><Button>Create edition</Button><Button variant="ghost">Import</Button></Row>} />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Page-level error
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { Button, EmptyState } from "@sableui/react"
|
|
95
|
+
|
|
96
|
+
<EmptyState tone="destructive" size="lg" code="404" title="Page not found" description="The page you are looking for has moved or never existed." action={<Button>Go home</Button>} />
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Error card
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { Button, EmptyState } from "@sableui/react"
|
|
103
|
+
|
|
104
|
+
<EmptyState tone="destructive" surface="card" title="Could not load this edition" description="Check your connection and try again." action={<Button variant="secondary">Retry</Button>} />
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Empty table body
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { DataTable, EmptyState } from "@sableui/react"
|
|
111
|
+
|
|
112
|
+
<DataTable data={[]} columns={[{ key: "name", header: "Collector" }, { key: "status", header: "Status" }]} empty={<EmptyState title="No collectors yet" description="Collectors appear here after the first mint." />} />
|
|
113
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Entity
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A list-row record: leading visual + title/subtitle column + trailing action, with truncating text. Presentational by default (no hover); `interactive` rows gain the hover highlight, focus ring, and keyboard activation. Compact/comfortable density.
|
|
6
|
+
|
|
7
|
+
**Keywords:** entity, identity, object, avatar row
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Entity to show a compact identity block for a person, organization, file, project, or other object.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use a clear title and one or two useful metadata lines.
|
|
16
|
+
- Do: Pair with Avatar or Badge (dot for a status) when identity needs state or type.
|
|
17
|
+
- Do: For a list, stack Entity rows directly in a column; each row owns its padding. For a list in a card, put the rows in CardList, which insets them to the card's own padding and divides them, so the rows line up with the card's header instead of being tuned by hand with px-* classes.
|
|
18
|
+
- Don't: Do not use Entity for long descriptions, editable fields, or full card content.
|
|
19
|
+
- Don't: Do not make the whole row a link when it also carries a trailing action. Nesting an interactive element inside a link is invalid, and the action becomes unreachable by keyboard.
|
|
20
|
+
|
|
21
|
+
## Props
|
|
22
|
+
|
|
23
|
+
| Name | Type | Default | Description |
|
|
24
|
+
| --- | --- | --- | --- |
|
|
25
|
+
| leading | ReactNode | — | Leading visual — typically an Avatar or an Icon. |
|
|
26
|
+
| title | ReactNode | — | Primary line (text-title-sm, truncated). Required. |
|
|
27
|
+
| subtitle | ReactNode | — | Secondary line (text-body-sm muted, truncated). |
|
|
28
|
+
| action | ReactNode | — | Trailing slot — typically a ghost icon Button or a StatusDot. |
|
|
29
|
+
| interactive | boolean | false | Make the whole row a target: the leading visual and text become a native <button> whose hit area stretches over the row (focus ring, Enter/Space). The action stays outside it. Requires onSelect. |
|
|
30
|
+
| onSelect | () => void | — | Activation handler for an interactive row (fired on click and Enter/Space). Required when interactive. |
|
|
31
|
+
| density | "compact" \| "comfortable" | "compact" | Row padding: compact (12px × 10px, the default) or comfortable (16px × 12px), on the spacing scale. The insets read --entity-px / --entity-py first, which CardList sets from the card's own padding so rows in a card line up with its header. |
|
|
32
|
+
|
|
33
|
+
## Variants
|
|
34
|
+
|
|
35
|
+
- **density**: compact, comfortable
|
|
36
|
+
|
|
37
|
+
## Tokens used
|
|
38
|
+
|
|
39
|
+
`muted`, `foreground`, `muted-foreground`, `ring`
|
|
40
|
+
|
|
41
|
+
## Accessibility
|
|
42
|
+
|
|
43
|
+
- Default rows are presentational layout; semantics come from the content you place in title/subtitle/action.
|
|
44
|
+
- interactive rows render a real <button> around the leading visual and text, with its ::after stretched over the whole row — native focus, a focus-visible ring, and Enter/Space. The action slot sits outside that button and above the overlay, so a Button there stays valid HTML and keeps its own clicks.
|
|
45
|
+
- Give any interactive action an accessible label (e.g. aria-label on a ghost icon Button).
|
|
46
|
+
|
|
47
|
+
## Related
|
|
48
|
+
|
|
49
|
+
- Components: Avatar, Badge, Description
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
### Person row
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { Avatar, Button, Entity, Icon } from "@sableui/react"
|
|
57
|
+
|
|
58
|
+
<Entity leading={<Avatar fallback="AM" />} title="Alex Morgan" subtitle="@alex" action={<Button variant="ghost" size="icon" aria-label="More"><Icon name="ellipsis" /></Button>} />
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Interactive (selectable) row
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { Avatar, Entity } from "@sableui/react"
|
|
65
|
+
|
|
66
|
+
<Entity interactive onSelect={() => selectUser(user)} leading={<Avatar fallback="AM" />} title="Alex Morgan" subtitle="@alex" />
|
|
67
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Feedback
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
A compact feedback popover. Composes Popover + Textarea + Button + Toggle into a rating row plus a message field; submitting reports the rating + message and closes the popover.
|
|
6
|
+
|
|
7
|
+
**Keywords:** feedback, sentiment, rating, prompt
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Feedback to collect lightweight sentiment, usefulness, or quality signals from users.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Ask for feedback at a moment when the user has enough context to answer.
|
|
16
|
+
- Do: Keep the prompt short and optional unless feedback is required by the workflow.
|
|
17
|
+
- Don't: Do not use Feedback for errors, alerts, or required support forms.
|
|
18
|
+
- Don't: Do not require the message field. Most feedback is a rating with no comment, and forcing prose is how a feedback control stops being used.
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Name | Type | Default | Description |
|
|
23
|
+
| --- | --- | --- | --- |
|
|
24
|
+
| label | string | "Feedback" | Trigger button text. |
|
|
25
|
+
| heading | string | "Send feedback" | Heading shown above the rating row. |
|
|
26
|
+
| placeholder | string | "What can we improve?" | Placeholder for the message field. |
|
|
27
|
+
| submitLabel | string | "Send" | Submit button text. |
|
|
28
|
+
| ratings | FeedbackRating[] | 🙁 / 😐 / 🙂 | The rating options shown as a row of toggles. Each is { value, label }. |
|
|
29
|
+
| onSubmit | (data: { rating?: string; message: string }) => void | — | Called with the selected rating + message when the form is submitted. |
|
|
30
|
+
|
|
31
|
+
## Tokens used
|
|
32
|
+
|
|
33
|
+
`popover`, `popover-foreground`, `border`, `input`, `card`, `foreground`, `muted-foreground`, `accent`, `accent-foreground`, `ring`
|
|
34
|
+
|
|
35
|
+
## Accessibility
|
|
36
|
+
|
|
37
|
+
- Trigger opens the popover on press; Base UI manages focus into the form and restores it to the trigger on close.
|
|
38
|
+
- The rating row is a Base UI ToggleGroup with arrow-key roving focus; each toggle carries an aria-label.
|
|
39
|
+
- The form is named by its heading (aria-labelledby), so it is announced as "Send feedback" rather than an unnamed form.
|
|
40
|
+
- The message field carries an aria-label matching its placeholder.
|
|
41
|
+
- Submit is disabled until there is a rating or a non-whitespace message. Either alone is a complete answer.
|
|
42
|
+
|
|
43
|
+
## Related
|
|
44
|
+
|
|
45
|
+
- Components: Toast, Banner, Form
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
### Basic
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { Feedback } from "@sableui/react"
|
|
53
|
+
|
|
54
|
+
<Feedback onSubmit={({ rating, message }) => console.log(rating, message)} />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Custom labels
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { Feedback } from "@sableui/react"
|
|
61
|
+
|
|
62
|
+
<Feedback label="Tell us" submitLabel="Send it" placeholder="Describe the issue…" />
|
|
63
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Fieldset
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Form section panel (Card-like bordered surface) over Base UI Fieldset, with Legend/Description/Content/Footer parts.
|
|
6
|
+
|
|
7
|
+
**Keywords:** fieldset, form group, legend, fields
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use Fieldset to group related controls under a shared label or explanation.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: Use it for grouped form sections such as contact details, preferences, or permissions.
|
|
16
|
+
- Do: Keep the legend meaningful so assistive tech and agents understand the group purpose.
|
|
17
|
+
- Don't: Do not use Fieldset as a generic spacing wrapper outside forms.
|
|
18
|
+
- Don't: Do not use a Legend that is really a heading. A legend names the group's question for assistive technology; using it as a section title puts a question mark where the outline should be.
|
|
19
|
+
|
|
20
|
+
### Anatomy
|
|
21
|
+
|
|
22
|
+
| Part | Required | Description |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| Fieldset | yes | The bordered group: a native <fieldset> whose disabled state reaches every control inside it. |
|
|
25
|
+
| Legend | yes | FieldsetLegend: the question the group's controls answer, announced before each of them. |
|
|
26
|
+
| Description | no | FieldsetDescription: muted copy under the legend. |
|
|
27
|
+
| Content | yes | FieldsetContent: the stacked body, one Field per control. |
|
|
28
|
+
| Footer | no | FieldsetFooter: the group's actions on a muted wash. |
|
|
29
|
+
|
|
30
|
+
## Subcomponents
|
|
31
|
+
|
|
32
|
+
| Name | Type | Description |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| FieldsetLegend | ComponentPropsWithRef<typeof Fieldset.Legend> | The group's name — the question its controls answer. Base UI renders it as a <div> wired to the <fieldset> through aria-labelledby, so a screen reader announces it before each control in the group. This is the whole reason to reach for Fieldset. |
|
|
35
|
+
| FieldsetDescription | ComponentPropsWithRef<"p"> | Muted supporting copy under the legend. |
|
|
36
|
+
| FieldsetContent | ComponentPropsWithRef<"div"> | The stacked body holding one Field per control. |
|
|
37
|
+
| FieldsetFooter | ComponentPropsWithRef<"div"> | Optional right-aligned action bar on a muted wash. |
|
|
38
|
+
|
|
39
|
+
## Props
|
|
40
|
+
|
|
41
|
+
| Name | Type | Default | Description |
|
|
42
|
+
| --- | --- | --- | --- |
|
|
43
|
+
| disabled | boolean | — | Disables the whole group. Renders <fieldset disabled> (native control disabling) and surfaces data-[disabled] for dimming. |
|
|
44
|
+
| render | ReactElement \| function | — | Base UI render prop to replace or compose the underlying element. |
|
|
45
|
+
| className | string | — | Merged onto each part via cn() for one-off overrides. |
|
|
46
|
+
|
|
47
|
+
## Tokens used
|
|
48
|
+
|
|
49
|
+
`card`, `card-foreground`, `border`, `foreground`, `muted-foreground`, `muted`
|
|
50
|
+
|
|
51
|
+
## Accessibility
|
|
52
|
+
|
|
53
|
+
- Base.Root renders a native <fieldset>; Base.Legend renders an auto-associated label, so screen readers announce the group name.
|
|
54
|
+
- disabled propagates as native <fieldset disabled>, disabling all contained controls.
|
|
55
|
+
- Wrap each control inside FieldsetContent in a Field, which wires its own label, description and error to the control.
|
|
56
|
+
|
|
57
|
+
## Related
|
|
58
|
+
|
|
59
|
+
- Components: Form, Label, Description
|
|
60
|
+
|
|
61
|
+
## Examples
|
|
62
|
+
|
|
63
|
+
### A grouped question
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { Button, Checkbox, Field, Fieldset, FieldsetContent, FieldsetDescription, FieldsetFooter, FieldsetLegend } from "@sableui/react"
|
|
67
|
+
|
|
68
|
+
<Fieldset>
|
|
69
|
+
<FieldsetLegend>Notify me about</FieldsetLegend>
|
|
70
|
+
<FieldsetDescription>Pick what reaches your inbox.</FieldsetDescription>
|
|
71
|
+
<FieldsetContent>
|
|
72
|
+
<Field label="Mentions" orientation="horizontal"><Checkbox defaultChecked /></Field>
|
|
73
|
+
<Field label="Replies" orientation="horizontal"><Checkbox /></Field>
|
|
74
|
+
<Field label="Direct messages" orientation="horizontal"><Checkbox /></Field>
|
|
75
|
+
</FieldsetContent>
|
|
76
|
+
<FieldsetFooter>
|
|
77
|
+
<Button>Save preferences</Button>
|
|
78
|
+
</FieldsetFooter>
|
|
79
|
+
</Fieldset>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Why a fieldset, and when it is wrong
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { Field, Fieldset, FieldsetContent, FieldsetLegend, Radio, RadioGroup } from "@sableui/react"
|
|
86
|
+
|
|
87
|
+
// Fieldset groups inputs that are ONE question with multiple parts, and gives
|
|
88
|
+
// the group a single accessible name — which is the part you cannot do with a
|
|
89
|
+
// div and a heading.
|
|
90
|
+
|
|
91
|
+
<Fieldset>
|
|
92
|
+
<FieldsetLegend>Digest frequency</FieldsetLegend>
|
|
93
|
+
<FieldsetContent>
|
|
94
|
+
<RadioGroup defaultValue="weekly">
|
|
95
|
+
<Field label="Daily" orientation="horizontal"><Radio value="daily" /></Field>
|
|
96
|
+
<Field label="Weekly" orientation="horizontal"><Radio value="weekly" /></Field>
|
|
97
|
+
</RadioGroup>
|
|
98
|
+
</FieldsetContent>
|
|
99
|
+
</Fieldset>
|
|
100
|
+
|
|
101
|
+
// ✓ Required for radio groups: without a fieldset the options have no shared
|
|
102
|
+
// name, so a screen reader announces "Weekly, radio" with no question.
|
|
103
|
+
// ✗ Not for visual grouping of unrelated fields. Use PanelSection on a panel or
|
|
104
|
+
// a Card section on a page; a legend that is really a heading lies to
|
|
105
|
+
// assistive technology about the form's structure.
|
|
106
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# FileDropzone
|
|
2
|
+
|
|
3
|
+
**Status:** stable
|
|
4
|
+
|
|
5
|
+
Drag-and-drop target and file picker in one keyboard-operable button. Checks accept, maxSize and maxFiles on every selection, announces each rejection with how to fix it, and hands accepted files to you to list as Attachments.
|
|
6
|
+
|
|
7
|
+
**Keywords:** file upload, dropzone, drag and drop, file picker, upload, attach file, file input, import
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Use FileDropzone where adding files is the main job of a section — an upload step, an import screen, a document request. It gives the drag gesture a visible target and the same action a button for everyone who does not drag. It holds no list: render accepted files as Attachments beneath it, where progress, retry and remove belong.
|
|
12
|
+
|
|
13
|
+
### Best practices
|
|
14
|
+
|
|
15
|
+
- Do: State the constraints in `description` ("PNG or PDF, up to 10 MB") and give `acceptLabel` the same words, so a rejection repeats the rule the user already read.
|
|
16
|
+
- Do: Pair it with a list of Attachment rows that you own. Once files are added, switch to `size="sm"` so the target stays available without pushing the list below the fold.
|
|
17
|
+
- Do: Cap the total by passing `maxFiles={limit - files.length}`; the dropzone only sees one selection at a time. When none remain, set `disabled` and say why in `description` ("Remove one to add another").
|
|
18
|
+
- Don't: Do not use it for an incidental attach action inside a composer or a toolbar. A paperclip Button that opens the picker is the right weight there; ChatComposer already has one.
|
|
19
|
+
- Don't: Do not rely on `accept` alone to protect the server. The dropzone's checks are for the user's benefit; validate type and size again where the file is received.
|
|
20
|
+
- Don't: Do not silently drop rejected files. The zone announces each one, but if you also discard files later (a server-side limit), say so on the Attachment with `status="error"` and a fix.
|
|
21
|
+
|
|
22
|
+
### Anatomy
|
|
23
|
+
|
|
24
|
+
| Part | Required | Description |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| Zone | yes | A full-width button with a dashed border. Click, Enter or Space opens the picker; it is also the drop target. |
|
|
27
|
+
| Icon | no | A decorative mark that lifts while a file is dragged over the zone. |
|
|
28
|
+
| Label and description | yes | The call to action and the constraints. Together they form the button's accessible name. |
|
|
29
|
+
| Status | yes | A role=status region, present from the first render, that lists each rejected file and how to fix it. |
|
|
30
|
+
|
|
31
|
+
## Props
|
|
32
|
+
|
|
33
|
+
| Name | Type | Default | Description |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| accept | string | — | Native accept syntax — `.pdf`, `application/pdf`, `image/*`, comma-separated. Enforced on drop and on pick. |
|
|
36
|
+
| maxSize | number | — | Largest accepted file, in bytes. |
|
|
37
|
+
| multiple | boolean | false | Accept more than one file per selection. |
|
|
38
|
+
| maxFiles | number | — | Most files accepted from one selection when `multiple`. Pass the remaining capacity to cap a total. |
|
|
39
|
+
| onFilesAccepted | (files: File[]) => void | — | Files that passed every check, in selection order. |
|
|
40
|
+
| onFilesRejected | (rejections: FileRejection[]) => void | — | Files that failed, each with `code` (file-invalid-type, file-too-large, too-many-files) and the announced `message`. |
|
|
41
|
+
| label | ReactNode | "Drop a file here or browse" | The visible call to action. Pluralised automatically when `multiple`. |
|
|
42
|
+
| description | ReactNode | — | The constraints in words, shown under the label. |
|
|
43
|
+
| acceptLabel | string | — | How the accepted types complete "Use … instead" in an error ("PNG or PDF", "an image or PDF"). Without it a type error can only say "Choose a different file". |
|
|
44
|
+
| formatError | (rejection) => string | — | Replace the rejection copy, e.g. to localize it. |
|
|
45
|
+
| icon | string | "arrow-up" | Semantic icon name. |
|
|
46
|
+
| size | "md" \| "sm" | "md" | `md` is the empty-section target; `sm` is a single-row strip above an existing list. |
|
|
47
|
+
| disabled / required / id / aria-invalid | — | — | Standard form-control props. `id` lands on the button, so `<Field>` labels it; `aria-invalid` draws the destructive border. |
|
|
48
|
+
|
|
49
|
+
## Variants
|
|
50
|
+
|
|
51
|
+
- **size**: md, sm
|
|
52
|
+
|
|
53
|
+
## Tokens used
|
|
54
|
+
|
|
55
|
+
`card`, `foreground`, `muted`, `muted-foreground`, `border-strong`, `primary`, `primary-foreground`, `accent`, `accent-foreground`, `ring`, `destructive`
|
|
56
|
+
|
|
57
|
+
## Motion
|
|
58
|
+
|
|
59
|
+
- **arrival** · Zone · _no motion_ — The zone is the section's content, present from the start. An entrance would only delay the drop target someone is already dragging toward.
|
|
60
|
+
- **response** · Zone · `motion-interactive` — Hover strengthens the border to primary, which confirms the whole dashed area — not just the word "browse" — is the click target.
|
|
61
|
+
- **transition** · Icon · `motion-interactive` — When a file crosses into the zone the icon fills and lifts and the surface tints. That is the only signal that releasing here will work; without it users drop on the page and the browser opens the file instead.
|
|
62
|
+
- **activity** · Zone · _no motion_ — The zone hands files off immediately and never uploads. Transfer progress belongs on each Attachment, where it can be per-file and retryable.
|
|
63
|
+
|
|
64
|
+
## Accessibility
|
|
65
|
+
|
|
66
|
+
- The zone is a native button: Enter or Space opens the system picker, so dragging is never the only way in.
|
|
67
|
+
- Rejections render into a role=status region that exists from the first render, and the button's aria-describedby points at it while it has content, so the reason is still read when focus returns.
|
|
68
|
+
- Every error names the file and says how to fix it ("Choose a smaller file"), rather than only what failed.
|
|
69
|
+
- The underlying file input is visually hidden and removed from the tab order; the button is the single focus stop.
|
|
70
|
+
|
|
71
|
+
## Related
|
|
72
|
+
|
|
73
|
+
- Components: Attachment, Input, Button, Progress
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
### Images and PDFs
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { FileDropzone } from "@sableui/react"
|
|
81
|
+
|
|
82
|
+
<FileDropzone accept="image/*,.pdf" acceptLabel="an image or PDF" maxSize={10 * 1024 * 1024} multiple description="Images or PDFs, up to 10 MB each" onFilesAccepted={(files) => console.log(files)} />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### With an Attachment list
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { Attachment, FileDropzone } from "@sableui/react"
|
|
89
|
+
|
|
90
|
+
<div className="flex flex-col gap-3">
|
|
91
|
+
<FileDropzone size="sm" multiple description="Up to 5 files" onFilesAccepted={(files) => console.log(files)} />
|
|
92
|
+
<Attachment name="contract.pdf" meta="2.4 MB · PDF" />
|
|
93
|
+
<Attachment name="site-photo.jpg" meta="Uploading…" status="uploading" progress={42} />
|
|
94
|
+
</div>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### In a field
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { Field, FileDropzone } from "@sableui/react"
|
|
101
|
+
|
|
102
|
+
<Field label="Proof of address" description="A bill or statement from the last 3 months."><FileDropzone accept=".pdf" acceptLabel="a PDF" maxSize={5 * 1024 * 1024} description="PDF, up to 5 MB" /></Field>
|
|
103
|
+
```
|