@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,139 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@hoardodile/ui/lib/utils"
4
+ import { Button } from "@hoardodile/ui/components/button"
5
+ import { AltArrowLeft } from "@hoardodile/ui/icons/registry"
6
+ import { AltArrowRight } from "@hoardodile/ui/icons/registry"
7
+ import { MenuDots } from "@hoardodile/ui/icons/registry"
8
+ import { useTranslation } from "react-i18next"
9
+
10
+ function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
11
+ const { t } = useTranslation("ui", { useSuspense: false })
12
+ return (
13
+ <nav
14
+ role="navigation"
15
+ aria-label={t("pagination.region")}
16
+ data-slot="pagination"
17
+ className={cn("mx-auto flex w-full justify-center", className)}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function PaginationContent({
24
+ className,
25
+ ...props
26
+ }: React.ComponentProps<"ul">) {
27
+ return (
28
+ <ul
29
+ data-slot="pagination-content"
30
+ className={cn("flex items-center gap-1", className)}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function PaginationItem({ ...props }: React.ComponentProps<"li">) {
37
+ return <li data-slot="pagination-item" {...props} />
38
+ }
39
+
40
+ type PaginationLinkProps = {
41
+ isActive?: boolean
42
+ } & Pick<React.ComponentProps<typeof Button>, "size"> &
43
+ React.ComponentProps<"a">
44
+
45
+ function PaginationLink({
46
+ className,
47
+ isActive,
48
+ size = "icon",
49
+ ...props
50
+ }: PaginationLinkProps) {
51
+ return (
52
+ <Button
53
+ variant={isActive ? "outline" : "ghost"}
54
+ size={size}
55
+ className={cn(className)}
56
+ nativeButton={false}
57
+ render={
58
+ <a
59
+ aria-current={isActive ? "page" : undefined}
60
+ data-slot="pagination-link"
61
+ data-active={isActive}
62
+ {...props}
63
+ />
64
+ }
65
+ />
66
+ )
67
+ }
68
+
69
+ function PaginationPrevious({
70
+ className,
71
+ text,
72
+ ...props
73
+ }: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
74
+ const { t } = useTranslation("ui", { useSuspense: false })
75
+ return (
76
+ <PaginationLink
77
+ aria-label={t("pagination.previous")}
78
+ size="default"
79
+ className={cn("pl-2!", className)}
80
+ {...props}
81
+ >
82
+ <AltArrowLeft data-icon="inline-start" />
83
+ <span className="hidden sm:block">
84
+ {text ?? t("pagination.previous")}
85
+ </span>
86
+ </PaginationLink>
87
+ )
88
+ }
89
+
90
+ function PaginationNext({
91
+ className,
92
+ text,
93
+ ...props
94
+ }: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
95
+ const { t } = useTranslation("ui", { useSuspense: false })
96
+ return (
97
+ <PaginationLink
98
+ aria-label={t("pagination.next")}
99
+ size="default"
100
+ className={cn("pr-2!", className)}
101
+ {...props}
102
+ >
103
+ <span className="hidden sm:block">{text ?? t("pagination.next")}</span>
104
+ <AltArrowRight data-icon="inline-end" />
105
+ </PaginationLink>
106
+ )
107
+ }
108
+
109
+ function PaginationEllipsis({
110
+ className,
111
+ ...props
112
+ }: React.ComponentProps<"span">) {
113
+ const { t } = useTranslation("ui", { useSuspense: false })
114
+ return (
115
+ <span
116
+ aria-hidden
117
+ data-slot="pagination-ellipsis"
118
+ className={cn(
119
+ "flex size-9 items-center justify-center [&_svg:not([class*='size-'])]:size-4",
120
+ className
121
+ )}
122
+ {...props}
123
+ >
124
+ <MenuDots
125
+ />
126
+ <span className="sr-only">{t("pagination.morePages")}</span>
127
+ </span>
128
+ )
129
+ }
130
+
131
+ export {
132
+ Pagination,
133
+ PaginationContent,
134
+ PaginationEllipsis,
135
+ PaginationItem,
136
+ PaginationLink,
137
+ PaginationNext,
138
+ PaginationPrevious,
139
+ }
@@ -0,0 +1,99 @@
1
+ import { Button } from "@hoardodile/ui/components/button"
2
+ import { Icon } from "@hoardodile/ui/components/icon"
3
+ import { Add } from "@hoardodile/ui/icons/actions"
4
+ import { HamburgerMenu } from "@hoardodile/ui/icons/registry"
5
+ import { useTranslation } from "react-i18next"
6
+ import { ChipButton } from "./chip-button"
7
+ import { SearchField } from "./search-field"
8
+
9
+ export type PanelToolbarProps = {
10
+ readonly placeholder: string
11
+ readonly query: string
12
+ readonly onQuery: (query: string) => void
13
+ readonly reorder: boolean
14
+ readonly onToggleReorder: () => void
15
+ readonly unusedCount: number
16
+ readonly unusedOnly: boolean
17
+ readonly onToggleUnused: () => void
18
+ /** Add button — opens the shared create dialog. */
19
+ readonly onAdd: () => void
20
+ /** Add button label — defaults to the shared "Add" copy. */
21
+ readonly addLabel?: string
22
+ /** Unused chip label — defaults to the shared "Unused" copy. */
23
+ readonly unusedLabel?: string
24
+ /** Reorder chip label — defaults to the shared "Reorder" copy. */
25
+ readonly reorderLabel?: string
26
+ /** Forwarded to the filter field's `maxLength` (e.g. the query limit). */
27
+ readonly maxLength?: number
28
+ readonly testIds?: {
29
+ readonly filter?: string
30
+ readonly unused?: string
31
+ readonly reorder?: string
32
+ readonly add?: string
33
+ }
34
+ }
35
+
36
+ /**
37
+ * One toolbar per custom-page panel: filter left; triage, structure and
38
+ * add right. Unused and Reorder ride ChipButton — the Unused filter wears
39
+ * the dashed outline, Reorder quiet text, both latching the accent fill
40
+ * when on. The Unused chip only appears when there is something to clean.
41
+ */
42
+ export function PanelToolbar(props: PanelToolbarProps) {
43
+ const {
44
+ placeholder,
45
+ query,
46
+ onQuery,
47
+ reorder,
48
+ onToggleReorder,
49
+ unusedCount,
50
+ unusedOnly,
51
+ onToggleUnused,
52
+ onAdd,
53
+ addLabel,
54
+ unusedLabel,
55
+ reorderLabel,
56
+ maxLength,
57
+ testIds,
58
+ } = props
59
+ const { t } = useTranslation("ui", { useSuspense: false })
60
+ const resolvedAddLabel = addLabel ?? t("panelToolbar.add")
61
+ const resolvedUnusedLabel = unusedLabel ?? t("panelToolbar.unused")
62
+ const resolvedReorderLabel = reorderLabel ?? t("panelToolbar.reorder")
63
+ return (
64
+ <div className="flex flex-wrap items-center justify-between gap-3">
65
+ <SearchField
66
+ value={query}
67
+ onCommit={onQuery}
68
+ placeholder={placeholder}
69
+ maxLength={maxLength}
70
+ className="w-60"
71
+ testId={testIds?.filter}
72
+ />
73
+ <div className="flex shrink-0 flex-wrap items-center gap-2">
74
+ {unusedCount > 0 ? (
75
+ <ChipButton
76
+ dashed
77
+ active={unusedOnly}
78
+ onClick={onToggleUnused}
79
+ testId={testIds?.unused}
80
+ >
81
+ {resolvedUnusedLabel} · {unusedCount}
82
+ </ChipButton>
83
+ ) : null}
84
+ <ChipButton
85
+ icon={HamburgerMenu}
86
+ active={reorder}
87
+ onClick={onToggleReorder}
88
+ testId={testIds?.reorder}
89
+ >
90
+ {resolvedReorderLabel}
91
+ </ChipButton>
92
+ <Button variant="secondary" onClick={onAdd} data-testid={testIds?.add}>
93
+ <Icon icon={Add} />
94
+ {resolvedAddLabel}
95
+ </Button>
96
+ </div>
97
+ </div>
98
+ )
99
+ }
@@ -0,0 +1,84 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react"
2
+ import { describe, expect, it, vi } from "vitest"
3
+ import { PillTabs, pillButtonClassName } from "./pill-tabs"
4
+
5
+ describe("pillButtonClassName", () => {
6
+ it("uses the chip-sized pill", () => {
7
+ expect(pillButtonClassName(false)).toContain("h-chip px-3")
8
+ })
9
+
10
+ it("lifts the active pill to the card fill", () => {
11
+ // tailwind-merge reorders classes — assert tokens, not a sequence.
12
+ const active = pillButtonClassName(true)
13
+ expect(active).toContain("bg-card")
14
+ expect(active).toContain("font-medium")
15
+ expect(active).toContain("text-foreground")
16
+ expect(pillButtonClassName(false)).not.toContain("bg-card")
17
+ })
18
+ })
19
+
20
+ describe("PillTabs", () => {
21
+ const items = [
22
+ { value: "a", label: "Alpha" },
23
+ { value: "b", label: "Beta" },
24
+ ]
25
+
26
+ it("renders the track with one button per item and the active state", () => {
27
+ render(<PillTabs value="b" items={items} />)
28
+ expect(screen.getByRole("button", { name: "Alpha" })).toBeInTheDocument()
29
+ const beta = screen.getByRole("button", { name: "Beta" })
30
+ expect(beta.className).toContain("bg-card")
31
+ expect(
32
+ screen.getByRole("button", { name: "Alpha" }).className,
33
+ ).not.toContain("bg-card")
34
+ })
35
+
36
+ it("reports the picked value through onChange", () => {
37
+ const onChange = vi.fn()
38
+ render(<PillTabs value="a" items={items} onChange={onChange} />)
39
+ fireEvent.click(screen.getByRole("button", { name: "Beta" }))
40
+ expect(onChange).toHaveBeenCalledWith("b")
41
+ })
42
+
43
+ it("lets a render slot replace the button (Link-style items)", () => {
44
+ render(
45
+ <PillTabs
46
+ value="a"
47
+ items={[
48
+ {
49
+ value: "a",
50
+ label: "Alpha",
51
+ render: (active, className) => (
52
+ <a href="/stats" className={className} data-active={active}>
53
+ Alpha link
54
+ </a>
55
+ ),
56
+ },
57
+ ]}
58
+ />,
59
+ )
60
+ const link = screen.getByRole("link", { name: "Alpha link" })
61
+ expect(link.getAttribute("data-active")).toBe("true")
62
+ expect(link.className).toContain("bg-card")
63
+ })
64
+
65
+ it("announces the active pill via aria-pressed when requested", () => {
66
+ render(
67
+ <PillTabs
68
+ value="a"
69
+ items={items.map((item) => ({
70
+ ...item,
71
+ ariaPressed: item.value === "a",
72
+ }))}
73
+ />,
74
+ )
75
+ expect(screen.getByRole("button", { name: "Alpha" })).toHaveAttribute(
76
+ "aria-pressed",
77
+ "true",
78
+ )
79
+ expect(screen.getByRole("button", { name: "Beta" })).toHaveAttribute(
80
+ "aria-pressed",
81
+ "false",
82
+ )
83
+ })
84
+ })
@@ -0,0 +1,77 @@
1
+ import { cn } from "@hoardodile/ui/lib/utils"
2
+ import type { ReactNode } from "react"
3
+
4
+ export type PillTabItem<T extends string> = {
5
+ readonly value: T
6
+ readonly label: ReactNode
7
+ readonly testId?: string
8
+ /** Toggle-state announcement for the settings variants; plain buttons
9
+ render without it. */
10
+ readonly ariaPressed?: boolean
11
+ /** Screen-reader name when the label alone is not descriptive. */
12
+ readonly ariaLabel?: string
13
+ /** Layout overrides for the pill (e.g. the relationship-kind grid). */
14
+ readonly className?: string
15
+ /** Replaces the button element entirely (e.g. a Link) — receives the
16
+ active state and the pill classes to apply. */
17
+ readonly render?: (active: boolean, className: string) => ReactNode
18
+ }
19
+
20
+ export type PillTabsProps<T extends string> = {
21
+ readonly value: T
22
+ readonly items: readonly PillTabItem<T>[]
23
+ readonly onChange?: (value: T) => void
24
+ readonly className?: string
25
+ /** Disables every pill (e.g. while a sibling control takes over). */
26
+ readonly disabled?: boolean
27
+ }
28
+
29
+ export function pillButtonClassName(active: boolean): string {
30
+ return cn(
31
+ "inline-flex cursor-pointer font-medium appearance-none items-center rounded-md border-0 text-xs h-chip px-3 disabled:pointer-events-none disabled:opacity-50",
32
+ active
33
+ ? "bg-card text-foreground"
34
+ : "bg-transparent text-muted-foreground hover:text-secondary-foreground",
35
+ )
36
+ }
37
+
38
+ /** Segmented pill tab group: a muted track whose active pill lifts to
39
+ the card fill. Shared by the stats Time/Views toggle, the settings
40
+ segmented controls, and the filter-bar toggles. */
41
+ export function PillTabs<T extends string>(props: PillTabsProps<T>) {
42
+ const { value, items, onChange, className, disabled } = props
43
+ return (
44
+ <span
45
+ className={cn(
46
+ "inline-flex shrink-0 items-center gap-0.5 rounded-lg bg-muted p-0.5",
47
+ className,
48
+ )}
49
+ >
50
+ {items.map((item) => {
51
+ const active = item.value === value
52
+ const buttonClassName = pillButtonClassName(active)
53
+ if (item.render !== undefined) {
54
+ return (
55
+ <span key={item.value} data-testid={item.testId}>
56
+ {item.render(active, buttonClassName)}
57
+ </span>
58
+ )
59
+ }
60
+ return (
61
+ <button
62
+ key={item.value}
63
+ type="button"
64
+ aria-pressed={item.ariaPressed}
65
+ aria-label={item.ariaLabel}
66
+ data-testid={item.testId}
67
+ disabled={disabled}
68
+ onClick={() => onChange?.(item.value)}
69
+ className={cn(buttonClassName, item.className)}
70
+ >
71
+ {item.label}
72
+ </button>
73
+ )
74
+ })}
75
+ </span>
76
+ )
77
+ }
@@ -0,0 +1,90 @@
1
+ import { cleanup, screen } from "@testing-library/react"
2
+ import userEvent from "@testing-library/user-event"
3
+ import { afterEach, describe, expect, it, vi } from "vitest"
4
+ import { renderWithI18n, testI18n } from "../test/i18n"
5
+ import {
6
+ PluginDownloadConsentDialog,
7
+ type PluginConsentTicket,
8
+ } from "./plugin-download-consent"
9
+
10
+ const TICKET: PluginConsentTicket = {
11
+ ticketId: "t-1",
12
+ pluginId: "p-1",
13
+ pluginName: "Gallery",
14
+ url: "https://example.com/runtime/live2d.min.js?x=1",
15
+ dest: "runtime/live2d.min.js",
16
+ sizeBytes: 2048,
17
+ reason: "live2d runtime",
18
+ }
19
+
20
+ afterEach(() => {
21
+ cleanup()
22
+ void testI18n.changeLanguage("en")
23
+ })
24
+
25
+ function renderDialog(
26
+ overrides: Partial<Parameters<typeof PluginDownloadConsentDialog>[0]> = {},
27
+ ) {
28
+ const onDeny = vi.fn()
29
+ const onAllow = vi.fn()
30
+ renderWithI18n(
31
+ <PluginDownloadConsentDialog
32
+ entry={TICKET}
33
+ onDeny={onDeny}
34
+ onAllow={onAllow}
35
+ formatBytes={(n) => `${n} B`}
36
+ {...overrides}
37
+ />,
38
+ )
39
+ return { onDeny, onAllow }
40
+ }
41
+
42
+ describe("PluginDownloadConsentDialog", () => {
43
+ it("shows the URL verbatim, destination and plugin reason", async () => {
44
+ renderDialog()
45
+ // Base UI mounts the dialog portal on effects; wait for the first
46
+ // assertion instead of querying the pre-mount tree.
47
+ expect(await screen.findByTestId("plugin-download-url")).toHaveTextContent(
48
+ "https://example.com/runtime/live2d.min.js?x=1",
49
+ )
50
+ expect(screen.getByTestId("plugin-download-dest")).toHaveTextContent(
51
+ "runtime/live2d.min.js",
52
+ )
53
+ expect(screen.getByText("live2d runtime")).toBeInTheDocument()
54
+ expect(screen.getByText(/2048 B/)).toBeInTheDocument()
55
+ expect(screen.getByTestId("plugin-download-deny")).toHaveTextContent("Deny")
56
+ expect(screen.getByTestId("plugin-download-allow")).toHaveTextContent("Allow")
57
+ })
58
+
59
+ it("Deny reports the ticket id", async () => {
60
+ const { onDeny } = renderDialog()
61
+ await userEvent.click(screen.getByTestId("plugin-download-deny"))
62
+ expect(onDeny).toHaveBeenCalledWith("t-1")
63
+ })
64
+
65
+ it("Allow reports the ticket id with the remember flag", async () => {
66
+ const { onAllow } = renderDialog()
67
+ await userEvent.click(screen.getByTestId("plugin-download-allow"))
68
+ expect(onAllow).toHaveBeenCalledWith("t-1", false)
69
+ await userEvent.click(screen.getByRole("checkbox"))
70
+ await userEvent.click(screen.getByTestId("plugin-download-allow"))
71
+ expect(onAllow).toHaveBeenLastCalledWith("t-1", true)
72
+ })
73
+
74
+ it("renders nothing actionable when no ticket is queued", () => {
75
+ renderDialog({ entry: null })
76
+ expect(
77
+ screen.queryByTestId("plugin-download-allow"),
78
+ ).not.toBeInTheDocument()
79
+ })
80
+
81
+ it("follows the host language via the ui catalog", async () => {
82
+ await testI18n.changeLanguage("zh")
83
+ renderDialog()
84
+ // Base UI mounts the dialog portal on effects; wait for the first
85
+ // assertion instead of querying the pre-mount tree.
86
+ expect(
87
+ await screen.findByRole("button", { name: "允许" }),
88
+ ).toBeInTheDocument()
89
+ })
90
+ })
@@ -0,0 +1,183 @@
1
+ import { Button } from "./button.tsx"
2
+ import { Checkbox } from "./checkbox.tsx"
3
+ import { AppDialog } from "./app-dialog.tsx"
4
+ import { useEffect, useState } from "react"
5
+ import { useTranslation } from "react-i18next"
6
+
7
+ /**
8
+ * The shared plugin asset-download consent dialog — host chrome used by
9
+ * both host surfaces (the app and the workbench) so the user sees one
10
+ * dialog with one contract wherever a plugin asks to download.
11
+ *
12
+ * Presentational by design: the queue lives in the host's consent store
13
+ * (`@hoardodile/host-web`), this component renders exactly one ticket
14
+ * and reports the decision through callbacks. Copy comes from the shared
15
+ * `ui` catalog namespace (`pluginDownload.*`), so every React surface
16
+ * renders the same localized strings; nothing here imports anything
17
+ * outside `@hoardodile/ui` + `@hoardodile/i18n`, keeping the SDK closure
18
+ * intact.
19
+ */
20
+
21
+ /**
22
+ * A queued consent ticket, structurally identical to the server's
23
+ * `pluginDownloadRequested` SSE event minus its `type` discriminator
24
+ * (and to `@hoardodile/host-web`'s consent-store entry). Declared here
25
+ * instead of imported so this package stays closure-clean.
26
+ */
27
+ export type PluginConsentTicket = {
28
+ readonly ticketId: string
29
+ readonly pluginId: string
30
+ readonly pluginName: string
31
+ readonly url: string
32
+ readonly dest: string
33
+ readonly sizeBytes?: number
34
+ readonly reason?: string
35
+ }
36
+
37
+ export type PluginDownloadConsentDialogProps = {
38
+ /** Ticket to show; `null` hides the dialog (one at a time). */
39
+ readonly entry: PluginConsentTicket | null
40
+ /** Decision callbacks — the host wires them to its own decide path. */
41
+ readonly onDeny: (ticketId: string) => void
42
+ readonly onAllow: (ticketId: string, remember: boolean) => void
43
+ /** Byte formatter; defaults to `"<n> B"`. */
44
+ readonly formatBytes?: (bytes: number) => string
45
+ }
46
+
47
+ /**
48
+ * Renders the shared consent question: URL verbatim (selectable
49
+ * monospace, never a link), the vault-relative destination and the
50
+ * plugin's stated reason, with Allow / Deny and a session-remember
51
+ * checkbox.
52
+ *
53
+ * Design contract (hd-plugin-design): card + hairline + `--radius-2xl`
54
+ * + `--shadow-dialog`, footer parted by an inset hairline, focus on the
55
+ * dialog container; the danger stays in copy and actions, never in color.
56
+ */
57
+ export function PluginDownloadConsentDialog(
58
+ props: PluginDownloadConsentDialogProps,
59
+ ) {
60
+ const { entry, onDeny, onAllow } = props
61
+ const { t } = useTranslation("ui", { useSuspense: false })
62
+ const formatBytes = props.formatBytes ?? ((bytes: number) => `${bytes} B`)
63
+ const activeId = entry === null ? null : entry.ticketId
64
+ const [remember, setRemember] = useState(false)
65
+
66
+ // A fresh ticket starts with "remember" unchecked — the choice must
67
+ // never leak from one download to the next.
68
+ useEffect(() => {
69
+ setRemember(false)
70
+ }, [activeId])
71
+
72
+ return (
73
+ <AppDialog
74
+ open={entry !== null}
75
+ onOpenChange={(open) => {
76
+ if (!open && activeId !== null) onDeny(activeId)
77
+ }}
78
+ title={t("pluginDownload.title", undefined)}
79
+ eyebrow={t("pluginDownload.eyebrow", undefined)}
80
+ description={
81
+ entry === null
82
+ ? undefined
83
+ : t("pluginDownload.description", {
84
+ pluginName: entry.pluginName,
85
+ })
86
+ }
87
+ size="md"
88
+ contentTestId="plugin-download-consent"
89
+ footer={
90
+ <div className="flex w-full items-center justify-between">
91
+ <Button
92
+ variant="outline"
93
+ size="sm"
94
+ disabled={activeId === null}
95
+ onClick={() => activeId !== null && onDeny(activeId)}
96
+ data-testid="plugin-download-deny"
97
+ >
98
+ {t("pluginDownload.deny", undefined)}
99
+ </Button>
100
+ <Button
101
+ size="sm"
102
+ disabled={activeId === null}
103
+ onClick={() =>
104
+ activeId !== null && onAllow(activeId, remember)
105
+ }
106
+ data-testid="plugin-download-allow"
107
+ >
108
+ {t("pluginDownload.allow", undefined)}
109
+ </Button>
110
+ </div>
111
+ }
112
+ >
113
+ {entry === null ? null : (
114
+ <ConsentBody
115
+ entry={entry}
116
+ remember={remember}
117
+ onRememberChange={setRemember}
118
+ formatBytes={formatBytes}
119
+ />
120
+ )}
121
+ </AppDialog>
122
+ )
123
+ }
124
+
125
+ function ConsentBody(props: {
126
+ readonly entry: PluginConsentTicket
127
+ readonly remember: boolean
128
+ readonly onRememberChange: (value: boolean) => void
129
+ readonly formatBytes: (bytes: number) => string
130
+ }) {
131
+ const { entry, remember, onRememberChange, formatBytes } = props
132
+ const { t } = useTranslation("ui", { useSuspense: false })
133
+ return (
134
+ <div className="flex flex-col gap-4">
135
+ <div className="flex flex-col gap-1.5">
136
+ <span className="text-xs tracking-label text-muted-foreground uppercase">
137
+ {t("pluginDownload.urlLabel", undefined)}
138
+ </span>
139
+ <code
140
+ className="rounded-sm bg-muted px-2 py-1.5 text-xs text-secondary-foreground break-all select-all"
141
+ data-testid="plugin-download-url"
142
+ >
143
+ {entry.url}
144
+ </code>
145
+ </div>
146
+ <div className="flex flex-col gap-1.5">
147
+ <span className="text-xs tracking-label text-muted-foreground uppercase">
148
+ {t("pluginDownload.destLabel", undefined)}
149
+ </span>
150
+ <code
151
+ className="text-xs text-secondary-foreground break-all select-all"
152
+ data-testid="plugin-download-dest"
153
+ >
154
+ {entry.dest}
155
+ </code>
156
+ </div>
157
+ <div className="text-xs text-muted-foreground">
158
+ {t("pluginDownload.sizeLabel", undefined)}:{" "}
159
+ {entry.sizeBytes === undefined
160
+ ? t("pluginDownload.unknownSize", undefined)
161
+ : formatBytes(entry.sizeBytes)}
162
+ </div>
163
+ {entry.reason !== undefined && entry.reason.length > 0 ? (
164
+ <p className="text-xs text-secondary-foreground italic">
165
+ {entry.reason}
166
+ </p>
167
+ ) : null}
168
+ <div className="flex items-center gap-2 border-t pt-3">
169
+ <Checkbox
170
+ checked={remember}
171
+ onCheckedChange={(checked) => onRememberChange(checked === true)}
172
+ id="plugin-download-remember"
173
+ />
174
+ <label
175
+ htmlFor="plugin-download-remember"
176
+ className="text-xs text-secondary-foreground select-none"
177
+ >
178
+ {t("pluginDownload.remember", undefined)}
179
+ </label>
180
+ </div>
181
+ </div>
182
+ )
183
+ }