@hoardodile/ui 0.0.0

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 (356) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +70 -0
  3. package/dist/components/alert.d.ts +13 -0
  4. package/dist/components/alert.js +97 -0
  5. package/dist/components/alert.js.map +1 -0
  6. package/dist/components/app-dialog.d.ts +62 -0
  7. package/dist/components/app-dialog.js +656 -0
  8. package/dist/components/app-dialog.js.map +1 -0
  9. package/dist/components/badge.d.ts +11 -0
  10. package/dist/components/badge.js +70 -0
  11. package/dist/components/badge.js.map +1 -0
  12. package/dist/components/breadcrumb.d.ts +12 -0
  13. package/dist/components/breadcrumb.js +617 -0
  14. package/dist/components/breadcrumb.js.map +1 -0
  15. package/dist/components/button.d.ts +17 -0
  16. package/dist/components/button.js +78 -0
  17. package/dist/components/button.js.map +1 -0
  18. package/dist/components/caption-bar.d.ts +44 -0
  19. package/dist/components/caption-bar.js +827 -0
  20. package/dist/components/caption-bar.js.map +1 -0
  21. package/dist/components/chart-card.d.ts +15 -0
  22. package/dist/components/chart-card.js +27 -0
  23. package/dist/components/chart-card.js.map +1 -0
  24. package/dist/components/checkbox.d.ts +6 -0
  25. package/dist/components/checkbox.js +76 -0
  26. package/dist/components/checkbox.js.map +1 -0
  27. package/dist/components/chip-button.d.ts +28 -0
  28. package/dist/components/chip-button.js +87 -0
  29. package/dist/components/chip-button.js.map +1 -0
  30. package/dist/components/close-confirm-dialog.d.ts +19 -0
  31. package/dist/components/close-confirm-dialog.js +743 -0
  32. package/dist/components/close-confirm-dialog.js.map +1 -0
  33. package/dist/components/color-picker.d.ts +32 -0
  34. package/dist/components/color-picker.js +667 -0
  35. package/dist/components/color-picker.js.map +1 -0
  36. package/dist/components/combobox.d.ts +32 -0
  37. package/dist/components/combobox.js +1211 -0
  38. package/dist/components/combobox.js.map +1 -0
  39. package/dist/components/confirm-by-typing-dialog.d.ts +48 -0
  40. package/dist/components/confirm-by-typing-dialog.js +759 -0
  41. package/dist/components/confirm-by-typing-dialog.js.map +1 -0
  42. package/dist/components/confirm-dialog.d.ts +34 -0
  43. package/dist/components/confirm-dialog.js +710 -0
  44. package/dist/components/confirm-dialog.js.map +1 -0
  45. package/dist/components/count-badge.d.ts +13 -0
  46. package/dist/components/count-badge.js +39 -0
  47. package/dist/components/count-badge.js.map +1 -0
  48. package/dist/components/dialog.d.ts +40 -0
  49. package/dist/components/dialog.js +571 -0
  50. package/dist/components/dialog.js.map +1 -0
  51. package/dist/components/dropdown-menu.d.ts +45 -0
  52. package/dist/components/dropdown-menu.js +1003 -0
  53. package/dist/components/dropdown-menu.js.map +1 -0
  54. package/dist/components/dropdown-select.d.ts +29 -0
  55. package/dist/components/dropdown-select.js +1036 -0
  56. package/dist/components/dropdown-select.js.map +1 -0
  57. package/dist/components/empty.d.ts +15 -0
  58. package/dist/components/empty.js +119 -0
  59. package/dist/components/empty.js.map +1 -0
  60. package/dist/components/filter-rail.d.ts +48 -0
  61. package/dist/components/filter-rail.js +231 -0
  62. package/dist/components/filter-rail.js.map +1 -0
  63. package/dist/components/fixed-action-bar.d.ts +24 -0
  64. package/dist/components/fixed-action-bar.js +48 -0
  65. package/dist/components/fixed-action-bar.js.map +1 -0
  66. package/dist/components/font-picker.d.ts +45 -0
  67. package/dist/components/font-picker.js +759 -0
  68. package/dist/components/font-picker.js.map +1 -0
  69. package/dist/components/form.d.ts +17 -0
  70. package/dist/components/form.js +150 -0
  71. package/dist/components/form.js.map +1 -0
  72. package/dist/components/group-label.d.ts +12 -0
  73. package/dist/components/group-label.js +10 -0
  74. package/dist/components/group-label.js.map +1 -0
  75. package/dist/components/icon-tile.d.ts +16 -0
  76. package/dist/components/icon-tile.js +67 -0
  77. package/dist/components/icon-tile.js.map +1 -0
  78. package/dist/components/icon-toggle.d.ts +24 -0
  79. package/dist/components/icon-toggle.js +81 -0
  80. package/dist/components/icon-toggle.js.map +1 -0
  81. package/dist/components/icon.d.ts +2 -0
  82. package/dist/components/icon.js +49 -0
  83. package/dist/components/icon.js.map +1 -0
  84. package/dist/components/image-crop-panel.d.ts +80 -0
  85. package/dist/components/image-crop-panel.js +1104 -0
  86. package/dist/components/image-crop-panel.js.map +1 -0
  87. package/dist/components/image-cropper.d.ts +63 -0
  88. package/dist/components/image-cropper.js +166 -0
  89. package/dist/components/image-cropper.js.map +1 -0
  90. package/dist/components/input-group.d.ts +22 -0
  91. package/dist/components/input-group.js +270 -0
  92. package/dist/components/input-group.js.map +1 -0
  93. package/dist/components/input.d.ts +16 -0
  94. package/dist/components/input.js +60 -0
  95. package/dist/components/input.js.map +1 -0
  96. package/dist/components/label.d.ts +5 -0
  97. package/dist/components/label.js +40 -0
  98. package/dist/components/label.js.map +1 -0
  99. package/dist/components/list-empty-row.d.ts +16 -0
  100. package/dist/components/list-empty-row.js +40 -0
  101. package/dist/components/list-empty-row.js.map +1 -0
  102. package/dist/components/management-empty.d.ts +14 -0
  103. package/dist/components/management-empty.js +40 -0
  104. package/dist/components/management-empty.js.map +1 -0
  105. package/dist/components/management-skeleton.d.ts +11 -0
  106. package/dist/components/management-skeleton.js +72 -0
  107. package/dist/components/management-skeleton.js.map +1 -0
  108. package/dist/components/meta-chip.d.ts +17 -0
  109. package/dist/components/meta-chip.js +49 -0
  110. package/dist/components/meta-chip.js.map +1 -0
  111. package/dist/components/mobile-drawer.d.ts +29 -0
  112. package/dist/components/mobile-drawer.js +331 -0
  113. package/dist/components/mobile-drawer.js.map +1 -0
  114. package/dist/components/page-header.d.ts +20 -0
  115. package/dist/components/page-header.js +44 -0
  116. package/dist/components/page-header.js.map +1 -0
  117. package/dist/components/page-scaffold.d.ts +34 -0
  118. package/dist/components/page-scaffold.js +157 -0
  119. package/dist/components/page-scaffold.js.map +1 -0
  120. package/dist/components/pagination-bar.d.ts +22 -0
  121. package/dist/components/pagination-bar.js +703 -0
  122. package/dist/components/pagination-bar.js.map +1 -0
  123. package/dist/components/pagination.d.ts +22 -0
  124. package/dist/components/pagination.js +680 -0
  125. package/dist/components/pagination.js.map +1 -0
  126. package/dist/components/panel-toolbar.d.ts +37 -0
  127. package/dist/components/panel-toolbar.js +781 -0
  128. package/dist/components/panel-toolbar.js.map +1 -0
  129. package/dist/components/pill-tabs.d.ts +33 -0
  130. package/dist/components/pill-tabs.js +66 -0
  131. package/dist/components/pill-tabs.js.map +1 -0
  132. package/dist/components/plugin-download-consent.d.ts +52 -0
  133. package/dist/components/plugin-download-consent.js +780 -0
  134. package/dist/components/plugin-download-consent.js.map +1 -0
  135. package/dist/components/popover.d.ts +26 -0
  136. package/dist/components/popover.js +343 -0
  137. package/dist/components/popover.js.map +1 -0
  138. package/dist/components/progress.d.ts +10 -0
  139. package/dist/components/progress.js +103 -0
  140. package/dist/components/progress.js.map +1 -0
  141. package/dist/components/query-state-view.d.ts +36 -0
  142. package/dist/components/query-state-view.js +17 -0
  143. package/dist/components/query-state-view.js.map +1 -0
  144. package/dist/components/radio-group.d.ts +8 -0
  145. package/dist/components/radio-group.js +60 -0
  146. package/dist/components/radio-group.js.map +1 -0
  147. package/dist/components/scroll-area.d.ts +7 -0
  148. package/dist/components/scroll-area.js +80 -0
  149. package/dist/components/scroll-area.js.map +1 -0
  150. package/dist/components/search-field.d.ts +49 -0
  151. package/dist/components/search-field.js +586 -0
  152. package/dist/components/search-field.js.map +1 -0
  153. package/dist/components/secondary-stat.d.ts +13 -0
  154. package/dist/components/secondary-stat.js +56 -0
  155. package/dist/components/secondary-stat.js.map +1 -0
  156. package/dist/components/section-header.d.ts +23 -0
  157. package/dist/components/section-header.js +76 -0
  158. package/dist/components/section-header.js.map +1 -0
  159. package/dist/components/section-label.d.ts +20 -0
  160. package/dist/components/section-label.js +46 -0
  161. package/dist/components/section-label.js.map +1 -0
  162. package/dist/components/section-tabs.d.ts +47 -0
  163. package/dist/components/section-tabs.js +145 -0
  164. package/dist/components/section-tabs.js.map +1 -0
  165. package/dist/components/separator.d.ts +14 -0
  166. package/dist/components/separator.js +49 -0
  167. package/dist/components/separator.js.map +1 -0
  168. package/dist/components/sheet.d.ts +22 -0
  169. package/dist/components/sheet.js +469 -0
  170. package/dist/components/sheet.js.map +1 -0
  171. package/dist/components/sidebar.d.ts +69 -0
  172. package/dist/components/sidebar.js +1645 -0
  173. package/dist/components/sidebar.js.map +1 -0
  174. package/dist/components/skeleton.d.ts +8 -0
  175. package/dist/components/skeleton.js +41 -0
  176. package/dist/components/skeleton.js.map +1 -0
  177. package/dist/components/slider.d.ts +6 -0
  178. package/dist/components/slider.js +75 -0
  179. package/dist/components/slider.js.map +1 -0
  180. package/dist/components/sortable-chip-list.d.ts +26 -0
  181. package/dist/components/sortable-chip-list.js +59 -0
  182. package/dist/components/sortable-chip-list.js.map +1 -0
  183. package/dist/components/spinner.d.ts +5 -0
  184. package/dist/components/spinner.js +65 -0
  185. package/dist/components/spinner.js.map +1 -0
  186. package/dist/components/surface.d.ts +13 -0
  187. package/dist/components/surface.js +51 -0
  188. package/dist/components/surface.js.map +1 -0
  189. package/dist/components/switch.d.ts +8 -0
  190. package/dist/components/switch.js +53 -0
  191. package/dist/components/switch.js.map +1 -0
  192. package/dist/components/table.d.ts +12 -0
  193. package/dist/components/table.js +129 -0
  194. package/dist/components/table.js.map +1 -0
  195. package/dist/components/textarea.d.ts +15 -0
  196. package/dist/components/textarea.js +58 -0
  197. package/dist/components/textarea.js.map +1 -0
  198. package/dist/components/toast.d.ts +19 -0
  199. package/dist/components/toast.js +805 -0
  200. package/dist/components/toast.js.map +1 -0
  201. package/dist/components/toggle.d.ts +12 -0
  202. package/dist/components/toggle.js +63 -0
  203. package/dist/components/toggle.js.map +1 -0
  204. package/dist/components/tooltip.d.ts +9 -0
  205. package/dist/components/tooltip.js +82 -0
  206. package/dist/components/tooltip.js.map +1 -0
  207. package/dist/hooks/use-dialog-footer-actions.d.ts +18 -0
  208. package/dist/hooks/use-dialog-footer-actions.js +11 -0
  209. package/dist/hooks/use-dialog-footer-actions.js.map +1 -0
  210. package/dist/hooks/use-form-field.d.ts +33 -0
  211. package/dist/hooks/use-form-field.js +36 -0
  212. package/dist/hooks/use-form-field.js.map +1 -0
  213. package/dist/hooks/use-mobile.d.ts +34 -0
  214. package/dist/hooks/use-mobile.js +29 -0
  215. package/dist/hooks/use-mobile.js.map +1 -0
  216. package/dist/hooks/use-pinch-pan.d.ts +56 -0
  217. package/dist/hooks/use-pinch-pan.js +223 -0
  218. package/dist/hooks/use-pinch-pan.js.map +1 -0
  219. package/dist/hooks/useMobileBackToClose.d.ts +61 -0
  220. package/dist/hooks/useMobileBackToClose.js +234 -0
  221. package/dist/hooks/useMobileBackToClose.js.map +1 -0
  222. package/dist/icon-BOgL5bpL.d.ts +85 -0
  223. package/dist/icons/actions.d.ts +4 -0
  224. package/dist/icons/actions.js +520 -0
  225. package/dist/icons/actions.js.map +1 -0
  226. package/dist/icons/icon-style.d.ts +2 -0
  227. package/dist/icons/icon-style.js +51 -0
  228. package/dist/icons/icon-style.js.map +1 -0
  229. package/dist/icons/marks.d.ts +15 -0
  230. package/dist/icons/marks.js +48 -0
  231. package/dist/icons/marks.js.map +1 -0
  232. package/dist/icons/registry.d.ts +177 -0
  233. package/dist/icons/registry.js +1180 -0
  234. package/dist/icons/registry.js.map +1 -0
  235. package/dist/index.d.ts +6 -0
  236. package/dist/index.js +653 -0
  237. package/dist/index.js.map +1 -0
  238. package/dist/lib/merge-config.d.ts +15 -0
  239. package/dist/lib/merge-config.js +20 -0
  240. package/dist/lib/merge-config.js.map +1 -0
  241. package/dist/lib/pagination.d.ts +19 -0
  242. package/dist/lib/pagination.js +26 -0
  243. package/dist/lib/pagination.js.map +1 -0
  244. package/dist/lib/utils.d.ts +5 -0
  245. package/dist/lib/utils.js +26 -0
  246. package/dist/lib/utils.js.map +1 -0
  247. package/dist/styles/theme.css +480 -0
  248. package/dist/styles/theme.css.map +1 -0
  249. package/dist/styles/theme.d.ts +2 -0
  250. package/dist/viewport.d.ts +64 -0
  251. package/dist/viewport.js +13 -0
  252. package/dist/viewport.js.map +1 -0
  253. package/package.json +122 -0
  254. package/src/components/alert.tsx +76 -0
  255. package/src/components/app-dialog.tsx +187 -0
  256. package/src/components/badge.tsx +52 -0
  257. package/src/components/breadcrumb.tsx +129 -0
  258. package/src/components/button.tsx +70 -0
  259. package/src/components/caption-bar.test.tsx +151 -0
  260. package/src/components/caption-bar.tsx +392 -0
  261. package/src/components/chart-card.tsx +31 -0
  262. package/src/components/checkbox.tsx +28 -0
  263. package/src/components/chip-button.tsx +69 -0
  264. package/src/components/close-confirm-dialog.tsx +99 -0
  265. package/src/components/color-picker.tsx +186 -0
  266. package/src/components/combobox.tsx +335 -0
  267. package/src/components/confirm-by-typing-dialog.tsx +130 -0
  268. package/src/components/confirm-dialog.test.tsx +39 -0
  269. package/src/components/confirm-dialog.tsx +98 -0
  270. package/src/components/count-badge.tsx +23 -0
  271. package/src/components/dialog.tsx +315 -0
  272. package/src/components/dropdown-menu.tsx +311 -0
  273. package/src/components/dropdown-select.tsx +211 -0
  274. package/src/components/empty.tsx +104 -0
  275. package/src/components/filter-rail.tsx +139 -0
  276. package/src/components/fixed-action-bar.tsx +46 -0
  277. package/src/components/font-picker.tsx +257 -0
  278. package/src/components/form.tsx +127 -0
  279. package/src/components/group-label.tsx +13 -0
  280. package/src/components/icon-tile.tsx +35 -0
  281. package/src/components/icon-toggle.tsx +57 -0
  282. package/src/components/icon.tsx +66 -0
  283. package/src/components/image-crop-panel.test.tsx +167 -0
  284. package/src/components/image-crop-panel.tsx +465 -0
  285. package/src/components/image-cropper.test.tsx +144 -0
  286. package/src/components/image-cropper.tsx +265 -0
  287. package/src/components/input-group.tsx +156 -0
  288. package/src/components/input.tsx +45 -0
  289. package/src/components/label.tsx +20 -0
  290. package/src/components/list-empty-row.tsx +27 -0
  291. package/src/components/management-empty.tsx +25 -0
  292. package/src/components/management-skeleton.tsx +23 -0
  293. package/src/components/meta-chip.tsx +40 -0
  294. package/src/components/mobile-drawer.test.tsx +52 -0
  295. package/src/components/mobile-drawer.tsx +98 -0
  296. package/src/components/page-header.tsx +47 -0
  297. package/src/components/page-scaffold.test.tsx +24 -0
  298. package/src/components/page-scaffold.tsx +131 -0
  299. package/src/components/pagination-bar.test.tsx +87 -0
  300. package/src/components/pagination-bar.tsx +139 -0
  301. package/src/components/pagination.tsx +139 -0
  302. package/src/components/panel-toolbar.tsx +99 -0
  303. package/src/components/pill-tabs.test.tsx +84 -0
  304. package/src/components/pill-tabs.tsx +77 -0
  305. package/src/components/plugin-download-consent.test.tsx +90 -0
  306. package/src/components/plugin-download-consent.tsx +183 -0
  307. package/src/components/popover.tsx +133 -0
  308. package/src/components/progress.tsx +88 -0
  309. package/src/components/query-state-view.tsx +47 -0
  310. package/src/components/radio-group.tsx +36 -0
  311. package/src/components/scroll-area.tsx +52 -0
  312. package/src/components/search-field.test.tsx +86 -0
  313. package/src/components/search-field.tsx +119 -0
  314. package/src/components/secondary-stat.tsx +24 -0
  315. package/src/components/section-header.tsx +54 -0
  316. package/src/components/section-label.tsx +40 -0
  317. package/src/components/section-tabs.test.tsx +154 -0
  318. package/src/components/section-tabs.tsx +190 -0
  319. package/src/components/separator.tsx +38 -0
  320. package/src/components/sheet.tsx +170 -0
  321. package/src/components/sidebar.tsx +725 -0
  322. package/src/components/skeleton.tsx +18 -0
  323. package/src/components/slider.tsx +52 -0
  324. package/src/components/sortable-chip-list.tsx +56 -0
  325. package/src/components/spinner.tsx +35 -0
  326. package/src/components/surface.tsx +37 -0
  327. package/src/components/switch.tsx +30 -0
  328. package/src/components/table.tsx +116 -0
  329. package/src/components/textarea.tsx +41 -0
  330. package/src/components/toast.tsx +239 -0
  331. package/src/components/toggle.tsx +45 -0
  332. package/src/components/tooltip.tsx +64 -0
  333. package/src/hooks/use-dialog-footer-actions.ts +23 -0
  334. package/src/hooks/use-form-field.ts +62 -0
  335. package/src/hooks/use-mobile.ts +54 -0
  336. package/src/hooks/use-pinch-pan.test.tsx +42 -0
  337. package/src/hooks/use-pinch-pan.ts +340 -0
  338. package/src/hooks/useMobileBackToClose.test.tsx +140 -0
  339. package/src/hooks/useMobileBackToClose.ts +508 -0
  340. package/src/icons/actions.ts +10 -0
  341. package/src/icons/icon-style.test.tsx +69 -0
  342. package/src/icons/icon-style.ts +104 -0
  343. package/src/icons/marks.tsx +72 -0
  344. package/src/icons/registry.test.ts +65 -0
  345. package/src/icons/registry.ts +1163 -0
  346. package/src/index.ts +22 -0
  347. package/src/lib/merge-config.test.ts +109 -0
  348. package/src/lib/merge-config.ts +22 -0
  349. package/src/lib/pagination.test.ts +55 -0
  350. package/src/lib/pagination.ts +42 -0
  351. package/src/lib/utils.ts +6 -0
  352. package/src/styles/scan-contract.test.ts +74 -0
  353. package/src/styles/theme.css +798 -0
  354. package/src/test/i18n.tsx +22 -0
  355. package/src/test/setup.ts +104 -0
  356. package/src/viewport.ts +69 -0
