@eifi1/ui-kit 0.6.1 → 0.7.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 (271) hide show
  1. package/README.md +69 -33
  2. package/dist/chart.d.ts +1 -0
  3. package/dist/components/account-settings.js +3 -2
  4. package/dist/components/account-settings.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +5 -0
  6. package/dist/components/amount-input.js +6 -4
  7. package/dist/components/amount-input.js.map +1 -1
  8. package/dist/components/autocomplete.d.ts +33 -6
  9. package/dist/components/autocomplete.js +62 -16
  10. package/dist/components/autocomplete.js.map +1 -1
  11. package/dist/components/calculator.d.ts +5 -0
  12. package/dist/components/calculator.js +2 -1
  13. package/dist/components/calculator.js.map +1 -1
  14. package/dist/components/chart-zoom.js +2 -1
  15. package/dist/components/chart-zoom.js.map +1 -1
  16. package/dist/components/chart.d.ts +33 -2
  17. package/dist/components/chart.js +20 -8
  18. package/dist/components/chart.js.map +1 -1
  19. package/dist/components/chip.d.ts +34 -14
  20. package/dist/components/chip.js +63 -28
  21. package/dist/components/chip.js.map +1 -1
  22. package/dist/components/choice-card.d.ts +18 -1
  23. package/dist/components/choice-card.js +21 -8
  24. package/dist/components/choice-card.js.map +1 -1
  25. package/dist/components/combobox-core.d.ts +61 -2
  26. package/dist/components/combobox-core.js +68 -17
  27. package/dist/components/combobox-core.js.map +1 -1
  28. package/dist/components/combobox.d.ts +11 -7
  29. package/dist/components/combobox.js +51 -28
  30. package/dist/components/combobox.js.map +1 -1
  31. package/dist/components/currency-select.js +3 -3
  32. package/dist/components/currency-select.js.map +1 -1
  33. package/dist/components/danger-confirm.d.ts +30 -6
  34. package/dist/components/danger-confirm.js +51 -32
  35. package/dist/components/danger-confirm.js.map +1 -1
  36. package/dist/components/data-table-filter-popover.d.ts +1 -1
  37. package/dist/components/data-table-filters.d.ts +1 -1
  38. package/dist/components/data-table-pagination.d.ts +6 -3
  39. package/dist/components/data-table-pagination.js +6 -5
  40. package/dist/components/data-table-pagination.js.map +1 -1
  41. package/dist/components/data-table.d.ts +1 -1
  42. package/dist/components/data-table.js +332 -248
  43. package/dist/components/data-table.js.map +1 -1
  44. package/dist/components/date-picker.js +17 -7
  45. package/dist/components/date-picker.js.map +1 -1
  46. package/dist/components/dialog-frame.d.ts +28 -3
  47. package/dist/components/dialog-frame.js +47 -11
  48. package/dist/components/dialog-frame.js.map +1 -1
  49. package/dist/components/disclosure.d.ts +22 -2
  50. package/dist/components/disclosure.js +41 -8
  51. package/dist/components/disclosure.js.map +1 -1
  52. package/dist/components/dropdown.d.ts +7 -3
  53. package/dist/components/dropdown.js +6 -2
  54. package/dist/components/dropdown.js.map +1 -1
  55. package/dist/components/entity-combobox.d.ts +17 -8
  56. package/dist/components/entity-combobox.js +7 -5
  57. package/dist/components/entity-combobox.js.map +1 -1
  58. package/dist/components/file-button.d.ts +38 -5
  59. package/dist/components/file-button.js +23 -2
  60. package/dist/components/file-button.js.map +1 -1
  61. package/dist/components/file-dropzone.d.ts +11 -5
  62. package/dist/components/file-dropzone.js +14 -19
  63. package/dist/components/file-dropzone.js.map +1 -1
  64. package/dist/components/grouped-picker.js +2 -2
  65. package/dist/components/grouped-picker.js.map +1 -1
  66. package/dist/components/hover-menu.d.ts +8 -3
  67. package/dist/components/hover-menu.js +88 -4
  68. package/dist/components/hover-menu.js.map +1 -1
  69. package/dist/components/icon-picker.d.ts +10 -1
  70. package/dist/components/icon-picker.js +5 -1
  71. package/dist/components/icon-picker.js.map +1 -1
  72. package/dist/components/measured-grid.d.ts +165 -0
  73. package/dist/components/measured-grid.js +559 -0
  74. package/dist/components/measured-grid.js.map +1 -0
  75. package/dist/components/mini-calendar.js +7 -8
  76. package/dist/components/mini-calendar.js.map +1 -1
  77. package/dist/components/month-picker.js +13 -9
  78. package/dist/components/month-picker.js.map +1 -1
  79. package/dist/components/multi-entity-combobox.js +2 -2
  80. package/dist/components/multi-entity-combobox.js.map +1 -1
  81. package/dist/components/multi-select.d.ts +9 -1
  82. package/dist/components/multi-select.js +22 -8
  83. package/dist/components/multi-select.js.map +1 -1
  84. package/dist/components/number-field.d.ts +5 -0
  85. package/dist/components/number-input.d.ts +5 -0
  86. package/dist/components/number-input.js +3 -3
  87. package/dist/components/number-input.js.map +1 -1
  88. package/dist/components/numpad-sheet.d.ts +5 -0
  89. package/dist/components/numpad-sheet.js +3 -3
  90. package/dist/components/numpad-sheet.js.map +1 -1
  91. package/dist/components/picker-sheet.js +1 -1
  92. package/dist/components/picker-sheet.js.map +1 -1
  93. package/dist/components/popover.js +11 -2
  94. package/dist/components/popover.js.map +1 -1
  95. package/dist/components/series-chart-ticks.d.ts +13 -0
  96. package/dist/components/series-chart-ticks.js +29 -0
  97. package/dist/components/series-chart-ticks.js.map +1 -0
  98. package/dist/components/series-chart.js +33 -27
  99. package/dist/components/series-chart.js.map +1 -1
  100. package/dist/components/settings-fields.d.ts +2 -0
  101. package/dist/components/swatch-picker.d.ts +10 -1
  102. package/dist/components/swatch-picker.js +2 -0
  103. package/dist/components/swatch-picker.js.map +1 -1
  104. package/dist/components/swipeable-row.d.ts +11 -2
  105. package/dist/components/swipeable-row.js +5 -3
  106. package/dist/components/swipeable-row.js.map +1 -1
  107. package/dist/components/tile-radio.js +5 -7
  108. package/dist/components/tile-radio.js.map +1 -1
  109. package/dist/components/time-input.d.ts +2 -0
  110. package/dist/components/toggle-group.d.ts +13 -3
  111. package/dist/components/toggle-group.js +21 -1
  112. package/dist/components/toggle-group.js.map +1 -1
  113. package/dist/components/toggle-legend.js +1 -2
  114. package/dist/components/toggle-legend.js.map +1 -1
  115. package/dist/components/tooltip.d.ts +10 -4
  116. package/dist/components/tooltip.js +23 -6
  117. package/dist/components/tooltip.js.map +1 -1
  118. package/dist/components/treemap.d.ts +6 -1
  119. package/dist/components/treemap.js +16 -6
  120. package/dist/components/treemap.js.map +1 -1
  121. package/dist/components/ui.d.ts +36 -8
  122. package/dist/components/ui.js +27 -19
  123. package/dist/components/ui.js.map +1 -1
  124. package/dist/components/use-anchor-dir.d.ts +15 -0
  125. package/dist/components/use-anchor-dir.js +14 -0
  126. package/dist/components/use-anchor-dir.js.map +1 -0
  127. package/dist/components/use-mobile-reveal.d.ts +8 -1
  128. package/dist/components/use-mobile-reveal.js +3 -2
  129. package/dist/components/use-mobile-reveal.js.map +1 -1
  130. package/dist/components/use-table-state.d.ts +4 -4
  131. package/dist/components/use-table-state.js +12 -5
  132. package/dist/components/use-table-state.js.map +1 -1
  133. package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
  134. package/dist/data-table.d.ts +1 -1
  135. package/dist/data-table.js.map +1 -1
  136. package/dist/feedback/feedback-attachment.d.ts +22 -3
  137. package/dist/feedback/feedback-attachment.js +15 -7
  138. package/dist/feedback/feedback-attachment.js.map +1 -1
  139. package/dist/feedback/feedback-dialog.d.ts +3 -2
  140. package/dist/feedback/feedback-dialog.js.map +1 -1
  141. package/dist/feedback.d.ts +1 -1
  142. package/dist/hooks/use-windowed-rows.d.ts +29 -0
  143. package/dist/hooks/use-windowed-rows.js +31 -0
  144. package/dist/hooks/use-windowed-rows.js.map +1 -0
  145. package/dist/i18n/defaults.d.ts +5 -0
  146. package/dist/i18n/defaults.js +5 -1
  147. package/dist/i18n/defaults.js.map +1 -1
  148. package/dist/i18n/kit-labels.d.ts +7 -0
  149. package/dist/i18n/kit-labels.js.map +1 -1
  150. package/dist/i18n/locales/de-CH.d.ts +45 -0
  151. package/dist/i18n/locales/de-CH.js +21 -0
  152. package/dist/i18n/locales/de-CH.js.map +1 -0
  153. package/dist/i18n/locales/de.d.ts +45 -0
  154. package/dist/i18n/locales/de.js +344 -0
  155. package/dist/i18n/locales/de.js.map +1 -0
  156. package/dist/i18n/locales/es.d.ts +45 -0
  157. package/dist/i18n/locales/es.js +339 -0
  158. package/dist/i18n/locales/es.js.map +1 -0
  159. package/dist/i18n/locales/fr.d.ts +45 -0
  160. package/dist/i18n/locales/fr.js +344 -0
  161. package/dist/i18n/locales/fr.js.map +1 -0
  162. package/dist/i18n/locales/hu.d.ts +45 -0
  163. package/dist/i18n/locales/hu.js +343 -0
  164. package/dist/i18n/locales/hu.js.map +1 -0
  165. package/dist/i18n/locales/it.d.ts +45 -0
  166. package/dist/i18n/locales/it.js +342 -0
  167. package/dist/i18n/locales/it.js.map +1 -0
  168. package/dist/i18n/locales/zh.d.ts +45 -0
  169. package/dist/i18n/locales/zh.js +338 -0
  170. package/dist/i18n/locales/zh.js.map +1 -0
  171. package/dist/index.d.ts +7 -4
  172. package/dist/index.js +4 -1
  173. package/dist/index.js.map +1 -1
  174. package/dist/lib/direction.d.ts +24 -0
  175. package/dist/lib/direction.js +19 -0
  176. package/dist/lib/direction.js.map +1 -0
  177. package/dist/rhf/form.d.ts +2 -0
  178. package/dist/rhf/form.js.map +1 -1
  179. package/dist/rhf.d.ts +2 -0
  180. package/dist/search/command-palette.d.ts +5 -0
  181. package/dist/search/command-palette.js +80 -50
  182. package/dist/search/command-palette.js.map +1 -1
  183. package/dist/shell/app-shell.d.ts +13 -1
  184. package/dist/shell/app-shell.js +134 -85
  185. package/dist/shell/app-shell.js.map +1 -1
  186. package/dist/shell/topbar-action-menu.js +1 -1
  187. package/dist/shell/topbar-action-menu.js.map +1 -1
  188. package/dist/shell/topbar-controls.d.ts +7 -3
  189. package/dist/shell/topbar-controls.js +38 -31
  190. package/dist/shell/topbar-controls.js.map +1 -1
  191. package/dist/theme/chart-palette.d.ts +7 -1
  192. package/dist/theme/chart-palette.js +2 -1
  193. package/dist/theme/chart-palette.js.map +1 -1
  194. package/dist/tour/tour.d.ts +11 -5
  195. package/dist/tour/tour.js +124 -58
  196. package/dist/tour/tour.js.map +1 -1
  197. package/dist/wizard/use-wizard.js +30 -13
  198. package/dist/wizard/use-wizard.js.map +1 -1
  199. package/dist/wizard/wizard-summary.js +1 -1
  200. package/dist/wizard/wizard-summary.js.map +1 -1
  201. package/package.json +12 -2
  202. package/src/components/account-settings.tsx +6 -2
  203. package/src/components/amount-input.tsx +10 -8
  204. package/src/components/autocomplete.tsx +110 -24
  205. package/src/components/calculator.tsx +5 -1
  206. package/src/components/chart-zoom.tsx +8 -1
  207. package/src/components/chart.tsx +68 -7
  208. package/src/components/chip.tsx +131 -49
  209. package/src/components/choice-card.tsx +77 -36
  210. package/src/components/combobox-core.tsx +147 -20
  211. package/src/components/combobox.tsx +100 -44
  212. package/src/components/currency-select.tsx +6 -6
  213. package/src/components/danger-confirm.tsx +90 -36
  214. package/src/components/data-table-pagination.tsx +32 -19
  215. package/src/components/data-table.tsx +198 -36
  216. package/src/components/date-picker.tsx +24 -7
  217. package/src/components/dialog-frame.tsx +97 -22
  218. package/src/components/disclosure.tsx +75 -13
  219. package/src/components/dropdown.tsx +17 -5
  220. package/src/components/entity-combobox.tsx +26 -13
  221. package/src/components/file-button.tsx +74 -5
  222. package/src/components/file-dropzone.tsx +44 -35
  223. package/src/components/grouped-picker.tsx +2 -2
  224. package/src/components/hover-menu.tsx +139 -10
  225. package/src/components/icon-picker.tsx +14 -1
  226. package/src/components/measured-grid.tsx +852 -0
  227. package/src/components/mini-calendar.tsx +11 -8
  228. package/src/components/month-picker.tsx +18 -11
  229. package/src/components/multi-entity-combobox.tsx +2 -2
  230. package/src/components/multi-select.tsx +34 -7
  231. package/src/components/number-input.tsx +4 -4
  232. package/src/components/numpad-sheet.tsx +5 -3
  233. package/src/components/picker-sheet.tsx +1 -1
  234. package/src/components/popover.tsx +18 -3
  235. package/src/components/series-chart-ticks.ts +52 -0
  236. package/src/components/series-chart.tsx +39 -31
  237. package/src/components/swatch-picker.tsx +11 -0
  238. package/src/components/swipeable-row.tsx +21 -6
  239. package/src/components/tile-radio.tsx +5 -7
  240. package/src/components/toggle-group.tsx +39 -3
  241. package/src/components/toggle-legend.tsx +5 -2
  242. package/src/components/tooltip.tsx +40 -12
  243. package/src/components/treemap.tsx +28 -5
  244. package/src/components/ui.tsx +79 -33
  245. package/src/components/use-anchor-dir.ts +20 -0
  246. package/src/components/use-mobile-reveal.ts +10 -2
  247. package/src/components/use-table-state.ts +44 -7
  248. package/src/data-table.ts +1 -0
  249. package/src/feedback/feedback-attachment.tsx +36 -9
  250. package/src/feedback/feedback-dialog.tsx +3 -2
  251. package/src/hooks/use-windowed-rows.ts +69 -0
  252. package/src/i18n/defaults.ts +4 -0
  253. package/src/i18n/kit-labels.tsx +4 -0
  254. package/src/i18n/locales/de-CH.ts +30 -0
  255. package/src/i18n/locales/de.ts +365 -0
  256. package/src/i18n/locales/es.ts +358 -0
  257. package/src/i18n/locales/fr.ts +371 -0
  258. package/src/i18n/locales/hu.ts +359 -0
  259. package/src/i18n/locales/it.ts +360 -0
  260. package/src/i18n/locales/zh.ts +348 -0
  261. package/src/index.ts +10 -1
  262. package/src/lib/direction.ts +37 -0
  263. package/src/rhf/form.tsx +21 -0
  264. package/src/search/command-palette.tsx +77 -19
  265. package/src/shell/app-shell.tsx +201 -130
  266. package/src/shell/topbar-action-menu.tsx +1 -1
  267. package/src/shell/topbar-controls.tsx +19 -8
  268. package/src/theme/chart-palette.ts +9 -2
  269. package/src/tour/tour.tsx +119 -18
  270. package/src/wizard/use-wizard.ts +55 -17
  271. package/src/wizard/wizard-summary.tsx +1 -1
