@sableui/react 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/AGENTS.md +157 -0
  2. package/CHANGELOG.md +55 -0
  3. package/CONTRIBUTING.md +221 -0
  4. package/DESIGN.md +548 -0
  5. package/LICENSE +21 -0
  6. package/README.md +194 -2
  7. package/THIRD_PARTY_NOTICES.md +44 -0
  8. package/bin/sable-lint.mjs +0 -0
  9. package/bin/sable.mjs +1223 -0
  10. package/dist/_contract-CYScOdf5.d.ts +150 -0
  11. package/dist/chunk-3IWZYBGH.js +398 -0
  12. package/dist/chunk-5Z5KLOAU.js +167 -0
  13. package/dist/chunk-7MG4SHYX.js +15 -0
  14. package/dist/chunk-ADLLDWLW.js +15 -0
  15. package/dist/chunk-EP55KSYA.js +14 -0
  16. package/dist/chunk-J23ELQQZ.js +9396 -0
  17. package/dist/chunk-JI7WJP2L.js +286 -0
  18. package/dist/chunk-K7RUHUGC.js +14 -0
  19. package/dist/chunk-KO3NAEBF.js +15 -0
  20. package/dist/chunk-L4IEIQQA.js +14 -0
  21. package/dist/chunk-L6Y46C62.js +270 -0
  22. package/dist/chunk-NEQNXI2G.js +25 -0
  23. package/dist/chunk-PAOZBZME.js +40 -0
  24. package/dist/chunk-RWP4XBSK.js +139 -0
  25. package/dist/chunk-SRQWQUTW.js +63 -0
  26. package/dist/chunk-YXBOYWBN.js +15 -0
  27. package/dist/foundations/index.d.ts +88 -0
  28. package/dist/foundations/index.js +792 -0
  29. package/dist/icon/index.d.ts +60 -0
  30. package/dist/icon/index.js +2 -0
  31. package/dist/icon-sets/brands.d.ts +44 -0
  32. package/dist/icon-sets/brands.js +147 -0
  33. package/dist/icon-sets/reicon.d.ts +8 -0
  34. package/dist/icon-sets/reicon.js +1 -0
  35. package/dist/index.d.ts +7013 -0
  36. package/dist/index.js +15462 -0
  37. package/dist/ios/chrome.css +83 -0
  38. package/dist/ios/frame.css +5 -0
  39. package/dist/ios/index.d.ts +70 -0
  40. package/dist/ios/index.js +313 -0
  41. package/dist/ios/preview.d.ts +64 -0
  42. package/dist/ios/preview.js +133 -0
  43. package/dist/ios/styles.css +77 -0
  44. package/dist/layout/index.d.ts +280 -0
  45. package/dist/layout/index.js +2 -0
  46. package/dist/meta-6dUzgC6M.d.ts +205 -0
  47. package/dist/patterns/index.d.ts +36 -0
  48. package/dist/patterns/index.js +934 -0
  49. package/dist/presets/index.d.ts +37 -0
  50. package/dist/presets/index.js +117 -0
  51. package/dist/registry-CyL28OzJ.d.ts +28 -0
  52. package/dist/styles.css +1893 -0
  53. package/dist/theme-provider-BWcizhAt.d.ts +105 -0
  54. package/dist/themes/atelier/atelier.css +213 -0
  55. package/dist/themes/atelier/index.d.ts +9 -0
  56. package/dist/themes/atelier/index.js +1 -0
  57. package/dist/themes/base/base.css +443 -0
  58. package/dist/themes/base/index.d.ts +9 -0
  59. package/dist/themes/base/index.js +1 -0
  60. package/dist/themes/ember/ember.css +270 -0
  61. package/dist/themes/ember/index.d.ts +9 -0
  62. package/dist/themes/ember/index.js +1 -0
  63. package/dist/themes/index.d.ts +10 -0
  64. package/dist/themes/index.js +8 -0
  65. package/dist/themes/meridian/index.d.ts +9 -0
  66. package/dist/themes/meridian/index.js +1 -0
  67. package/dist/themes/meridian/meridian.css +255 -0
  68. package/dist/themes/poppy/index.d.ts +6 -0
  69. package/dist/themes/poppy/index.js +1 -0
  70. package/dist/themes/poppy/poppy.css +174 -0
  71. package/dist/themes/prism/index.d.ts +6 -0
  72. package/dist/themes/prism/index.js +1 -0
  73. package/dist/themes/prism/prism.css +183 -0
  74. package/dist/themes/verdant/index.d.ts +9 -0
  75. package/dist/themes/verdant/index.js +1 -0
  76. package/dist/themes/verdant/verdant.css +210 -0
  77. package/dist/themes.css +30 -0
  78. package/llms/components/accordion.md +93 -0
  79. package/llms/components/app-header.md +70 -0
  80. package/llms/components/app-shell.md +117 -0
  81. package/llms/components/attachment.md +105 -0
  82. package/llms/components/avatar.md +141 -0
  83. package/llms/components/badge.md +117 -0
  84. package/llms/components/banner.md +119 -0
  85. package/llms/components/bottom-nav.md +80 -0
  86. package/llms/components/breadcrumbs.md +75 -0
  87. package/llms/components/browser.md +67 -0
  88. package/llms/components/bubble.md +119 -0
  89. package/llms/components/button.md +199 -0
  90. package/llms/components/calendar.md +75 -0
  91. package/llms/components/card.md +135 -0
  92. package/llms/components/carousel.md +87 -0
  93. package/llms/components/chat-composer.md +126 -0
  94. package/llms/components/checkbox-card.md +98 -0
  95. package/llms/components/checkbox.md +90 -0
  96. package/llms/components/code-block.md +81 -0
  97. package/llms/components/code.md +62 -0
  98. package/llms/components/collapsible.md +83 -0
  99. package/llms/components/color-field.md +100 -0
  100. package/llms/components/combobox.md +119 -0
  101. package/llms/components/command-menu.md +158 -0
  102. package/llms/components/context-card.md +73 -0
  103. package/llms/components/context-menu.md +79 -0
  104. package/llms/components/data-table.md +99 -0
  105. package/llms/components/date-picker.md +97 -0
  106. package/llms/components/description.md +96 -0
  107. package/llms/components/destructive-action-modal.md +76 -0
  108. package/llms/components/dialog.md +127 -0
  109. package/llms/components/dots-menu.md +64 -0
  110. package/llms/components/empty-state.md +113 -0
  111. package/llms/components/entity.md +67 -0
  112. package/llms/components/feedback.md +63 -0
  113. package/llms/components/fieldset.md +106 -0
  114. package/llms/components/file-dropzone.md +103 -0
  115. package/llms/components/form.md +111 -0
  116. package/llms/components/gliding-highlight.md +84 -0
  117. package/llms/components/inline-status.md +62 -0
  118. package/llms/components/input.md +127 -0
  119. package/llms/components/kbd.md +57 -0
  120. package/llms/components/label.md +73 -0
  121. package/llms/components/marker.md +99 -0
  122. package/llms/components/marquee.md +66 -0
  123. package/llms/components/menu.md +122 -0
  124. package/llms/components/message-scroller.md +110 -0
  125. package/llms/components/message.md +114 -0
  126. package/llms/components/meter.md +124 -0
  127. package/llms/components/middle-truncate.md +62 -0
  128. package/llms/components/note.md +81 -0
  129. package/llms/components/notification-badge.md +67 -0
  130. package/llms/components/number-field.md +70 -0
  131. package/llms/components/otp-field.md +101 -0
  132. package/llms/components/page-header.md +76 -0
  133. package/llms/components/page-loader.md +67 -0
  134. package/llms/components/pagination.md +92 -0
  135. package/llms/components/panel-group.md +120 -0
  136. package/llms/components/panel-section.md +122 -0
  137. package/llms/components/phone.md +70 -0
  138. package/llms/components/pill.md +73 -0
  139. package/llms/components/popover.md +79 -0
  140. package/llms/components/progress.md +98 -0
  141. package/llms/components/radio-card.md +90 -0
  142. package/llms/components/radio.md +86 -0
  143. package/llms/components/relative-time.md +60 -0
  144. package/llms/components/scroller.md +88 -0
  145. package/llms/components/scrub-field.md +97 -0
  146. package/llms/components/search-input.md +84 -0
  147. package/llms/components/select.md +106 -0
  148. package/llms/components/separator.md +70 -0
  149. package/llms/components/settings-menu.md +139 -0
  150. package/llms/components/sheet.md +97 -0
  151. package/llms/components/show-more.md +59 -0
  152. package/llms/components/side-nav.md +106 -0
  153. package/llms/components/sidebar.md +87 -0
  154. package/llms/components/skeleton.md +62 -0
  155. package/llms/components/slider.md +114 -0
  156. package/llms/components/spinner.md +87 -0
  157. package/llms/components/status-bar.md +91 -0
  158. package/llms/components/status-dot.md +70 -0
  159. package/llms/components/stepper.md +125 -0
  160. package/llms/components/streaming-text.md +86 -0
  161. package/llms/components/switch.md +87 -0
  162. package/llms/components/table.md +112 -0
  163. package/llms/components/tabs.md +84 -0
  164. package/llms/components/textarea.md +88 -0
  165. package/llms/components/theme-selector.md +95 -0
  166. package/llms/components/theme-switcher.md +71 -0
  167. package/llms/components/thinking-orb.md +91 -0
  168. package/llms/components/toast.md +130 -0
  169. package/llms/components/toggle.md +95 -0
  170. package/llms/components/toolbar.md +168 -0
  171. package/llms/components/tooltip.md +93 -0
  172. package/llms/components/tree.md +227 -0
  173. package/llms/components/video.md +66 -0
  174. package/llms/components/workspace.md +138 -0
  175. package/llms/foundations/accessibility.md +60 -0
  176. package/llms/foundations/motion.md +96 -0
  177. package/llms/foundations/scrolling.md +41 -0
  178. package/llms/foundations/shape.md +73 -0
  179. package/llms/foundations/spacing.md +82 -0
  180. package/llms/foundations/surfaces.md +69 -0
  181. package/llms/foundations/typography.md +120 -0
  182. package/llms/layout/aspect-box.md +37 -0
  183. package/llms/layout/bleed.md +31 -0
  184. package/llms/layout/center.md +37 -0
  185. package/llms/layout/columns.md +51 -0
  186. package/llms/layout/grid-overlay.md +40 -0
  187. package/llms/layout/grid.md +47 -0
  188. package/llms/layout/page.md +39 -0
  189. package/llms/layout/region.md +49 -0
  190. package/llms/layout/row.md +40 -0
  191. package/llms/layout/safe-area.md +37 -0
  192. package/llms/layout/spacer.md +39 -0
  193. package/llms/layout/split-pane.md +41 -0
  194. package/llms/layout/stack.md +39 -0
  195. package/llms/patterns/auth-page.md +68 -0
  196. package/llms/patterns/brand-landing-page.md +423 -0
  197. package/llms/patterns/chat-panel.md +210 -0
  198. package/llms/patterns/dashboard-page.md +170 -0
  199. package/llms/patterns/feed-page.md +103 -0
  200. package/llms/patterns/landing-page.md +122 -0
  201. package/llms/patterns/list-detail-page.md +100 -0
  202. package/llms/patterns/settings-page.md +121 -0
  203. package/llms/reference/token-intent.md +324 -0
  204. package/llms/reference/utilities.md +129 -0
  205. package/llms/tasks/adding-a-component.md +113 -0
  206. package/llms/tasks/auditing-an-app.md +216 -0
  207. package/llms/tasks/building-a-page.md +109 -0
  208. package/llms/tasks/building-a-tool.md +141 -0
  209. package/llms/tasks/building-an-ios-prototype.md +105 -0
  210. package/llms/tasks/building-with-sable.md +73 -0
  211. package/llms/tasks/choosing-a-component.md +109 -0
  212. package/llms/tasks/converting-an-app.md +139 -0
  213. package/llms/tasks/creating-a-preset.md +43 -0
  214. package/llms/tasks/creating-design-assets.md +96 -0
  215. package/llms/tasks/designing-a-surface.md +115 -0
  216. package/llms/tasks/developing-taste.md +157 -0
  217. package/llms/tasks/getting-started.md +63 -0
  218. package/llms/tasks/learning-from-builds.md +74 -0
  219. package/llms/tasks/reviewing-a-design.md +114 -0
  220. package/llms/tasks/reviewing-a-journey.md +84 -0
  221. package/llms/tasks/reviewing-motion.md +92 -0
  222. package/llms/tasks/using-presets.md +87 -0
  223. package/llms/taste/asset-plan.template.json +53 -0
  224. package/llms/taste/composition-follows-content.md +20 -0
  225. package/llms/taste/hierarchy-is-a-relationship.md +22 -0
  226. package/llms/taste/lessons.json +57 -0
  227. package/llms/taste/motion-needs-a-reason.md +21 -0
  228. package/llms/taste/surface-states-travel-together.md +20 -0
  229. package/llms-full.txt +14906 -0
  230. package/llms.txt +405 -0
  231. package/package.json +185 -3
  232. package/registry.json +18812 -0
  233. package/scripts/agent-setup.mjs +54 -0
  234. package/scripts/design-atlas.mjs +221 -0
  235. package/scripts/design-directions.mjs +123 -0
  236. package/scripts/design-memory.mjs +166 -0
  237. package/scripts/design-preferences.mjs +227 -0
  238. package/scripts/design-presets.mjs +48 -0
  239. package/scripts/grade.mjs +509 -0
  240. package/scripts/lint-rules.mjs +1065 -0
  241. package/scripts/review-evidence.mjs +120 -0
  242. package/scripts/skeleton.mjs +248 -0
  243. package/skills/entry/SKILL.md +34 -0
  244. package/skills/sable/SKILL.md +187 -0
  245. package/skills/sable/agents/openai.yaml +6 -0
  246. package/targets/ios.json +982 -0
  247. package/third-party/reicon-LICENSE.txt +20 -0
@@ -0,0 +1,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
+ ```