@lerianstudio/sindarian-ui 2.0.0-beta.25 → 2.0.0-beta.26

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 (305) hide show
  1. package/dist/charts/chart.js +4 -231
  2. package/dist/charts/index.js +1 -11
  3. package/dist/components/breadcrumb/get-breadcrumb-paths.js +1 -18
  4. package/dist/components/breadcrumb/index.js +1 -37
  5. package/dist/components/card/account-card/index.js +1 -69
  6. package/dist/components/card/basic-card/index.js +1 -20
  7. package/dist/components/card/dashboard-card/index.js +1 -20
  8. package/dist/components/card/entity-card/index.js +1 -68
  9. package/dist/components/card/index.js +1 -20
  10. package/dist/components/dialog/confirmation-dialog/index.js +1 -96
  11. package/dist/components/dialog/confirmation-dialog/use-confirm-dialog.js +1 -55
  12. package/dist/components/dialog/index.js +1 -19
  13. package/dist/components/dialog/large-dialog/index.js +1 -21
  14. package/dist/components/entity-box/index.js +1 -67
  15. package/dist/components/entity-data-table/index.js +1 -20
  16. package/dist/components/form/combo-box-field/index.js +1 -35
  17. package/dist/components/form/country-field/index.js +1 -11
  18. package/dist/components/form/date-picker-field/index.js +1 -83
  19. package/dist/components/form/date-range-field/index.js +1 -58
  20. package/dist/components/form/date-time-picker-field/index.js +1 -114
  21. package/dist/components/form/file-upload-field/index.js +1 -69
  22. package/dist/components/form/index.js +1 -31
  23. package/dist/components/form/input-field/index.js +1 -96
  24. package/dist/components/form/otp-field/index.js +1 -19
  25. package/dist/components/form/pagination-limit-field/index.js +1 -10
  26. package/dist/components/form/password-field/index.js +1 -16
  27. package/dist/components/form/radio-group-field/index.js +1 -36
  28. package/dist/components/form/renderable-label.js +1 -57
  29. package/dist/components/form/select-field/index.js +1 -68
  30. package/dist/components/form/state-field/index.js +1 -42
  31. package/dist/components/form/switch-field/index.js +1 -16
  32. package/dist/components/form/textarea-field/index.js +1 -23
  33. package/dist/components/page/index.js +1 -20
  34. package/dist/components/page-header/index.js +1 -80
  35. package/dist/components/pagination/index.js +1 -10
  36. package/dist/components/table/id-table-cell.js +1 -109
  37. package/dist/components/table/index.js +1 -19
  38. package/dist/components/table/locked-table-actions.js +1 -41
  39. package/dist/components/table/name-table-cell.js +1 -58
  40. package/dist/components/ui/accordion/index.js +1 -62
  41. package/dist/components/ui/alert/index.js +1 -45
  42. package/dist/components/ui/alert-dialog/index.js +1 -84
  43. package/dist/components/ui/autocomplete/index.js +1 -440
  44. package/dist/components/ui/autosize-textarea/index.js +1 -89
  45. package/dist/components/ui/avatar/index.js +1 -51
  46. package/dist/components/ui/badge/index.js +1 -83
  47. package/dist/components/ui/breadcrumb/index.js +1 -35
  48. package/dist/components/ui/button/index.js +1 -65
  49. package/dist/components/ui/calendar/index.js +1 -118
  50. package/dist/components/ui/card/index.js +1 -32
  51. package/dist/components/ui/checkbox/index.js +1 -44
  52. package/dist/components/ui/collapsible/index.js +1 -50
  53. package/dist/components/ui/command/index.js +1 -95
  54. package/dist/components/ui/copy-field/index.js +1 -163
  55. package/dist/components/ui/dialog/index.js +1 -80
  56. package/dist/components/ui/dropdown-menu/index.js +1 -110
  57. package/dist/components/ui/field/index.js +1 -82
  58. package/dist/components/ui/file-upload/index.js +1 -574
  59. package/dist/components/ui/form.js +1 -162
  60. package/dist/components/ui/hover-card/index.js +1 -51
  61. package/dist/components/ui/icon-button/index.js +1 -35
  62. package/dist/components/ui/input/adornment.js +1 -21
  63. package/dist/components/ui/input/index.js +1 -99
  64. package/dist/components/ui/label/index.js +1 -45
  65. package/dist/components/ui/lerian-wordmark/index.js +1 -7
  66. package/dist/components/ui/loading-button/index.js +1 -9
  67. package/dist/components/ui/multiple-select/index.js +1 -235
  68. package/dist/components/ui/otp-input/index.js +1 -62
  69. package/dist/components/ui/paper/index.js +1 -8
  70. package/dist/components/ui/paper-collapsible/index.js +1 -24
  71. package/dist/components/ui/popover/index.js +1 -55
  72. package/dist/components/ui/progress/index.js +1 -66
  73. package/dist/components/ui/qr-code/index.js +1 -27
  74. package/dist/components/ui/radio-group/index.js +1 -48
  75. package/dist/components/ui/scroll-area/index.js +1 -49
  76. package/dist/components/ui/select/index.js +1 -119
  77. package/dist/components/ui/separator/index.js +1 -43
  78. package/dist/components/ui/sheet/index.js +1 -136
  79. package/dist/components/ui/sidebar/index.js +1 -27
  80. package/dist/components/ui/sidebar/next-router.cjs +1 -119
  81. package/dist/components/ui/sidebar/sidebar-back-button.js +1 -8
  82. package/dist/components/ui/sidebar/sidebar-components.js +1 -23
  83. package/dist/components/ui/sidebar/sidebar-expand-button.js +1 -49
  84. package/dist/components/ui/sidebar/sidebar-item-button.js +1 -21
  85. package/dist/components/ui/sidebar/sidebar-item-collapsible.js +1 -52
  86. package/dist/components/ui/sidebar/sidebar-item-icon-button.js +1 -18
  87. package/dist/components/ui/sidebar/sidebar-item.js +1 -44
  88. package/dist/components/ui/sidebar/sidebar-provider.js +1 -201
  89. package/dist/components/ui/sidebar/sidebar-root.js +1 -130
  90. package/dist/components/ui/sidebar/sidebar-router.js +1 -126
  91. package/dist/components/ui/sidebar/sidebar-trigger.js +1 -57
  92. package/dist/components/ui/skeleton/index.js +1 -8
  93. package/dist/components/ui/stepper/index.js +1 -29
  94. package/dist/components/ui/switch/index.js +1 -43
  95. package/dist/components/ui/table/index.js +1 -56
  96. package/dist/components/ui/tabs/index.js +1 -55
  97. package/dist/components/ui/textarea/index.js +1 -8
  98. package/dist/components/ui/time-picker/index.js +1 -85
  99. package/dist/components/ui/toast/index.js +1 -3
  100. package/dist/components/ui/toast/toaster.js +1 -16
  101. package/dist/components/ui/toggle-group/index.js +1 -156
  102. package/dist/components/ui/tooltip/index.js +1 -81
  103. package/dist/domain/blotter/index.js +1 -20
  104. package/dist/domain/delinquency-aging/aging-buckets.js +1 -120
  105. package/dist/domain/delinquency-aging/index.js +1 -222
  106. package/dist/domain/figure/index.js +1 -19
  107. package/dist/domain/format.js +1 -137
  108. package/dist/domain/index.js +1 -43
  109. package/dist/domain/key-id/index.js +1 -174
  110. package/dist/domain/ledger-sheet/index.js +1 -28
  111. package/dist/domain/money-diff.js +1 -48
  112. package/dist/domain/money-math.js +1 -104
  113. package/dist/domain/money-text/index.js +1 -123
  114. package/dist/domain/section-label/index.js +1 -17
  115. package/dist/domain/status-rail/index.js +1 -76
  116. package/dist/domain/threshold-gauge/index.js +1 -161
  117. package/dist/enterprise/alert-banner/index.js +1 -24
  118. package/dist/enterprise/app-shell/index.js +1 -87
  119. package/dist/enterprise/cursor-pager/index.js +1 -26
  120. package/dist/enterprise/data-table/column-size.js +1 -103
  121. package/dist/enterprise/data-table/index.js +1 -345
  122. package/dist/enterprise/date-range-picker/index.js +1 -115
  123. package/dist/enterprise/detail-panel/index.js +1 -20
  124. package/dist/enterprise/empty-state/index.js +1 -14
  125. package/dist/enterprise/index.js +1 -39
  126. package/dist/enterprise/number-input/index.js +1 -252
  127. package/dist/enterprise/search-input/index.js +1 -121
  128. package/dist/enterprise/stat-card/index.js +1 -75
  129. package/dist/enterprise/status-badge/index.js +1 -120
  130. package/dist/enterprise/use-is-mobile.js +1 -68
  131. package/dist/enterprise/virtualized-table/index.js +1 -141
  132. package/dist/esm/charts/chart.js +4 -190
  133. package/dist/esm/charts/index.js +1 -3
  134. package/dist/esm/components/breadcrumb/get-breadcrumb-paths.js +1 -12
  135. package/dist/esm/components/breadcrumb/index.js +1 -30
  136. package/dist/esm/components/card/account-card/index.js +1 -53
  137. package/dist/esm/components/card/basic-card/index.js +1 -14
  138. package/dist/esm/components/card/dashboard-card/index.js +1 -14
  139. package/dist/esm/components/card/entity-card/index.js +1 -52
  140. package/dist/esm/components/card/index.js +1 -4
  141. package/dist/esm/components/dialog/confirmation-dialog/index.js +1 -60
  142. package/dist/esm/components/dialog/confirmation-dialog/use-confirm-dialog.js +1 -49
  143. package/dist/esm/components/dialog/index.js +1 -3
  144. package/dist/esm/components/dialog/large-dialog/index.js +1 -16
  145. package/dist/esm/components/entity-box/index.js +1 -56
  146. package/dist/esm/components/entity-data-table/index.js +1 -17
  147. package/dist/esm/components/form/combo-box-field/index.js +1 -28
  148. package/dist/esm/components/form/country-field/index.js +1 -7
  149. package/dist/esm/components/form/date-picker-field/index.js +1 -43
  150. package/dist/esm/components/form/date-range-field/index.js +1 -18
  151. package/dist/esm/components/form/date-time-picker-field/index.js +1 -74
  152. package/dist/esm/components/form/file-upload-field/index.js +1 -65
  153. package/dist/esm/components/form/index.js +1 -15
  154. package/dist/esm/components/form/input-field/index.js +1 -92
  155. package/dist/esm/components/form/otp-field/index.js +1 -15
  156. package/dist/esm/components/form/pagination-limit-field/index.js +1 -6
  157. package/dist/esm/components/form/password-field/index.js +1 -10
  158. package/dist/esm/components/form/radio-group-field/index.js +1 -32
  159. package/dist/esm/components/form/renderable-label.js +1 -54
  160. package/dist/esm/components/form/select-field/index.js +1 -64
  161. package/dist/esm/components/form/state-field/index.js +1 -35
  162. package/dist/esm/components/form/switch-field/index.js +1 -12
  163. package/dist/esm/components/form/textarea-field/index.js +1 -19
  164. package/dist/esm/components/page/index.js +1 -15
  165. package/dist/esm/components/page-header/index.js +1 -71
  166. package/dist/esm/components/pagination/index.js +1 -6
  167. package/dist/esm/components/table/id-table-cell.js +1 -105
  168. package/dist/esm/components/table/index.js +1 -3
  169. package/dist/esm/components/table/locked-table-actions.js +1 -37
  170. package/dist/esm/components/table/name-table-cell.js +1 -54
  171. package/dist/esm/components/ui/accordion/index.js +1 -24
  172. package/dist/esm/components/ui/alert/index.js +1 -38
  173. package/dist/esm/components/ui/alert-dialog/index.js +1 -39
  174. package/dist/esm/components/ui/autocomplete/index.js +1 -392
  175. package/dist/esm/components/ui/autosize-textarea/index.js +1 -51
  176. package/dist/esm/components/ui/avatar/index.js +1 -14
  177. package/dist/esm/components/ui/badge/index.js +1 -79
  178. package/dist/esm/components/ui/breadcrumb/index.js +1 -27
  179. package/dist/esm/components/ui/button/index.js +1 -60
  180. package/dist/esm/components/ui/calendar/index.js +1 -82
  181. package/dist/esm/components/ui/card/index.js +1 -24
  182. package/dist/esm/components/ui/checkbox/index.js +1 -9
  183. package/dist/esm/components/ui/collapsible/index.js +1 -13
  184. package/dist/esm/components/ui/command/index.js +1 -51
  185. package/dist/esm/components/ui/copy-field/index.js +1 -127
  186. package/dist/esm/components/ui/dialog/index.js +1 -36
  187. package/dist/esm/components/ui/dropdown-menu/index.js +1 -60
  188. package/dist/esm/components/ui/field/index.js +1 -71
  189. package/dist/esm/components/ui/file-upload/index.js +1 -536
  190. package/dist/esm/components/ui/form.js +1 -118
  191. package/dist/esm/components/ui/hover-card/index.js +1 -14
  192. package/dist/esm/components/ui/icon-button/index.js +1 -31
  193. package/dist/esm/components/ui/input/adornment.js +1 -17
  194. package/dist/esm/components/ui/input/index.js +1 -60
  195. package/dist/esm/components/ui/label/index.js +1 -10
  196. package/dist/esm/components/ui/lerian-wordmark/index.js +1 -4
  197. package/dist/esm/components/ui/loading-button/index.js +1 -7
  198. package/dist/esm/components/ui/multiple-select/index.js +1 -196
  199. package/dist/esm/components/ui/otp-input/index.js +1 -21
  200. package/dist/esm/components/ui/paper/index.js +1 -5
  201. package/dist/esm/components/ui/paper-collapsible/index.js +1 -19
  202. package/dist/esm/components/ui/popover/index.js +1 -17
  203. package/dist/esm/components/ui/progress/index.js +1 -31
  204. package/dist/esm/components/ui/qr-code/index.js +1 -24
  205. package/dist/esm/components/ui/radio-group/index.js +1 -12
  206. package/dist/esm/components/ui/scroll-area/index.js +1 -13
  207. package/dist/esm/components/ui/select/index.js +1 -74
  208. package/dist/esm/components/ui/separator/index.js +1 -8
  209. package/dist/esm/components/ui/sheet/index.js +1 -92
  210. package/dist/esm/components/ui/sidebar/index.js +1 -11
  211. package/dist/esm/components/ui/sidebar/next-router.cjs +1 -119
  212. package/dist/esm/components/ui/sidebar/sidebar-back-button.js +1 -5
  213. package/dist/esm/components/ui/sidebar/sidebar-components.js +1 -15
  214. package/dist/esm/components/ui/sidebar/sidebar-expand-button.js +1 -42
  215. package/dist/esm/components/ui/sidebar/sidebar-item-button.js +1 -18
  216. package/dist/esm/components/ui/sidebar/sidebar-item-collapsible.js +1 -44
  217. package/dist/esm/components/ui/sidebar/sidebar-item-icon-button.js +1 -14
  218. package/dist/esm/components/ui/sidebar/sidebar-item.js +1 -40
  219. package/dist/esm/components/ui/sidebar/sidebar-provider.js +1 -193
  220. package/dist/esm/components/ui/sidebar/sidebar-root.js +1 -126
  221. package/dist/esm/components/ui/sidebar/sidebar-router.js +1 -89
  222. package/dist/esm/components/ui/sidebar/sidebar-trigger.js +1 -53
  223. package/dist/esm/components/ui/skeleton/index.js +1 -6
  224. package/dist/esm/components/ui/stepper/index.js +1 -20
  225. package/dist/esm/components/ui/switch/index.js +1 -8
  226. package/dist/esm/components/ui/table/index.js +1 -44
  227. package/dist/esm/components/ui/tabs/index.js +1 -17
  228. package/dist/esm/components/ui/textarea/index.js +1 -6
  229. package/dist/esm/components/ui/time-picker/index.js +1 -49
  230. package/dist/esm/components/ui/toast/index.js +1 -2
  231. package/dist/esm/components/ui/toast/toaster.js +1 -13
  232. package/dist/esm/components/ui/toggle-group/index.js +1 -120
  233. package/dist/esm/components/ui/tooltip/index.js +1 -43
  234. package/dist/esm/domain/blotter/index.js +1 -16
  235. package/dist/esm/domain/delinquency-aging/aging-buckets.js +1 -115
  236. package/dist/esm/domain/delinquency-aging/index.js +1 -218
  237. package/dist/esm/domain/figure/index.js +1 -15
  238. package/dist/esm/domain/format.js +1 -128
  239. package/dist/esm/domain/index.js +1 -19
  240. package/dist/esm/domain/key-id/index.js +1 -169
  241. package/dist/esm/domain/ledger-sheet/index.js +1 -24
  242. package/dist/esm/domain/money-diff.js +1 -45
  243. package/dist/esm/domain/money-math.js +1 -98
  244. package/dist/esm/domain/money-text/index.js +1 -118
  245. package/dist/esm/domain/section-label/index.js +1 -14
  246. package/dist/esm/domain/status-rail/index.js +1 -71
  247. package/dist/esm/domain/threshold-gauge/index.js +1 -157
  248. package/dist/esm/enterprise/alert-banner/index.js +1 -21
  249. package/dist/esm/enterprise/app-shell/index.js +1 -51
  250. package/dist/esm/enterprise/cursor-pager/index.js +1 -23
  251. package/dist/esm/enterprise/data-table/column-size.js +1 -97
  252. package/dist/esm/enterprise/data-table/index.js +1 -342
  253. package/dist/esm/enterprise/date-range-picker/index.js +1 -112
  254. package/dist/esm/enterprise/detail-panel/index.js +1 -17
  255. package/dist/esm/enterprise/empty-state/index.js +1 -11
  256. package/dist/esm/enterprise/index.js +1 -22
  257. package/dist/esm/enterprise/number-input/index.js +1 -214
  258. package/dist/esm/enterprise/search-input/index.js +1 -85
  259. package/dist/esm/enterprise/stat-card/index.js +1 -72
  260. package/dist/esm/enterprise/status-badge/index.js +1 -116
  261. package/dist/esm/enterprise/use-is-mobile.js +1 -31
  262. package/dist/esm/enterprise/virtualized-table/index.js +1 -138
  263. package/dist/esm/hooks/use-click-away.js +1 -22
  264. package/dist/esm/hooks/use-pagination.js +1 -38
  265. package/dist/esm/hooks/use-stepper.js +1 -22
  266. package/dist/esm/hooks/use-time.js +1 -13
  267. package/dist/esm/hooks/use-toast.js +1 -107
  268. package/dist/esm/index.js +1 -77
  269. package/dist/esm/lib/storage/get-storage-object.js +1 -17
  270. package/dist/esm/lib/storage/get-storage.js +1 -14
  271. package/dist/esm/lib/storage/index.js +1 -2
  272. package/dist/esm/lib/typography.js +1 -31
  273. package/dist/esm/lib/utils.js +1 -41
  274. package/dist/esm/theme/get-theme-script.js +1 -41
  275. package/dist/esm/theme/index.js +1 -4
  276. package/dist/esm/theme/mode-toggle.js +1 -45
  277. package/dist/esm/theme/theme-provider.js +1 -122
  278. package/dist/esm/toast/helpers.js +1 -36
  279. package/dist/esm/toast/index.js +1 -4
  280. package/dist/esm/types/metadata-type.js +1 -1
  281. package/dist/esm/utils/countries.cjs +1 -17
  282. package/dist/esm/utils/country-utils.js +1 -18
  283. package/dist/esm/utils/currency-codes.js +1 -181
  284. package/dist/hooks/use-click-away.js +1 -29
  285. package/dist/hooks/use-pagination.js +1 -41
  286. package/dist/hooks/use-stepper.js +1 -26
  287. package/dist/hooks/use-time.js +1 -20
  288. package/dist/hooks/use-toast.js +1 -111
  289. package/dist/index.js +1 -96
  290. package/dist/lib/storage/get-storage-object.js +1 -23
  291. package/dist/lib/storage/get-storage.js +1 -17
  292. package/dist/lib/storage/index.js +1 -18
  293. package/dist/lib/typography.js +1 -34
  294. package/dist/lib/utils.js +1 -45
  295. package/dist/theme/get-theme-script.js +1 -44
  296. package/dist/theme/index.js +1 -11
  297. package/dist/theme/mode-toggle.js +1 -48
  298. package/dist/theme/theme-provider.js +1 -160
  299. package/dist/toast/helpers.js +1 -41
  300. package/dist/toast/index.js +1 -10
  301. package/dist/types/metadata-type.js +1 -2
  302. package/dist/utils/countries.cjs +1 -17
  303. package/dist/utils/country-utils.js +1 -25
  304. package/dist/utils/currency-codes.js +1 -184
  305. package/package.json +4 -3