@@ -42,6 +42,15 @@ export interface DialogFrameProps extends Omit<ModalProps, "labelledBy" | "child
42
42
  * calling `onClose` directly unmounts it at once.
43
43
  */
44
44
  actions?: ReactNode | ((close: () => void) => ReactNode);
45
+ /**
46
+ * Controls that belong to the HEADER rather than to the actions row — an "Edit"
47
+ * toggle, a status badge with a menu, a "Copy link" (kastlan). Rendered beside the
48
+ * title, before the optional X; on a phone, where title and controls do not fit on
49
+ * one line, they wrap under the title and the X keeps its corner.
50
+ *
51
+ * A function receives the animated close, as `actions` does.
52
+ */
53
+ headerActions?: ReactNode | ((close: () => void) => ReactNode);
45
54
  /**
46
55
  * Show an X in the header. Off by default: a centred dialog has a backdrop and
47
56
  * Escape, and a form dialog has a Cancel. On for a dialog that commits as it goes
@@ -52,8 +61,24 @@ export interface DialogFrameProps extends Omit<ModalProps, "labelledBy" | "child
52
61
  closeLabel?: string;
53
62
  /** Extra classes for the scrolling body (default spacing `space-y-3`). */
54
63
  bodyClassName?: string;
55
- /** The body. */
56
- children: ReactNode;
64
+ /**
65
+ * Extra classes for the header row (default padding `px-4 pt-4 pb-3`), merged last —
66
+ * `px-3` for a phone sheet whose body runs at `px-3`, so heading and fields share
67
+ * one gutter.
68
+ */
69
+ headerClassName?: string;
70
+ /**
71
+ * A rule under the header, the twin of the one over the actions row — for a tall or
72
+ * full-screen body that scrolls under a header which stays. The body then starts a
73
+ * step below the rule instead of flush against it.
74
+ */
75
+ headerDivider?: boolean;
76
+ /**
77
+ * The body. Optional: a dialog whose title, description and actions are the whole
78
+ * of it (a "you have unsynced changes" question) leaves it out, and the frame then
79
+ * renders no body at all rather than an empty padded one.
80
+ */
81
+ children?: ReactNode;
57
82
  }