@@ -0,0 +1,311 @@
1
+ import * as React from "react"
2
+ import { Menu as MenuPrimitive } from "@base-ui/react/menu"
3
+
4
+ import { cn } from "@hoardodile/ui/lib/utils"
5
+ import { useMobileBackToClose } from "@hoardodile/ui/hooks/useMobileBackToClose"
6
+ import { AltArrowRight } from "@hoardodile/ui/icons/registry"
7
+ import { Check } from "@hoardodile/ui/icons/marks"
8
+
9
+ function DropdownMenu({
10
+ open,
11
+ defaultOpen,
12
+ onOpenChange,
13
+ ...props
14
+ }: Omit<MenuPrimitive.Root.Props, "onOpenChange"> & {
15
+ /**
16
+ * `eventDetails` is absent for closes triggered by the mobile back
17
+ * gesture (a synthetic close with no underlying Base UI event).
18
+ */
19
+ onOpenChange?: (
20
+ open: boolean,
21
+ eventDetails?: MenuPrimitive.Root.ChangeEventDetails,
22
+ ) => void
23
+ }) {
24
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
25
+ defaultOpen ?? false,
26
+ )
27
+ const isControlled = open !== undefined
28
+ const currentOpen = isControlled ? open : uncontrolledOpen
29
+ function handleOpenChange(
30
+ next: boolean,
31
+ eventDetails?: MenuPrimitive.Root.ChangeEventDetails,
32
+ ) {
33
+ if (!isControlled) setUncontrolledOpen(next)
34
+ onOpenChange?.(next, eventDetails)
35
+ }
36
+ useMobileBackToClose(currentOpen, handleOpenChange)
37
+ return (
38
+ <MenuPrimitive.Root
39
+ data-slot="dropdown-menu"
40
+ open={currentOpen}
41
+ onOpenChange={handleOpenChange}
42
+ {...props}
43
+ />
44
+ )
45
+ }
46
+
47
+ function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
48
+ return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
49
+ }
50
+
51
+ function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
52
+ return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
53
+ }
54
+
55
+ function DropdownMenuContent({
56
+ align = "start",
57
+ alignOffset = 0,
58
+ side = "bottom",
59
+ sideOffset = 4,
60
+ className,
61
+ container,
62
+ ...props
63
+ }: MenuPrimitive.Popup.Props &
64
+ Pick<
65
+ MenuPrimitive.Positioner.Props,
66
+ "align" | "alignOffset" | "side" | "sideOffset"
67
+ > & {
68
+ /**
69
+ * Override the portal target. Mirrors the escape hatch on
70
+ * `SelectContent` / `PopoverContent`: the video player provides
71
+ * its own fullscreen container so the menu stays interactive in
72
+ * fullscreen mode.
73
+ */
74
+ container?: MenuPrimitive.Portal.Props["container"]
75
+ }) {
76
+ return (
77
+ <MenuPrimitive.Portal container={container}>
78
+ <MenuPrimitive.Positioner
79
+ className="isolate z-50 outline-none"
80
+ align={align}
81
+ alignOffset={alignOffset}
82
+ side={side}
83
+ sideOffset={sideOffset}
84
+ >
85
+ <MenuPrimitive.Popup
86
+ data-slot="dropdown-menu-content"
87
+ className={cn("z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95", className )}
88
+ {...props}
89
+ />
90
+ </MenuPrimitive.Positioner>
91
+ </MenuPrimitive.Portal>
92
+ )
93
+ }
94
+
95
+ function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
96
+ return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
97
+ }
98
+
99
+ function DropdownMenuLabel({
100
+ className,
101
+ inset,
102
+ ...props
103
+ }: MenuPrimitive.GroupLabel.Props & {
104
+ inset?: boolean
105
+ }) {
106
+ return (
107
+ <MenuPrimitive.GroupLabel
108
+ data-slot="dropdown-menu-label"
109
+ data-inset={inset}
110
+ className={cn(
111
+ "px-2 py-1.5 text-xs font-medium text-muted-foreground data-inset:pl-8",
112
+ className
113
+ )}
114
+ {...props}
115
+ />
116
+ )
117
+ }
118
+
119
+ function DropdownMenuItem({
120
+ className,
121
+ inset,
122
+ variant = "default",
123
+ ...props
124
+ }: MenuPrimitive.Item.Props & {
125
+ inset?: boolean
126
+ variant?: "default" | "destructive"
127
+ }) {
128
+ return (
129
+ <MenuPrimitive.Item
130
+ data-slot="dropdown-menu-item"
131
+ data-inset={inset}
132
+ data-variant={variant}
133
+ className={cn(
134
+ "group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
135
+ className
136
+ )}
137
+ {...props}
138
+ />
139
+ )
140
+ }
141
+
142
+ function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
143
+ return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
144
+ }
145
+
146
+ function DropdownMenuSubTrigger({
147
+ className,
148
+ inset,
149
+ children,
150
+ ...props
151
+ }: MenuPrimitive.SubmenuTrigger.Props & {
152
+ inset?: boolean
153
+ }) {
154
+ return (
155
+ <MenuPrimitive.SubmenuTrigger
156
+ data-slot="dropdown-menu-sub-trigger"
157
+ data-inset={inset}
158
+ className={cn(
159
+ "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-8 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
160
+ className
161
+ )}
162
+ {...props}
163
+ >
164
+ {children}
165
+ <AltArrowRight className="ml-auto" />
166
+ </MenuPrimitive.SubmenuTrigger>
167
+ )
168
+ }
169
+
170
+ function DropdownMenuSubContent({
171
+ align = "start",
172
+ alignOffset = -3,
173
+ side = "right",
174
+ sideOffset = 0,
175
+ className,
176
+ ...props
177
+ }: React.ComponentProps<typeof DropdownMenuContent>) {
178
+ return (
179
+ <DropdownMenuContent
180
+ data-slot="dropdown-menu-sub-content"
181
+ className={cn("w-auto min-w-[96px] rounded-md bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
182
+ align={align}
183
+ alignOffset={alignOffset}
184
+ side={side}
185
+ sideOffset={sideOffset}
186
+ {...props}
187
+ />
188
+ )
189
+ }
190
+
191
+ function DropdownMenuCheckboxItem({
192
+ className,
193
+ children,
194
+ checked,
195
+ inset,
196
+ ...props
197
+ }: MenuPrimitive.CheckboxItem.Props & {
198
+ inset?: boolean
199
+ }) {
200
+ return (
201
+ <MenuPrimitive.CheckboxItem
202
+ data-slot="dropdown-menu-checkbox-item"
203
+ data-inset={inset}
204
+ className={cn(
205
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
206
+ className
207
+ )}
208
+ checked={checked}
209
+ {...props}
210
+ >
211
+ <span
212
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
213
+ data-slot="dropdown-menu-checkbox-item-indicator"
214
+ >
215
+ <MenuPrimitive.CheckboxItemIndicator>
216
+ <Check
217
+ />
218
+ </MenuPrimitive.CheckboxItemIndicator>
219
+ </span>
220
+ {children}
221
+ </MenuPrimitive.CheckboxItem>
222
+ )
223
+ }
224
+
225
+ function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
226
+ return (
227
+ <MenuPrimitive.RadioGroup
228
+ data-slot="dropdown-menu-radio-group"
229
+ {...props}
230
+ />
231
+ )
232
+ }
233
+
234
+ function DropdownMenuRadioItem({
235
+ className,
236
+ children,
237
+ inset,
238
+ ...props
239
+ }: MenuPrimitive.RadioItem.Props & {
240
+ inset?: boolean
241
+ }) {
242
+ return (
243
+ <MenuPrimitive.RadioItem
244
+ data-slot="dropdown-menu-radio-item"
245
+ data-inset={inset}
246
+ className={cn(
247
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-8 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
248
+ className
249
+ )}
250
+ {...props}
251
+ >
252
+ <span
253
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
254
+ data-slot="dropdown-menu-radio-item-indicator"
255
+ >
256
+ <MenuPrimitive.RadioItemIndicator>
257
+ <Check
258
+ />
259
+ </MenuPrimitive.RadioItemIndicator>
260
+ </span>
261
+ {children}
262
+ </MenuPrimitive.RadioItem>
263
+ )
264
+ }
265
+
266
+ function DropdownMenuSeparator({
267
+ className,
268
+ ...props
269
+ }: MenuPrimitive.Separator.Props) {
270
+ return (
271
+ <MenuPrimitive.Separator
272
+ data-slot="dropdown-menu-separator"
273
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
274
+ {...props}
275
+ />
276
+ )
277
+ }
278
+
279
+ function DropdownMenuShortcut({
280
+ className,
281
+ ...props
282
+ }: React.ComponentProps<"span">) {
283
+ return (
284
+ <span
285
+ data-slot="dropdown-menu-shortcut"
286
+ className={cn(
287
+ "ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
288
+ className
289
+ )}
290
+ {...props}
291
+ />
292
+ )
293
+ }
294
+
295
+ export {
296
+ DropdownMenu,
297
+ DropdownMenuPortal,
298
+ DropdownMenuTrigger,
299
+ DropdownMenuContent,
300
+ DropdownMenuGroup,
301
+ DropdownMenuLabel,
302
+ DropdownMenuItem,
303
+ DropdownMenuCheckboxItem,
304
+ DropdownMenuRadioGroup,
305
+ DropdownMenuRadioItem,
306
+ DropdownMenuSeparator,
307
+ DropdownMenuShortcut,
308
+ DropdownMenuSub,
309
+ DropdownMenuSubTrigger,
310
+ DropdownMenuSubContent,
311
+ }
@@ -0,0 +1,211 @@
1
+ import { cn } from "@hoardodile/ui/lib/utils"
2
+ import { AltArrowDown } from "@hoardodile/ui/icons/registry"
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuCheckboxItem,
6
+ DropdownMenuContent,
7
+ DropdownMenuRadioGroup,
8
+ DropdownMenuRadioItem,
9
+ DropdownMenuTrigger,
10
+ } from "./dropdown-menu"
11
+ import type * as React from "react"
12
+
13
+ export type DropdownSelectOption = {
14
+ readonly value: string
15
+ readonly label: React.ReactNode
16
+ }
17
+
18
+ type SharedProps = {
19
+ readonly options: readonly DropdownSelectOption[]
20
+ readonly placeholder?: string
21
+ readonly triggerClassName?: string
22
+ readonly contentClassName?: string
23
+ readonly container?: HTMLElement | null
24
+ readonly disabled?: boolean
25
+ readonly modal?: boolean
26
+ readonly "data-testid"?: string
27
+ readonly "aria-label"?: string
28
+ }
29
+
30
+ function TriggerButton(
31
+ props: {
32
+ readonly label: React.ReactNode
33
+ readonly placeholder?: string
34
+ readonly className?: string
35
+ readonly "data-testid"?: string
36
+ readonly "aria-label"?: string
37
+ } & Omit<React.ComponentProps<"button">, "aria-label">,
38
+ ) {
39
+ const {
40
+ label,
41
+ placeholder,
42
+ className,
43
+ "data-testid": testId,
44
+ "aria-label": ariaLabel,
45
+ ...rest
46
+ } = props
47
+ return (
48
+ <button
49
+ type="button"
50
+ data-slot="dropdown-select-trigger"
51
+ data-placeholder={label === undefined ? true : undefined}
52
+ className={cn(
53
+ "inline-flex h-control w-fit shrink-0 cursor-pointer items-center gap-2 rounded-lg bg-muted px-3 text-ui whitespace-nowrap text-secondary-foreground outline-hidden focus-visible:ring-3 focus-visible:ring-ring/50 hover:bg-accent active:bg-accent disabled:cursor-not-allowed disabled:opacity-50 data-placeholder:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
54
+ className,
55
+ )}
56
+ data-testid={testId}
57
+ aria-label={ariaLabel}
58
+ {...rest}
59
+ >
60
+ {/* No line-clamp here: `display:-webkit-box` inside the w-fit
61
+ trigger would inflate the width computation and leave an empty
62
+ gap; the button's own whitespace-nowrap already prevents wrap. */}
63
+ <span className="flex min-w-0 items-center gap-1.5">
64
+ {label ?? placeholder}
65
+ </span>
66
+ <AltArrowDown className="pointer-events-none size-4 text-muted-foreground" />
67
+ </button>
68
+ )
69
+ }
70
+
71
+ export function DropdownSelect(props: {
72
+ readonly value: string
73
+ readonly onValueChange: (value: string) => void
74
+ } & SharedProps) {
75
+ const {
76
+ value,
77
+ onValueChange,
78
+ options,
79
+ placeholder,
80
+ triggerClassName,
81
+ contentClassName,
82
+ container,
83
+ disabled,
84
+ modal,
85
+ "data-testid": testId,
86
+ "aria-label": ariaLabel,
87
+ } = props
88
+
89
+ const selectedLabel = options.find((o) => o.value === value)?.label
90
+
91
+ return (
92
+ <DropdownMenu modal={modal}>
93
+ <DropdownMenuTrigger
94
+ disabled={disabled}
95
+ render={
96
+ <TriggerButton
97
+ label={selectedLabel}
98
+ placeholder={placeholder}
99
+ className={triggerClassName}
100
+ data-testid={testId}
101
+ aria-label={ariaLabel}
102
+ />
103
+ }
104
+ />
105
+ <DropdownMenuContent
106
+ container={container}
107
+ // Size to the longest option instead of the trigger's width
108
+ // (the base content is anchored to --anchor-width), capped
109
+ // by the viewport. The `!` keeps the override deterministic.
110
+ className={cn(
111
+ "w-max! min-w-36 max-w-(--available-width)",
112
+ contentClassName,
113
+ )}
114
+ >
115
+ <DropdownMenuRadioGroup
116
+ value={value}
117
+ onValueChange={onValueChange}
118
+ >
119
+ {options.map((opt) => (
120
+ <DropdownMenuRadioItem
121
+ key={opt.value}
122
+ value={opt.value}
123
+ // The longest option ("Associated time") must never
124
+ // wrap to a second line.
125
+ className="whitespace-nowrap"
126
+ // Base UI radio items default to `closeOnClick={false}`;
127
+ // a single-select must close on pick (Radix behavior).
128
+ closeOnClick
129
+ >
130
+ {opt.label}
131
+ </DropdownMenuRadioItem>
132
+ ))}
133
+ </DropdownMenuRadioGroup>
134
+ </DropdownMenuContent>
135
+ </DropdownMenu>
136
+ )
137
+ }
138
+
139
+ export function DropdownMultiSelect(props: {
140
+ readonly value: readonly string[]
141
+ readonly onValueChange: (values: readonly string[]) => void
142
+ /** Trigger label once more than one option is selected (e.g. "3 selected"). */
143
+ readonly countLabel: (count: number) => React.ReactNode
144
+ } & SharedProps) {
145
+ const {
146
+ value,
147
+ onValueChange,
148
+ options,
149
+ placeholder,
150
+ countLabel,
151
+ triggerClassName,
152
+ contentClassName,
153
+ container,
154
+ disabled,
155
+ "data-testid": testId,
156
+ "aria-label": ariaLabel,
157
+ } = props
158
+
159
+ const selectedCount = value.length
160
+ const triggerLabel =
161
+ selectedCount === 0
162
+ ? undefined
163
+ : selectedCount === 1
164
+ ? options.find((o) => o.value === value[0])?.label
165
+ : countLabel(selectedCount)
166
+
167
+ return (
168
+ <DropdownMenu>
169
+ <DropdownMenuTrigger
170
+ disabled={disabled}
171
+ render={
172
+ <TriggerButton
173
+ label={triggerLabel}
174
+ placeholder={placeholder}
175
+ className={triggerClassName}
176
+ data-testid={testId}
177
+ aria-label={ariaLabel}
178
+ />
179
+ }
180
+ />
181
+ <DropdownMenuContent
182
+ container={container}
183
+ // Size to the longest option instead of the trigger's width,
184
+ // capped by the viewport (see DropdownSelect).
185
+ className={cn(
186
+ "w-max! min-w-36 max-w-(--available-width)",
187
+ contentClassName,
188
+ )}
189
+ >
190
+ {options.map((opt) => (
191
+ <DropdownMenuCheckboxItem
192
+ key={opt.value}
193
+ className="whitespace-nowrap"
194
+ checked={value.includes(opt.value)}
195
+ onCheckedChange={(checked) => {
196
+ if (checked === true) {
197
+ onValueChange([...value, opt.value])
198
+ } else {
199
+ onValueChange(
200
+ value.filter((v) => v !== opt.value),
201
+ )
202
+ }
203
+ }}
204
+ >
205
+ {opt.label}
206
+ </DropdownMenuCheckboxItem>
207
+ ))}
208
+ </DropdownMenuContent>
209
+ </DropdownMenu>
210
+ )
211
+ }
@@ -0,0 +1,104 @@
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+
3
+ import { cn } from "@hoardodile/ui/lib/utils"
4
+
5
+ function Empty({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="empty"
9
+ className={cn(
10
+ "flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-lg border-dashed p-12 text-center text-balance",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="empty-header"
22
+ className={cn("flex max-w-sm flex-col items-center gap-2", className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ const emptyMediaVariants = cva(
29
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
30
+ {
31
+ variants: {
32
+ variant: {
33
+ default: "bg-transparent",
34
+ icon: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-6",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: "default",
39
+ },
40
+ }
41
+ )
42
+
43
+ function EmptyMedia({
44
+ className,
45
+ variant = "default",
46
+ ...props
47
+ }: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
48
+ return (
49
+ <div
50
+ data-slot="empty-icon"
51
+ data-variant={variant}
52
+ className={cn(emptyMediaVariants({ variant, className }))}
53
+ {...props}
54
+ />
55
+ )
56
+ }
57
+
58
+ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
59
+ return (
60
+ <div
61
+ data-slot="empty-title"
62
+ className={cn(
63
+ "font-heading text-lg font-medium tracking-tight",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ )
69
+ }
70
+
71
+ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
72
+ return (
73
+ <div
74
+ data-slot="empty-description"
75
+ className={cn(
76
+ "text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
85
+ return (
86
+ <div
87
+ data-slot="empty-content"
88
+ className={cn(
89
+ "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
90
+ className
91
+ )}
92
+ {...props}
93
+ />
94
+ )
95
+ }
96
+
97
+ export {
98
+ Empty,
99
+ EmptyHeader,
100
+ EmptyTitle,
101
+ EmptyDescription,
102
+ EmptyContent,
103
+ EmptyMedia,
104
+ }