@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,64 @@
1
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
2
+
3
+ import { cn } from "@hoardodile/ui/lib/utils"
4
+
5
+ function TooltipProvider({
6
+ delay = 0,
7
+ ...props
8
+ }: TooltipPrimitive.Provider.Props) {
9
+ return (
10
+ <TooltipPrimitive.Provider
11
+ data-slot="tooltip-provider"
12
+ delay={delay}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
19
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
20
+ }
21
+
22
+ function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
23
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
24
+ }
25
+
26
+ function TooltipContent({
27
+ className,
28
+ side = "top",
29
+ sideOffset = 4,
30
+ align = "center",
31
+ alignOffset = 0,
32
+ children,
33
+ ...props
34
+ }: TooltipPrimitive.Popup.Props &
35
+ Pick<
36
+ TooltipPrimitive.Positioner.Props,
37
+ "align" | "alignOffset" | "side" | "sideOffset"
38
+ >) {
39
+ return (
40
+ <TooltipPrimitive.Portal>
41
+ <TooltipPrimitive.Positioner
42
+ align={align}
43
+ alignOffset={alignOffset}
44
+ side={side}
45
+ sideOffset={sideOffset}
46
+ className="isolate z-50"
47
+ >
48
+ <TooltipPrimitive.Popup
49
+ data-slot="tooltip-content"
50
+ className={cn(
51
+ "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 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-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 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",
52
+ className
53
+ )}
54
+ {...props}
55
+ >
56
+ {children}
57
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
58
+ </TooltipPrimitive.Popup>
59
+ </TooltipPrimitive.Positioner>
60
+ </TooltipPrimitive.Portal>
61
+ )
62
+ }
63
+
64
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,23 @@
1
+ import { createContext, type ReactNode, useContext } from "react"
2
+
3
+ /**
4
+ * Footer action slot plumbing for `AppDialog` — the context shared
5
+ * between the dialog shell (`@hoardodile/ui/components/app-dialog`) and
6
+ * body panels that contribute their primary action through
7
+ * `DialogFooterActions`, so action buttons land in the footer instead of
8
+ * the body.
9
+ */
10
+
11
+ type DialogFooterActionsValue = {
12
+ readonly setFooterActions: (node: ReactNode | null) => void
13
+ }
14
+
15
+ const DialogFooterActionsContext =
16
+ createContext<DialogFooterActionsValue | null>(null)
17
+
18
+ /** The nearest dialog's footer action slot, or null outside a dialog. */
19
+ function useDialogFooterActions(): DialogFooterActionsValue | null {
20
+ return useContext(DialogFooterActionsContext)
21
+ }
22
+
23
+ export { DialogFooterActionsContext, useDialogFooterActions }
@@ -0,0 +1,62 @@
1
+ import * as React from "react"
2
+ import {
3
+ type FieldPath,
4
+ type FieldValues,
5
+ useFormContext,
6
+ useFormState,
7
+ } from "react-hook-form"
8
+
9
+ /**
10
+ * Field-state plumbing shared by the `Form*` components in
11
+ * `@hoardodile/ui/components/form`: connects a `<FormField>` (the
12
+ * react-hook-form `Controller` wrapper) to its `<FormItem>` via two
13
+ * private contexts and exposes the resolved field state — id parts and
14
+ * error — to the label/control/description/message render pieces.
15
+ */
16
+
17
+ type FormFieldContextValue<
18
+ TFieldValues extends FieldValues = FieldValues,
19
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
20
+ > = {
21
+ name: TName
22
+ }
23
+
24
+ const FormFieldContext = React.createContext<FormFieldContextValue | undefined>(
25
+ undefined,
26
+ )
27
+
28
+ type FormItemContextValue = {
29
+ id: string
30
+ }
31
+
32
+ const FormItemContext = React.createContext<FormItemContextValue | undefined>(
33
+ undefined,
34
+ )
35
+
36
+ function useFormField() {
37
+ const fieldContext = React.useContext(FormFieldContext)
38
+ const itemContext = React.useContext(FormItemContext)
39
+ const { getFieldState } = useFormContext()
40
+ const formState = useFormState({ name: fieldContext?.name })
41
+
42
+ if (!fieldContext) {
43
+ throw new Error("useFormField should be used within <FormField>")
44
+ }
45
+ if (!itemContext) {
46
+ throw new Error("useFormField should be used within <FormItem>")
47
+ }
48
+
49
+ const fieldState = getFieldState(fieldContext.name, formState)
50
+ const { id } = itemContext
51
+
52
+ return {
53
+ id,
54
+ name: fieldContext.name,
55
+ formItemId: `${id}-form-item`,
56
+ formDescriptionId: `${id}-form-item-description`,
57
+ formMessageId: `${id}-form-item-message`,
58
+ ...fieldState,
59
+ }
60
+ }
61
+
62
+ export { FormFieldContext, FormItemContext, useFormField }
@@ -0,0 +1,54 @@
1
+ import {
2
+ MOBILE_QUERY,
3
+ PANEL_QUERY,
4
+ SIDEBAR_QUERY,
5
+ } from "@hoardodile/ui/viewport"
6
+ import { useMedia } from "react-use"
7
+
8
+ /**
9
+ * One-shot check for viewports below the `md` breakpoint (< 768 px).
10
+ *
11
+ * Prefer {@link useBelowMd} in React components so they re-render when the
12
+ * viewport crosses the breakpoint. Use this helper only in event handlers,
13
+ * utility functions, or effects where a reactive hook is not appropriate.
14
+ */
15
+ export function isBelowMd(): boolean {
16
+ if (typeof window === "undefined") return false
17
+ return window.matchMedia(MOBILE_QUERY).matches
18
+ }
19
+
20
+ export function useBelowMd(): boolean {
21
+ return useMedia(MOBILE_QUERY, false)
22
+ }
23
+
24
+ /**
25
+ * Reactive check for viewports below the app-shell sidebar breakpoint
26
+ * (`max-sidebar`), where the fixed sidebar is replaced by the hamburger
27
+ * drawer. Keep in sync with `SIDEBAR_BREAKPOINT_PX` and the
28
+ * `sidebar:` / `max-sidebar:` CSS variants. The shell alone decides its own chrome;
29
+ * compact-UI consumers keep using {@link useBelowMd}.
30
+ */
31
+ export function useBelowSidebar(): boolean {
32
+ return useMedia(SIDEBAR_QUERY, false)
33
+ }
34
+
35
+ /**
36
+ * Reactive check for viewports below the filter-rail breakpoint
37
+ * (< 1440 px), where the rail becomes a drawer instead of a fixed right
38
+ * column. Keep in sync with `PANEL_BREAKPOINT_PX` and the
39
+ * `panel:` / `max-panel:` CSS variants.
40
+ */
41
+ export function useBelowPanel(): boolean {
42
+ return useMedia(PANEL_QUERY, false)
43
+ }
44
+
45
+ /**
46
+ * Compatibility alias for official shadcn-generated components (e.g.
47
+ * `sidebar.tsx`), which import `useIsMobile` from this module. Mobile
48
+ * detection rules are owned by THIS file for the whole repo (apps and
49
+ * plugins alike) — never let the registry version of `use-mobile.ts`
50
+ * replace it; restore after every `shadcn add`.
51
+ */
52
+ export function useIsMobile(): boolean {
53
+ return useBelowMd()
54
+ }
@@ -0,0 +1,42 @@
1
+ // @vitest-environment node
2
+ import { describe, expect, test } from "vitest"
3
+ import {
4
+ clampViewportScale,
5
+ isTapGesture,
6
+ panViewport,
7
+ type ViewportTransform,
8
+ zoomViewportAt,
9
+ } from "./use-pinch-pan"
10
+
11
+ const RANGE = { minScale: 0.5, maxScale: 4 }
12
+
13
+ describe("viewport transform math", () => {
14
+ test("clamps zoom into the viewer range", () => {
15
+ expect(clampViewportScale(8, RANGE)).toBe(4)
16
+ expect(clampViewportScale(0.1, RANGE)).toBe(0.5)
17
+ expect(clampViewportScale(1.5, RANGE)).toBe(1.5)
18
+ })
19
+
20
+ test("pans by screen-space pixels", () => {
21
+ const start: ViewportTransform = { x: 10, y: -4, scale: 2 }
22
+ expect(panViewport(start, { x: 5, y: 7 })).toEqual({
23
+ x: 15,
24
+ y: 3,
25
+ scale: 2,
26
+ })
27
+ })
28
+
29
+ test("zooms while keeping the anchor point fixed", () => {
30
+ const start: ViewportTransform = { x: 10, y: 20, scale: 1 }
31
+ expect(zoomViewportAt(start, { x: 40, y: -10 }, 2, RANGE)).toEqual({
32
+ x: -30,
33
+ y: 30,
34
+ scale: 2,
35
+ })
36
+ })
37
+
38
+ test("a move within the threshold is a tap", () => {
39
+ expect(isTapGesture({ x: 0, y: 0 }, { x: 5, y: 5 }, 8)).toBe(true)
40
+ expect(isTapGesture({ x: 0, y: 0 }, { x: 8, y: 0 }, 8)).toBe(false)
41
+ })
42
+ })
@@ -0,0 +1,340 @@
1
+ import { type RefObject, useEffect, useRef, useState } from "react"
2
+
3
+ /**
4
+ * Shared pinch/pan viewport gesture for media viewers. One finger pans,
5
+ * two fingers zoom around their midpoint, a quick tap reports a click,
6
+ * and a second quick tap reports a double tap. The transform is
7
+ * `translate(x, y) scale(scale)` with the target's center as origin.
8
+ */
9
+
10
+ export type ViewportTransform = {
11
+ readonly x: number
12
+ readonly y: number
13
+ readonly scale: number
14
+ }
15
+
16
+ export type ViewportPoint = {
17
+ readonly x: number
18
+ readonly y: number
19
+ }
20
+
21
+ export type PinchPanOptions = {
22
+ readonly target: RefObject<HTMLElement | null>
23
+ readonly initial?: ViewportTransform
24
+ readonly minScale?: number
25
+ readonly maxScale?: number
26
+ /** Max travel for a tap before it becomes a drag, in CSS pixels. */
27
+ readonly tapThreshold?: number
28
+ /** When this value changes, the viewport returns to `initial`. */
29
+ readonly resetKey?: string | number
30
+ readonly onChange?: (next: ViewportTransform) => void
31
+ readonly onTap?: (point: ViewportPoint) => void
32
+ readonly onDoubleTap?: (point: ViewportPoint) => void
33
+ }
34
+
35
+ const DEFAULT_TRANSFORM: ViewportTransform = { x: 0, y: 0, scale: 1 }
36
+
37
+ type ActivePointer = {
38
+ readonly id: number
39
+ readonly x: number
40
+ readonly y: number
41
+ readonly startX: number
42
+ readonly startY: number
43
+ }
44
+
45
+ type GestureSession = {
46
+ readonly pointers: ReadonlyMap<number, ActivePointer>
47
+ readonly start: ViewportTransform
48
+ readonly startDistance: number
49
+ readonly startMidpoint: ViewportPoint
50
+ moved: boolean
51
+ }
52
+
53
+ /**
54
+ * Clamp a scale into the viewer's zoom range. Pure so tests can hold the
55
+ * same arithmetic the DOM hook uses.
56
+ */
57
+ export function clampViewportScale(
58
+ scale: number,
59
+ options: { readonly minScale: number; readonly maxScale: number },
60
+ ): number {
61
+ return Math.max(options.minScale, Math.min(options.maxScale, scale))
62
+ }
63
+
64
+ /** Pan a transform by screen-space pixels. */
65
+ export function panViewport(
66
+ transform: ViewportTransform,
67
+ delta: ViewportPoint,
68
+ ): ViewportTransform {
69
+ return { ...transform, x: transform.x + delta.x, y: transform.y + delta.y }
70
+ }
71
+
72
+ /**
73
+ * Zoom `transform` to `nextScale` while keeping the content under
74
+ * `anchor` (relative to the transform origin) visually fixed.
75
+ */
76
+ export function zoomViewportAt(
77
+ transform: ViewportTransform,
78
+ anchor: ViewportPoint,
79
+ nextScale: number,
80
+ options: { readonly minScale: number; readonly maxScale: number },
81
+ ): ViewportTransform {
82
+ const scale = clampViewportScale(nextScale, options)
83
+ const scaleDelta = scale - transform.scale
84
+ return {
85
+ scale,
86
+ x: transform.x - anchor.x * scaleDelta,
87
+ y: transform.y - anchor.y * scaleDelta,
88
+ }
89
+ }
90
+
91
+ /** True when a press counts as a tap rather than a drag. */
92
+ export function isTapGesture(
93
+ start: ViewportPoint,
94
+ end: ViewportPoint,
95
+ tapThreshold: number,
96
+ ): boolean {
97
+ return Math.hypot(end.x - start.x, end.y - start.y) < tapThreshold
98
+ }
99
+
100
+ export function usePinchPan(options: PinchPanOptions): {
101
+ readonly transform: ViewportTransform
102
+ readonly reset: () => void
103
+ } {
104
+ const {
105
+ target,
106
+ initial = DEFAULT_TRANSFORM,
107
+ minScale = 0.5,
108
+ maxScale = 4,
109
+ tapThreshold = 8,
110
+ resetKey,
111
+ onChange,
112
+ onTap,
113
+ onDoubleTap,
114
+ } = options
115
+ const [transform, setTransform] = useState<ViewportTransform>(initial)
116
+ const initialRef = useRef(initial)
117
+ initialRef.current = initial
118
+ const transformRef = useRef(transform)
119
+ transformRef.current = transform
120
+ const sessionRef = useRef<GestureSession | null>(null)
121
+ const lastTapRef = useRef<{
122
+ readonly point: ViewportPoint
123
+ readonly time: number
124
+ } | null>(null)
125
+ const boundsRef = useRef({ minScale, maxScale, tapThreshold })
126
+ boundsRef.current = { minScale, maxScale, tapThreshold }
127
+
128
+ function update(next: ViewportTransform) {
129
+ setTransform(next)
130
+ onChange?.(next)
131
+ }
132
+
133
+ function reset() {
134
+ update({ ...DEFAULT_TRANSFORM })
135
+ }
136
+
137
+ useEffect(() => {
138
+ if (resetKey === undefined) return
139
+ update({ ...initialRef.current })
140
+ }, [resetKey])
141
+
142
+ useEffect(() => {
143
+ const currentTarget = target.current
144
+ if (currentTarget === null) return
145
+ const element: HTMLElement = currentTarget
146
+
147
+ function startPointer(event: PointerEvent): void {
148
+ const current = transformRef.current
149
+ const session = sessionRef.current
150
+ if (session === null) {
151
+ sessionRef.current = {
152
+ pointers: new Map([[event.pointerId, activePointer(event)]]),
153
+ start: current,
154
+ startDistance: 0,
155
+ startMidpoint: eventPoint(event),
156
+ moved: false,
157
+ }
158
+ element.setPointerCapture(event.pointerId)
159
+ return
160
+ }
161
+ const pointers = new Map(session.pointers)
162
+ pointers.set(event.pointerId, activePointer(event))
163
+ const entries = [...pointers.values()]
164
+ if (entries.length >= 2) {
165
+ const first = entries[0]
166
+ const second = entries[1]
167
+ if (first !== undefined && second !== undefined) {
168
+ sessionRef.current = {
169
+ pointers,
170
+ start: current,
171
+ startDistance: distance(first, second),
172
+ startMidpoint: midpoint(first, second),
173
+ moved: session.moved,
174
+ }
175
+ }
176
+ }
177
+ element.setPointerCapture(event.pointerId)
178
+ }
179
+
180
+ function movePointer(event: PointerEvent): void {
181
+ const session = sessionRef.current
182
+ if (session === null) return
183
+ const pointers = new Map(session.pointers)
184
+ pointers.set(
185
+ event.pointerId,
186
+ activePointer(event, session.pointers.get(event.pointerId)),
187
+ )
188
+ const entries = [...pointers.values()]
189
+ if (entries.length === 0) return
190
+
191
+ if (entries.length === 1) {
192
+ const pointer = entries[0]
193
+ if (pointer === undefined) return
194
+ const delta = {
195
+ x: pointer.x - pointer.startX,
196
+ y: pointer.y - pointer.startY,
197
+ }
198
+ if (
199
+ !isTapGesture(
200
+ session.startMidpoint,
201
+ pointer,
202
+ boundsRef.current.tapThreshold,
203
+ )
204
+ ) {
205
+ session.moved = true
206
+ }
207
+ sessionRef.current = {
208
+ ...session,
209
+ pointers,
210
+ moved:
211
+ session.moved ||
212
+ movedBeyondTap(pointer, boundsRef.current.tapThreshold),
213
+ }
214
+ update(panViewport(session.start, delta))
215
+ return
216
+ }
217
+
218
+ const first = entries[0]
219
+ const second = entries[1]
220
+ if (first === undefined || second === undefined) return
221
+ const nextDistance = Math.max(1, distance(first, second))
222
+ const ratio = nextDistance / Math.max(1, session.startDistance)
223
+ const nextMidpoint = midpoint(first, second)
224
+ const centerMidpoint = relativeMidpoint(element, nextMidpoint)
225
+ const next = zoomViewportAt(
226
+ session.start,
227
+ centerMidpoint,
228
+ session.start.scale * ratio,
229
+ {
230
+ minScale: boundsRef.current.minScale,
231
+ maxScale: boundsRef.current.maxScale,
232
+ },
233
+ )
234
+ sessionRef.current = { ...session, pointers, moved: true }
235
+ update(next)
236
+ }
237
+
238
+ function endPointer(event: PointerEvent): void {
239
+ const session = sessionRef.current
240
+ if (session === null) return
241
+ const pointers = new Map(session.pointers)
242
+ pointers.delete(event.pointerId)
243
+ if (pointers.size > 0) {
244
+ sessionRef.current = { ...session, pointers }
245
+ return
246
+ }
247
+ sessionRef.current = null
248
+ if (!session.moved && session.pointers.size === 1) {
249
+ const point = eventPoint(event)
250
+ handleTap(point)
251
+ }
252
+ }
253
+
254
+ function cancelPointer(event: PointerEvent): void {
255
+ const session = sessionRef.current
256
+ if (session === null) return
257
+ const pointers = new Map(session.pointers)
258
+ pointers.delete(event.pointerId)
259
+ sessionRef.current = pointers.size > 0 ? { ...session, pointers } : null
260
+ }
261
+
262
+ function handleTap(point: ViewportPoint): void {
263
+ onTap?.(point)
264
+ const now = performance.now()
265
+ const last = lastTapRef.current
266
+ lastTapRef.current = { point, time: now }
267
+ if (
268
+ last !== null &&
269
+ now - last.time < 300 &&
270
+ Math.hypot(point.x - last.point.x, point.y - last.point.y) < 30
271
+ ) {
272
+ lastTapRef.current = null
273
+ onDoubleTap?.(point)
274
+ }
275
+ }
276
+
277
+ element.addEventListener("pointerdown", startPointer)
278
+ element.addEventListener("pointermove", movePointer)
279
+ element.addEventListener("pointerup", endPointer)
280
+ element.addEventListener("pointercancel", cancelPointer)
281
+ return () => {
282
+ element.removeEventListener("pointerdown", startPointer)
283
+ element.removeEventListener("pointermove", movePointer)
284
+ element.removeEventListener("pointerup", endPointer)
285
+ element.removeEventListener("pointercancel", cancelPointer)
286
+ }
287
+ }, [target, onTap, onDoubleTap, onChange])
288
+
289
+ return { transform, reset }
290
+ }
291
+
292
+ function activePointer(
293
+ event: PointerEvent,
294
+ start?: ActivePointer | ViewportPoint,
295
+ ): ActivePointer {
296
+ const origin = start ?? eventPoint(event)
297
+ return {
298
+ id: event.pointerId,
299
+ x: event.clientX,
300
+ y: event.clientY,
301
+ startX: origin.x,
302
+ startY: origin.y,
303
+ }
304
+ }
305
+
306
+ function eventPoint(event: {
307
+ readonly clientX: number
308
+ readonly clientY: number
309
+ }): ViewportPoint {
310
+ return { x: event.clientX, y: event.clientY }
311
+ }
312
+
313
+ function distance(a: ViewportPoint, b: ViewportPoint): number {
314
+ return Math.hypot(a.x - b.x, a.y - b.y)
315
+ }
316
+
317
+ function midpoint(a: ViewportPoint, b: ViewportPoint): ViewportPoint {
318
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }
319
+ }
320
+
321
+ function movedBeyondTap(pointer: ActivePointer, tapThreshold: number): boolean {
322
+ return (
323
+ isTapGesture(
324
+ { x: pointer.startX, y: pointer.startY },
325
+ { x: pointer.x, y: pointer.y },
326
+ tapThreshold,
327
+ ) === false
328
+ )
329
+ }
330
+
331
+ function relativeMidpoint(
332
+ element: HTMLElement,
333
+ point: ViewportPoint,
334
+ ): ViewportPoint {
335
+ const rect = element.getBoundingClientRect()
336
+ return {
337
+ x: point.x - (rect.left + rect.width / 2),
338
+ y: point.y - (rect.top + rect.height / 2),
339
+ }
340
+ }