58
83
 
59
84
  /**
@@ -92,9 +117,12 @@ export function DialogFrame({
92
117
  description,
93
118
  headingAs: Heading = "h2",
94
119
  actions,
120
+ headerActions,
95
121
  closeButton = false,
96
122
  closeLabel,
97
123
  bodyClassName,
124
+ headerClassName,
125
+ headerDivider = false,
98
126
  className,
99
127
  children,
100
128
  "aria-describedby": describedBy,
@@ -103,6 +131,8 @@ export function DialogFrame({
103
131
  const titleId = useId();
104
132
  const descriptionId = useId();
105
133
  const hasDescription = description !== undefined && description !== null;
134
+ // `false` too, so `{cond && <Body/>}` with a false `cond` means "no body".
135
+ const hasBody = children !== undefined && children !== null && children !== false;
106
136
 
107
137
  return (
108
138
  <Modal
@@ -116,30 +146,50 @@ export function DialogFrame({
116
146
  // to the body, which is the only part that should move.
117
147
  className={cn("flex flex-col overflow-hidden p-0", className)}
118
148
  >
119
- <div className="flex shrink-0 items-start justify-between gap-2 px-4 pt-4 pb-3">
120
- <div className="min-w-0">
121
- <Heading id={titleId} className="text-lg font-semibold leading-snug text-[var(--text-primary)]">
122
- {title}
123
- </Heading>
124
- {hasDescription && (
125
- <p id={descriptionId} className="mt-0.5 text-sm text-[var(--text-muted)]">
126
- {description}
127
- </p>
128
- )}
129
- </div>
130
- {closeButton && <FrameClose label={closeLabel} onClose={modal.onClose} />}
131
- </div>
132
149
  <div
133
150
  className={cn(
134
- // `min-h-0` is what lets a flex child shrink below its content and scroll;
135
- // `last:pb-4` closes a frame that has no actions row under it.
136
- "min-h-0 flex-1 space-y-3 overflow-y-auto overscroll-contain px-4 pb-3 last:pb-4",
137
- bodyClassName,
151
+ // `last:pb-4`: a frame with neither body nor actions closes on its header.
152
+ "flex shrink-0 items-start justify-between gap-2 px-4 pt-4 pb-3 last:pb-4",
153
+ headerDivider && "border-b border-[var(--border)]",
154
+ headerClassName,
138
155
  )}
139
156
  >
140
- {children}
157
+ {/* Title and header actions share a wrapping row of their own, so it is THEY
158
+ that wrap on a phone — the actions drop under a title that needs its 12rem —
159
+ while the X stays outside it, pinned to the top-end corner. */}
160
+ <div className="flex min-w-0 flex-1 flex-wrap items-start justify-between gap-x-3 gap-y-2">
161
+ <div className="min-w-0 grow basis-48">
162
+ <Heading id={titleId} className="text-lg font-semibold leading-snug text-[var(--text-primary)]">
163
+ {title}
164
+ </Heading>
165
+ {hasDescription && (
166
+ <p id={descriptionId} className="mt-0.5 text-sm text-[var(--text-muted)]">
167
+ {description}
168
+ </p>
169
+ )}
170
+ </div>
171
+ {headerActions !== undefined && headerActions !== null && headerActions !== false && (
172
+ <FrameHeaderActions actions={headerActions} onClose={modal.onClose} />
173
+ )}
174
+ </div>
175
+ {closeButton && <FrameClose label={closeLabel} onClose={modal.onClose} />}
141
176
  </div>