@@ -1,71 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useMemo } from 'react';
4
- import { cva } from 'class-variance-authority';
5
- import { cn } from '../../../lib/utils.js';
6
- import { Label } from '../../../components/ui/label/index.js';
7
- import { Separator } from '../../../components/ui/separator/index.js';
8
- function FieldSet({ className, ...props }) {
9
- return (_jsx("fieldset", { "data-slot": "field-set", className: cn('flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3', className), ...props }));
10
- }
11
- function FieldLegend({ className, variant = 'legend', ...props }) {
12
- return (_jsx("legend", { "data-slot": "field-legend", "data-variant": variant, className: cn('mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base', className), ...props }));
13
- }
14
- function FieldGroup({ className, ...props }) {
15
- return (_jsx("div", { "data-slot": "field-group", className: cn('group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4', className), ...props }));
16
- }
17
- const fieldVariants = cva('data-[invalid=true]:text-system-error-h1a gap-2 group/field flex w-full', {
18
- variants: {
19
- orientation: {
20
- vertical: 'flex-col [&>*]:w-full [&>.sr-only]:w-auto',
21
- horizontal: 'flex-row items-center [&>[data-slot=field-label]]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
22
- responsive: 'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto @md/field-group:[&>[data-slot=field-label]]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
23
- }
24
- },
25
- defaultVariants: {
26
- orientation: 'vertical'
27
- }
28
- });
29
- function Field({ className, orientation = 'vertical', ...props }) {
30
- return (_jsx("div", { role: "group", "data-slot": "field", "data-orientation": orientation, className: cn(fieldVariants({ orientation }), className), ...props }));
31
- }
32
- function FieldContent({ className, ...props }) {
33
- return (_jsx("div", { "data-slot": "field-content", className: cn('group/field-content flex flex-1 flex-col gap-0.5 leading-snug', className), ...props }));
34
- }
35
- function FieldLabel({ className, ...props }) {
36
- return (_jsx(Label, { "data-slot": "field-label", className: cn('has-data-checked:bg-primary/5 has-data-checked:border-primary dark:has-data-checked:bg-primary/10 group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-2.5', 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col', className), ...props }));
37
- }
38
- function FieldTitle({ className, ...props }) {
39
- return (_jsx("div", { "data-slot": "field-label", className: cn('flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50', className), ...props }));
40
- }
41
- function FieldDescription({ className, ...props }) {
42
- return (_jsx("p", { "data-slot": "field-description", className: cn('text-muted-foreground text-left text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance [[data-variant=legend]+&]:-mt-1.5', 'last:mt-0 nth-last-2:-mt-1', '[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4', className), ...props }));
43
- }
44
- function FieldSeparator({ children, className, ...props }) {
45
- return (_jsxs("div", { "data-slot": "field-separator", "data-content": !!children, className: cn('relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2', className), ...props, children: [_jsx(Separator, { className: "absolute inset-0 top-1/2" }), children && (_jsx("span", { className: "text-muted-foreground bg-background relative mx-auto block w-fit px-2", "data-slot": "field-separator-content", children: children }))] }));
46
- }
47
- function FieldError({ className, children, errors, ...props }) {
48
- const content = useMemo(() => {
49
- if (children) {
50
- return children;
51
- }
52
- if (!errors?.length) {
53
- return null;
54
- }
55
- const uniqueErrors = [
56
- ...new Map(errors.map((error) => [error?.message, error])).values()
57
- ];
58
- if (uniqueErrors?.length === 1) {
59
- return uniqueErrors[0]?.message;
60
- }
61
- return (_jsx("ul", { className: "ml-4 flex list-disc flex-col gap-1", children: uniqueErrors.map((error, index) => error?.message && _jsx("li", { children: error.message }, index)) }));
62
- }, [children, errors]);
63
- if (!content) {
64
- return null;
65
- }
66
- return (_jsx("div", { role: "alert", "data-slot": "field-error",
67
- // The error TEXT token, not `text-destructive`: that is the badge/fill
68
- // family and measures 3.80:1 as ink on the dark card, under AA.
69
- className: cn('text-system-error-h1a text-sm font-normal', className), ...props, children: content }));
70
- }
71
- export { Field, FieldLabel, FieldDescription, FieldError, FieldGroup, FieldLegend, FieldSeparator, FieldSet, FieldContent, FieldTitle };
1
+ "use client";import{jsx as a,jsxs as f}from"react/jsx-runtime";import{useMemo as u}from"react";import{cva as c}from"class-variance-authority";import{cn as l}from"../../../lib/utils.js";import{Label as m}from"../../../components/ui/label/index.js";import{Separator as p}from"../../../components/ui/separator/index.js";function g({className:t,...e}){return a("fieldset",{"data-slot":"field-set",className:l("flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",t),...e})}function x({className:t,variant:e="legend",...o}){return a("legend",{"data-slot":"field-legend","data-variant":e,className:l("mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",t),...o})}function h({className:t,...e}){return a("div",{"data-slot":"field-group",className:l("group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",t),...e})}const b=c("data-[invalid=true]:text-system-error-h1a gap-2 group/field flex w-full",{variants:{orientation:{vertical:"flex-col [&>*]:w-full [&>.sr-only]:w-auto",horizontal:"flex-row items-center [&>[data-slot=field-label]]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",responsive:"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto @md/field-group:[&>[data-slot=field-label]]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px"}},defaultVariants:{orientation:"vertical"}});function v({className:t,orientation:e="vertical",...o}){return a("div",{role:"group","data-slot":"field","data-orientation":e,className:l(b({orientation:e}),t),...o})}function w({className:t,...e}){return a("div",{"data-slot":"field-content",className:l("group/field-content flex flex-1 flex-col gap-0.5 leading-snug",t),...e})}function N({className:t,...e}){return a(m,{"data-slot":"field-label",className:l("has-data-checked:bg-primary/5 has-data-checked:border-primary dark:has-data-checked:bg-primary/10 group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-2.5","has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",t),...e})}function y({className:t,...e}){return a("div",{"data-slot":"field-label",className:l("flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",t),...e})}function k({className:t,...e}){return a("p",{"data-slot":"field-description",className:l("text-muted-foreground text-left text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance [[data-variant=legend]+&]:-mt-1.5","last:mt-0 nth-last-2:-mt-1","[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",t),...e})}function F({children:t,className:e,...o}){return f("div",{"data-slot":"field-separator","data-content":!!t,className:l("relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",e),...o,children:[a(p,{className:"absolute inset-0 top-1/2"}),t&&a("span",{className:"text-muted-foreground bg-background relative mx-auto block w-fit px-2","data-slot":"field-separator-content",children:t})]})}function j({className:t,children:e,errors:o,...s}){const i=u(()=>{if(e){return e}if(!o?.length){return null}const d=[...new Map(o.map(r=>[r?.message,r])).values()];if(d?.length===1){return d[0]?.message}return a("ul",{className:"ml-4 flex list-disc flex-col gap-1",children:d.map((r,n)=>r?.message&&a("li",{children:r.message},n))})},[e,o]);if(!i){return null}return a("div",{role:"alert","data-slot":"field-error",className:l("text-system-error-h1a text-sm font-normal",t),...s,children:i})}export{v as Field,N as FieldLabel,k as FieldDescription,j as FieldError,h as FieldGroup,x as FieldLegend,F as FieldSeparator,g as FieldSet,w as FieldContent,y as FieldTitle};
@@ -1,536 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- /**
4
- * FileUpload — a controlled "pick a file, hand it back" primitive. The defining
5
- * job: select one file (by click, keyboard, or drag-and-drop), validate it
6
- * against an accept filter + a byte ceiling, and emit `{ file, text }`.
7
- *
8
- * `readAs` decides whether the bytes are decoded on the way through. `'text'`
9
- * (the default) reads UTF-8 via FileReader.readAsText, which is what a PEM/CSV
10
- * host wants. `'none'` hands the `File` over untouched and is the only mode a
11
- * BINARY file can use: a PDF, XLSX or PFX put through readAsText decodes into
12
- * replacement-character garbage, and that garbage string is then retained for
13
- * as long as the host holds the value — 20 MiB of PDF became a useless 20 MiB
14
- * string, and the host still had to read the file a second time to get at the
15
- * real bytes.
16
- *
17
- * The real `<input type="file">` IS the accessible control: it is `sr-only`
18
- * (visually hidden) but focusable and labelable — never `aria-hidden`, never
19
- * `tabIndex={-1}`. The FormControl-injected ARIA (`id`, `aria-invalid`,
20
- * `aria-required`, `aria-describedby`) flows onto the input via `...rest`, so
21
- * react-hook-form's `field.ref` (forwarded here) focuses the real control on
22
- * error and FormLabel's `htmlFor` names it. The styled zone is a plain `<div>`
23
- * with a click handler, NOT a wrapping `<label>`: a label would contribute its
24
- * text to the input's accessible name and double-name it alongside a field's
25
- * own label. Keyboard users Tab to the focusable input and Enter/Space opens
26
- * the picker natively, so no key proxy is needed. Drag-and-drop is an
27
- * enhancement layered on the zone.
28
- *
29
- * Errors are DUAL by design and both announced: size/type/read failures are
30
- * the primitive's own concern, surfaced inline via `role="alert"` so it works
31
- * standalone outside a form; a form wrapper (FileUploadField) adds the
32
- * react-hook-form validation surface separately. A rejected pick never writes
33
- * a value, so it cannot masquerade as valid. `accept` is validated against
34
- * BOTH extension and MIME because the native `accept` attribute is only a
35
- * browser hint and is bypassable via drag-drop.
36
- *
37
- * A host that already announces the rejection itself — a toast in its own
38
- * locale, driven off `onError` — silences this one by returning nothing from
39
- * `labels.error`. Two announcements for one event, in two languages, is the
40
- * accessibility defect; the wording is not.
41
- */
42
- import * as React from 'react';
43
- import { Upload, X } from 'lucide-react';
44
- import { IconButton } from '../../../components/ui/icon-button/index.js';
45
- import { cn } from '../../../lib/utils.js';
46
- /**
47
- * Validate a chosen file against an accept filter and a byte ceiling. Exported
48
- * so hosts can pre-validate before handing a file over. `accept` is matched
49
- * against BOTH the filename extension and the MIME type — a `.pem`/`text/plain`
50
- * style filter passes if EITHER matches a token. When `accept` is omitted, any
51
- * type passes. Returns the first error, or null when the file is acceptable.
52
- */
53
- export function validateFile(file, opts) {
54
- const { accept, maxSizeBytes } = opts;
55
- if (typeof maxSizeBytes === 'number' && file.size > maxSizeBytes) {
56
- return { kind: 'too-large', file, maxSizeBytes };
57
- }
58
- if (accept && accept.trim() !== '' && !matchesAccept(file, accept)) {
59
- return { kind: 'wrong-type', file, accept };
60
- }
61
- return null;
62
- }
63
- /**
64
- * Does the file satisfy a comma-separated accept list? Each token is either a
65
- * dot-extension (`.pem`, case-insensitive against the filename), an exact MIME
66
- * (`text/plain`), or a MIME wildcard (`text/*`). True if ANY token matches.
67
- */
68
- function matchesAccept(file, accept) {
69
- const name = file.name.toLowerCase();
70
- const mime = file.type.toLowerCase();
71
- return accept
72
- .split(',')
73
- .map((t) => t.trim().toLowerCase())
74
- .filter((t) => t !== '')
75
- .some((token) => {
76
- if (token.startsWith('.'))
77
- return name.endsWith(token);
78
- if (token.endsWith('/*'))
79
- return mime.startsWith(token.slice(0, -1));
80
- return mime === token;
81
- });
82
- }
83
- /**
84
- * Humanize a byte count for the selected-file chip. Binary units, 1 decimal.
85
- * Exported so a `labels.error` override can format `maxSizeBytes` exactly the
86
- * way the chip and the default message do, instead of re-deriving binary units.
87
- */
88
- export function humanizeSize(bytes) {
89
- if (bytes < 1024)
90
- return `${bytes} B`;
91
- const units = ['KB', 'MB', 'GB'];
92
- let size = bytes / 1024;
93
- let unit = 0;
94
- while (size >= 1024 && unit < units.length - 1) {
95
- size /= 1024;
96
- unit += 1;
97
- }
98
- return `${size.toFixed(1)} ${units[unit]}`;
99
- }
100
- /** The default English announcement for each rejection kind. */
101
- function defaultErrorMessage(error) {
102
- switch (error.kind) {
103
- case 'too-large':
104
- return `File is too large (max ${humanizeSize(error.maxSizeBytes)}).`;
105
- case 'wrong-type':
106
- return `File type not allowed (expected ${error.accept}).`;
107
- case 'read-failed':
108
- return 'Could not read the file.';
109
- }
110
- }
111
- export const FileUpload = React.forwardRef(function FileUpload({ accept, maxSizeBytes, value, readAs = 'text', labels, onSelect, onError, disabled = false, id, className, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, ...rest }, ref) {
112
- const internalRef = React.useRef(null);
113
- React.useImperativeHandle(ref, () => internalRef.current);
114
- const reactId = React.useId();
115
- const inputId = id ?? reactId;
116
- const errorId = `${inputId}-file-upload-error`;
117
- const [dragActive, setDragActive] = React.useState(false);
118
- const [error, setError] = React.useState(null);
119
- // The destructive styling is driven by the consumer's aria-invalid OR the
120
- // component's own validation/read error — a self-detected bad pick must read
121
- // as invalid even outside a form.
122
- const invalid = ariaInvalid || error !== null;
123
- // Resolve the announcement BEFORE deciding whether the alert exists: a
124
- // consumer that returns nothing is opting out of this surface entirely,
125
- // and an association pointing at an unrendered node is worse than none.
126
- const resolved = error
127
- ? labels?.error
128
- ? labels.error(error)
129
- : defaultErrorMessage(error)
130
- : null;
131
- const errorText = resolved ? resolved : null;
132
- // Radix Slot OVERWRITES aria-describedby (it does not merge), so merge the
133
- // primitive's own role=alert error id with the FormControl-injected one so
134
- // both associations coexist on the input. A plain join, never `cn` —
135
- // tailwind-merge treats these as class names and would drop an id that
136
- // happens to look like a conflicting utility.
137
- const describedBy = [ariaDescribedby, errorText ? errorId : undefined]
138
- .filter(Boolean)
139
- .join(' ') || undefined;
140
- // Last-resolved-wins: a slow read for pick A must not overwrite a newer
141
- // pick B. Track the active reader and abort any in-flight read first.
142
- const readerRef = React.useRef(null);
143
- const handleFile = (file) => {
144
- if (!file)
145
- return;
146
- readerRef.current?.abort();
147
- const validationError = validateFile(file, { accept, maxSizeBytes });
148
- if (validationError) {
149
- setError(validationError);
150
- onError?.(validationError);
151
- return;
152
- }
153
- // Binary path: no decode, no reader, no retained garbage string. The
154
- // stale-rejection clear still has to happen here — the alert is this
155
- // component's own state and a good pick must retire it.
156
- if (readAs === 'none') {
157
- readerRef.current = null;
158
- setError(null);
159
- onSelect({ file, text: '' });
160
- return;
161
- }
162
- const reader = new FileReader();
163
- readerRef.current = reader;
164
- reader.onload = () => {
165
- if (readerRef.current !== reader)
166
- return;
167
- readerRef.current = null;
168
- setError(null);
169
- onSelect({ file, text: String(reader.result ?? '') });
170
- };
171
- reader.onerror = () => {
172
- if (readerRef.current !== reader)
173
- return;
174
- readerRef.current = null;
175
- const readError = { kind: 'read-failed', file };
176
- setError(readError);
177
- onError?.(readError);
178
- };
179
- reader.readAsText(file);
180
- };
181
- const onInputChange = (event) => {
182
- const file = event.target.files?.[0];
183
- // Release the FileList the moment it has been read. The browser fires
184
- // `change` only when the selection DIFFERS from what the input already
185
- // holds, so keeping it meant re-picking the same file after a host reset
186
- // (or retrying a rejected one) was silently a no-op — the picker opened,
187
- // the user chose the file, and nothing happened.
188
- event.target.value = '';
189
- handleFile(file);
190
- };
191
- // `preventDefault` FIRST, and unconditionally. Returning before it does
192
- // not merely refuse the drop: nothing cancels the browser's OWN action for
193
- // a dropped file, so the window navigates to the file and whatever the
194
- // page held unsaved is gone. A zone the host turned off has to swallow the
195
- // drop, never hand it back to the browser. `dropEffect` says so to the
196
- // cursor while the file is still in the air.
197
- const onDragOver = (event) => {
198
- event.preventDefault();
199
- if (disabled) {
200
- event.dataTransfer.dropEffect = 'none';
201
- return;
202
- }
203
- setDragActive(true);
204
- };
205
- const onDragLeave = (event) => {
206
- event.preventDefault();
207
- setDragActive(false);
208
- };
209
- const onDrop = (event) => {
210
- event.preventDefault();
211
- // Cleared before the gate, not after, so a host that disables the zone
212
- // mid-drag does not leave it highlighted for a drag that can no longer
213
- // land.
214
- setDragActive(false);
215
- if (disabled)
216
- return;
217
- handleFile(event.dataTransfer.files?.[0]);
218
- };
219
- // Mouse convenience only: clicking the styled zone opens the picker. A
220
- // click that ORIGINATED on the input already opens it natively and bubbles
221
- // up to here — re-firing .click() would open the picker twice, so ignore it.
222
- const openPicker = (event) => {
223
- if (disabled || event.target === internalRef.current)
224
- return;
225
- internalRef.current?.click();
226
- };
227
- const clear = () => {
228
- readerRef.current?.abort();
229
- readerRef.current = null;
230
- setError(null);
231
- onSelect(null);
232
- // Reset the native input so re-picking the same filename fires onChange.
233
- if (internalRef.current)
234
- internalRef.current.value = '';
235
- };
236
- return (_jsxs("div", { className: cn('space-y-2', className), children: [_jsxs("div", { onClick: openPicker, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, className: cn('border-input-border bg-card focus-within:ring-ring focus-within:ring-offset-background aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-within:ring-destructive flex w-full items-center gap-3 rounded-md border px-3 py-4 text-sm shadow-xs transition-colors focus-within:ring-2 focus-within:ring-offset-1 focus-within:outline-none', dragActive &&
237
- 'border-ring ring-ring ring-offset-background ring-2 ring-offset-1', disabled
238
- ? 'border-muted bg-muted/30 cursor-not-allowed shadow-none'
239
- : 'cursor-pointer'), "aria-invalid": invalid || undefined, children: [_jsx("input", { ref: internalRef, id: inputId, type: "file", accept: accept, disabled: disabled, className: "sr-only", "aria-invalid": invalid || undefined, "aria-required": ariaRequired || undefined, "aria-describedby": describedBy, "aria-label": ariaLabel, onChange: onInputChange, ...rest }), _jsx(Upload, { className: "text-muted-foreground size-4 shrink-0", "aria-hidden": "true" }), value ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "min-w-0 flex-1 truncate", children: [_jsx("span", { className: "text-foreground font-medium", children: value.file.name }), ' ', _jsx("span", { className: "text-muted-foreground tabular-nums", children: humanizeSize(value.file.size) })] }), _jsx(IconButton, { type: "button", variant: "plain", size: "small", disabled: disabled, "aria-label": labels?.remove?.trim() ? labels.remove : 'Remove file', onClick: (event) => {
240
- // Don't bubble to the zone's openPicker and re-open the dialog.
241
- event.stopPropagation();
242
- clear();
243
- }, children: _jsx(X, { className: "size-4", "aria-hidden": "true" }) })] })) : (_jsxs("span", { className: "text-muted-foreground", children: [_jsx("span", { className: "text-foreground font-medium", children: labels?.action ?? 'Choose a file' }), ' ', labels?.hint ?? 'or drag and drop'] }))] }), errorText ? (_jsx("p", { id: errorId, role: "alert", className: "text-system-error-h1a text-xs font-medium", children: errorText })) : null] }));
244
- });
245
- FileUpload.displayName = 'FileUpload';
246
- /** The default English announcement for each rejection kind. */
247
- function defaultMultipleErrorMessage(error) {
248
- if (error.kind === 'too-many') {
249
- return `Too many files (max ${error.maxFiles}). ${error.file.name} was not added.`;
250
- }
251
- return defaultErrorMessage(error);
252
- }
253
- /** The default zone copy once the cap is reached, in grammatical English. */
254
- function defaultFullMessage(maxFiles) {
255
- return maxFiles === 1
256
- ? 'Maximum of 1 file reached.'
257
- : `Maximum of ${maxFiles} files reached.`;
258
- }
259
- /**
260
- * MultipleFileUpload: the plural sibling of FileUpload. Pick SEVERAL files,
261
- * accumulate them across repeated picks, validate each one, cap the total, and
262
- * hand back `FileUploadResult[]`.
263
- *
264
- * A SIBLING COMPONENT, not a `multiple` flag. This library already answers
265
- * "this one takes many" that way (`Select` / `MultipleSelect`), and FileUpload
266
- * strips `'multiple'` from its props on purpose: single-file is its contract,
267
- * not a default it happens to have. A boolean would have forced every prop
268
- * here into a union that means one thing when the flag is set and another when
269
- * it is not: `value` as `Result | Result[] | null`, a `maxFiles` that is
270
- * meaningless in half the configurations, and a remove control whose
271
- * accessible name is fixed copy in one mode and per-file in the other. The
272
- * plural props follow the house shape for plural components: `value?: T[]`
273
- * with `onValueChange?: (values: T[]) => void`.
274
- *
275
- * WHERE THE LINE SITS: this component owns SELECTION and nothing after it.
276
- * Choosing, validating, capping, listing and removing are its job; uploading
277
- * is not. That split is not squeamishness about scope, it is where the
278
- * knowledge actually lives. An upload needs an endpoint, an auth scheme, a
279
- * concurrency policy, a retry policy and, very often, a parent id that does
280
- * not exist yet when the files are chosen: the motivating host stages evidence
281
- * files while a form is being filled and can only upload them against the id
282
- * that its create call returns afterwards. None of that is knowable from
283
- * inside a library primitive, and a component that guessed would have to be
284
- * fought rather than used. So the host keeps its own per-file record with
285
- * status and retry, and this component keeps the part a form can hold and
286
- * validate: the chosen files. `FileUploadResult[]` is a value; an upload state
287
- * machine is not.
288
- *
289
- * ACCUMULATION is the defining behaviour. A second pick ADDS to the selection
290
- * rather than replacing it, because a user assembling five documents does it
291
- * in two or three trips to the file dialog, not one. Everything else follows
292
- * from that: room is measured against what is already selected, and the batch
293
- * that overflows the cap still contributes the files that fit.
294
- *
295
- * A BATCH SURVIVES ITS OWN CASUALTIES. One file rejected for type, size or a
296
- * failed read does not discard the rest of the batch, and it does not consume
297
- * a slot either: validation runs over the WHOLE batch before the cap is
298
- * applied, so a file that was never eligible cannot cost an eligible one its
299
- * place, and `'too-many'` always names a file a slot would genuinely have
300
- * taken. The alternative punishes
301
- * a user for a mistake in one file by throwing away four good ones, and hands
302
- * back no way to tell which was which. Every rejection is reported through
303
- * `onError` and announced together in one `role="alert"`.
304
- *
305
- * Accessibility follows the sibling BELOW THE CAP: the real `<input
306
- * type="file">` is `sr-only` but focusable and labelable, and never
307
- * `aria-hidden`, so FormControl-injected ARIA and react-hook-form's focus on
308
- * error both work while the picker is enabled. At the cap that changes, and
309
- * the paragraph below says how.
310
- * The file list sits OUTSIDE the click zone, so activating a remove control
311
- * cannot also reopen the picker, and each remove control is named after its
312
- * own file: a column of identical "Remove file" buttons tells a screen-reader
313
- * user nothing about which row they are on.
314
- *
315
- * AT THE CAP the picker takes the native `disabled` attribute, and that DOES
316
- * take it out of the tab order. This is the one state in which the input is
317
- * not a focus target, and the one state in which focus-on-error cannot land on
318
- * it, so it is a real cost rather than a free win. The cap has no counterpart
319
- * in the single-file sibling, so the precedent followed here is
320
- * `DateRangePicker`'s trigger: a control whose only job is to open a dialog has
321
- * no state worth keeping focusable, and native `disabled` is what both removes
322
- * it from the tab order and keeps the dialog shut. The alternative, an enabled
323
- * picker that opens the file dialog and then refuses every file with
324
- * `too-many`, is a control that lies about being available. What keeps the cap
325
- * from being a dead end is the escape hatch: the remove controls answer to
326
- * `disabled` alone and NEVER to the cap, so they stay focusable and removing
327
- * one file reopens the picker.
328
- */
329
- export const MultipleFileUpload = React.forwardRef(function MultipleFileUpload({ accept, maxSizeBytes, maxFiles, value = [], readAs = 'text', labels, onValueChange, onError, disabled = false, id, className, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, ...rest }, ref) {
330
- const internalRef = React.useRef(null);
331
- React.useImperativeHandle(ref, () => internalRef.current);
332
- const reactId = React.useId();
333
- const inputId = id ?? reactId;
334
- const errorId = `${inputId}-file-upload-error`;
335
- const [dragActive, setDragActive] = React.useState(false);
336
- const [errors, setErrors] = React.useState([]);
337
- // The authoritative base for the next append. Props win on every render, so
338
- // the host stays in control; the commit below also writes through, so a
339
- // second batch that settles before the host has re-rendered still appends to
340
- // the first batch's result instead of overwriting it.
341
- const valueRef = React.useRef(value);
342
- React.useEffect(() => {
343
- valueRef.current = value;
344
- });
345
- // Superseding is NOT the contract here the way it is in the single-file
346
- // sibling — batches accumulate, so an in-flight read is never stale. The
347
- // readers are tracked for the one case that does have to stop them: an
348
- // unmount. A read that lands afterwards would settle its batch and commit,
349
- // calling the host's `onValueChange` for a component that no longer exists.
350
- const readersRef = React.useRef(new Set());
351
- React.useEffect(() => () => {
352
- for (const reader of readersRef.current)
353
- reader.abort();
354
- readersRef.current.clear();
355
- }, []);
356
- const invalid = ariaInvalid || errors.length > 0;
357
- // Resolve the announcements BEFORE deciding whether the alert exists: a
358
- // consumer that returns nothing for every rejection is opting out of this
359
- // surface, and an association pointing at an unrendered node is worse than
360
- // none.
361
- const messages = errors
362
- .map((failure) => labels?.error
363
- ? labels.error(failure)
364
- : defaultMultipleErrorMessage(failure))
365
- .filter((message) => Boolean(message));
366
- // A plain join, never `cn`: tailwind-merge treats these as class names and
367
- // would drop an id that happens to look like a conflicting utility.
368
- const describedBy = [ariaDescribedby, messages.length > 0 ? errorId : undefined]
369
- .filter(Boolean)
370
- .join(' ') || undefined;
371
- const full = maxFiles !== undefined && value.length >= maxFiles;
372
- // Removing must stay possible at the cap, so only the PICKER closes.
373
- const pickerDisabled = disabled || full;
374
- const roomFor = (selected) => maxFiles === undefined
375
- ? Number.POSITIVE_INFINITY
376
- : Math.max(maxFiles - selected, 0);
377
- // The cap is enforced HERE, at the only place that appends, because only the
378
- // commit knows the base it lands on. `ingest` measures room too, but for an
379
- // asynchronous batch it measures it BEFORE any read settles: two overlapping
380
- // batches both see the pre-commit selection and would each believe they fit.
381
- const commit = (accepted, rejections) => {
382
- const room = roomFor(valueRef.current.length);
383
- const fitting = accepted.slice(0, room);
384
- const overflow = accepted[room];
385
- const failures = overflow !== undefined && maxFiles !== undefined
386
- ? [
387
- ...rejections,
388
- { kind: 'too-many', file: overflow.file, maxFiles }
389
- ]
390
- : rejections;
391
- setErrors(failures);
392
- for (const rejection of failures)
393
- onError?.(rejection);
394
- if (fitting.length === 0)
395
- return;
396
- const next = [...valueRef.current, ...fitting];
397
- valueRef.current = next;
398
- onValueChange(next);
399
- };
400
- const ingest = (incoming) => {
401
- if (incoming.length === 0)
402
- return;
403
- const room = roomFor(valueRef.current.length);
404
- const rejections = [];
405
- // Validate EVERY file BEFORE the cap is applied. Slicing to the remaining
406
- // room first would let a file that was never eligible consume a slot a
407
- // good file could have used — one bad pick costing a good one, which is
408
- // the opposite of a batch surviving its own casualties — and it would
409
- // leave every file past the slice window neither validated nor reported.
410
- // Validation is pure metadata (size and accept), so running it over files
411
- // that may not fit costs nothing.
412
- const eligible = [];
413
- for (const file of incoming) {
414
- const rejection = validateFile(file, { accept, maxSizeBytes });
415
- // Continue rather than abort: one bad file must not cost the good ones.
416
- if (rejection) {
417
- rejections.push(rejection);
418
- continue;
419
- }
420
- eligible.push(file);
421
- }
422
- // The cap then applies to the SURVIVORS. One rejection for the batch,
423
- // naming the FIRST eligible file that did not fit: naming one already
424
- // refused for its size or type would blame the cap for the wrong thing,
425
- // and repeating it per overflowing file buries the actionable part.
426
- if (eligible.length > room && maxFiles !== undefined) {
427
- rejections.push({ kind: 'too-many', file: eligible[room], maxFiles });
428
- }
429
- const candidates = eligible.slice(0, room);
430
- // Binary path: no decode, no reader, no retained garbage string.
431
- if (readAs === 'none') {
432
- commit(candidates.map((file) => ({ file, text: '' })), rejections);
433
- return;
434
- }
435
- if (candidates.length === 0) {
436
- commit([], rejections);
437
- return;
438
- }
439
- // Slot-per-candidate so the emitted batch keeps PICK order regardless of
440
- // the order the reads settle in. A null slot is a read that failed; the
441
- // batch commits once every read has settled, one way or the other.
442
- const slots = new Array(candidates.length).fill(null);
443
- let remaining = candidates.length;
444
- const settle = () => {
445
- remaining -= 1;
446
- if (remaining > 0)
447
- return;
448
- const accepted = [];
449
- const readFailures = [];
450
- slots.forEach((slot, index) => {
451
- if (slot)
452
- accepted.push(slot);
453
- else
454
- readFailures.push({ kind: 'read-failed', file: candidates[index] });
455
- });
456
- commit(accepted, [...rejections, ...readFailures]);
457
- };
458
- candidates.forEach((file, index) => {
459
- const reader = new FileReader();
460
- readersRef.current.add(reader);
461
- // Deliberately no `onabort` handler: an aborted read must NOT settle,
462
- // or the batch would commit at exactly the moment we are stopping it.
463
- reader.onload = () => {
464
- readersRef.current.delete(reader);
465
- slots[index] = { file, text: String(reader.result ?? '') };
466
- settle();
467
- };
468
- reader.onerror = () => {
469
- readersRef.current.delete(reader);
470
- settle();
471
- };
472
- reader.readAsText(file);
473
- });
474
- };
475
- const onInputChange = (event) => {
476
- const files = Array.from(event.target.files ?? []);
477
- // Release the FileList the moment it has been read, or re-picking the same
478
- // file is silently a no-op: the browser fires `change` only when the
479
- // selection DIFFERS from what the input already holds.
480
- event.target.value = '';
481
- ingest(files);
482
- };
483
- // `preventDefault` FIRST, and unconditionally. Returning before it does
484
- // not merely refuse the drop: nothing cancels the browser's OWN action for
485
- // a dropped file, so the window navigates to the file and whatever the
486
- // page held unsaved is gone. A zone the host turned off has to swallow the
487
- // drop, never hand it back to the browser. `dropEffect` says so to the
488
- // cursor while the file is still in the air.
489
- const onDragOver = (event) => {
490
- event.preventDefault();
491
- if (disabled) {
492
- event.dataTransfer.dropEffect = 'none';
493
- return;
494
- }
495
- setDragActive(true);
496
- };
497
- const onDragLeave = (event) => {
498
- event.preventDefault();
499
- setDragActive(false);
500
- };
501
- const onDrop = (event) => {
502
- event.preventDefault();
503
- // Cleared before the gate, not after, so a host that disables the zone
504
- // mid-drag does not leave it highlighted for a drag that can no longer
505
- // land.
506
- setDragActive(false);
507
- if (disabled)
508
- return;
509
- // Deliberately NOT gated on `full`: a drop onto a full zone is answered
510
- // with the too-many rejection, which says why, instead of nothing at all.
511
- ingest(Array.from(event.dataTransfer.files ?? []));
512
- };
513
- // Mouse convenience only. A click that ORIGINATED on the input already opens
514
- // the picker natively and bubbles up here, so ignore it or it opens twice.
515
- const openPicker = (event) => {
516
- if (pickerDisabled || event.target === internalRef.current)
517
- return;
518
- internalRef.current?.click();
519
- };
520
- const removeAt = (index) => {
521
- // By index, not by name: two files can share a filename and identity is
522
- // what the row actually stands for.
523
- const next = valueRef.current.filter((_, position) => position !== index);
524
- valueRef.current = next;
525
- onValueChange(next);
526
- };
527
- return (_jsxs("div", { className: cn('space-y-2', className), children: [_jsxs("div", { onClick: openPicker, onDragOver: onDragOver, onDragLeave: onDragLeave, onDrop: onDrop, className: cn('border-input-border bg-card focus-within:ring-ring focus-within:ring-offset-background aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-within:ring-destructive flex w-full items-center gap-3 rounded-md border px-3 py-4 text-sm shadow-xs transition-colors focus-within:ring-2 focus-within:ring-offset-1 focus-within:outline-none', dragActive &&
528
- 'border-ring ring-ring ring-offset-background ring-2 ring-offset-1', pickerDisabled
529
- ? 'border-muted bg-muted/30 cursor-not-allowed shadow-none'
530
- : 'cursor-pointer'), "aria-invalid": invalid || undefined, children: [_jsx("input", { ref: internalRef, id: inputId, type: "file", multiple: true, accept: accept, disabled: pickerDisabled, className: "sr-only", "aria-invalid": invalid || undefined, "aria-required": ariaRequired || undefined, "aria-describedby": describedBy, "aria-label": ariaLabel, onChange: onInputChange, ...rest }), _jsx(Upload, { className: "text-muted-foreground size-4 shrink-0", "aria-hidden": "true" }), full && maxFiles !== undefined ? (_jsx("span", { className: "text-muted-foreground", children: labels?.full
531
- ? labels.full(maxFiles)
532
- : defaultFullMessage(maxFiles) })) : (_jsxs("span", { className: "text-muted-foreground", children: [_jsx("span", { className: "text-foreground font-medium", children: labels?.action ?? 'Choose files' }), ' ', labels?.hint ?? 'or drag and drop'] }))] }), value.length > 0 ? (_jsx("ul", { className: "space-y-1", children: value.map((entry, index) => (_jsxs("li", { className: "flex items-center gap-3 text-sm", children: [_jsxs("span", { className: "min-w-0 flex-1 truncate", children: [_jsx("span", { className: "text-foreground font-medium", children: entry.file.name }), ' ', _jsx("span", { className: "text-muted-foreground tabular-nums", children: humanizeSize(entry.file.size) })] }), _jsx(IconButton, { type: "button", variant: "plain", size: "small", disabled: disabled, "aria-label": labels?.remove
533
- ? labels.remove(entry.file)
534
- : `Remove ${entry.file.name}`, onClick: () => removeAt(index), children: _jsx(X, { className: "size-4", "aria-hidden": "true" }) })] }, `${entry.file.name}-${index}`))) })) : null, messages.length > 0 ? (_jsx("div", { id: errorId, role: "alert", className: "space-y-1", children: messages.map((message, index) => (_jsx("p", { className: "text-system-error-h1a text-xs font-medium", children: message }, index))) })) : null] }));
535
- });
536
- MultipleFileUpload.displayName = 'MultipleFileUpload';
1
+ "use client";import{jsx as i,jsxs as g,Fragment as le}from"react/jsx-runtime";import*as o from"react";import{Upload as ne,X as te}from"lucide-react";import{IconButton as ie}from"../../../components/ui/icon-button/index.js";import{cn as K}from"../../../lib/utils.js";export function validateFile(n,u){const{accept:l,maxSizeBytes:r}=u;if(typeof r==="number"&&n.size>r){return{kind:"too-large",file:n,maxSizeBytes:r}}if(l&&l.trim()!==""&&!ce(n,l)){return{kind:"wrong-type",file:n,accept:l}}return null}function ce(n,u){const l=n.name.toLowerCase();const r=n.type.toLowerCase();return u.split(",").map(a=>a.trim().toLowerCase()).filter(a=>a!=="").some(a=>{if(a.startsWith("."))return l.endsWith(a);if(a.endsWith("/*"))return r.startsWith(a.slice(0,-1));return r===a})}export function humanizeSize(n){if(n<1024)return`${n} B`;const u=["KB","MB","GB"];let l=n/1024;let r=0;while(l>=1024&&r<u.length-1){l/=1024;r+=1}return`${l.toFixed(1)} ${u[r]}`}function ae(n){switch(n.kind){case"too-large":return`File is too large (max ${humanizeSize(n.maxSizeBytes)}).`;case"wrong-type":return`File type not allowed (expected ${n.accept}).`;case"read-failed":return"Could not read the file."}}export const FileUpload=o.forwardRef(function n({accept:u,maxSizeBytes:l,value:r,readAs:a="text",labels:y,onSelect:d,onError:B,disabled:b=false,id:C,className:X,"aria-invalid":Y,"aria-required":J,"aria-describedby":Q,"aria-label":V,...Z},ee){const D=o.useRef(null);o.useImperativeHandle(ee,()=>D.current);const $=o.useId();const j=C??$;const E=`${j}-file-upload-error`;const[L,A]=o.useState(false);const[k,I]=o.useState(null);const U=Y||k!==null;const f=k?y?.error?y.error(k):ae(k):null;const h=f?f:null;const O=[Q,h?E:void 0].filter(Boolean).join(" ")||void 0;const c=o.useRef(null);const _=t=>{if(!t)return;c.current?.abort();const R=validateFile(t,{accept:u,maxSizeBytes:l});if(R){I(R);B?.(R);return}if(a==="none"){c.current=null;I(null);d({file:t,text:""});return}const v=new FileReader;c.current=v;v.onload=()=>{if(c.current!==v)return;c.current=null;I(null);d({file:t,text:String(v.result??"")})};v.onerror=()=>{if(c.current!==v)return;c.current=null;const H={kind:"read-failed",file:t};I(H);B?.(H)};v.readAsText(t)};const q=t=>{const R=t.target.files?.[0];t.target.value="";_(R)};const M=t=>{t.preventDefault();if(b){t.dataTransfer.dropEffect="none";return}A(true)};const P=t=>{t.preventDefault();A(false)};const T=t=>{t.preventDefault();A(false);if(b)return;_(t.dataTransfer.files?.[0])};const W=t=>{if(b||t.target===D.current)return;D.current?.click()};const re=()=>{c.current?.abort();c.current=null;I(null);d(null);if(D.current)D.current.value=""};return g("div",{className:K("space-y-2",X),children:[g("div",{onClick:W,onDragOver:M,onDragLeave:P,onDrop:T,className:K("border-input-border bg-card focus-within:ring-ring focus-within:ring-offset-background aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-within:ring-destructive flex w-full items-center gap-3 rounded-md border px-3 py-4 text-sm shadow-xs transition-colors focus-within:ring-2 focus-within:ring-offset-1 focus-within:outline-none",L&&"border-ring ring-ring ring-offset-background ring-2 ring-offset-1",b?"border-muted bg-muted/30 cursor-not-allowed shadow-none":"cursor-pointer"),"aria-invalid":U||void 0,children:[i("input",{ref:D,id:j,type:"file",accept:u,disabled:b,className:"sr-only","aria-invalid":U||void 0,"aria-required":J||void 0,"aria-describedby":O,"aria-label":V,onChange:q,...Z}),i(ne,{className:"text-muted-foreground size-4 shrink-0","aria-hidden":"true"}),r?g(le,{children:[g("span",{className:"min-w-0 flex-1 truncate",children:[i("span",{className:"text-foreground font-medium",children:r.file.name})," ",i("span",{className:"text-muted-foreground tabular-nums",children:humanizeSize(r.file.size)})]}),i(ie,{type:"button",variant:"plain",size:"small",disabled:b,"aria-label":y?.remove?.trim()?y.remove:"Remove file",onClick:t=>{t.stopPropagation();re()},children:i(te,{className:"size-4","aria-hidden":"true"})})]}):g("span",{className:"text-muted-foreground",children:[i("span",{className:"text-foreground font-medium",children:y?.action??"Choose a file"})," ",y?.hint??"or drag and drop"]})]}),h?i("p",{id:E,role:"alert",className:"text-system-error-h1a text-xs font-medium",children:h}):null]})});FileUpload.displayName="FileUpload";function ue(n){if(n.kind==="too-many"){return`Too many files (max ${n.maxFiles}). ${n.file.name} was not added.`}return ae(n)}function de(n){return n===1?"Maximum of 1 file reached.":`Maximum of ${n} files reached.`}export const MultipleFileUpload=o.forwardRef(function n({accept:u,maxSizeBytes:l,maxFiles:r,value:a=[],readAs:y="text",labels:d,onValueChange:B,onError:b,disabled:C=false,id:X,className:Y,"aria-invalid":J,"aria-required":Q,"aria-describedby":V,"aria-label":Z,...ee},D){const $=o.useRef(null);o.useImperativeHandle(D,()=>$.current);const j=o.useId();const E=X??j;const L=`${E}-file-upload-error`;const[A,k]=o.useState(false);const[I,U]=o.useState([]);const f=o.useRef(a);o.useEffect(()=>{f.current=a});const h=o.useRef(new Set);o.useEffect(()=>()=>{for(const e of h.current)e.abort();h.current.clear()},[]);const O=J||I.length>0;const c=I.map(e=>d?.error?d.error(e):ue(e)).filter(e=>Boolean(e));const _=[V,c.length>0?L:void 0].filter(Boolean).join(" ")||void 0;const q=r!==void 0&&a.length>=r;const M=C||q;const P=e=>r===void 0?Number.POSITIVE_INFINITY:Math.max(r-e,0);const T=(e,s)=>{const x=P(f.current.length);const w=e.slice(0,x);const N=e[x];const F=N!==void 0&&r!==void 0?[...s,{kind:"too-many",file:N.file,maxFiles:r}]:s;U(F);for(const G of F)b?.(G);if(w.length===0)return;const S=[...f.current,...w];f.current=S;B(S)};const W=e=>{if(e.length===0)return;const s=P(f.current.length);const x=[];const w=[];for(const m of e){const z=validateFile(m,{accept:u,maxSizeBytes:l});if(z){x.push(z);continue}w.push(m)}if(w.length>s&&r!==void 0){x.push({kind:"too-many",file:w[s],maxFiles:r})}const N=w.slice(0,s);if(y==="none"){T(N.map(m=>({file:m,text:""})),x);return}if(N.length===0){T([],x);return}const F=new Array(N.length).fill(null);let S=N.length;const G=()=>{S-=1;if(S>0)return;const m=[];const z=[];F.forEach((p,se)=>{if(p)m.push(p);else z.push({kind:"read-failed",file:N[se]})});T(m,[...x,...z])};N.forEach((m,z)=>{const p=new FileReader;h.current.add(p);p.onload=()=>{h.current.delete(p);F[z]={file:m,text:String(p.result??"")};G()};p.onerror=()=>{h.current.delete(p);G()};p.readAsText(m)})};const re=e=>{const s=Array.from(e.target.files??[]);e.target.value="";W(s)};const t=e=>{e.preventDefault();if(C){e.dataTransfer.dropEffect="none";return}k(true)};const R=e=>{e.preventDefault();k(false)};const v=e=>{e.preventDefault();k(false);if(C)return;W(Array.from(e.dataTransfer.files??[]))};const H=e=>{if(M||e.target===$.current)return;$.current?.click()};const oe=e=>{const s=f.current.filter((x,w)=>w!==e);f.current=s;B(s)};return g("div",{className:K("space-y-2",Y),children:[g("div",{onClick:H,onDragOver:t,onDragLeave:R,onDrop:v,className:K("border-input-border bg-card focus-within:ring-ring focus-within:ring-offset-background aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-within:ring-destructive flex w-full items-center gap-3 rounded-md border px-3 py-4 text-sm shadow-xs transition-colors focus-within:ring-2 focus-within:ring-offset-1 focus-within:outline-none",A&&"border-ring ring-ring ring-offset-background ring-2 ring-offset-1",M?"border-muted bg-muted/30 cursor-not-allowed shadow-none":"cursor-pointer"),"aria-invalid":O||void 0,children:[i("input",{ref:$,id:E,type:"file",multiple:true,accept:u,disabled:M,className:"sr-only","aria-invalid":O||void 0,"aria-required":Q||void 0,"aria-describedby":_,"aria-label":Z,onChange:re,...ee}),i(ne,{className:"text-muted-foreground size-4 shrink-0","aria-hidden":"true"}),q&&r!==void 0?i("span",{className:"text-muted-foreground",children:d?.full?d.full(r):de(r)}):g("span",{className:"text-muted-foreground",children:[i("span",{className:"text-foreground font-medium",children:d?.action??"Choose files"})," ",d?.hint??"or drag and drop"]})]}),a.length>0?i("ul",{className:"space-y-1",children:a.map((e,s)=>g("li",{className:"flex items-center gap-3 text-sm",children:[g("span",{className:"min-w-0 flex-1 truncate",children:[i("span",{className:"text-foreground font-medium",children:e.file.name})," ",i("span",{className:"text-muted-foreground tabular-nums",children:humanizeSize(e.file.size)})]}),i(ie,{type:"button",variant:"plain",size:"small",disabled:C,"aria-label":d?.remove?d.remove(e.file):`Remove ${e.file.name}`,onClick:()=>oe(s),children:i(te,{className:"size-4","aria-hidden":"true"})})]},`${e.file.name}-${s}`))}):null,c.length>0?i("div",{id:L,role:"alert",className:"space-y-1",children:c.map((e,s)=>i("p",{className:"text-system-error-h1a text-xs font-medium",children:e},s))}):null]})});MultipleFileUpload.displayName="MultipleFileUpload";