@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,29 @@
1
+ import { useMedia } from 'react-use';
2
+
3
+ // src/viewport.ts
4
+ var MOBILE_BREAKPOINT_PX = 768;
5
+ var MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT_PX - 1}px)`;
6
+ var SIDEBAR_BREAKPOINT_PX = 1150;
7
+ var SIDEBAR_QUERY = `(max-width: ${SIDEBAR_BREAKPOINT_PX - 1}px)`;
8
+ var PANEL_BREAKPOINT_PX = 1440;
9
+ var PANEL_QUERY = `(max-width: ${PANEL_BREAKPOINT_PX - 1}px)`;
10
+ function isBelowMd() {
11
+ if (typeof window === "undefined") return false;
12
+ return window.matchMedia(MOBILE_QUERY).matches;
13
+ }
14
+ function useBelowMd() {
15
+ return useMedia(MOBILE_QUERY, false);
16
+ }
17
+ function useBelowSidebar() {
18
+ return useMedia(SIDEBAR_QUERY, false);
19
+ }
20
+ function useBelowPanel() {
21
+ return useMedia(PANEL_QUERY, false);
22
+ }
23
+ function useIsMobile() {
24
+ return useBelowMd();
25
+ }
26
+
27
+ export { isBelowMd, useBelowMd, useBelowPanel, useBelowSidebar, useIsMobile };
28
+ //# sourceMappingURL=use-mobile.js.map
29
+ //# sourceMappingURL=use-mobile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/viewport.ts","../../src/hooks/use-mobile.ts"],"names":[],"mappings":";;;AAeO,IAAM,oBAAA,GAAuB,GAAA;AAgB7B,IAAM,YAAA,GAAe,CAAA,YAAA,EAAe,oBAAA,GAAuB,CAAC,CAAA,GAAA,CAAA;AAW5D,IAAM,qBAAA,GAAwB,IAAA;AAQ9B,IAAM,aAAA,GAAgB,CAAA,YAAA,EAAe,qBAAA,GAAwB,CAAC,CAAA,GAAA,CAAA;AAW9D,IAAM,mBAAA,GAAsB,IAAA;AAO5B,IAAM,WAAA,GAAc,CAAA,YAAA,EAAe,mBAAA,GAAsB,CAAC,CAAA,GAAA,CAAA;ACtD1D,SAAS,SAAA,GAAqB;AACpC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,KAAA;AAC1C,EAAA,OAAO,MAAA,CAAO,UAAA,CAAW,YAAY,CAAA,CAAE,OAAA;AACxC;AAEO,SAAS,UAAA,GAAsB;AACrC,EAAA,OAAO,QAAA,CAAS,cAAc,KAAK,CAAA;AACpC;AASO,SAAS,eAAA,GAA2B;AAC1C,EAAA,OAAO,QAAA,CAAS,eAAe,KAAK,CAAA;AACrC;AAQO,SAAS,aAAA,GAAyB;AACxC,EAAA,OAAO,QAAA,CAAS,aAAa,KAAK,CAAA;AACnC;AASO,SAAS,WAAA,GAAuB;AACtC,EAAA,OAAO,UAAA,EAAW;AACnB","file":"use-mobile.js","sourcesContent":["/**\n * Mobile viewport initial-scale factor used across the app shell and\n * plugin iframe previews. Kept in one place so it can be changed\n * without hunting through HTML templates and CSS rules.\n */\nexport const MOBILE_INITIAL_SCALE = 0.8\n\n/**\n * Width in pixels at which the app switches from below-md to md+ layouts.\n *\n * This value is intentionally aligned with Tailwind's `md` breakpoint\n * (768 px) so that JS detection and CSS responsive prefixes stay in sync.\n * Components that need to know whether they are in the \"below md\" viewport\n * should use {@link isBelowMd} or {@link useBelowMd} from @hoardodile/ui/hooks/use-mobile.\n */\nexport const MOBILE_BREAKPOINT_PX = 768\n\n/**\n * Same breakpoint expressed in rems, matching Tailwind's default theme\n * (`--breakpoint-md: 48rem`). Useful for CSS media queries and comments.\n */\nexport const MOBILE_BREAKPOINT_REM = MOBILE_BREAKPOINT_PX / 16\n\n/**\n * Media query string that matches viewports below the `md` breakpoint\n * (< 768 px). Shared between JS hooks and anywhere else that needs to detect\n * a below-md viewport.\n *\n * Keep this in sync with the CSS/Tailwind breakpoint so JS and CSS never\n * disagree about the viewport size.\n */\nexport const MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT_PX - 1}px)`\n\n/**\n * Width in pixels at which the app switches from a drawer to a fixed left\n * sidebar (the \"app shell\"). Raised above `md` so mid-size viewports keep\n * the whole width for their content column — below this the sidebar hides\n * behind the hamburger drawer.\n *\n * Use the CSS `sidebar:` / `max-sidebar:` variants (`--breakpoint-sidebar`)\n * at call sites — this value is the single source of truth for both.\n */\nexport const SIDEBAR_BREAKPOINT_PX = 1150\n\n/**\n * Media query string that matches viewports below the sidebar breakpoint\n * (`max-sidebar`), i.e. where the shell shows the drawer instead of the fixed\n * sidebar. Shared between JS hooks and anywhere else that needs the\n * below-sidebar viewport.\n */\nexport const SIDEBAR_QUERY = `(max-width: ${SIDEBAR_BREAKPOINT_PX - 1}px)`\n\n/**\n * Width in pixels at which the right filter rail switches from a fixed\n * column to a drawer (DESIGN.md — Layout: \"the panel becomes an\n * overlay below 1440px\"). Below this the sidebar + panel + content no\n * longer fit comfortably, so the rail hides behind a Filters button.\n *\n * Use the CSS `panel:` / `max-panel:` variants (`--breakpoint-panel`)\n * at call sites — this value is the single source of truth for both.\n */\nexport const PANEL_BREAKPOINT_PX = 1440\n\n/**\n * Media query string that matches viewports below the panel breakpoint\n * (< 1440 px), i.e. where the filter rail lives in a drawer. Shared\n * between JS hooks and anywhere else that needs the below-panel viewport.\n */\nexport const PANEL_QUERY = `(max-width: ${PANEL_BREAKPOINT_PX - 1}px)`\n","import {\n\tMOBILE_QUERY,\n\tPANEL_QUERY,\n\tSIDEBAR_QUERY,\n} from \"@hoardodile/ui/viewport\"\nimport { useMedia } from \"react-use\"\n\n/**\n * One-shot check for viewports below the `md` breakpoint (< 768 px).\n *\n * Prefer {@link useBelowMd} in React components so they re-render when the\n * viewport crosses the breakpoint. Use this helper only in event handlers,\n * utility functions, or effects where a reactive hook is not appropriate.\n */\nexport function isBelowMd(): boolean {\n\tif (typeof window === \"undefined\") return false\n\treturn window.matchMedia(MOBILE_QUERY).matches\n}\n\nexport function useBelowMd(): boolean {\n\treturn useMedia(MOBILE_QUERY, false)\n}\n\n/**\n * Reactive check for viewports below the app-shell sidebar breakpoint\n * (`max-sidebar`), where the fixed sidebar is replaced by the hamburger\n * drawer. Keep in sync with `SIDEBAR_BREAKPOINT_PX` and the\n * `sidebar:` / `max-sidebar:` CSS variants. The shell alone decides its own chrome;\n * compact-UI consumers keep using {@link useBelowMd}.\n */\nexport function useBelowSidebar(): boolean {\n\treturn useMedia(SIDEBAR_QUERY, false)\n}\n\n/**\n * Reactive check for viewports below the filter-rail breakpoint\n * (< 1440 px), where the rail becomes a drawer instead of a fixed right\n * column. Keep in sync with `PANEL_BREAKPOINT_PX` and the\n * `panel:` / `max-panel:` CSS variants.\n */\nexport function useBelowPanel(): boolean {\n\treturn useMedia(PANEL_QUERY, false)\n}\n\n/**\n * Compatibility alias for official shadcn-generated components (e.g.\n * `sidebar.tsx`), which import `useIsMobile` from this module. Mobile\n * detection rules are owned by THIS file for the whole repo (apps and\n * plugins alike) — never let the registry version of `use-mobile.ts`\n * replace it; restore after every `shadcn add`.\n */\nexport function useIsMobile(): boolean {\n\treturn useBelowMd()\n}\n"]}
@@ -0,0 +1,56 @@
1
+ import { RefObject } 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
+ type ViewportTransform = {
10
+ readonly x: number;
11
+ readonly y: number;
12
+ readonly scale: number;
13
+ };
14
+ type ViewportPoint = {
15
+ readonly x: number;
16
+ readonly y: number;
17
+ };
18
+ type PinchPanOptions = {
19
+ readonly target: RefObject<HTMLElement | null>;
20
+ readonly initial?: ViewportTransform;
21
+ readonly minScale?: number;
22
+ readonly maxScale?: number;
23
+ /** Max travel for a tap before it becomes a drag, in CSS pixels. */
24
+ readonly tapThreshold?: number;
25
+ /** When this value changes, the viewport returns to `initial`. */
26
+ readonly resetKey?: string | number;
27
+ readonly onChange?: (next: ViewportTransform) => void;
28
+ readonly onTap?: (point: ViewportPoint) => void;
29
+ readonly onDoubleTap?: (point: ViewportPoint) => void;
30
+ };
31
+ /**
32
+ * Clamp a scale into the viewer's zoom range. Pure so tests can hold the
33
+ * same arithmetic the DOM hook uses.
34
+ */
35
+ declare function clampViewportScale(scale: number, options: {
36
+ readonly minScale: number;
37
+ readonly maxScale: number;
38
+ }): number;
39
+ /** Pan a transform by screen-space pixels. */
40
+ declare function panViewport(transform: ViewportTransform, delta: ViewportPoint): ViewportTransform;
41
+ /**
42
+ * Zoom `transform` to `nextScale` while keeping the content under
43
+ * `anchor` (relative to the transform origin) visually fixed.
44
+ */
45
+ declare function zoomViewportAt(transform: ViewportTransform, anchor: ViewportPoint, nextScale: number, options: {
46
+ readonly minScale: number;
47
+ readonly maxScale: number;
48
+ }): ViewportTransform;
49
+ /** True when a press counts as a tap rather than a drag. */
50
+ declare function isTapGesture(start: ViewportPoint, end: ViewportPoint, tapThreshold: number): boolean;
51
+ declare function usePinchPan(options: PinchPanOptions): {
52
+ readonly transform: ViewportTransform;
53
+ readonly reset: () => void;
54
+ };
55
+
56
+ export { type PinchPanOptions, type ViewportPoint, type ViewportTransform, clampViewportScale, isTapGesture, panViewport, usePinchPan, zoomViewportAt };
@@ -0,0 +1,223 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+
3
+ // src/hooks/use-pinch-pan.ts
4
+ var DEFAULT_TRANSFORM = { x: 0, y: 0, scale: 1 };
5
+ function clampViewportScale(scale, options) {
6
+ return Math.max(options.minScale, Math.min(options.maxScale, scale));
7
+ }
8
+ function panViewport(transform, delta) {
9
+ return { ...transform, x: transform.x + delta.x, y: transform.y + delta.y };
10
+ }
11
+ function zoomViewportAt(transform, anchor, nextScale, options) {
12
+ const scale = clampViewportScale(nextScale, options);
13
+ const scaleDelta = scale - transform.scale;
14
+ return {
15
+ scale,
16
+ x: transform.x - anchor.x * scaleDelta,
17
+ y: transform.y - anchor.y * scaleDelta
18
+ };
19
+ }
20
+ function isTapGesture(start, end, tapThreshold) {
21
+ return Math.hypot(end.x - start.x, end.y - start.y) < tapThreshold;
22
+ }
23
+ function usePinchPan(options) {
24
+ const {
25
+ target,
26
+ initial = DEFAULT_TRANSFORM,
27
+ minScale = 0.5,
28
+ maxScale = 4,
29
+ tapThreshold = 8,
30
+ resetKey,
31
+ onChange,
32
+ onTap,
33
+ onDoubleTap
34
+ } = options;
35
+ const [transform, setTransform] = useState(initial);
36
+ const initialRef = useRef(initial);
37
+ initialRef.current = initial;
38
+ const transformRef = useRef(transform);
39
+ transformRef.current = transform;
40
+ const sessionRef = useRef(null);
41
+ const lastTapRef = useRef(null);
42
+ const boundsRef = useRef({ minScale, maxScale, tapThreshold });
43
+ boundsRef.current = { minScale, maxScale, tapThreshold };
44
+ function update(next) {
45
+ setTransform(next);
46
+ onChange?.(next);
47
+ }
48
+ function reset() {
49
+ update({ ...DEFAULT_TRANSFORM });
50
+ }
51
+ useEffect(() => {
52
+ if (resetKey === void 0) return;
53
+ update({ ...initialRef.current });
54
+ }, [resetKey]);
55
+ useEffect(() => {
56
+ const currentTarget = target.current;
57
+ if (currentTarget === null) return;
58
+ const element = currentTarget;
59
+ function startPointer(event) {
60
+ const current = transformRef.current;
61
+ const session = sessionRef.current;
62
+ if (session === null) {
63
+ sessionRef.current = {
64
+ pointers: /* @__PURE__ */ new Map([[event.pointerId, activePointer(event)]]),
65
+ start: current,
66
+ startDistance: 0,
67
+ startMidpoint: eventPoint(event),
68
+ moved: false
69
+ };
70
+ element.setPointerCapture(event.pointerId);
71
+ return;
72
+ }
73
+ const pointers = new Map(session.pointers);
74
+ pointers.set(event.pointerId, activePointer(event));
75
+ const entries = [...pointers.values()];
76
+ if (entries.length >= 2) {
77
+ const first = entries[0];
78
+ const second = entries[1];
79
+ if (first !== void 0 && second !== void 0) {
80
+ sessionRef.current = {
81
+ pointers,
82
+ start: current,
83
+ startDistance: distance(first, second),
84
+ startMidpoint: midpoint(first, second),
85
+ moved: session.moved
86
+ };
87
+ }
88
+ }
89
+ element.setPointerCapture(event.pointerId);
90
+ }
91
+ function movePointer(event) {
92
+ const session = sessionRef.current;
93
+ if (session === null) return;
94
+ const pointers = new Map(session.pointers);
95
+ pointers.set(
96
+ event.pointerId,
97
+ activePointer(event, session.pointers.get(event.pointerId))
98
+ );
99
+ const entries = [...pointers.values()];
100
+ if (entries.length === 0) return;
101
+ if (entries.length === 1) {
102
+ const pointer = entries[0];
103
+ if (pointer === void 0) return;
104
+ const delta = {
105
+ x: pointer.x - pointer.startX,
106
+ y: pointer.y - pointer.startY
107
+ };
108
+ if (!isTapGesture(
109
+ session.startMidpoint,
110
+ pointer,
111
+ boundsRef.current.tapThreshold
112
+ )) {
113
+ session.moved = true;
114
+ }
115
+ sessionRef.current = {
116
+ ...session,
117
+ pointers,
118
+ moved: session.moved || movedBeyondTap(pointer, boundsRef.current.tapThreshold)
119
+ };
120
+ update(panViewport(session.start, delta));
121
+ return;
122
+ }
123
+ const first = entries[0];
124
+ const second = entries[1];
125
+ if (first === void 0 || second === void 0) return;
126
+ const nextDistance = Math.max(1, distance(first, second));
127
+ const ratio = nextDistance / Math.max(1, session.startDistance);
128
+ const nextMidpoint = midpoint(first, second);
129
+ const centerMidpoint = relativeMidpoint(element, nextMidpoint);
130
+ const next = zoomViewportAt(
131
+ session.start,
132
+ centerMidpoint,
133
+ session.start.scale * ratio,
134
+ {
135
+ minScale: boundsRef.current.minScale,
136
+ maxScale: boundsRef.current.maxScale
137
+ }
138
+ );
139
+ sessionRef.current = { ...session, pointers, moved: true };
140
+ update(next);
141
+ }
142
+ function endPointer(event) {
143
+ const session = sessionRef.current;
144
+ if (session === null) return;
145
+ const pointers = new Map(session.pointers);
146
+ pointers.delete(event.pointerId);
147
+ if (pointers.size > 0) {
148
+ sessionRef.current = { ...session, pointers };
149
+ return;
150
+ }
151
+ sessionRef.current = null;
152
+ if (!session.moved && session.pointers.size === 1) {
153
+ const point = eventPoint(event);
154
+ handleTap(point);
155
+ }
156
+ }
157
+ function cancelPointer(event) {
158
+ const session = sessionRef.current;
159
+ if (session === null) return;
160
+ const pointers = new Map(session.pointers);
161
+ pointers.delete(event.pointerId);
162
+ sessionRef.current = pointers.size > 0 ? { ...session, pointers } : null;
163
+ }
164
+ function handleTap(point) {
165
+ onTap?.(point);
166
+ const now = performance.now();
167
+ const last = lastTapRef.current;
168
+ lastTapRef.current = { point, time: now };
169
+ if (last !== null && now - last.time < 300 && Math.hypot(point.x - last.point.x, point.y - last.point.y) < 30) {
170
+ lastTapRef.current = null;
171
+ onDoubleTap?.(point);
172
+ }
173
+ }
174
+ element.addEventListener("pointerdown", startPointer);
175
+ element.addEventListener("pointermove", movePointer);
176
+ element.addEventListener("pointerup", endPointer);
177
+ element.addEventListener("pointercancel", cancelPointer);
178
+ return () => {
179
+ element.removeEventListener("pointerdown", startPointer);
180
+ element.removeEventListener("pointermove", movePointer);
181
+ element.removeEventListener("pointerup", endPointer);
182
+ element.removeEventListener("pointercancel", cancelPointer);
183
+ };
184
+ }, [target, onTap, onDoubleTap, onChange]);
185
+ return { transform, reset };
186
+ }
187
+ function activePointer(event, start) {
188
+ const origin = start ?? eventPoint(event);
189
+ return {
190
+ id: event.pointerId,
191
+ x: event.clientX,
192
+ y: event.clientY,
193
+ startX: origin.x,
194
+ startY: origin.y
195
+ };
196
+ }
197
+ function eventPoint(event) {
198
+ return { x: event.clientX, y: event.clientY };
199
+ }
200
+ function distance(a, b) {
201
+ return Math.hypot(a.x - b.x, a.y - b.y);
202
+ }
203
+ function midpoint(a, b) {
204
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
205
+ }
206
+ function movedBeyondTap(pointer, tapThreshold) {
207
+ return isTapGesture(
208
+ { x: pointer.startX, y: pointer.startY },
209
+ { x: pointer.x, y: pointer.y },
210
+ tapThreshold
211
+ ) === false;
212
+ }
213
+ function relativeMidpoint(element, point) {
214
+ const rect = element.getBoundingClientRect();
215
+ return {
216
+ x: point.x - (rect.left + rect.width / 2),
217
+ y: point.y - (rect.top + rect.height / 2)
218
+ };
219
+ }
220
+
221
+ export { clampViewportScale, isTapGesture, panViewport, usePinchPan, zoomViewportAt };
222
+ //# sourceMappingURL=use-pinch-pan.js.map
223
+ //# sourceMappingURL=use-pinch-pan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/use-pinch-pan.ts"],"names":[],"mappings":"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","file":"use-pinch-pan.js","sourcesContent":["import { type RefObject, useEffect, useRef, useState } from \"react\"\n\n/**\n * Shared pinch/pan viewport gesture for media viewers. One finger pans,\n * two fingers zoom around their midpoint, a quick tap reports a click,\n * and a second quick tap reports a double tap. The transform is\n * `translate(x, y) scale(scale)` with the target's center as origin.\n */\n\nexport type ViewportTransform = {\n\treadonly x: number\n\treadonly y: number\n\treadonly scale: number\n}\n\nexport type ViewportPoint = {\n\treadonly x: number\n\treadonly y: number\n}\n\nexport type PinchPanOptions = {\n\treadonly target: RefObject<HTMLElement | null>\n\treadonly initial?: ViewportTransform\n\treadonly minScale?: number\n\treadonly maxScale?: number\n\t/** Max travel for a tap before it becomes a drag, in CSS pixels. */\n\treadonly tapThreshold?: number\n\t/** When this value changes, the viewport returns to `initial`. */\n\treadonly resetKey?: string | number\n\treadonly onChange?: (next: ViewportTransform) => void\n\treadonly onTap?: (point: ViewportPoint) => void\n\treadonly onDoubleTap?: (point: ViewportPoint) => void\n}\n\nconst DEFAULT_TRANSFORM: ViewportTransform = { x: 0, y: 0, scale: 1 }\n\ntype ActivePointer = {\n\treadonly id: number\n\treadonly x: number\n\treadonly y: number\n\treadonly startX: number\n\treadonly startY: number\n}\n\ntype GestureSession = {\n\treadonly pointers: ReadonlyMap<number, ActivePointer>\n\treadonly start: ViewportTransform\n\treadonly startDistance: number\n\treadonly startMidpoint: ViewportPoint\n\tmoved: boolean\n}\n\n/**\n * Clamp a scale into the viewer's zoom range. Pure so tests can hold the\n * same arithmetic the DOM hook uses.\n */\nexport function clampViewportScale(\n\tscale: number,\n\toptions: { readonly minScale: number; readonly maxScale: number },\n): number {\n\treturn Math.max(options.minScale, Math.min(options.maxScale, scale))\n}\n\n/** Pan a transform by screen-space pixels. */\nexport function panViewport(\n\ttransform: ViewportTransform,\n\tdelta: ViewportPoint,\n): ViewportTransform {\n\treturn { ...transform, x: transform.x + delta.x, y: transform.y + delta.y }\n}\n\n/**\n * Zoom `transform` to `nextScale` while keeping the content under\n * `anchor` (relative to the transform origin) visually fixed.\n */\nexport function zoomViewportAt(\n\ttransform: ViewportTransform,\n\tanchor: ViewportPoint,\n\tnextScale: number,\n\toptions: { readonly minScale: number; readonly maxScale: number },\n): ViewportTransform {\n\tconst scale = clampViewportScale(nextScale, options)\n\tconst scaleDelta = scale - transform.scale\n\treturn {\n\t\tscale,\n\t\tx: transform.x - anchor.x * scaleDelta,\n\t\ty: transform.y - anchor.y * scaleDelta,\n\t}\n}\n\n/** True when a press counts as a tap rather than a drag. */\nexport function isTapGesture(\n\tstart: ViewportPoint,\n\tend: ViewportPoint,\n\ttapThreshold: number,\n): boolean {\n\treturn Math.hypot(end.x - start.x, end.y - start.y) < tapThreshold\n}\n\nexport function usePinchPan(options: PinchPanOptions): {\n\treadonly transform: ViewportTransform\n\treadonly reset: () => void\n} {\n\tconst {\n\t\ttarget,\n\t\tinitial = DEFAULT_TRANSFORM,\n\t\tminScale = 0.5,\n\t\tmaxScale = 4,\n\t\ttapThreshold = 8,\n\t\tresetKey,\n\t\tonChange,\n\t\tonTap,\n\t\tonDoubleTap,\n\t} = options\n\tconst [transform, setTransform] = useState<ViewportTransform>(initial)\n\tconst initialRef = useRef(initial)\n\tinitialRef.current = initial\n\tconst transformRef = useRef(transform)\n\ttransformRef.current = transform\n\tconst sessionRef = useRef<GestureSession | null>(null)\n\tconst lastTapRef = useRef<{\n\t\treadonly point: ViewportPoint\n\t\treadonly time: number\n\t} | null>(null)\n\tconst boundsRef = useRef({ minScale, maxScale, tapThreshold })\n\tboundsRef.current = { minScale, maxScale, tapThreshold }\n\n\tfunction update(next: ViewportTransform) {\n\t\tsetTransform(next)\n\t\tonChange?.(next)\n\t}\n\n\tfunction reset() {\n\t\tupdate({ ...DEFAULT_TRANSFORM })\n\t}\n\n\tuseEffect(() => {\n\t\tif (resetKey === undefined) return\n\t\tupdate({ ...initialRef.current })\n\t}, [resetKey])\n\n\tuseEffect(() => {\n\t\tconst currentTarget = target.current\n\t\tif (currentTarget === null) return\n\t\tconst element: HTMLElement = currentTarget\n\n\t\tfunction startPointer(event: PointerEvent): void {\n\t\t\tconst current = transformRef.current\n\t\t\tconst session = sessionRef.current\n\t\t\tif (session === null) {\n\t\t\t\tsessionRef.current = {\n\t\t\t\t\tpointers: new Map([[event.pointerId, activePointer(event)]]),\n\t\t\t\t\tstart: current,\n\t\t\t\t\tstartDistance: 0,\n\t\t\t\t\tstartMidpoint: eventPoint(event),\n\t\t\t\t\tmoved: false,\n\t\t\t\t}\n\t\t\t\telement.setPointerCapture(event.pointerId)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst pointers = new Map(session.pointers)\n\t\t\tpointers.set(event.pointerId, activePointer(event))\n\t\t\tconst entries = [...pointers.values()]\n\t\t\tif (entries.length >= 2) {\n\t\t\t\tconst first = entries[0]\n\t\t\t\tconst second = entries[1]\n\t\t\t\tif (first !== undefined && second !== undefined) {\n\t\t\t\t\tsessionRef.current = {\n\t\t\t\t\t\tpointers,\n\t\t\t\t\t\tstart: current,\n\t\t\t\t\t\tstartDistance: distance(first, second),\n\t\t\t\t\t\tstartMidpoint: midpoint(first, second),\n\t\t\t\t\t\tmoved: session.moved,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\telement.setPointerCapture(event.pointerId)\n\t\t}\n\n\t\tfunction movePointer(event: PointerEvent): void {\n\t\t\tconst session = sessionRef.current\n\t\t\tif (session === null) return\n\t\t\tconst pointers = new Map(session.pointers)\n\t\t\tpointers.set(\n\t\t\t\tevent.pointerId,\n\t\t\t\tactivePointer(event, session.pointers.get(event.pointerId)),\n\t\t\t)\n\t\t\tconst entries = [...pointers.values()]\n\t\t\tif (entries.length === 0) return\n\n\t\t\tif (entries.length === 1) {\n\t\t\t\tconst pointer = entries[0]\n\t\t\t\tif (pointer === undefined) return\n\t\t\t\tconst delta = {\n\t\t\t\t\tx: pointer.x - pointer.startX,\n\t\t\t\t\ty: pointer.y - pointer.startY,\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\t!isTapGesture(\n\t\t\t\t\t\tsession.startMidpoint,\n\t\t\t\t\t\tpointer,\n\t\t\t\t\t\tboundsRef.current.tapThreshold,\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\tsession.moved = true\n\t\t\t\t}\n\t\t\t\tsessionRef.current = {\n\t\t\t\t\t...session,\n\t\t\t\t\tpointers,\n\t\t\t\t\tmoved:\n\t\t\t\t\t\tsession.moved ||\n\t\t\t\t\t\tmovedBeyondTap(pointer, boundsRef.current.tapThreshold),\n\t\t\t\t}\n\t\t\t\tupdate(panViewport(session.start, delta))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst first = entries[0]\n\t\t\tconst second = entries[1]\n\t\t\tif (first === undefined || second === undefined) return\n\t\t\tconst nextDistance = Math.max(1, distance(first, second))\n\t\t\tconst ratio = nextDistance / Math.max(1, session.startDistance)\n\t\t\tconst nextMidpoint = midpoint(first, second)\n\t\t\tconst centerMidpoint = relativeMidpoint(element, nextMidpoint)\n\t\t\tconst next = zoomViewportAt(\n\t\t\t\tsession.start,\n\t\t\t\tcenterMidpoint,\n\t\t\t\tsession.start.scale * ratio,\n\t\t\t\t{\n\t\t\t\t\tminScale: boundsRef.current.minScale,\n\t\t\t\t\tmaxScale: boundsRef.current.maxScale,\n\t\t\t\t},\n\t\t\t)\n\t\t\tsessionRef.current = { ...session, pointers, moved: true }\n\t\t\tupdate(next)\n\t\t}\n\n\t\tfunction endPointer(event: PointerEvent): void {\n\t\t\tconst session = sessionRef.current\n\t\t\tif (session === null) return\n\t\t\tconst pointers = new Map(session.pointers)\n\t\t\tpointers.delete(event.pointerId)\n\t\t\tif (pointers.size > 0) {\n\t\t\t\tsessionRef.current = { ...session, pointers }\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsessionRef.current = null\n\t\t\tif (!session.moved && session.pointers.size === 1) {\n\t\t\t\tconst point = eventPoint(event)\n\t\t\t\thandleTap(point)\n\t\t\t}\n\t\t}\n\n\t\tfunction cancelPointer(event: PointerEvent): void {\n\t\t\tconst session = sessionRef.current\n\t\t\tif (session === null) return\n\t\t\tconst pointers = new Map(session.pointers)\n\t\t\tpointers.delete(event.pointerId)\n\t\t\tsessionRef.current = pointers.size > 0 ? { ...session, pointers } : null\n\t\t}\n\n\t\tfunction handleTap(point: ViewportPoint): void {\n\t\t\tonTap?.(point)\n\t\t\tconst now = performance.now()\n\t\t\tconst last = lastTapRef.current\n\t\t\tlastTapRef.current = { point, time: now }\n\t\t\tif (\n\t\t\t\tlast !== null &&\n\t\t\t\tnow - last.time < 300 &&\n\t\t\t\tMath.hypot(point.x - last.point.x, point.y - last.point.y) < 30\n\t\t\t) {\n\t\t\t\tlastTapRef.current = null\n\t\t\t\tonDoubleTap?.(point)\n\t\t\t}\n\t\t}\n\n\t\telement.addEventListener(\"pointerdown\", startPointer)\n\t\telement.addEventListener(\"pointermove\", movePointer)\n\t\telement.addEventListener(\"pointerup\", endPointer)\n\t\telement.addEventListener(\"pointercancel\", cancelPointer)\n\t\treturn () => {\n\t\t\telement.removeEventListener(\"pointerdown\", startPointer)\n\t\t\telement.removeEventListener(\"pointermove\", movePointer)\n\t\t\telement.removeEventListener(\"pointerup\", endPointer)\n\t\t\telement.removeEventListener(\"pointercancel\", cancelPointer)\n\t\t}\n\t}, [target, onTap, onDoubleTap, onChange])\n\n\treturn { transform, reset }\n}\n\nfunction activePointer(\n\tevent: PointerEvent,\n\tstart?: ActivePointer | ViewportPoint,\n): ActivePointer {\n\tconst origin = start ?? eventPoint(event)\n\treturn {\n\t\tid: event.pointerId,\n\t\tx: event.clientX,\n\t\ty: event.clientY,\n\t\tstartX: origin.x,\n\t\tstartY: origin.y,\n\t}\n}\n\nfunction eventPoint(event: {\n\treadonly clientX: number\n\treadonly clientY: number\n}): ViewportPoint {\n\treturn { x: event.clientX, y: event.clientY }\n}\n\nfunction distance(a: ViewportPoint, b: ViewportPoint): number {\n\treturn Math.hypot(a.x - b.x, a.y - b.y)\n}\n\nfunction midpoint(a: ViewportPoint, b: ViewportPoint): ViewportPoint {\n\treturn { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }\n}\n\nfunction movedBeyondTap(pointer: ActivePointer, tapThreshold: number): boolean {\n\treturn (\n\t\tisTapGesture(\n\t\t\t{ x: pointer.startX, y: pointer.startY },\n\t\t\t{ x: pointer.x, y: pointer.y },\n\t\t\ttapThreshold,\n\t\t) === false\n\t)\n}\n\nfunction relativeMidpoint(\n\telement: HTMLElement,\n\tpoint: ViewportPoint,\n): ViewportPoint {\n\tconst rect = element.getBoundingClientRect()\n\treturn {\n\t\tx: point.x - (rect.left + rect.width / 2),\n\t\ty: point.y - (rect.top + rect.height / 2),\n\t}\n}\n"]}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Optional navigation-resolver registration injected by the host app.
3
+ *
4
+ * When set (via {@link setNavigationResolver}), `scheduleStateRestore`
5
+ * uses it to receive a deterministic "navigation complete" signal from
6
+ * the router instead of guessing timing via `requestAnimationFrame`.
7
+ *
8
+ * The registered function accepts a one-shot callback and returns an
9
+ * unsubscribe function. The hook calls it, inspects `history.state`
10
+ * inside the callback, then unsubscribes.
11
+ */
12
+ type NavigationResolver = (onResolved: () => void) => () => void;
13
+ /**
14
+ * Registers a navigation-resolver factory so that
15
+ * {@link useMobileBackToClose} can wait for the router to finish
16
+ * flushing its history mutations before inspecting `history.state`.
17
+ *
18
+ * Call this once at app startup, after the router is created:
19
+ *
20
+ * ```ts
21
+ * import { setNavigationResolver } from "@hoardodile/ui"
22
+ * import { router } from "./router"
23
+ *
24
+ * setNavigationResolver((fn) => router.subscribe("onResolved", fn))
25
+ * ```
26
+ *
27
+ * When no resolver is registered, the hook falls back to
28
+ * `requestAnimationFrame` (correct for microtask-based routers but
29
+ * less deterministic).
30
+ */
31
+ declare function setNavigationResolver(resolver: NavigationResolver): void;
32
+ /**
33
+ * On mobile (< sm), when an overlay (dialog/sheet/popover) opens we push a
34
+ * synthetic history entry so the device back gesture closes it instead of
35
+ * navigating away from the page.
36
+ *
37
+ * **Closing via UI** calls `history.back()` to navigate past the
38
+ * synthetic entry, then uses `replaceState` (inside the resulting
39
+ * popstate handler) to restore the original history state.
40
+ *
41
+ * **Closing via back gesture** is handled by the popstate listener,
42
+ * which pops the stack entry and calls the close callback.
43
+ *
44
+ * **Closing via route navigation**: the `pushState` interceptor
45
+ * closes all overlays synchronously but does NOT strip the synthetic
46
+ * marker. The React effect's `scheduleStateRestore` waits for the
47
+ * router's navigation-resolved signal (or falls back to
48
+ * `requestAnimationFrame`); by then the router's push has completed
49
+ * and the marker is gone from `history.state`, so `back()` is
50
+ * skipped. The stranded synthetic entry is later auto-skipped by the
51
+ * capture-phase popstate listener when the user navigates past it.
52
+ *
53
+ * Multiple overlays open at once stack: pressing back closes the most
54
+ * recently opened one first, matching the visual stacking order.
55
+ *
56
+ * No-op on non-mobile viewports and when no `onOpenChange` is provided
57
+ * (uncontrolled overlays cannot be closed externally).
58
+ */
59
+ declare function useMobileBackToClose(open: boolean | undefined, onOpenChange: ((open: boolean) => void) | undefined): void;
60
+
61
+ export { setNavigationResolver, useMobileBackToClose };
@@ -0,0 +1,234 @@
1
+ import 'react-use';
2
+ import { useRef, useEffect } from 'react';
3
+
4
+ // src/viewport.ts
5
+ var MOBILE_BREAKPOINT_PX = 768;
6
+ var MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT_PX - 1}px)`;
7
+ function isBelowMd() {
8
+ if (typeof window === "undefined") return false;
9
+ return window.matchMedia(MOBILE_QUERY).matches;
10
+ }
11
+ var HISTORY_KEY = "__appMobileOverlay";
12
+ var overlayStack = [];
13
+ var popListenerInstalled = false;
14
+ var historyPatched = false;
15
+ var nextOverlayId = 0;
16
+ var suppressPopCount = 0;
17
+ var seenSyntheticIds = /* @__PURE__ */ new Set();
18
+ var navigationResolver;
19
+ function setNavigationResolver(resolver) {
20
+ navigationResolver = resolver;
21
+ }
22
+ function patchHistory() {
23
+ if (historyPatched) return;
24
+ if (typeof window === "undefined") return;
25
+ historyPatched = true;
26
+ const origPush = window.history.pushState.bind(window.history);
27
+ window.history.pushState = (data, unused, url) => {
28
+ const state = data;
29
+ if (state !== null && typeof state === "object" && HISTORY_KEY in state) {
30
+ origPush(data, unused, url);
31
+ return;
32
+ }
33
+ if (overlayStack.length > 0) {
34
+ closeAllOverlays();
35
+ }
36
+ origPush(data, unused, url);
37
+ };
38
+ }
39
+ function closeAllOverlays() {
40
+ while (overlayStack.length > 0) {
41
+ const entry = overlayStack.pop();
42
+ if (entry?.close !== void 0) {
43
+ const fn = entry.close;
44
+ entry.close = void 0;
45
+ fn();
46
+ }
47
+ }
48
+ }
49
+ function ensurePopListener() {
50
+ if (popListenerInstalled) return;
51
+ if (typeof window === "undefined") return;
52
+ popListenerInstalled = true;
53
+ window.addEventListener(
54
+ "popstate",
55
+ (e) => {
56
+ if (suppressPopCount > 0) return;
57
+ const state = e.state;
58
+ if (state !== null && typeof state === "object" && HISTORY_KEY in state) {
59
+ const id = state[HISTORY_KEY];
60
+ if (typeof id === "number" && overlayStack.some((entry) => entry.id === id)) {
61
+ closeTopOverlay();
62
+ e.stopImmediatePropagation();
63
+ return;
64
+ }
65
+ e.stopImmediatePropagation();
66
+ if (typeof id === "number" && seenSyntheticIds.has(id)) {
67
+ window.history.back();
68
+ }
69
+ }
70
+ },
71
+ true
72
+ // capture phase
73
+ );
74
+ window.addEventListener("popstate", handlePop);
75
+ patchHistory();
76
+ }
77
+ function closeTopOverlay() {
78
+ const top = overlayStack.pop();
79
+ if (top === void 0) return;
80
+ if (top.close === void 0) return;
81
+ const closeFn = top.close;
82
+ top.close = void 0;
83
+ closeFn();
84
+ }
85
+ function handlePop() {
86
+ if (suppressPopCount > 0) {
87
+ suppressPopCount--;
88
+ return;
89
+ }
90
+ closeTopOverlay();
91
+ }
92
+ function removeFromStack(id) {
93
+ const idx = overlayStack.findIndex((e) => e.id === id);
94
+ if (idx === -1) return { removed: false, parkedBelow: 0 };
95
+ const wasTop = idx === overlayStack.length - 1;
96
+ if (!wasTop) {
97
+ const entry = overlayStack[idx];
98
+ if (entry !== void 0) entry.close = void 0;
99
+ return { removed: true, parkedBelow: 0 };
100
+ }
101
+ overlayStack.pop();
102
+ const remaining = overlayStack.splice(0, overlayStack.length);
103
+ const active = remaining.filter((e) => e.close !== void 0);
104
+ const parkedBelow = remaining.length - active.length;
105
+ overlayStack.push(...active);
106
+ return { removed: true, parkedBelow };
107
+ }
108
+ function scheduleStateRestore(id, baseState, parkedBelow) {
109
+ function check() {
110
+ const currentState = window.history.state;
111
+ if (currentState === null || typeof currentState !== "object" || !(HISTORY_KEY in currentState) || currentState[HISTORY_KEY] !== id) {
112
+ sweepParkedEntries();
113
+ return;
114
+ }
115
+ const totalBacks = 1 + parkedBelow;
116
+ let done = false;
117
+ function restore() {
118
+ if (done) return;
119
+ done = true;
120
+ window.removeEventListener("popstate", onPop);
121
+ window.history.replaceState(baseState, "");
122
+ const sweptIds = sweepParkedEntries();
123
+ cleanupSeenIds(sweptIds);
124
+ }
125
+ function onPop() {
126
+ if (suppressPopCount > 0) {
127
+ suppressPopCount--;
128
+ return;
129
+ }
130
+ restore();
131
+ }
132
+ window.addEventListener("popstate", onPop);
133
+ suppressPopCount += totalBacks;
134
+ for (let i = 0; i < totalBacks; i++) {
135
+ window.history.back();
136
+ }
137
+ }
138
+ let disposed = false;
139
+ let rafId;
140
+ let unsub;
141
+ function run() {
142
+ if (disposed) return;
143
+ disposed = true;
144
+ unsub?.();
145
+ if (rafId !== void 0) {
146
+ cancelAnimationFrame(rafId);
147
+ }
148
+ check();
149
+ }
150
+ if (navigationResolver !== void 0) {
151
+ unsub = navigationResolver(run);
152
+ }
153
+ rafId = requestAnimationFrame(run);
154
+ }
155
+ function sweepParkedEntries() {
156
+ const sweptIds = [];
157
+ for (let i = overlayStack.length - 1; i >= 0; i--) {
158
+ const entry = overlayStack[i];
159
+ if (entry !== void 0 && entry.close === void 0) {
160
+ sweptIds.push(entry.id);
161
+ overlayStack.splice(i, 1);
162
+ }
163
+ }
164
+ return sweptIds;
165
+ }
166
+ function cleanupSeenIds(sweptIds) {
167
+ const activeIds = new Set(overlayStack.map((e) => e.id));
168
+ for (const id of sweptIds) {
169
+ if (!activeIds.has(id)) {
170
+ seenSyntheticIds.delete(id);
171
+ }
172
+ }
173
+ }
174
+ function useMobileBackToClose(open, onOpenChange) {
175
+ const idRef = useRef(void 0);
176
+ const onChangeRef = useRef(onOpenChange);
177
+ onChangeRef.current = onOpenChange;
178
+ useEffect(() => {
179
+ if (typeof window === "undefined") return;
180
+ if (onChangeRef.current === void 0) return;
181
+ if (!isBelowMd()) return;
182
+ ensurePopListener();
183
+ if (open === true && idRef.current === void 0) {
184
+ const id = ++nextOverlayId;
185
+ idRef.current = id;
186
+ seenSyntheticIds.add(id);
187
+ const baseState = window.history.state ?? null;
188
+ window.history.pushState(
189
+ {
190
+ ...baseState ?? {},
191
+ [HISTORY_KEY]: id
192
+ },
193
+ ""
194
+ );
195
+ overlayStack.push({
196
+ id,
197
+ baseState,
198
+ close() {
199
+ idRef.current = void 0;
200
+ onChangeRef.current?.(false);
201
+ }
202
+ });
203
+ return;
204
+ }
205
+ if (open !== true && idRef.current !== void 0) {
206
+ const id = idRef.current;
207
+ idRef.current = void 0;
208
+ const entryIdx = overlayStack.findIndex((e) => e.id === id);
209
+ const entry = entryIdx !== -1 ? overlayStack[entryIdx] : void 0;
210
+ const { removed, parkedBelow } = removeFromStack(id);
211
+ if (removed && entry !== void 0) {
212
+ scheduleStateRestore(id, entry.baseState, parkedBelow);
213
+ }
214
+ }
215
+ }, [open]);
216
+ useEffect(() => {
217
+ return () => {
218
+ if (idRef.current !== void 0) {
219
+ const id = idRef.current;
220
+ idRef.current = void 0;
221
+ const entryIdx = overlayStack.findIndex((e) => e.id === id);
222
+ const entry = entryIdx !== -1 ? overlayStack[entryIdx] : void 0;
223
+ const { removed, parkedBelow } = removeFromStack(id);
224
+ if (removed && entry !== void 0) {
225
+ scheduleStateRestore(id, entry.baseState, parkedBelow);
226
+ }
227
+ }
228
+ };
229
+ }, []);
230
+ }
231
+
232
+ export { setNavigationResolver, useMobileBackToClose };
233
+ //# sourceMappingURL=useMobileBackToClose.js.map
234
+ //# sourceMappingURL=useMobileBackToClose.js.map