142
- {actions !== undefined && actions !== null && <FrameActions actions={actions} onClose={modal.onClose} />}
177
+ {hasBody && (
178
+ <div
179
+ className={cn(
180
+ // `min-h-0` is what lets a flex child shrink below its content and scroll;
181
+ // `last:pb-4` closes a frame that has no actions row under it.
182
+ "min-h-0 flex-1 space-y-3 overflow-y-auto overscroll-contain px-4 pb-3 last:pb-4",
183
+ headerDivider && "pt-3",
184
+ bodyClassName,
185
+ )}
186
+ >
187
+ {children}
188
+ </div>
189
+ )}
190
+ {actions !== undefined && actions !== null && (
191
+ <FrameActions actions={actions} onClose={modal.onClose} divider={hasBody} />
192
+ )}
143
193
  </Modal>
144
194
  );
145
195
  }
@@ -161,18 +211,43 @@ function FrameClose({ label, onClose }: { label?: string; onClose: () => void })
161
211
  );
162
212
  }
163
213
 
214
+ /** The header's own controls; a component for the same reason as {@link FrameClose}. */
215
+ function FrameHeaderActions({
216
+ actions,
217
+ onClose,
218
+ }: {
219
+ actions: NonNullable<DialogFrameProps["headerActions"]>;
220
+ onClose: () => void;
221
+ }) {
222
+ const close = useContext(ModalCloseContext) ?? onClose;
223
+ return (
224
+ <div data-dialog-header-actions="" className="flex shrink-0 flex-wrap items-center gap-2">
225
+ {typeof actions === "function" ? actions(close) : actions}
226
+ </div>
227
+ );
228
+ }
229
+
164
230
  function FrameActions({
165
231
  actions,
166
232
  onClose,
233
+ divider,
167
234
  }: {
168
235
  actions: NonNullable<DialogFrameProps["actions"]>;
169
236
  onClose: () => void;
237
+ /** Off when there is no body: nothing scrolls, so there is no edge to mark, and a
238
+ * rule straight under the description would cut the question from its answers. */
239
+ divider: boolean;
170
240
  }) {
171
241
  const close = useContext(ModalCloseContext) ?? onClose;
172
242
  return (
173
243
  // The border marks where the scrolling stops; `flex-wrap` keeps three long
174
244
  // translated labels on a phone from pushing the row wider than the sheet.
175
- <div className="flex shrink-0 flex-wrap items-center justify-end gap-2 border-t border-[var(--border)] px-4 py-3">
245
+ <div
246
+ className={cn(
247
+ "flex shrink-0 flex-wrap items-center justify-end gap-2 px-4 py-3",
248
+ divider && "border-t border-[var(--border)]",
249
+ )}
250
+ >
176
251
  {typeof actions === "function" ? actions(close) : actions}
177
252
  </div>
178
253
  );
@@ -149,7 +149,27 @@ export interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "
149
149
  headerClassName?: string;
150
150
  /** Extra classes for the body's wrapper — where its padding and spacing live. */
151
151
  bodyClassName?: string;
152
- children: ReactNode;
152
+ /**
153
+ * Content at the far end of the header row, beside the title: a count, a date, a
154
+ * status chip, an action button. It is a SIBLING of the header button, never inside
155
+ * it — a button may not contain another interactive element, and a count inside it
156
+ * would also be read as part of the button's name. The header button still spans
157
+ * the row under it (a stretched hit area), so clicking the empty space or the
158
+ * card's chevron still toggles, while whatever sits in `trailing` gets its own
159
+ * clicks. With `trailing`, the card's chevron moves after it, to the row's end.
160
+ */
161
+ trailing?: ReactNode;
162
+ /**
163
+ * Trigger-only mode: the id of an element the CALLER renders elsewhere — the hidden
164
+ * rows of a table, a panel in another column — which this header shows and hides.
165
+ * The header's `aria-controls` points at it and the disclosure renders no body of
166
+ * its own (`children`, `bodyClassName` and `keepMounted` are ignored). Pair it with
167
+ * `open` / `onOpenChange`: the caller owns the state, since the caller renders what
168
+ * it governs.
169
+ */
170
+ controls?: string;
171
+ /** The body. Not rendered in trigger-only mode ({@link DisclosureProps.controls}). */
172
+ children?: ReactNode;
153
173
  }
154
174
 
155
175
  /**
@@ -178,6 +198,8 @@ export function Disclosure({
178
198
  disabled,
179
199
  headerClassName,
180
200
  bodyClassName,
201
+ trailing,
202
+ controls,
181
203
  className,
182
204
  children,
183
205
  ...rest
@@ -186,6 +208,10 @@ export function Disclosure({
186
208
  const open = controlled ?? own;
187
209
  const bodyId = useId();
188
210
  const card = variant === "card";
211
+ const triggerOnly = controls !== undefined;
212
+ // The card's header squares its lower corners only when a body opens under it.
213
+ const joined = open && !triggerOnly;
214
+ const hasTrailing = trailing !== undefined && trailing !== null && trailing !== false;
189
215
 
190
216
  const toggle = () => {
191
217
  const next = !open;
@@ -197,43 +223,79 @@ export function Disclosure({
197
223
  <button
198
224
  type="button"
199
225
  aria-expanded={open}
200
- aria-controls={bodyId}
226
+ aria-controls={triggerOnly ? controls : bodyId}
201
227
  disabled={disabled}
202
228
  onClick={toggle}
229
+ data-disclosure-trigger=""
203
230
  className={cn(
204
231
  "flex w-full gap-2 text-start outline-none disabled:cursor-not-allowed disabled:opacity-50",
205
- "focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
232
+ hasTrailing
233
+ ? // Stretched over the whole header row (the row is `relative`), so the space
234
+ // round `trailing` and the card's chevron still toggle; the ring and the
235
+ // card's hover paint on the stretched area / the row, not the text box.
236
+ cn(
237
+ "min-w-0 flex-1 after:absolute after:inset-0 after:content-['']",
238
+ "focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-[var(--brand)]",
239
+ card ? cn("after:rounded-lg", joined && "after:rounded-b-none") : "after:rounded-sm",
240
+ )
241
+ : "focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
206
242
  card
207
243
  ? cn(
208
- "items-center justify-between rounded-lg p-4 hover:bg-[var(--bg-hover)] focus-visible:ring-inset",
209
- open && "rounded-b-none",
244
+ "items-center justify-between rounded-lg p-4",
245
+ hasTrailing ? "pe-0" : "hover:bg-[var(--bg-hover)] focus-visible:ring-inset",
246
+ joined && "rounded-b-none",
210
247
  )
211
248
  : "items-center rounded-sm text-sm font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
212
249
  headerClassName,
213
250
  )}
214
251
  >
215
252
  {!card && <Chevron open={open} leading />}
216
- <span className="min-w-0">
253
+ {/* `flex-1`: the title takes the row, so whatever a caller puts at its end sits
254
+ at the header's far edge. */}
255
+ <span className="min-w-0 flex-1">
217
256
  <span className={cn("block", card && "text-sm font-semibold text-[var(--text-primary)]")}>{title}</span>
218
257
  {hint !== undefined && (
219
258
  <span className="mt-0.5 block text-xs font-normal text-[var(--text-muted)]">{hint}</span>
220
259
  )}
221
260
  </span>
222
- {card && <Chevron open={open} />}
261
+ {card && !hasTrailing && <Chevron open={open} />}
223
262
  </button>
224
263
  );
225
264
 
265
+ // Preflight already makes h1–h6 inherit size and weight, so the heading adds
266
+ // structure and nothing visible.
267
+ const header = Heading ? <Heading className={hasTrailing ? "min-w-0 flex-1" : undefined}>{button}</Heading> : button;
268
+
226
269
  return (
227
270
  <div
228
271
  {...rest}
229
272
  className={cn(card && "rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] shadow-sm", className)}
230
273
  >
231
- {/* Preflight already makes h1–h6 inherit size and weight, so the heading adds
232
- structure and nothing visible. */}
233
- {Heading ? <Heading>{button}</Heading> : button}
234
- <Collapse id={bodyId} open={open} keepMounted={keepMounted}>
235
- <div className={cn(card ? "space-y-3 px-4 pb-4" : "space-y-2 pt-2", bodyClassName)}>{children}</div>
236
- </Collapse>
274
+ {hasTrailing ? (
275
+ <div
276
+ className={cn(
277
+ "relative flex items-center gap-2",
278
+ card &&
279
+ cn(
280
+ "rounded-lg pe-4 has-[[data-disclosure-trigger]:enabled:hover]:bg-[var(--bg-hover)]",
281
+ joined && "rounded-b-none",
282
+ ),
283
+ )}
284
+ >
285
+ {header}
286
+ {/* Positioned, so it paints over the stretched button and takes its own
287
+ clicks; the chevron is not, so a click on it lands on the button. */}
288
+ <div className="relative flex shrink-0 items-center gap-2">{trailing}</div>
289
+ {card && <Chevron open={open} />}
290
+ </div>
291
+ ) : (
292
+ header
293
+ )}
294
+ {!triggerOnly && (
295
+ <Collapse id={bodyId} open={open} keepMounted={keepMounted}>
296
+ <div className={cn(card ? "space-y-3 px-4 pb-4" : "space-y-2 pt-2", bodyClassName)}>{children}</div>
297
+ </Collapse>
298
+ )}
237
299
  </div>
238
300
  );
239
301
  }
@@ -11,6 +11,7 @@ import { useAnchoredPanel } from "../hooks/use-anchored-panel";
11
11
  import { useEscapeKey, useOutsideClick } from "../hooks/use-dismiss";
12
12
  import { useOverlayHistory } from "../hooks/use-overlay-history";
13
13
  import { cn } from "../lib/cn";
14
+ import { useAnchorDir } from "./use-anchor-dir";
14
15
 
15
16
  /**
16
17
  * Open state + close-on-outside-click for the custom dropdowns (MultiSelect,
@@ -225,8 +226,12 @@ export interface DropdownPanelProps extends ComponentPropsWithoutRef<"div"> {
225
226
  panelRef?: RefObject<HTMLDivElement | null>;
226
227
  /** Panel width in px, for the anchored form (the CSS `w-*` cannot be measured). */
227
228
  width?: number;
228
- /** Which of the panel's edges lines up with the trigger's, room permitting. */
229
- align?: "left" | "right";
229
+ /** Which of the panel's edges lines up with the trigger's, room permitting.
230
+ * `start`/`end` follow the trigger's reading direction — `end` is the right edge
231
+ * in a left-to-right form and the left one in a right-to-left form — and are what
232
+ * the kit's own pickers pass; `left`/`right` stay physical. Default `end`, which
233
+ * is the old `right` in a left-to-right page. */
234
+ align?: "left" | "right" | "start" | "end";
230
235
  children: ReactNode;
231
236
  }
232
237
 
@@ -239,7 +244,7 @@ export interface DropdownPanelProps extends ComponentPropsWithoutRef<"div"> {
239
244
  *
240
245
  * Without `anchorRef` the panel is `position: absolute` inside whatever relative box
241
246
  * the caller put it in, and the caller places it with `className` (`w-64`,
242
- * `right-0 top-full`). That is fine in a page that does not scroll around it, and
247
+ * `end-0 top-full`). That is fine in a page that does not scroll around it, and
243
248
  * wrong the moment an ancestor has `overflow` — the panel is then CLIPPED by that
244
249
  * ancestor's box, with no error and no scrollbar, just a list with its side sliced
245
250
  * off. Keksdose dev#548 is what that looks like in practice: the app's own content
@@ -266,12 +271,13 @@ export function DropdownPanel({
266
271
  anchorRef,
267
272
  panelRef,
268
273
  width = 256,
269
- align = "right",
274
+ align = "end",
270
275
  children,
271
276
  style,
272
277
  ...rest
273
278
  }: DropdownPanelProps) {
274
279
  const anchored = useAnchoredPanel(anchorRef ?? EMPTY_REF, Boolean(anchorRef));
280
+ const dir = useAnchorDir(anchorRef ?? EMPTY_REF, Boolean(anchorRef));
275
281
  const body = (
276
282
  <>
277
283
  {header}
@@ -313,12 +319,18 @@ export function DropdownPanel({
313
319
  if (!rect) return null;
314
320
  // Clamped exactly as `Popover` clamps: the panel may leave the trigger's edge to
315
321
  // stay on screen, because a list half off the viewport is the bug this exists for.
316
- const wanted = align === "right" ? rect.right - width : rect.left;
322
+ // The portal leaves the subtree the trigger's `dir` came from, so it is read off
323
+ // the trigger — for the alignment here, and for the panel's own `dir` below,
324
+ // without which a right-to-left form got a left-to-right list.
325
+ const physical =
326
+ align === "start" ? (dir === "rtl" ? "right" : "left") : align === "end" ? (dir === "rtl" ? "left" : "right") : align;
327
+ const wanted = physical === "right" ? rect.right - width : rect.left;
317
328
  const left = Math.min(Math.max(8, wanted), window.innerWidth - width - 8);
318
329
  return createPortal(
319
330
  <div
320
331
  {...rest}
321
332
  ref={panelRef}
333
+ dir={dir}
322
334
  // The caller's `style` is kept underneath, but the placement is measured rather
323
335
  // than chosen (dev#548: an `overflow` ancestor clipped a third of this panel
324
336
  // away), so those five values are the panel's own.
@@ -7,20 +7,30 @@ import {
7
7
  ComboboxPanel,
8
8
  useComboboxCore,
9
9
  useComboboxFieldError,
10
+ type ComboClearValue,
10
11
  type ComboOption,
11
12
  } from "./combobox-core";
12
13
  import { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
13
14
 
14
- export type { ComboOption } from "./combobox-core";
15
+ export type { ComboClearValue, ComboOption } from "./combobox-core";
15
16
 
16
17
  /** `onChange` is the kit's — "a selection was made", carrying values — rather
17
18
  * than the div's form event, so the DOM's spelling of it is omitted. */
18
- export interface EntityComboboxProps<V extends string | number>
19
+ export interface EntityComboboxProps<V extends string | number, C extends ComboClearValue = null>
19
20
  extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
20
- /** Selected id, or null/undefined when nothing is selected. */
21
- value: V | null | undefined;
22
- /** Selecting an option emits its value; the clear button emits `null`. */
23
- onChange: (value: V | null) => void;
21
+ /** Selected id; `null`, `undefined` or `clearValue` when nothing is selected. */
22
+ value: V | C | null | undefined;
23
+ /** Selecting an option emits its value; the clear button emits `clearValue`. */
24
+ onChange: (value: V | C) => void;
25
+ /**
26
+ * What the clear button emits. Default `null`. Pass `""` for a form whose schema
27
+ * wants an empty string for "no choice" (a zod `z.string()` field), rather than
28
+ * mapping `null` in every `onChange` — the type of `onChange` follows it, so the
29
+ * mapping cannot be forgotten on one field and not another. The picker also reads
30
+ * the value back as empty, so `value=""` shows the placeholder and no "×".
31
+ * See {@link ComboClearValue} for why the choice is closed.
32
+ */
33
+ clearValue?: C;
24
34
  /** Already-loaded options (client-side filtered). Also used to resolve the
25
35
  * trigger label for the current `value`. */
26
36
  options?: ComboOption<V>[];
@@ -70,9 +80,10 @@ export interface EntityComboboxProps<V extends string | number>
70
80
  * the shared field/anchor/dismiss/search primitives (no cmdk/Radix). For picking
71
81
  * several entities use {@link MultiEntityCombobox}.
72
82
  */
73
- export function EntityCombobox<V extends string | number>({
83
+ export function EntityCombobox<V extends string | number, C extends ComboClearValue = null>({
74
84
  value,
75
85
  onChange,
86
+ clearValue = null as C,
76
87
  options,
77
88
  loadOptions,
78
89
  loading,
@@ -95,7 +106,7 @@ export function EntityCombobox<V extends string | number>({
95
106
  loadErrorLabel,
96
107
  "aria-label": ariaLabel,
97
108
  ...rest
98
- }: EntityComboboxProps<V>) {
109
+ }: EntityComboboxProps<V, C>) {
99
110
  const core = useComboboxCore<V>({
100
111
  options,
101
112
  loadOptions,
@@ -120,11 +131,13 @@ export function EntityCombobox<V extends string | number>({
120
131
  // belongs to whoever renders both ends of it.
121
132
  const listboxId = useId();
122
133
 
123
- const selectedOption = value == null ? null : resolve(value);
134
+ // The clear value is "nothing selected" too, whichever one the caller picked.
135
+ const chosen: V | null = value == null || value === clearValue ? null : (value as V);
136
+ const selectedOption = chosen == null ? null : resolve(chosen);
124
137
  const q = core.query.trim();
125
138
  const showCreate =
126
139
  Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());
127
- const showClear = Boolean(clearable && value != null && !disabled);
140
+ const showClear = Boolean(clearable && chosen != null && !disabled);
128
141
  /** What the closed control is showing — the second half of its accessible name. */
129
142
  const triggerText = selectedOption?.label ?? placeholder ?? "";
130
143
 
@@ -184,7 +197,7 @@ export function EntityCombobox<V extends string | number>({
184
197
  }}
185
198
  className={cn(
186
199
  FIELD_TRIGGER,
187
- "pr-9",
200
+ "pe-9",
188
201
  label !== undefined && FIELD_FLOATING_PAD,
189
202
  disabled && "cursor-not-allowed opacity-50",
190
203
  field.isInvalid && FIELD_INVALID,
@@ -215,9 +228,9 @@ export function EntityCombobox<V extends string | number>({
215
228
  aria-label={labels.clear}
216
229
  onClick={(e) => {
217
230
  e.stopPropagation();
218
- onChange(null);
231
+ onChange(clearValue);
219
232
  }}
220
- className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
233
+ className="absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
221
234
  >
222
235
  <X className="size-4" />
223
236
  </span>
@@ -36,8 +36,14 @@ import { Button, Spinner } from "./ui";
36
36
  /** Every string the file pickers ({@link FileButton}, `FileDropzone`) render or speak.
37
37
  * Messages are functions of the file name so a translation can put it anywhere. */
38
38
  export interface FilePickerLabels {
39
- /** The file's type is not in `accept`. */
39
+ /** The file's type is not in `accept`, when no `accept` is known to name (see
40
+ * `rejectedTypeOnly`). */
40
41
  rejectedType: (name: string) => string;
42
+ /** The file's type is not in `accept`, naming what IS accepted: `accept`'s tokens,
43
+ * lower-cased and joined with ", " (`".csv"` => "Only .csv files"). Used whenever
44
+ * `accept` is set — kastlan kept a custom `isValid` + `invalidMessage` per form only
45
+ * to say this, and `accept` already knows it. */
46
+ rejectedTypeOnly: (accept: string, name: string) => string;
41
47
  /** The file is larger than `maxSize`; `maxSize` arrives formatted ("5 MB"). */
42
48
  rejectedSize: (name: string, maxSize: string) => string;
43
49
  /** The file was one too many for `maxFiles`. */
@@ -46,6 +52,9 @@ export interface FilePickerLabels {
46
52
  rejectedInvalid: (name: string) => string;
47
53
  /** Spoken instead of the per-file message when more than one file was refused. */
48
54
  rejectedMany: (count: number) => string;
55
+ /** Spoken (and shown, on a dropzone) when `onPick` refused the WHOLE pick.
56
+ * Receives the number of files in it. */
57
+ rejectedPick: (count: number) => string;
49
58
  /** Spoken after a pick the component itself echoes (the dropzone). */
50
59
  selected: (count: number, firstName: string) => string;
51
60
  /** The dropzone's remove button for one file. */
@@ -59,11 +68,14 @@ export interface FilePickerLabels {
59
68
 
60
69
  export const DEFAULT_FILE_PICKER_LABELS: FilePickerLabels = {
61
70
  rejectedType: (name) => `“${name}” is not a supported file type`,
71
+ rejectedTypeOnly: (accept) => `Only ${accept} files`,
62
72
  rejectedSize: (name, maxSize) => `“${name}” is larger than ${maxSize}`,
63
73
  rejectedCount: (name, maxFiles) =>
64
74
  `“${name}” was not added: at most ${maxFiles} ${maxFiles === 1 ? "file" : "files"}`,
65
75
  rejectedInvalid: (name) => `“${name}” cannot be used here`,
66
76
  rejectedMany: (count) => `${count} files were not added`,
77
+ rejectedPick: (count) =>
78
+ count === 1 ? "The file was not added" : `None of the ${count} files were added`,
67
79
  selected: (count, firstName) => (count === 1 ? `“${firstName}” selected` : `${count} files selected`),
68
80
  remove: (name) => `Remove “${name}”`,
69
81
  clearAll: "Remove all files",
@@ -136,6 +148,15 @@ function typeFromExtension(name: string): string | undefined {
136
148
  return dot === -1 ? undefined : EXTENSION_TYPES[name.slice(dot + 1)];
137
149
  }
138
150
 
151
+ /** `" .PDF, image/png ,"` => `".pdf, image/png"` — `accept` as a person reads it. */
152
+ function formatAccept(accept: string): string {
153
+ return accept
154
+ .split(",")
155
+ .map((t) => t.trim().toLowerCase())
156
+ .filter(Boolean)
157
+ .join(", ");
158
+ }
159
+
139
160
  /** What the pickers screen a pick with. All optional; nothing set accepts everything. */
140
161
  export interface FileScreenOptions {
141
162
  accept?: string;
@@ -160,6 +181,15 @@ export function screenFiles(
160
181
  ): { accepted: File[]; rejected: FileRejection[] } {
161
182
  const accepted: File[] = [];
162
183
  const rejected: FileRejection[] = [];
184
+ // Name what `accept` takes ("Only .csv files") — unless the host translated
185
+ // `rejectedType` but not yet `rejectedTypeOnly`: its own sentence beats an English one.
186
+ const acceptText = opts.accept ? formatAccept(opts.accept) : "";
187
+ const useOnly =
188
+ acceptText !== "" &&
189
+ (labels.rejectedTypeOnly !== DEFAULT_FILE_PICKER_LABELS.rejectedTypeOnly ||
190
+ labels.rejectedType === DEFAULT_FILE_PICKER_LABELS.rejectedType);
191
+ const typeMessage = (name: string) =>
192
+ useOnly ? labels.rejectedTypeOnly(acceptText, name) : labels.rejectedType(name);
163
193
  for (const file of files) {
164
194
  let reason: FileRejectionReason | null = null;
165
195
  if (!matchesAccept(file, opts.accept)) reason = "type";
@@ -174,7 +204,7 @@ export function screenFiles(
174
204
  }
175
205
  const message =
176
206
  reason === "type"
177
- ? labels.rejectedType(file.name)
207
+ ? typeMessage(file.name)
178
208
  : reason === "size"
179
209
  ? labels.rejectedSize(file.name, formatSize(opts.maxSize ?? 0))
180
210
  : reason === "count"
@@ -185,6 +215,34 @@ export function screenFiles(
185
215
  return { accepted, rejected };
186
216
  }
187
217
 
218
+ /**
219
+ * Judges a pick as a whole — keksdose's "refuse the pick if any file is bad", which
220
+ * with `onFiles` + `onReject` alone had to be rebuilt from two calls in a microtask.
221
+ *
222
+ * Called ONCE per pick, after screening and before `onFiles` / `onReject`, with both
223
+ * halves (either may be empty). Return `false` to refuse the whole pick: the accepted
224
+ * files are not delivered, `onReject` still receives what screening refused, and one
225
+ * sentence (`labels.rejectedPick`) is spoken for the pick instead of a per-file one.
226
+ * Return nothing to let the pick through as usual.
227
+ *
228
+ * ```tsx
229
+ * onPick={(ok, bad) => bad.length === 0 || false} // all or nothing
230
+ * ```
231
+ */
232
+ export type FilePickHandler = (accepted: File[], rejected: FileRejection[]) => boolean | void;
233
+
234
+ /** @internal The shared tail of both pickers: run `onPick`, and say whether the pick
235
+ * stands. A pick with nothing accepted has nothing to refuse, so `false` there is
236
+ * treated as no verdict and the usual per-file messages are kept. */
237
+ export function judgePick(
238
+ onPick: FilePickHandler | undefined,
239
+ accepted: File[],
240
+ rejected: FileRejection[],
241
+ ): boolean {
242
+ if (!onPick) return true;
243
+ return onPick(accepted, rejected) !== false || accepted.length === 0;
244
+ }
245
+
188
246
  /** One sentence for a whole batch of refusals: the file's own message for one, a
189
247
  * count for several — reading out five sentences in a row helps nobody. */
190
248
  export function summariseRejections(rejected: readonly FileRejection[], labels: FilePickerLabels): string {
@@ -205,8 +263,11 @@ export interface UseFilePickerOptions extends FileScreenOptions {
205
263
  * keksdose's camera input has no `multiple`. Pass one or the other.
206
264
  */
207
265
  capture?: boolean | "user" | "environment";
208
- /** The files that passed, in pick order. Never called with an empty array. */
209
- onFiles: (files: File[]) => void;
266
+ /** The files that passed, in pick order. Never called with an empty array.
267
+ * Optional since 0.7 for a caller that takes the pick through `onPick` alone. */
268
+ onFiles?: (files: File[]) => void;
269
+ /** See {@link FilePickHandler}: the whole pick at once, with the power to refuse it. */
270
+ onPick?: FilePickHandler;
210
271
  /** The files that did not, with a translated message each. The refusals are also
211
272
  * spoken through a live region, so this is for SHOWING them, not for a11y. */
212
273
  onReject?: (rejections: FileRejection[]) => void;
@@ -246,6 +307,7 @@ export function useFilePicker({
246
307
  isValid,
247
308
  invalidMessage,
248
309
  onFiles,
310
+ onPick,
249
311
  onReject,
250
312
  disabled,
251
313
  labels: labelsProp,
@@ -269,7 +331,12 @@ export function useFilePicker({
269
331
  labels,
270
332
  fileText.size,
271
333
  );
272
- if (accepted.length > 0) onFiles(accepted);
334
+ if (!judgePick(onPick, accepted, rejected)) {
335
+ if (rejected.length > 0) onReject?.(rejected);
336
+ announce(labels.rejectedPick(files.length));
337
+ return;
338
+ }
339
+ if (accepted.length > 0) onFiles?.(accepted);
273
340
  if (rejected.length > 0) {
274
341
  onReject?.(rejected);
275
342
  announce(summariseRejections(rejected, labels));
@@ -355,6 +422,7 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
355
422
  isValid,
356
423
  invalidMessage,
357
424
  onFiles,
425
+ onPick,
358
426
  onReject,
359
427
  labels,
360
428
  pending,
@@ -381,6 +449,7 @@ export const FileButton = forwardRef<HTMLButtonElement, FileButtonProps>(functio
381
449
  isValid,
382
450
  invalidMessage,
383
451
  onFiles,
452
+ onPick,
384
453
  onReject,
385
454
  labels,
386
455
  disabled: inert,