@ahrowe/ui 0.34.0 → 0.35.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 (516) hide show
  1. package/README.md +101 -40
  2. package/dist/esm/common/accordion/accordion.mjs +1 -1
  3. package/dist/esm/common/accordion/accordion.mjs.map +1 -1
  4. package/dist/esm/common/accordion/accordion.module.mjs +1 -1
  5. package/dist/esm/common/accordion/accordion.module.mjs.map +1 -1
  6. package/dist/esm/common/accordion/accordion.plain.module.mjs +1 -1
  7. package/dist/esm/common/accordion/accordion.plain.module.mjs.map +1 -1
  8. package/dist/esm/common/accordion/accordion.primary.module.mjs +1 -1
  9. package/dist/esm/common/accordion/accordion.primary.module.mjs.map +1 -1
  10. package/dist/esm/common/accordion/accordion.types.mjs +1 -1
  11. package/dist/esm/common/accordion/accordion.types.mjs.map +1 -1
  12. package/dist/esm/common/actionButtons/actionButtons.mjs +1 -1
  13. package/dist/esm/common/actionButtons/actionButtons.mjs.map +1 -1
  14. package/dist/esm/common/actionButtons/actionButtons.module.mjs +1 -1
  15. package/dist/esm/common/actionIcon/actionIcon.mjs +1 -1
  16. package/dist/esm/common/actionIcon/actionIcon.mjs.map +1 -1
  17. package/dist/esm/common/actionIcon/actionIcon.module.mjs +1 -1
  18. package/dist/esm/common/alert/alert.mjs +1 -1
  19. package/dist/esm/common/alert/alert.mjs.map +1 -1
  20. package/dist/esm/common/alert/alert.module.mjs +1 -1
  21. package/dist/esm/common/alert/alert.types.mjs +1 -1
  22. package/dist/esm/common/alert/alert.types.mjs.map +1 -1
  23. package/dist/esm/common/animatedIcon/animatedIcon.mjs +1 -1
  24. package/dist/esm/common/animatedIcon/animatedIcon.mjs.map +1 -1
  25. package/dist/esm/common/animatedIcon/animatedIcon.module.mjs +1 -1
  26. package/dist/esm/common/animatedLogo/animatedLogo.mjs +1 -1
  27. package/dist/esm/common/animatedLogo/animatedLogo.mjs.map +1 -1
  28. package/dist/esm/common/animatedLogo/animatedLogo.module.mjs +1 -1
  29. package/dist/esm/common/animatedText/animatedText.mjs +1 -1
  30. package/dist/esm/common/animatedText/animatedText.mjs.map +1 -1
  31. package/dist/esm/common/animatedText/animatedText.module.mjs +1 -1
  32. package/dist/esm/common/animatedText/animatedText.types.mjs +1 -1
  33. package/dist/esm/common/animatedText/animatedText.types.mjs.map +1 -1
  34. package/dist/esm/common/avatar/avatar.mjs +1 -1
  35. package/dist/esm/common/avatar/avatar.mjs.map +1 -1
  36. package/dist/esm/common/avatar/avatar.module.mjs +1 -1
  37. package/dist/esm/common/avatar/avatarGroup.mjs +1 -1
  38. package/dist/esm/common/avatar/avatarGroup.mjs.map +1 -1
  39. package/dist/esm/common/badge/badge.mjs +1 -1
  40. package/dist/esm/common/badge/badge.mjs.map +1 -1
  41. package/dist/esm/common/badge/badge.module.mjs +1 -1
  42. package/dist/esm/common/badge/badge.types.mjs +1 -1
  43. package/dist/esm/common/badge/badge.types.mjs.map +1 -1
  44. package/dist/esm/common/bodyEnd/bodyEnd.mjs +1 -1
  45. package/dist/esm/common/bodyEnd/bodyEnd.mjs.map +1 -1
  46. package/dist/esm/common/breadcrumb/breadcrumb.mjs +1 -1
  47. package/dist/esm/common/breadcrumb/breadcrumb.mjs.map +1 -1
  48. package/dist/esm/common/breadcrumb/breadcrumb.module.mjs +1 -1
  49. package/dist/esm/common/button/button.mjs +1 -1
  50. package/dist/esm/common/button/button.mjs.map +1 -1
  51. package/dist/esm/common/button/button.module.mjs +1 -1
  52. package/dist/esm/common/button/button.types.mjs +1 -1
  53. package/dist/esm/common/button/button.types.mjs.map +1 -1
  54. package/dist/esm/common/buttonGroup/buttonGroup.mjs +1 -1
  55. package/dist/esm/common/buttonGroup/buttonGroup.mjs.map +1 -1
  56. package/dist/esm/common/buttonGroup/buttonGroup.module.mjs +1 -1
  57. package/dist/esm/common/buttonGroup/buttonGroup.types.mjs +1 -1
  58. package/dist/esm/common/buttonGroup/buttonGroup.types.mjs.map +1 -1
  59. package/dist/esm/common/card/Card.mjs +1 -1
  60. package/dist/esm/common/card/Card.mjs.map +1 -1
  61. package/dist/esm/common/card/CardActions.mjs +1 -1
  62. package/dist/esm/common/card/CardActions.mjs.map +1 -1
  63. package/dist/esm/common/card/CardContent.mjs +1 -1
  64. package/dist/esm/common/card/CardContent.mjs.map +1 -1
  65. package/dist/esm/common/card/CardHeader.mjs +1 -1
  66. package/dist/esm/common/card/CardHeader.mjs.map +1 -1
  67. package/dist/esm/common/card/CardMedia.mjs +1 -1
  68. package/dist/esm/common/card/CardMedia.mjs.map +1 -1
  69. package/dist/esm/common/card/card.module.mjs +1 -1
  70. package/dist/esm/common/card/card.module.mjs.map +1 -1
  71. package/dist/esm/common/card/card.types.mjs +1 -1
  72. package/dist/esm/common/card/card.types.mjs.map +1 -1
  73. package/dist/esm/common/carousel/carousel.mjs +1 -1
  74. package/dist/esm/common/carousel/carousel.mjs.map +1 -1
  75. package/dist/esm/common/carousel/carousel.module.mjs +1 -1
  76. package/dist/esm/common/checkbox/checkbox.mjs +1 -1
  77. package/dist/esm/common/checkbox/checkbox.mjs.map +1 -1
  78. package/dist/esm/common/checkbox/checkbox.module.mjs +1 -1
  79. package/dist/esm/common/checkbox/checkbox.module.mjs.map +1 -1
  80. package/dist/esm/common/chip/chip.mjs +1 -1
  81. package/dist/esm/common/chip/chip.mjs.map +1 -1
  82. package/dist/esm/common/chip/chip.module.mjs +1 -1
  83. package/dist/esm/common/chip/chip.types.mjs +1 -1
  84. package/dist/esm/common/chip/chip.types.mjs.map +1 -1
  85. package/dist/esm/common/chip/chipContainer.mjs +1 -1
  86. package/dist/esm/common/chip/chipContainer.mjs.map +1 -1
  87. package/dist/esm/common/colorPicker/colorPicker.mjs +1 -1
  88. package/dist/esm/common/colorPicker/colorPicker.mjs.map +1 -1
  89. package/dist/esm/common/colorPicker/colorPicker.module.mjs +1 -1
  90. package/dist/esm/common/colorPicker/colorPicker.module.mjs.map +1 -1
  91. package/dist/esm/common/colorPicker/toHex.mjs +1 -1
  92. package/dist/esm/common/colorPicker/toHex.mjs.map +1 -1
  93. package/dist/esm/common/configProvider/configProvider.mjs +1 -1
  94. package/dist/esm/common/configProvider/configProvider.mjs.map +1 -1
  95. package/dist/esm/common/configProvider/presentation.types.mjs +1 -1
  96. package/dist/esm/common/configProvider/presentation.types.mjs.map +1 -1
  97. package/dist/esm/common/configProvider/useComponentDefaults.mjs +1 -1
  98. package/dist/esm/common/configProvider/useComponentDefaults.mjs.map +1 -1
  99. package/dist/esm/common/configProvider/useLabels.mjs +1 -1
  100. package/dist/esm/common/configProvider/useLabels.mjs.map +1 -1
  101. package/dist/esm/common/configProvider/usePresentationDefault.mjs +1 -1
  102. package/dist/esm/common/configProvider/usePresentationDefault.mjs.map +1 -1
  103. package/dist/esm/common/confirmModal/confirmModal.mjs +1 -1
  104. package/dist/esm/common/confirmModal/confirmModal.mjs.map +1 -1
  105. package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.mjs +1 -1
  106. package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.mjs.map +1 -1
  107. package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.module.mjs +1 -1
  108. package/dist/esm/common/datePicker/dateFunctions.mjs +1 -1
  109. package/dist/esm/common/datePicker/dateFunctions.mjs.map +1 -1
  110. package/dist/esm/common/datePicker/datePicker.mjs +1 -1
  111. package/dist/esm/common/datePicker/datePicker.mjs.map +1 -1
  112. package/dist/esm/common/datePicker/datePicker.module.mjs +1 -1
  113. package/dist/esm/common/divider/divider.mjs +1 -1
  114. package/dist/esm/common/divider/divider.mjs.map +1 -1
  115. package/dist/esm/common/divider/divider.module.mjs +1 -1
  116. package/dist/esm/common/divider/divider.types.mjs +1 -1
  117. package/dist/esm/common/divider/divider.types.mjs.map +1 -1
  118. package/dist/esm/common/drawer/drawer.mjs +1 -1
  119. package/dist/esm/common/drawer/drawer.mjs.map +1 -1
  120. package/dist/esm/common/drawer/drawer.module.mjs +1 -1
  121. package/dist/esm/common/dropZone/dropZone.mjs +1 -1
  122. package/dist/esm/common/dropZone/dropZone.mjs.map +1 -1
  123. package/dist/esm/common/dropZone/dropZone.module.mjs +1 -1
  124. package/dist/esm/common/dropZone/dropZone.types.mjs +1 -1
  125. package/dist/esm/common/dropZone/dropZone.types.mjs.map +1 -1
  126. package/dist/esm/common/dropdown/dropdown.mjs +1 -1
  127. package/dist/esm/common/dropdown/dropdown.mjs.map +1 -1
  128. package/dist/esm/common/dropdown/dropdown.module.mjs +1 -1
  129. package/dist/esm/common/dropdown/dropdown.module.mjs.map +1 -1
  130. package/dist/esm/common/emptyState/emptyState.mjs +1 -1
  131. package/dist/esm/common/emptyState/emptyState.mjs.map +1 -1
  132. package/dist/esm/common/emptyState/emptyState.module.mjs +1 -1
  133. package/dist/esm/common/errorBoundary/errorBoundary.mjs +1 -1
  134. package/dist/esm/common/errorBoundary/errorBoundary.mjs.map +1 -1
  135. package/dist/esm/common/errorBoundary/errorBoundary.module.mjs +1 -1
  136. package/dist/esm/common/fab/components/fabBase/fabBase.mjs +1 -1
  137. package/dist/esm/common/fab/components/fabBase/fabBase.mjs.map +1 -1
  138. package/dist/esm/common/fab/components/fabBase/fabBase.module.mjs +1 -1
  139. package/dist/esm/common/fab/fab.mjs +1 -1
  140. package/dist/esm/common/fab/fab.mjs.map +1 -1
  141. package/dist/esm/common/fab/fab.module.mjs +1 -1
  142. package/dist/esm/common/fab/fab.module.mjs.map +1 -1
  143. package/dist/esm/common/flip/flip.mjs +1 -1
  144. package/dist/esm/common/flip/flip.mjs.map +1 -1
  145. package/dist/esm/common/flip/flip.module.mjs +1 -1
  146. package/dist/esm/common/floorPlan/floorPlan.faces.mjs +1 -1
  147. package/dist/esm/common/floorPlan/floorPlan.faces.mjs.map +1 -1
  148. package/dist/esm/common/floorPlan/floorPlan.geometry.mjs +1 -1
  149. package/dist/esm/common/floorPlan/floorPlan.geometry.mjs.map +1 -1
  150. package/dist/esm/common/floorPlan/floorPlan.graph.mjs +1 -1
  151. package/dist/esm/common/floorPlan/floorPlan.graph.mjs.map +1 -1
  152. package/dist/esm/common/floorPlan/floorPlan.hit.mjs +1 -1
  153. package/dist/esm/common/floorPlan/floorPlan.hit.mjs.map +1 -1
  154. package/dist/esm/common/floorPlan/floorPlan.json.mjs +1 -1
  155. package/dist/esm/common/floorPlan/floorPlan.json.mjs.map +1 -1
  156. package/dist/esm/common/floorPlan/floorPlan.mesh.mjs +1 -1
  157. package/dist/esm/common/floorPlan/floorPlan.mesh.mjs.map +1 -1
  158. package/dist/esm/common/floorPlan/floorPlan.path.mjs +1 -1
  159. package/dist/esm/common/floorPlan/floorPlan.path.mjs.map +1 -1
  160. package/dist/esm/common/floorPlan/floorPlan.plan.mjs +1 -1
  161. package/dist/esm/common/floorPlan/floorPlan.plan.mjs.map +1 -1
  162. package/dist/esm/common/floorPlan/floorPlan.rooms.mjs +1 -1
  163. package/dist/esm/common/floorPlan/floorPlan.rooms.mjs.map +1 -1
  164. package/dist/esm/common/floorPlan/floorPlan.snap.mjs +1 -1
  165. package/dist/esm/common/floorPlan/floorPlan.snap.mjs.map +1 -1
  166. package/dist/esm/common/floorPlan/floorPlan.triangulate.mjs +1 -1
  167. package/dist/esm/common/floorPlan/floorPlan.triangulate.mjs.map +1 -1
  168. package/dist/esm/common/floorPlan/floorPlan.types.mjs +1 -1
  169. package/dist/esm/common/floorPlan/floorPlan.types.mjs.map +1 -1
  170. package/dist/esm/common/floorPlan/legacySvg.mjs +1 -1
  171. package/dist/esm/common/floorPlan/legacySvg.mjs.map +1 -1
  172. package/dist/esm/common/hooks/useAnchorTracking.mjs +1 -1
  173. package/dist/esm/common/hooks/useAnchorTracking.mjs.map +1 -1
  174. package/dist/esm/common/hooks/useCoarsePointer.mjs +1 -1
  175. package/dist/esm/common/hooks/useCoarsePointer.mjs.map +1 -1
  176. package/dist/esm/common/hooks/useFocusBoundary.mjs +1 -1
  177. package/dist/esm/common/hooks/useFocusBoundary.mjs.map +1 -1
  178. package/dist/esm/common/hooks/useMediaQuery.mjs +1 -1
  179. package/dist/esm/common/hooks/useMediaQuery.mjs.map +1 -1
  180. package/dist/esm/common/hooks/useOverlay.mjs +1 -1
  181. package/dist/esm/common/hooks/useOverlay.mjs.map +1 -1
  182. package/dist/esm/common/hooks/useSheet.mjs +1 -1
  183. package/dist/esm/common/hooks/useSheet.mjs.map +1 -1
  184. package/dist/esm/common/hooks/useSwipeDismiss.mjs +1 -1
  185. package/dist/esm/common/hooks/useSwipeDismiss.mjs.map +1 -1
  186. package/dist/esm/common/hooks/useVisualViewportSize.mjs +1 -1
  187. package/dist/esm/common/hooks/useVisualViewportSize.mjs.map +1 -1
  188. package/dist/esm/common/iconPicker/iconPicker.mjs +1 -1
  189. package/dist/esm/common/iconPicker/iconPicker.mjs.map +1 -1
  190. package/dist/esm/common/iconPicker/iconPicker.module.mjs +1 -1
  191. package/dist/esm/common/idleManager/idleManager.mjs +1 -1
  192. package/dist/esm/common/idleManager/idleManager.mjs.map +1 -1
  193. package/dist/esm/common/infiniteBlock/infiniteBlock.mjs +1 -1
  194. package/dist/esm/common/infiniteBlock/infiniteBlock.mjs.map +1 -1
  195. package/dist/esm/common/infiniteBlock/infiniteBlock.module.mjs +1 -1
  196. package/dist/esm/common/input/input.mjs +1 -1
  197. package/dist/esm/common/input/input.mjs.map +1 -1
  198. package/dist/esm/common/input/input.module.mjs +1 -1
  199. package/dist/esm/common/input/input.module.mjs.map +1 -1
  200. package/dist/esm/common/input/input.types.mjs +1 -1
  201. package/dist/esm/common/input/input.types.mjs.map +1 -1
  202. package/dist/esm/common/inputDropdown/inputDropdown.mjs +1 -1
  203. package/dist/esm/common/inputDropdown/inputDropdown.mjs.map +1 -1
  204. package/dist/esm/common/inputDropdown/inputDropdown.module.mjs +1 -1
  205. package/dist/esm/common/interactableDiv/index.mjs +1 -1
  206. package/dist/esm/common/interactableDiv/index.mjs.map +1 -1
  207. package/dist/esm/common/interactableDiv/interactableDiv.mjs +1 -1
  208. package/dist/esm/common/interactableDiv/interactableDiv.mjs.map +1 -1
  209. package/dist/esm/common/interactableDiv/interactableDiv.module.mjs +1 -1
  210. package/dist/esm/common/kanbanBoard/KanbanBoard.mjs +1 -1
  211. package/dist/esm/common/kanbanBoard/KanbanBoard.mjs.map +1 -1
  212. package/dist/esm/common/kanbanBoard/KanbanColumn.mjs +1 -1
  213. package/dist/esm/common/kanbanBoard/KanbanColumn.mjs.map +1 -1
  214. package/dist/esm/common/kanbanBoard/KanbanItem.mjs +1 -1
  215. package/dist/esm/common/kanbanBoard/KanbanItem.mjs.map +1 -1
  216. package/dist/esm/common/kanbanBoard/kanbanBoard.module.mjs +1 -1
  217. package/dist/esm/common/kanbanBoard/kanbanBoard.utils.mjs +1 -1
  218. package/dist/esm/common/kanbanBoard/kanbanBoard.utils.mjs.map +1 -1
  219. package/dist/esm/common/klipyPicker/components/gifPreview/gifPreview.mjs +1 -1
  220. package/dist/esm/common/klipyPicker/components/gifPreview/gifPreview.mjs.map +1 -1
  221. package/dist/esm/common/klipyPicker/components/gifPreview/gifPreview.module.mjs +1 -1
  222. package/dist/esm/common/klipyPicker/components/gifView/gifView.mjs +1 -1
  223. package/dist/esm/common/klipyPicker/components/gifView/gifView.mjs.map +1 -1
  224. package/dist/esm/common/klipyPicker/components/gifView/gifView.module.mjs +1 -1
  225. package/dist/esm/common/klipyPicker/klipyPicker.mjs +1 -1
  226. package/dist/esm/common/klipyPicker/klipyPicker.mjs.map +1 -1
  227. package/dist/esm/common/klipyPicker/klipyPicker.module.mjs +1 -1
  228. package/dist/esm/common/loading/iconLoading/iconLoading.mjs +1 -1
  229. package/dist/esm/common/loading/iconLoading/iconLoading.mjs.map +1 -1
  230. package/dist/esm/common/loading/iconLoading/iconLoading.module.mjs +1 -1
  231. package/dist/esm/common/loading/iconLoading/index.mjs +1 -1
  232. package/dist/esm/common/loading/iconLoading/index.mjs.map +1 -1
  233. package/dist/esm/common/loading/spinnerLoading/index.mjs +1 -1
  234. package/dist/esm/common/loading/spinnerLoading/index.mjs.map +1 -1
  235. package/dist/esm/common/loading/spinnerLoading/spinnerLoading.mjs +1 -1
  236. package/dist/esm/common/loading/spinnerLoading/spinnerLoading.mjs.map +1 -1
  237. package/dist/esm/common/loading/spinnerLoading/spinnerLoading.module.mjs +1 -1
  238. package/dist/esm/common/menu/menu.mjs +1 -1
  239. package/dist/esm/common/menu/menu.mjs.map +1 -1
  240. package/dist/esm/common/menu/menu.module.mjs +1 -1
  241. package/dist/esm/common/menu/menu.types.mjs +1 -1
  242. package/dist/esm/common/menu/menu.types.mjs.map +1 -1
  243. package/dist/esm/common/modal/modal.mjs +1 -1
  244. package/dist/esm/common/modal/modal.mjs.map +1 -1
  245. package/dist/esm/common/modal/modal.module.mjs +1 -1
  246. package/dist/esm/common/modal/modal.module.mjs.map +1 -1
  247. package/dist/esm/common/multiSelect/multiSelect.mjs +1 -1
  248. package/dist/esm/common/multiSelect/multiSelect.mjs.map +1 -1
  249. package/dist/esm/common/multiSelect/multiSelect.module.mjs +1 -1
  250. package/dist/esm/common/multiSelect/multiSelect.module.mjs.map +1 -1
  251. package/dist/esm/common/numberInput/index.mjs +1 -1
  252. package/dist/esm/common/numberInput/index.mjs.map +1 -1
  253. package/dist/esm/common/numberInput/numberInput.mjs +1 -1
  254. package/dist/esm/common/numberInput/numberInput.mjs.map +1 -1
  255. package/dist/esm/common/numberInput/numberInput.module.mjs +1 -1
  256. package/dist/esm/common/numberInput/useStepRepeat.mjs +1 -1
  257. package/dist/esm/common/numberInput/useStepRepeat.mjs.map +1 -1
  258. package/dist/esm/common/optionPicker/optionPicker.mjs +1 -1
  259. package/dist/esm/common/optionPicker/optionPicker.mjs.map +1 -1
  260. package/dist/esm/common/optionPicker/optionPicker.module.mjs +1 -1
  261. package/dist/esm/common/otpInput/otpInput.mjs +1 -1
  262. package/dist/esm/common/otpInput/otpInput.mjs.map +1 -1
  263. package/dist/esm/common/otpInput/otpInput.module.mjs +1 -1
  264. package/dist/esm/common/otpInput/otpInput.types.mjs +1 -1
  265. package/dist/esm/common/otpInput/otpInput.types.mjs.map +1 -1
  266. package/dist/esm/common/overscroll/overscroll.mjs +1 -1
  267. package/dist/esm/common/overscroll/overscroll.mjs.map +1 -1
  268. package/dist/esm/common/overscroll/overscroll.module.mjs +1 -1
  269. package/dist/esm/common/pagination/pagination.mjs +1 -1
  270. package/dist/esm/common/pagination/pagination.mjs.map +1 -1
  271. package/dist/esm/common/pagination/pagination.module.mjs +1 -1
  272. package/dist/esm/common/planCanvas/planCanvas.mjs +1 -1
  273. package/dist/esm/common/planCanvas/planCanvas.mjs.map +1 -1
  274. package/dist/esm/common/planCanvas/planCanvas.module.mjs +1 -1
  275. package/dist/esm/common/popover/popover.mjs +1 -1
  276. package/dist/esm/common/popover/popover.mjs.map +1 -1
  277. package/dist/esm/common/popover/popover.module.mjs +1 -1
  278. package/dist/esm/common/popover/popover.types.mjs +1 -1
  279. package/dist/esm/common/popover/popover.types.mjs.map +1 -1
  280. package/dist/esm/common/popover/usePopoverPosition.mjs +1 -1
  281. package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -1
  282. package/dist/esm/common/progressBar/progressBar.mjs +1 -1
  283. package/dist/esm/common/progressBar/progressBar.mjs.map +1 -1
  284. package/dist/esm/common/progressBar/progressBar.module.mjs +1 -1
  285. package/dist/esm/common/progressBar/progressBar.module.mjs.map +1 -1
  286. package/dist/esm/common/radioGroup/radioGroup.mjs +1 -1
  287. package/dist/esm/common/radioGroup/radioGroup.mjs.map +1 -1
  288. package/dist/esm/common/radioGroup/radioGroup.module.mjs +1 -1
  289. package/dist/esm/common/radioGroup/radioGroup.module.mjs.map +1 -1
  290. package/dist/esm/common/radioGroup/radioGroup.types.mjs +1 -1
  291. package/dist/esm/common/radioGroup/radioGroup.types.mjs.map +1 -1
  292. package/dist/esm/common/rating/rating.mjs +1 -1
  293. package/dist/esm/common/rating/rating.mjs.map +1 -1
  294. package/dist/esm/common/rating/rating.module.mjs +1 -1
  295. package/dist/esm/common/revealLens/revealLens.mjs +1 -1
  296. package/dist/esm/common/revealLens/revealLens.mjs.map +1 -1
  297. package/dist/esm/common/revealLens/revealLens.module.mjs +1 -1
  298. package/dist/esm/common/ripple/ripple.mjs +1 -1
  299. package/dist/esm/common/ripple/ripple.mjs.map +1 -1
  300. package/dist/esm/common/ripple/ripple.module.mjs +1 -1
  301. package/dist/esm/common/ripple/ripple.module.mjs.map +1 -1
  302. package/dist/esm/common/roomDrawer/roomDrawer.mjs +1 -1
  303. package/dist/esm/common/roomDrawer/roomDrawer.mjs.map +1 -1
  304. package/dist/esm/common/roomDrawer/roomDrawer.module.mjs +1 -1
  305. package/dist/esm/common/roomDrawer/usePlanHistory.mjs +1 -1
  306. package/dist/esm/common/roomDrawer/usePlanHistory.mjs.map +1 -1
  307. package/dist/esm/common/roomViewer/roomViewer.mjs +1 -1
  308. package/dist/esm/common/roomViewer/roomViewer.mjs.map +1 -1
  309. package/dist/esm/common/roomViewer/roomViewer.module.mjs +1 -1
  310. package/dist/esm/common/scrollbarProvider/index.mjs +1 -1
  311. package/dist/esm/common/scrollbarProvider/index.mjs.map +1 -1
  312. package/dist/esm/common/scrollbarProvider/scrollbarProvider.mjs +1 -1
  313. package/dist/esm/common/scrollbarProvider/scrollbarProvider.mjs.map +1 -1
  314. package/dist/esm/common/scrollbarProvider/scrollbarProvider.module.mjs +1 -1
  315. package/dist/esm/common/searchInput/searchInput.mjs +1 -1
  316. package/dist/esm/common/searchInput/searchInput.mjs.map +1 -1
  317. package/dist/esm/common/searchInput/searchInput.module.mjs +1 -1
  318. package/dist/esm/common/sectionHeader/sectionHeader.mjs +1 -1
  319. package/dist/esm/common/sectionHeader/sectionHeader.mjs.map +1 -1
  320. package/dist/esm/common/sectionHeader/sectionHeader.module.mjs +1 -1
  321. package/dist/esm/common/sectionHeader/sectionHeader.types.mjs +1 -1
  322. package/dist/esm/common/sectionHeader/sectionHeader.types.mjs.map +1 -1
  323. package/dist/esm/common/sheetBackdrop/sheetBackdrop.mjs +1 -1
  324. package/dist/esm/common/sheetBackdrop/sheetBackdrop.mjs.map +1 -1
  325. package/dist/esm/common/skeleton/skeleton.mjs +1 -1
  326. package/dist/esm/common/skeleton/skeleton.mjs.map +1 -1
  327. package/dist/esm/common/skeleton/skeleton.module.mjs +1 -1
  328. package/dist/esm/common/skeleton/skeleton.types.mjs +1 -1
  329. package/dist/esm/common/skeleton/skeleton.types.mjs.map +1 -1
  330. package/dist/esm/common/slider/slider.mjs +1 -1
  331. package/dist/esm/common/slider/slider.mjs.map +1 -1
  332. package/dist/esm/common/slider/slider.module.mjs +1 -1
  333. package/dist/esm/common/splitButton/splitButton.mjs +1 -1
  334. package/dist/esm/common/splitButton/splitButton.mjs.map +1 -1
  335. package/dist/esm/common/splitButton/splitButton.module.mjs +1 -1
  336. package/dist/esm/common/stepper/stepper.mjs +1 -1
  337. package/dist/esm/common/stepper/stepper.mjs.map +1 -1
  338. package/dist/esm/common/stepper/stepper.module.mjs +1 -1
  339. package/dist/esm/common/stepper/stepper.types.mjs +1 -1
  340. package/dist/esm/common/stepper/stepper.types.mjs.map +1 -1
  341. package/dist/esm/common/sticky/sticky.mjs +1 -1
  342. package/dist/esm/common/sticky/sticky.mjs.map +1 -1
  343. package/dist/esm/common/sticky/sticky.module.mjs +1 -1
  344. package/dist/esm/common/sticky/stickyStack.mjs +1 -1
  345. package/dist/esm/common/sticky/stickyStack.mjs.map +1 -1
  346. package/dist/esm/common/styles/sheet.module.mjs +1 -1
  347. package/dist/esm/common/styles/sheet.module.mjs.map +1 -1
  348. package/dist/esm/common/switch/switch.mjs +1 -1
  349. package/dist/esm/common/switch/switch.mjs.map +1 -1
  350. package/dist/esm/common/switch/switch.module.mjs +1 -1
  351. package/dist/esm/common/switch/switch.module.mjs.map +1 -1
  352. package/dist/esm/common/tabHeader/tabHeader.mjs +1 -1
  353. package/dist/esm/common/tabHeader/tabHeader.mjs.map +1 -1
  354. package/dist/esm/common/tabHeader/tabHeader.module.mjs +1 -1
  355. package/dist/esm/common/textarea/textarea.mjs +1 -1
  356. package/dist/esm/common/textarea/textarea.mjs.map +1 -1
  357. package/dist/esm/common/themeProvider/defaultTheme.mjs +1 -1
  358. package/dist/esm/common/themeProvider/defaultTheme.mjs.map +1 -1
  359. package/dist/esm/common/themeProvider/lightTheme.mjs +2 -0
  360. package/dist/esm/common/themeProvider/lightTheme.mjs.map +1 -0
  361. package/dist/esm/common/themeProvider/theme.module.mjs +1 -1
  362. package/dist/esm/common/themeProvider/themeProvider.mjs +1 -1
  363. package/dist/esm/common/themeProvider/themeProvider.mjs.map +1 -1
  364. package/dist/esm/common/themeProvider/themeVariablesContext.mjs +1 -1
  365. package/dist/esm/common/themeProvider/themeVariablesContext.mjs.map +1 -1
  366. package/dist/esm/common/themeProvider/useTheme.mjs +1 -1
  367. package/dist/esm/common/themeProvider/useTheme.mjs.map +1 -1
  368. package/dist/esm/common/tilt/tilt.mjs +1 -1
  369. package/dist/esm/common/tilt/tilt.mjs.map +1 -1
  370. package/dist/esm/common/tilt/tilt.module.mjs +1 -1
  371. package/dist/esm/common/tilt/tiltMath.mjs +1 -1
  372. package/dist/esm/common/tilt/tiltMath.mjs.map +1 -1
  373. package/dist/esm/common/tiltGroup/tiltGroup.mjs +1 -1
  374. package/dist/esm/common/tiltGroup/tiltGroup.mjs.map +1 -1
  375. package/dist/esm/common/timeInput/components/clockDial/clockDial.mjs +1 -1
  376. package/dist/esm/common/timeInput/components/clockDial/clockDial.mjs.map +1 -1
  377. package/dist/esm/common/timeInput/components/clockDial/clockDial.module.mjs +1 -1
  378. package/dist/esm/common/timeInput/timeFunctions.mjs +1 -1
  379. package/dist/esm/common/timeInput/timeFunctions.mjs.map +1 -1
  380. package/dist/esm/common/timeInput/timeInput.mjs +1 -1
  381. package/dist/esm/common/timeInput/timeInput.mjs.map +1 -1
  382. package/dist/esm/common/timeInput/timeInput.module.mjs +1 -1
  383. package/dist/esm/common/timeline/timeline.mjs +1 -1
  384. package/dist/esm/common/timeline/timeline.mjs.map +1 -1
  385. package/dist/esm/common/timeline/timeline.module.mjs +1 -1
  386. package/dist/esm/common/timeline/timeline.types.mjs +1 -1
  387. package/dist/esm/common/timeline/timeline.types.mjs.map +1 -1
  388. package/dist/esm/common/timer/timer.chime.mjs +1 -1
  389. package/dist/esm/common/timer/timer.chime.mjs.map +1 -1
  390. package/dist/esm/common/timer/timer.mjs +1 -1
  391. package/dist/esm/common/timer/timer.mjs.map +1 -1
  392. package/dist/esm/common/timer/timer.module.mjs +1 -1
  393. package/dist/esm/common/toast/toast.mjs +1 -1
  394. package/dist/esm/common/toast/toast.mjs.map +1 -1
  395. package/dist/esm/common/toast/toast.module.mjs +1 -1
  396. package/dist/esm/common/toast/toast.module.mjs.map +1 -1
  397. package/dist/esm/common/toast/toastProvider.mjs +1 -1
  398. package/dist/esm/common/toast/toastProvider.mjs.map +1 -1
  399. package/dist/esm/common/toast/toastService.mjs +1 -1
  400. package/dist/esm/common/toast/toastService.mjs.map +1 -1
  401. package/dist/esm/common/tooltip/tooltip.mjs +1 -1
  402. package/dist/esm/common/tooltip/tooltip.mjs.map +1 -1
  403. package/dist/esm/common/tooltip/tooltip.module.mjs +1 -1
  404. package/dist/esm/common/tree/flattenTree.mjs +1 -1
  405. package/dist/esm/common/tree/flattenTree.mjs.map +1 -1
  406. package/dist/esm/common/tree/tree.mjs +1 -1
  407. package/dist/esm/common/tree/tree.mjs.map +1 -1
  408. package/dist/esm/common/tree/tree.module.mjs +1 -1
  409. package/dist/esm/common/tree/tree.module.mjs.map +1 -1
  410. package/dist/esm/common/utils/activationKey.mjs +1 -1
  411. package/dist/esm/common/utils/activationKey.mjs.map +1 -1
  412. package/dist/esm/common/utils/mergeRefs.mjs +2 -0
  413. package/dist/esm/common/utils/mergeRefs.mjs.map +1 -0
  414. package/dist/esm/common/utils/renderIcon.mjs +1 -1
  415. package/dist/esm/common/utils/renderIcon.mjs.map +1 -1
  416. package/dist/esm/common/utils/scrollAncestors.mjs +1 -1
  417. package/dist/esm/common/utils/scrollAncestors.mjs.map +1 -1
  418. package/dist/esm/common/utils/visualViewport.mjs +1 -1
  419. package/dist/esm/common/utils/visualViewport.mjs.map +1 -1
  420. package/dist/esm/common/virtualList/useColumnResize.mjs +1 -1
  421. package/dist/esm/common/virtualList/useColumnResize.mjs.map +1 -1
  422. package/dist/esm/common/virtualList/useColumns.mjs +1 -1
  423. package/dist/esm/common/virtualList/useColumns.mjs.map +1 -1
  424. package/dist/esm/common/virtualList/useHeaderNavigation.mjs +1 -1
  425. package/dist/esm/common/virtualList/useHeaderNavigation.mjs.map +1 -1
  426. package/dist/esm/common/virtualList/useRowDrag.mjs +1 -1
  427. package/dist/esm/common/virtualList/useRowDrag.mjs.map +1 -1
  428. package/dist/esm/common/virtualList/useRowNavigation.mjs +1 -1
  429. package/dist/esm/common/virtualList/useRowNavigation.mjs.map +1 -1
  430. package/dist/esm/common/virtualList/useSelection.mjs +1 -1
  431. package/dist/esm/common/virtualList/useSelection.mjs.map +1 -1
  432. package/dist/esm/common/virtualList/useSorting.mjs +1 -1
  433. package/dist/esm/common/virtualList/useSorting.mjs.map +1 -1
  434. package/dist/esm/common/virtualList/useVirtualWindow.mjs +1 -1
  435. package/dist/esm/common/virtualList/useVirtualWindow.mjs.map +1 -1
  436. package/dist/esm/common/virtualList/virtualList.mjs +1 -1
  437. package/dist/esm/common/virtualList/virtualList.mjs.map +1 -1
  438. package/dist/esm/common/virtualList/virtualList.module.mjs +1 -1
  439. package/dist/esm/common/virtualList/virtualList.utils.mjs +1 -1
  440. package/dist/esm/common/virtualList/virtualList.utils.mjs.map +1 -1
  441. package/dist/esm/common/virtualList/virtualListHeader.mjs +1 -1
  442. package/dist/esm/common/virtualList/virtualListHeader.mjs.map +1 -1
  443. package/dist/esm/common/virtualList/virtualRow.mjs +1 -1
  444. package/dist/esm/common/virtualList/virtualRow.mjs.map +1 -1
  445. package/dist/esm/common/wizard/wizard.mjs +1 -1
  446. package/dist/esm/common/wizard/wizard.mjs.map +1 -1
  447. package/dist/esm/common/wizard/wizard.module.mjs +1 -1
  448. package/dist/esm/hooks/useWindowSize.mjs +1 -1
  449. package/dist/esm/hooks/useWindowSize.mjs.map +1 -1
  450. package/dist/esm/index.mjs +1 -1
  451. package/dist/esm/services/formValidation/FormValidatorGroupWithForm.mjs +1 -1
  452. package/dist/esm/services/formValidation/FormValidatorGroupWithForm.mjs.map +1 -1
  453. package/dist/esm/services/formValidation/index.mjs +1 -1
  454. package/dist/esm/services/formValidation/useFormValidatorGroup.mjs +1 -1
  455. package/dist/esm/services/formValidation/useFormValidatorGroup.mjs.map +1 -1
  456. package/dist/esm/services/formValidation/validatableComponent.mjs +1 -1
  457. package/dist/esm/services/formValidation/validatableComponent.mjs.map +1 -1
  458. package/dist/esm/services/klipy.mjs +1 -1
  459. package/dist/esm/services/klipy.mjs.map +1 -1
  460. package/dist/esm/services/localization.mjs +1 -1
  461. package/dist/esm/services/localization.mjs.map +1 -1
  462. package/dist/index.cjs +3 -3
  463. package/dist/index.cjs.map +1 -1
  464. package/dist/mcp.js +21096 -5
  465. package/dist/style.css +1 -1
  466. package/dist/types/common/actionIcon/actionIcon.types.d.ts +3 -1
  467. package/dist/types/common/card/card.types.d.ts +3 -1
  468. package/dist/types/common/checkbox/checkbox.types.d.ts +3 -1
  469. package/dist/types/common/colorPicker/colorPicker.types.d.ts +4 -0
  470. package/dist/types/common/configProvider/configProvider.types.d.ts +4 -0
  471. package/dist/types/common/flip/flip.d.ts +2 -2
  472. package/dist/types/common/flip/flip.types.d.ts +3 -1
  473. package/dist/types/common/input/input.types.d.ts +5 -0
  474. package/dist/types/common/interactableDiv/interactableDiv.d.ts +3 -2
  475. package/dist/types/common/loading/spinnerLoading/spinnerLoading.d.ts +0 -5
  476. package/dist/types/common/numberInput/numberInput.types.d.ts +2 -0
  477. package/dist/types/common/otpInput/otpInput.types.d.ts +2 -0
  478. package/dist/types/common/planCanvas/planCanvas.d.ts +1 -1
  479. package/dist/types/common/planCanvas/planCanvas.types.d.ts +3 -1
  480. package/dist/types/common/ripple/ripple.d.ts +1 -1
  481. package/dist/types/common/ripple/ripple.types.d.ts +1 -0
  482. package/dist/types/common/roomDrawer/roomDrawer.d.ts +2 -2
  483. package/dist/types/common/roomDrawer/roomDrawer.types.d.ts +3 -1
  484. package/dist/types/common/roomViewer/roomViewer.d.ts +2 -2
  485. package/dist/types/common/roomViewer/roomViewer.types.d.ts +3 -1
  486. package/dist/types/common/searchInput/searchInput.d.ts +1 -1
  487. package/dist/types/common/searchInput/searchInput.types.d.ts +3 -0
  488. package/dist/types/common/switch/switch.types.d.ts +3 -1
  489. package/dist/types/common/themeProvider/index.d.ts +1 -0
  490. package/dist/types/common/themeProvider/lightTheme.d.ts +13 -0
  491. package/dist/types/common/tilt/tilt.d.ts +2 -2
  492. package/dist/types/common/tilt/tilt.types.d.ts +3 -1
  493. package/dist/types/common/timer/timer.d.ts +2 -2
  494. package/dist/types/common/timer/timer.types.d.ts +3 -1
  495. package/dist/types/common/tooltip/tooltip.d.ts +1 -1
  496. package/dist/types/common/tooltip/tooltip.types.d.ts +7 -0
  497. package/dist/types/common/types/baseProps.types.d.ts +6 -0
  498. package/dist/types/common/utils/mergeRefs.d.ts +12 -0
  499. package/dist/types/common/virtualList/virtualList.d.ts +2 -4
  500. package/dist/types/common/virtualList/virtualList.types.d.ts +3 -1
  501. package/docs/Accordion.md +9 -0
  502. package/docs/ActionIcon.md +1 -0
  503. package/docs/CLAUDE.md +37 -0
  504. package/docs/Card.md +1 -0
  505. package/docs/Checkbox.md +1 -0
  506. package/docs/ColorPicker.md +12 -2
  507. package/docs/Input.md +1 -0
  508. package/docs/KlipyPicker.md +1 -1
  509. package/docs/NumberInput.md +1 -0
  510. package/docs/OtpInput.md +1 -0
  511. package/docs/SearchInput.md +1 -0
  512. package/docs/Switch.md +1 -0
  513. package/docs/Textarea.md +1 -0
  514. package/docs/ThemeProvider.md +30 -1
  515. package/docs/Tooltip.md +23 -1
  516. package/package.json +8 -7
@@ -1 +1 @@
1
- {"version":3,"file":"floorPlan.snap.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.snap.ts"],"sourcesContent":["/**\n * Snapping, and the screen-to-world conversion it depends on.\n *\n * Two things here are deliberately different from the previous implementation:\n *\n * 1. Conversion and snapping are separate. `clientToWorld` only converts; `resolveSnap`\n * is the only thing that decides. The old `clientToSVG` called `snapPt` internally,\n * which made grid snap unconditional and left no way to hold Alt to disable it.\n *\n * 2. Every tolerance is in SCREEN PIXELS, converted through the current zoom at the\n * point of use. A fixed world-unit tolerance (the old `WALL_TOLERANCE = 8`) means\n * snapping covers a smaller and smaller part of the screen as you zoom in, until it\n * stops working.\n */\n\nimport { dist, pointToSegment, projectOnSegment, segmentIntersection } from './floorPlan.geometry';\nimport type { PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport enum SnapKind {\n None = 'none',\n Node = 'node',\n /** A point projected onto a wall's centreline. */\n Wall = 'wall',\n /** The crossing of two walls, or of a wall with the active angle ray. */\n Intersection = 'intersection',\n /** On the infinite line continuing an existing wall. */\n Extension = 'extension',\n /** Shares an x or y coordinate with an existing node. */\n Alignment = 'alignment',\n /** On a ray at a multiple of `angleStepDeg` from `origin`. */\n Angle = 'angle',\n Grid = 'grid',\n}\n\nexport interface SnapGuide {\n kind: SnapKind;\n a: PlanPoint;\n b: PlanPoint;\n /** Render as an unbounded construction line rather than a segment. */\n infinite?: boolean;\n}\n\nexport interface SnapOptions {\n /** Which snaps are active. Pass `[]` (Alt held) to disable snapping entirely. */\n enabled: SnapKind[];\n /** Millimetres. 0 disables grid snap. */\n gridSize: number;\n /** Degrees. 45 gives 0/45/90. 0 disables angle lock. */\n angleStepDeg: number;\n toleranceScreenPx: number;\n /** Nodes are the most valuable snap, so they get a little more room. */\n nodeToleranceScreenPx?: number;\n}\n\nexport const DEFAULT_SNAP_OPTIONS: SnapOptions = {\n enabled: [\n SnapKind.Node,\n SnapKind.Intersection,\n SnapKind.Wall,\n SnapKind.Angle,\n SnapKind.Extension,\n SnapKind.Alignment,\n SnapKind.Grid,\n ],\n gridSize: 100,\n angleStepDeg: 0,\n toleranceScreenPx: 10,\n nodeToleranceScreenPx: 12,\n};\n\nexport interface SnapContext {\n floor: PlanFloor;\n /** World units per screen pixel at the current zoom. See `screenScale`. */\n pxToWorld: number;\n /** The anchored point. Required for angle lock; also enables the length readout. */\n origin?: PlanPoint;\n /**\n * A direction in radians that angle lock measures from, in ADDITION to the world axes.\n *\n * Pass the direction of the wall being continued and the lock offers perpendicular and\n * parallel to that wall, not just to the world. Without it, angle lock is only useful\n * on a building that happens to be aligned to the page.\n */\n referenceAngle?: number;\n excludeWallIds?: ReadonlySet<string>;\n excludeNodeIds?: ReadonlySet<string>;\n options?: Partial<SnapOptions>;\n}\n\nexport interface SnapResult {\n point: PlanPoint;\n kind: SnapKind;\n nodeId?: string;\n wallId?: string;\n /** Parameter along `wallId`, for `SnapKind.Wall`. */\n t?: number;\n /** Everything an overlay needs to draw to explain the snap. */\n guides: SnapGuide[];\n /** Radians from `origin` to `point`, when an origin was given. */\n angle?: number;\n /** Distance from `origin` to `point`, when an origin was given. */\n distance?: number;\n /**\n * Set when angle lock produced the result, saying what it locked against: the world\n * axes, or the reference direction that was passed in.\n */\n angleRelativeTo?: 'world' | 'reference';\n}\n\n/** An infinite line as a point and a unit direction. */\ninterface Line {\n kind: SnapKind;\n point: PlanPoint;\n dir: PlanPoint;\n residual: number;\n /** Angle lock only: which basis the ray was measured from. */\n relativeTo?: 'world' | 'reference';\n}\n\n/** World units per screen pixel, from the element's current transform. */\nexport function screenScale(svg: SVGSVGElement): number {\n const ctm = svg.getScreenCTM();\n if (!ctm) return 1;\n // Measured rather than read off ctm.a, so a non-uniform or rotated transform still\n // gives a sensible scalar.\n const scale = Math.hypot(ctm.a, ctm.b);\n return scale === 0 ? 1 : 1 / scale;\n}\n\n/**\n * Client (screen) coordinates to world coordinates. Pure conversion, no snapping.\n *\n * Uses `getBoundingClientRect` arithmetic against an explicit viewport rather than\n * `getScreenCTM`, which returns `null` in jsdom and so makes anything built on it\n * untestable.\n */\nexport function clientToWorld(\n clientX: number,\n clientY: number,\n rect: { left: number; top: number },\n view: { x: number; y: number; zoom: number },\n): PlanPoint {\n return {\n x: view.x + (clientX - rect.left) / view.zoom,\n y: view.y + (clientY - rect.top) / view.zoom,\n };\n}\n\nfunction perpendicularDistanceToLine(p: PlanPoint, line: { point: PlanPoint; dir: PlanPoint }): number {\n const vx = p.x - line.point.x;\n const vy = p.y - line.point.y;\n return Math.abs(vx * line.dir.y - vy * line.dir.x);\n}\n\nfunction projectOnLine(p: PlanPoint, line: { point: PlanPoint; dir: PlanPoint }): PlanPoint {\n const vx = p.x - line.point.x;\n const vy = p.y - line.point.y;\n const along = vx * line.dir.x + vy * line.dir.y;\n return { x: line.point.x + line.dir.x * along, y: line.point.y + line.dir.y * along };\n}\n\nfunction intersectLines(l1: Line, l2: Line): PlanPoint | null {\n const denom = l1.dir.x * l2.dir.y - l1.dir.y * l2.dir.x;\n if (Math.abs(denom) < 1e-9) return null;\n const dx = l2.point.x - l1.point.x;\n const dy = l2.point.y - l1.point.y;\n const t = (dx * l2.dir.y - dy * l2.dir.x) / denom;\n return { x: l1.point.x + l1.dir.x * t, y: l1.point.y + l1.dir.y * t };\n}\n\nfunction angleBetween(l1: Line, l2: Line): number {\n const dot = Math.abs(l1.dir.x * l2.dir.x + l1.dir.y * l2.dir.y);\n return Math.acos(Math.min(1, dot));\n}\n\nfunction roundTo(value: number, step: number): number {\n return step > 0 ? Math.round(value / step) * step : value;\n}\n\n/**\n * Resolves a raw pointer position to the point the user meant.\n *\n * Two stages, because half of these snaps are constraints rather than points. A hard\n * snap (node, intersection, wall) fixes both degrees of freedom, so the first match\n * wins. A constraint (angle lock, extension, alignment) removes only one, so a flat\n * \"first match wins\" list would kick the point off the angle ray the moment an\n * alignment also matched. Two constraints intersect; one constraint is projected onto\n * and then slid ALONG to the nearest grid position, which is what lets angle lock and\n * grid coexist instead of fighting.\n */\nexport function resolveSnap(raw: PlanPoint, ctx: SnapContext): SnapResult {\n const options: SnapOptions = { ...DEFAULT_SNAP_OPTIONS, ...ctx.options };\n const enabled = new Set(options.enabled);\n\n /**\n * Measures the resolved point back against the origin, so every branch reports the\n * same angle and distance without each one having to compute them. Taking them from\n * the final point rather than from whichever branch produced it means they always\n * describe what the user actually gets.\n */\n const finish = (result: SnapResult): SnapResult => {\n if (!ctx.origin) return result;\n const dx = result.point.x - ctx.origin.x;\n const dy = result.point.y - ctx.origin.y;\n return { ...result, angle: Math.atan2(dy, dx), distance: Math.hypot(dx, dy) };\n };\n\n if (enabled.size === 0) return finish({ point: raw, kind: SnapKind.None, guides: [] });\n\n const tol = options.toleranceScreenPx * ctx.pxToWorld;\n const nodeTol = (options.nodeToleranceScreenPx ?? options.toleranceScreenPx) * ctx.pxToWorld;\n const { floor } = ctx;\n\n /**\n * Angle lock, when the caller has asked for it, is a HARD constraint rather than\n * another proximity snap: `angleStepDeg > 0` only happens because the user latched the\n * lock or held the modifier. Gating it by tolerance like the ambient snaps would mean\n * it silently stopped applying as soon as you dragged more than a few pixels off the\n * ray, which is the opposite of what a lock is for.\n */\n const lockedRay: Line | null = (() => {\n if (!enabled.has(SnapKind.Angle) || !ctx.origin || options.angleStepDeg <= 0) return null;\n const step = (options.angleStepDeg * Math.PI) / 180;\n const rawAngle = Math.atan2(raw.y - ctx.origin.y, raw.x - ctx.origin.x);\n\n // Measure from the world axes, and also from the reference direction when one was\n // given, so \"square to the wall I am continuing\" is available as well as \"square to\n // the page\". Whichever ray the pointer is closest to wins.\n const bases: Array<{ angle: number; relativeTo: 'world' | 'reference' }> = [{ angle: 0, relativeTo: 'world' }];\n if (ctx.referenceAngle !== undefined) {\n bases.push({ angle: ctx.referenceAngle, relativeTo: 'reference' });\n }\n\n let best: Line | null = null;\n for (const base of bases) {\n const snapped = base.angle + Math.round((rawAngle - base.angle) / step) * step;\n const dir = { x: Math.cos(snapped), y: Math.sin(snapped) };\n const line: Line = {\n kind: SnapKind.Angle,\n point: ctx.origin,\n dir,\n residual: 0,\n relativeTo: base.relativeTo,\n };\n line.residual = perpendicularDistanceToLine(raw, line);\n if (!best || line.residual < best.residual) best = line;\n }\n return best;\n })();\n\n /**\n * While the lock is on, a hard snap is only allowed to win if it sits ON the ray.\n * Otherwise \"hold the angle\" would be quietly overridden by any nearby corner, which\n * is exactly the case the lock exists to prevent.\n */\n const allowedByLock = (p: PlanPoint): boolean => !lockedRay || perpendicularDistanceToLine(p, lockedRay) <= tol;\n\n // ── Stage A: hard snaps ──────────────────────────────────────────────────────\n\n if (enabled.has(SnapKind.Node)) {\n let best: { id: string; point: PlanPoint; d: number } | null = null;\n for (const node of Object.values(floor.nodes)) {\n if (ctx.excludeNodeIds?.has(node.id)) continue;\n const d = dist(raw, node);\n if (d <= nodeTol && allowedByLock(node) && (!best || d < best.d)) {\n best = { id: node.id, point: { x: node.x, y: node.y }, d };\n }\n }\n if (best) {\n return finish({ point: best.point, kind: SnapKind.Node, nodeId: best.id, guides: [] });\n }\n }\n\n const walls = Object.values(floor.walls).filter((w) => !ctx.excludeWallIds?.has(w.id));\n\n if (enabled.has(SnapKind.Intersection)) {\n let best: { point: PlanPoint; d: number } | null = null;\n for (let i = 0; i < walls.length; i++) {\n for (let j = i + 1; j < walls.length; j++) {\n const a = floor.nodes[walls[i].a];\n const b = floor.nodes[walls[i].b];\n const c = floor.nodes[walls[j].a];\n const d2 = floor.nodes[walls[j].b];\n if (!a || !b || !c || !d2) continue;\n const hit = segmentIntersection(a, b, c, d2);\n if (!hit.point) continue;\n const d = dist(raw, hit.point);\n if (d <= tol && allowedByLock(hit.point) && (!best || d < best.d)) {\n best = { point: hit.point, d };\n }\n }\n }\n if (best) return finish({ point: best.point, kind: SnapKind.Intersection, guides: [] });\n }\n\n if (enabled.has(SnapKind.Wall)) {\n let best: { wallId: string; point: PlanPoint; t: number; d: number } | null = null;\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const r = pointToSegment(raw, a, b);\n if (r.dist <= tol && allowedByLock(r.point) && (!best || r.dist < best.d)) {\n // Project exactly: intent is allowed to be approximate, the model is not.\n best = { wallId: wall.id, point: projectOnSegment(raw, a, b), t: r.t, d: r.dist };\n }\n }\n if (best) {\n return finish({\n point: best.point,\n kind: SnapKind.Wall,\n wallId: best.wallId,\n t: best.t,\n guides: [],\n });\n }\n }\n\n // ── Stage B: constraints ─────────────────────────────────────────────────────\n\n const constraints: Line[] = [];\n\n if (enabled.has(SnapKind.Extension)) {\n let best: Line | null = null;\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const len = dist(a, b);\n if (len < 1e-6) continue;\n const dir = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n const line: Line = { kind: SnapKind.Extension, point: { x: a.x, y: a.y }, dir, residual: 0 };\n line.residual = perpendicularDistanceToLine(raw, line);\n // Only beyond the segment itself; on the segment, the wall snap already applies.\n const onSegment = pointToSegment(raw, a, b).dist <= tol;\n if (line.residual <= tol && !onSegment && (!best || line.residual < best.residual)) {\n best = line;\n }\n }\n if (best) constraints.push(best);\n }\n\n if (enabled.has(SnapKind.Alignment)) {\n let bestV: Line | null = null;\n let bestH: Line | null = null;\n for (const node of Object.values(floor.nodes)) {\n if (ctx.excludeNodeIds?.has(node.id)) continue;\n const dx = Math.abs(raw.x - node.x);\n if (dx <= tol && (!bestV || dx < bestV.residual)) {\n bestV = {\n kind: SnapKind.Alignment,\n point: { x: node.x, y: node.y },\n dir: { x: 0, y: 1 },\n residual: dx,\n };\n }\n const dy = Math.abs(raw.y - node.y);\n if (dy <= tol && (!bestH || dy < bestH.residual)) {\n bestH = {\n kind: SnapKind.Alignment,\n point: { x: node.x, y: node.y },\n dir: { x: 1, y: 0 },\n residual: dy,\n };\n }\n }\n if (bestV) constraints.push(bestV);\n if (bestH) constraints.push(bestH);\n }\n\n const guideFor = (line: Line, through: PlanPoint): SnapGuide => ({\n kind: line.kind,\n a: { x: through.x - line.dir.x * 1e5, y: through.y - line.dir.y * 1e5 },\n b: { x: through.x + line.dir.x * 1e5, y: through.y + line.dir.y * 1e5 },\n infinite: true,\n });\n\n if (lockedRay) {\n // The ray is fixed; the only freedom left is where along it the point sits. Another\n // constraint that genuinely crosses it pins that down, which is the useful\n // \"45 degrees AND level with that corner\" case.\n const crossing = constraints\n .filter((c) => c.residual <= tol && angleBetween(lockedRay, c) > (15 * Math.PI) / 180)\n .sort((a, b) => a.residual - b.residual)[0];\n\n if (crossing) {\n const point = intersectLines(lockedRay, crossing);\n if (point && dist(point, raw) <= tol * 4) {\n return finish({\n point,\n kind: SnapKind.Angle,\n guides: [guideFor(lockedRay, point), guideFor(crossing, point)],\n angleRelativeTo: lockedRay.relativeTo,\n });\n }\n }\n\n let point = projectOnLine(raw, lockedRay);\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n // Slide along the ray to a round length, rather than rounding both coordinates,\n // which would take the point straight back off it.\n const along = (point.x - lockedRay.point.x) * lockedRay.dir.x + (point.y - lockedRay.point.y) * lockedRay.dir.y;\n const snappedAlong = roundTo(along, options.gridSize);\n point = {\n x: lockedRay.point.x + lockedRay.dir.x * snappedAlong,\n y: lockedRay.point.y + lockedRay.dir.y * snappedAlong,\n };\n }\n return finish({\n point,\n kind: SnapKind.Angle,\n guides: [guideFor(lockedRay, point)],\n angleRelativeTo: lockedRay.relativeTo,\n });\n }\n\n if (constraints.length >= 2) {\n const sorted = [...constraints].sort((a, b) => a.residual - b.residual);\n const [l1, l2] = sorted;\n const point = intersectLines(l1, l2);\n // Two nearly parallel constraints intersect somewhere useless and far away.\n if (point && angleBetween(l1, l2) > (15 * Math.PI) / 180 && dist(point, raw) <= tol * 2) {\n return finish({\n point,\n kind: SnapKind.Intersection,\n guides: [guideFor(l1, point), guideFor(l2, point)],\n });\n }\n }\n\n if (constraints.length >= 1) {\n const line = [...constraints].sort((a, b) => a.residual - b.residual)[0];\n let point = projectOnLine(raw, line);\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n // Slide along the constraint to a round distance, rather than rounding both\n // coordinates, which would take the point back off the line.\n const along = (point.x - line.point.x) * line.dir.x + (point.y - line.point.y) * line.dir.y;\n const snappedAlong = roundTo(along, options.gridSize);\n point = {\n x: line.point.x + line.dir.x * snappedAlong,\n y: line.point.y + line.dir.y * snappedAlong,\n };\n }\n return finish({\n point,\n kind: line.kind,\n guides: [guideFor(line, point)],\n angleRelativeTo: line.relativeTo,\n });\n }\n\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n return finish({\n point: { x: roundTo(raw.x, options.gridSize), y: roundTo(raw.y, options.gridSize) },\n kind: SnapKind.Grid,\n guides: [],\n });\n }\n\n return finish({ point: raw, kind: SnapKind.None, guides: [] });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"floorPlan.snap.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.snap.ts"],"sourcesContent":["/**\n * Snapping, and the screen-to-world conversion it depends on.\n *\n * Two things here are deliberately different from the previous implementation:\n *\n * 1. Conversion and snapping are separate. `clientToWorld` only converts; `resolveSnap`\n * is the only thing that decides. The old `clientToSVG` called `snapPt` internally,\n * which made grid snap unconditional and left no way to hold Alt to disable it.\n *\n * 2. Every tolerance is in SCREEN PIXELS, converted through the current zoom at the\n * point of use. A fixed world-unit tolerance (the old `WALL_TOLERANCE = 8`) means\n * snapping covers a smaller and smaller part of the screen as you zoom in, until it\n * stops working.\n */\n\nimport { dist, pointToSegment, projectOnSegment, segmentIntersection } from './floorPlan.geometry';\nimport type { PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport enum SnapKind {\n None = 'none',\n Node = 'node',\n /** A point projected onto a wall's centreline. */\n Wall = 'wall',\n /** The crossing of two walls, or of a wall with the active angle ray. */\n Intersection = 'intersection',\n /** On the infinite line continuing an existing wall. */\n Extension = 'extension',\n /** Shares an x or y coordinate with an existing node. */\n Alignment = 'alignment',\n /** On a ray at a multiple of `angleStepDeg` from `origin`. */\n Angle = 'angle',\n Grid = 'grid',\n}\n\nexport interface SnapGuide {\n kind: SnapKind;\n a: PlanPoint;\n b: PlanPoint;\n /** Render as an unbounded construction line rather than a segment. */\n infinite?: boolean;\n}\n\nexport interface SnapOptions {\n /** Which snaps are active. Pass `[]` (Alt held) to disable snapping entirely. */\n enabled: SnapKind[];\n /** Millimetres. 0 disables grid snap. */\n gridSize: number;\n /** Degrees. 45 gives 0/45/90. 0 disables angle lock. */\n angleStepDeg: number;\n toleranceScreenPx: number;\n /** Nodes are the most valuable snap, so they get a little more room. */\n nodeToleranceScreenPx?: number;\n}\n\nexport const DEFAULT_SNAP_OPTIONS: SnapOptions = {\n enabled: [\n SnapKind.Node,\n SnapKind.Intersection,\n SnapKind.Wall,\n SnapKind.Angle,\n SnapKind.Extension,\n SnapKind.Alignment,\n SnapKind.Grid,\n ],\n gridSize: 100,\n angleStepDeg: 0,\n toleranceScreenPx: 10,\n nodeToleranceScreenPx: 12,\n};\n\nexport interface SnapContext {\n floor: PlanFloor;\n /** World units per screen pixel at the current zoom. See `screenScale`. */\n pxToWorld: number;\n /** The anchored point. Required for angle lock; also enables the length readout. */\n origin?: PlanPoint;\n /**\n * A direction in radians that angle lock measures from, in ADDITION to the world axes.\n *\n * Pass the direction of the wall being continued and the lock offers perpendicular and\n * parallel to that wall, not just to the world. Without it, angle lock is only useful\n * on a building that happens to be aligned to the page.\n */\n referenceAngle?: number;\n excludeWallIds?: ReadonlySet<string>;\n excludeNodeIds?: ReadonlySet<string>;\n options?: Partial<SnapOptions>;\n}\n\nexport interface SnapResult {\n point: PlanPoint;\n kind: SnapKind;\n nodeId?: string;\n wallId?: string;\n /** Parameter along `wallId`, for `SnapKind.Wall`. */\n t?: number;\n /** Everything an overlay needs to draw to explain the snap. */\n guides: SnapGuide[];\n /** Radians from `origin` to `point`, when an origin was given. */\n angle?: number;\n /** Distance from `origin` to `point`, when an origin was given. */\n distance?: number;\n /**\n * Set when angle lock produced the result, saying what it locked against: the world\n * axes, or the reference direction that was passed in.\n */\n angleRelativeTo?: 'world' | 'reference';\n}\n\n/** An infinite line as a point and a unit direction. */\ninterface Line {\n kind: SnapKind;\n point: PlanPoint;\n dir: PlanPoint;\n residual: number;\n /** Angle lock only: which basis the ray was measured from. */\n relativeTo?: 'world' | 'reference';\n}\n\n/** World units per screen pixel, from the element's current transform. */\nexport function screenScale(svg: SVGSVGElement): number {\n const ctm = svg.getScreenCTM();\n if (!ctm) return 1;\n // Measured rather than read off ctm.a, so a non-uniform or rotated transform still\n // gives a sensible scalar.\n const scale = Math.hypot(ctm.a, ctm.b);\n return scale === 0 ? 1 : 1 / scale;\n}\n\n/**\n * Client (screen) coordinates to world coordinates. Pure conversion, no snapping.\n *\n * Uses `getBoundingClientRect` arithmetic against an explicit viewport rather than\n * `getScreenCTM`, which returns `null` in jsdom and so makes anything built on it\n * untestable.\n */\nexport function clientToWorld(\n clientX: number,\n clientY: number,\n rect: { left: number; top: number },\n view: { x: number; y: number; zoom: number },\n): PlanPoint {\n return {\n x: view.x + (clientX - rect.left) / view.zoom,\n y: view.y + (clientY - rect.top) / view.zoom,\n };\n}\n\nfunction perpendicularDistanceToLine(p: PlanPoint, line: { point: PlanPoint; dir: PlanPoint }): number {\n const vx = p.x - line.point.x;\n const vy = p.y - line.point.y;\n return Math.abs(vx * line.dir.y - vy * line.dir.x);\n}\n\nfunction projectOnLine(p: PlanPoint, line: { point: PlanPoint; dir: PlanPoint }): PlanPoint {\n const vx = p.x - line.point.x;\n const vy = p.y - line.point.y;\n const along = vx * line.dir.x + vy * line.dir.y;\n return { x: line.point.x + line.dir.x * along, y: line.point.y + line.dir.y * along };\n}\n\nfunction intersectLines(l1: Line, l2: Line): PlanPoint | null {\n const denom = l1.dir.x * l2.dir.y - l1.dir.y * l2.dir.x;\n if (Math.abs(denom) < 1e-9) return null;\n const dx = l2.point.x - l1.point.x;\n const dy = l2.point.y - l1.point.y;\n const t = (dx * l2.dir.y - dy * l2.dir.x) / denom;\n return { x: l1.point.x + l1.dir.x * t, y: l1.point.y + l1.dir.y * t };\n}\n\nfunction angleBetween(l1: Line, l2: Line): number {\n const dot = Math.abs(l1.dir.x * l2.dir.x + l1.dir.y * l2.dir.y);\n return Math.acos(Math.min(1, dot));\n}\n\nfunction roundTo(value: number, step: number): number {\n return step > 0 ? Math.round(value / step) * step : value;\n}\n\n/**\n * Resolves a raw pointer position to the point the user meant.\n *\n * Two stages, because half of these snaps are constraints rather than points. A hard\n * snap (node, intersection, wall) fixes both degrees of freedom, so the first match\n * wins. A constraint (angle lock, extension, alignment) removes only one, so a flat\n * \"first match wins\" list would kick the point off the angle ray the moment an\n * alignment also matched. Two constraints intersect; one constraint is projected onto\n * and then slid ALONG to the nearest grid position, which is what lets angle lock and\n * grid coexist instead of fighting.\n */\nexport function resolveSnap(raw: PlanPoint, ctx: SnapContext): SnapResult {\n const options: SnapOptions = { ...DEFAULT_SNAP_OPTIONS, ...ctx.options };\n const enabled = new Set(options.enabled);\n\n /**\n * Measures the resolved point back against the origin, so every branch reports the\n * same angle and distance without each one having to compute them. Taking them from\n * the final point rather than from whichever branch produced it means they always\n * describe what the user actually gets.\n */\n const finish = (result: SnapResult): SnapResult => {\n if (!ctx.origin) return result;\n const dx = result.point.x - ctx.origin.x;\n const dy = result.point.y - ctx.origin.y;\n return { ...result, angle: Math.atan2(dy, dx), distance: Math.hypot(dx, dy) };\n };\n\n if (enabled.size === 0) return finish({ point: raw, kind: SnapKind.None, guides: [] });\n\n const tol = options.toleranceScreenPx * ctx.pxToWorld;\n const nodeTol = (options.nodeToleranceScreenPx ?? options.toleranceScreenPx) * ctx.pxToWorld;\n const { floor } = ctx;\n\n /**\n * Angle lock, when the caller has asked for it, is a HARD constraint rather than\n * another proximity snap: `angleStepDeg > 0` only happens because the user latched the\n * lock or held the modifier. Gating it by tolerance like the ambient snaps would mean\n * it silently stopped applying as soon as you dragged more than a few pixels off the\n * ray, which is the opposite of what a lock is for.\n */\n const lockedRay: Line | null = (() => {\n if (!enabled.has(SnapKind.Angle) || !ctx.origin || options.angleStepDeg <= 0) return null;\n const step = (options.angleStepDeg * Math.PI) / 180;\n const rawAngle = Math.atan2(raw.y - ctx.origin.y, raw.x - ctx.origin.x);\n\n // Measure from the world axes, and also from the reference direction when one was\n // given, so \"square to the wall I am continuing\" is available as well as \"square to\n // the page\". Whichever ray the pointer is closest to wins.\n const bases: Array<{ angle: number; relativeTo: 'world' | 'reference' }> = [{ angle: 0, relativeTo: 'world' }];\n if (ctx.referenceAngle !== undefined) {\n bases.push({ angle: ctx.referenceAngle, relativeTo: 'reference' });\n }\n\n let best: Line | null = null;\n for (const base of bases) {\n const snapped = base.angle + Math.round((rawAngle - base.angle) / step) * step;\n const dir = { x: Math.cos(snapped), y: Math.sin(snapped) };\n const line: Line = {\n kind: SnapKind.Angle,\n point: ctx.origin,\n dir,\n residual: 0,\n relativeTo: base.relativeTo,\n };\n line.residual = perpendicularDistanceToLine(raw, line);\n if (!best || line.residual < best.residual) best = line;\n }\n return best;\n })();\n\n /**\n * While the lock is on, a hard snap is only allowed to win if it sits ON the ray.\n * Otherwise \"hold the angle\" would be quietly overridden by any nearby corner, which\n * is exactly the case the lock exists to prevent.\n */\n const allowedByLock = (p: PlanPoint): boolean => !lockedRay || perpendicularDistanceToLine(p, lockedRay) <= tol;\n\n // ── Stage A: hard snaps ──────────────────────────────────────────────────────\n\n if (enabled.has(SnapKind.Node)) {\n let best: { id: string; point: PlanPoint; d: number } | null = null;\n for (const node of Object.values(floor.nodes)) {\n if (ctx.excludeNodeIds?.has(node.id)) continue;\n const d = dist(raw, node);\n if (d <= nodeTol && allowedByLock(node) && (!best || d < best.d)) {\n best = { id: node.id, point: { x: node.x, y: node.y }, d };\n }\n }\n if (best) {\n return finish({ point: best.point, kind: SnapKind.Node, nodeId: best.id, guides: [] });\n }\n }\n\n const walls = Object.values(floor.walls).filter((w) => !ctx.excludeWallIds?.has(w.id));\n\n if (enabled.has(SnapKind.Intersection)) {\n let best: { point: PlanPoint; d: number } | null = null;\n for (let i = 0; i < walls.length; i++) {\n for (let j = i + 1; j < walls.length; j++) {\n const a = floor.nodes[walls[i].a];\n const b = floor.nodes[walls[i].b];\n const c = floor.nodes[walls[j].a];\n const d2 = floor.nodes[walls[j].b];\n if (!a || !b || !c || !d2) continue;\n const hit = segmentIntersection(a, b, c, d2);\n if (!hit.point) continue;\n const d = dist(raw, hit.point);\n if (d <= tol && allowedByLock(hit.point) && (!best || d < best.d)) {\n best = { point: hit.point, d };\n }\n }\n }\n if (best) return finish({ point: best.point, kind: SnapKind.Intersection, guides: [] });\n }\n\n if (enabled.has(SnapKind.Wall)) {\n let best: { wallId: string; point: PlanPoint; t: number; d: number } | null = null;\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const r = pointToSegment(raw, a, b);\n if (r.dist <= tol && allowedByLock(r.point) && (!best || r.dist < best.d)) {\n // Project exactly: intent is allowed to be approximate, the model is not.\n best = { wallId: wall.id, point: projectOnSegment(raw, a, b), t: r.t, d: r.dist };\n }\n }\n if (best) {\n return finish({\n point: best.point,\n kind: SnapKind.Wall,\n wallId: best.wallId,\n t: best.t,\n guides: [],\n });\n }\n }\n\n // ── Stage B: constraints ─────────────────────────────────────────────────────\n\n const constraints: Line[] = [];\n\n if (enabled.has(SnapKind.Extension)) {\n let best: Line | null = null;\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const len = dist(a, b);\n if (len < 1e-6) continue;\n const dir = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n const line: Line = { kind: SnapKind.Extension, point: { x: a.x, y: a.y }, dir, residual: 0 };\n line.residual = perpendicularDistanceToLine(raw, line);\n // Only beyond the segment itself; on the segment, the wall snap already applies.\n const onSegment = pointToSegment(raw, a, b).dist <= tol;\n if (line.residual <= tol && !onSegment && (!best || line.residual < best.residual)) {\n best = line;\n }\n }\n if (best) constraints.push(best);\n }\n\n if (enabled.has(SnapKind.Alignment)) {\n let bestV: Line | null = null;\n let bestH: Line | null = null;\n for (const node of Object.values(floor.nodes)) {\n if (ctx.excludeNodeIds?.has(node.id)) continue;\n const dx = Math.abs(raw.x - node.x);\n if (dx <= tol && (!bestV || dx < bestV.residual)) {\n bestV = {\n kind: SnapKind.Alignment,\n point: { x: node.x, y: node.y },\n dir: { x: 0, y: 1 },\n residual: dx,\n };\n }\n const dy = Math.abs(raw.y - node.y);\n if (dy <= tol && (!bestH || dy < bestH.residual)) {\n bestH = {\n kind: SnapKind.Alignment,\n point: { x: node.x, y: node.y },\n dir: { x: 1, y: 0 },\n residual: dy,\n };\n }\n }\n if (bestV) constraints.push(bestV);\n if (bestH) constraints.push(bestH);\n }\n\n const guideFor = (line: Line, through: PlanPoint): SnapGuide => ({\n kind: line.kind,\n a: { x: through.x - line.dir.x * 1e5, y: through.y - line.dir.y * 1e5 },\n b: { x: through.x + line.dir.x * 1e5, y: through.y + line.dir.y * 1e5 },\n infinite: true,\n });\n\n if (lockedRay) {\n // The ray is fixed; the only freedom left is where along it the point sits. Another\n // constraint that genuinely crosses it pins that down, which is the useful\n // \"45 degrees AND level with that corner\" case.\n const crossing = constraints\n .filter((c) => c.residual <= tol && angleBetween(lockedRay, c) > (15 * Math.PI) / 180)\n .sort((a, b) => a.residual - b.residual)[0];\n\n if (crossing) {\n const point = intersectLines(lockedRay, crossing);\n if (point && dist(point, raw) <= tol * 4) {\n return finish({\n point,\n kind: SnapKind.Angle,\n guides: [guideFor(lockedRay, point), guideFor(crossing, point)],\n angleRelativeTo: lockedRay.relativeTo,\n });\n }\n }\n\n let point = projectOnLine(raw, lockedRay);\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n // Slide along the ray to a round length, rather than rounding both coordinates,\n // which would take the point straight back off it.\n const along = (point.x - lockedRay.point.x) * lockedRay.dir.x + (point.y - lockedRay.point.y) * lockedRay.dir.y;\n const snappedAlong = roundTo(along, options.gridSize);\n point = {\n x: lockedRay.point.x + lockedRay.dir.x * snappedAlong,\n y: lockedRay.point.y + lockedRay.dir.y * snappedAlong,\n };\n }\n return finish({\n point,\n kind: SnapKind.Angle,\n guides: [guideFor(lockedRay, point)],\n angleRelativeTo: lockedRay.relativeTo,\n });\n }\n\n if (constraints.length >= 2) {\n const sorted = [...constraints].sort((a, b) => a.residual - b.residual);\n const [l1, l2] = sorted;\n const point = intersectLines(l1, l2);\n // Two nearly parallel constraints intersect somewhere useless and far away.\n if (point && angleBetween(l1, l2) > (15 * Math.PI) / 180 && dist(point, raw) <= tol * 2) {\n return finish({\n point,\n kind: SnapKind.Intersection,\n guides: [guideFor(l1, point), guideFor(l2, point)],\n });\n }\n }\n\n if (constraints.length >= 1) {\n const line = [...constraints].sort((a, b) => a.residual - b.residual)[0];\n let point = projectOnLine(raw, line);\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n // Slide along the constraint to a round distance, rather than rounding both\n // coordinates, which would take the point back off the line.\n const along = (point.x - line.point.x) * line.dir.x + (point.y - line.point.y) * line.dir.y;\n const snappedAlong = roundTo(along, options.gridSize);\n point = {\n x: line.point.x + line.dir.x * snappedAlong,\n y: line.point.y + line.dir.y * snappedAlong,\n };\n }\n return finish({\n point,\n kind: line.kind,\n guides: [guideFor(line, point)],\n angleRelativeTo: line.relativeTo,\n });\n }\n\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n return finish({\n point: { x: roundTo(raw.x, options.gridSize), y: roundTo(raw.y, options.gridSize) },\n kind: SnapKind.Grid,\n guides: [],\n });\n }\n\n return finish({ point: raw, kind: SnapKind.None, guides: [] });\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{signedArea as e,stripSpurs as t}from"./floorPlan.geometry.mjs";var n=1e-9;function r(e,t,n){return(t.x-e.x)*(n.y-e.y)-(t.y-e.y)*(n.x-e.x)}function i(e,t){return Math.abs(e.x-t.x)<n&&Math.abs(e.y-t.y)<n}function a(e,t,i,a){let o=r(e,t,i),s=r(e,i,a),c=r(e,a,t);return!((o<-1e-9||s<-1e-9||c<-1e-9)&&(o>n||s>n||c>n))}function o(e,t){let n=e.slice(),r=t.filter(e=>e.length>=3).map(e=>({ring:e,index:e.reduce((t,n,r)=>n.x>e[t].x?r:t,0)})).sort((e,t)=>t.ring[t.index].x-e.ring[e.index].x);for(let e of r){let t=e.ring[e.index],r=-1,i=1/0;for(let a=0;a<n.length;a++){if(n[a].x<t.x)continue;let o=(n[a].x-t.x)**2+(n[a].y-t.y)**2;o<i&&s(t,n[a],n,e.ring)&&(i=o,r=a)}if(r<0)for(let e=0;e<n.length;e++){let a=(n[e].x-t.x)**2+(n[e].y-t.y)**2;a<i&&(i=a,r=e)}let a=[];for(let t=0;t<=e.ring.length;t++)a.push(e.ring[(e.index+t)%e.ring.length]);n=[...n.slice(0,r+1),...a,...n.slice(r)]}return n}function s(e,t,n,i){for(let a of[n,i])for(let n=0;n<a.length;n++){let i=a[n],o=a[(n+1)%a.length];if(i===e||o===e||i===t||o===t)continue;let s=r(e,t,i),c=r(e,t,o),l=r(i,o,e),u=r(i,o,t);if(s*c<0&&l*u<0)return!1}return!0}function c(s,c=[]){let l=t(s);if(l.length<3)return{vertices:[],indices:[]};let u=o(l,c.map(e=>t(e)).filter(e=>e.length>=3)),d=e(u)<0?u.slice().reverse():u.slice(),f=d.map((e,t)=>t),p=[],m=f.length*f.length;for(;f.length>3&&m-->0;){let e=!1;for(let t=0;t<f.length;t++){let o=f[(t+f.length-1)%f.length],s=f[t],c=f[(t+1)%f.length],l=d[o],u=d[s],m=d[c];if(r(l,u,m)<=n)continue;let h=!1;for(let e of f){if(e===o||e===s||e===c)continue;let t=d[e];if(!(i(t,l)||i(t,u)||i(t,m))&&a(t,l,u,m)){h=!0;break}}if(!h){p.push(o,s,c),f.splice(t,1),e=!0;break}}if(!e)break}return f.length===3&&p.push(f[0],f[1],f[2]),{vertices:d,indices:p}}export{c as triangulate};
1
+ "use client";import{signedArea as e,stripSpurs as t}from"./floorPlan.geometry.mjs";var n=1e-9;function r(e,t,n){return(t.x-e.x)*(n.y-e.y)-(t.y-e.y)*(n.x-e.x)}function i(e,t){return Math.abs(e.x-t.x)<n&&Math.abs(e.y-t.y)<n}function a(e,t,i,a){let o=r(e,t,i),s=r(e,i,a),c=r(e,a,t);return!((o<-1e-9||s<-1e-9||c<-1e-9)&&(o>n||s>n||c>n))}function o(e,t){let n=e.slice(),r=t.filter(e=>e.length>=3).map(e=>({ring:e,index:e.reduce((t,n,r)=>n.x>e[t].x?r:t,0)})).sort((e,t)=>t.ring[t.index].x-e.ring[e.index].x);for(let e of r){let t=e.ring[e.index],r=-1,i=1/0;for(let a=0;a<n.length;a++){if(n[a].x<t.x)continue;let o=(n[a].x-t.x)**2+(n[a].y-t.y)**2;o<i&&s(t,n[a],n,e.ring)&&(i=o,r=a)}if(r<0)for(let e=0;e<n.length;e++){let a=(n[e].x-t.x)**2+(n[e].y-t.y)**2;a<i&&(i=a,r=e)}let a=[];for(let t=0;t<=e.ring.length;t++)a.push(e.ring[(e.index+t)%e.ring.length]);n=[...n.slice(0,r+1),...a,...n.slice(r)]}return n}function s(e,t,n,i){for(let a of[n,i])for(let n=0;n<a.length;n++){let i=a[n],o=a[(n+1)%a.length];if(i===e||o===e||i===t||o===t)continue;let s=r(e,t,i),c=r(e,t,o),l=r(i,o,e),u=r(i,o,t);if(s*c<0&&l*u<0)return!1}return!0}function c(s,c=[]){let l=t(s);if(l.length<3)return{vertices:[],indices:[]};let u=o(l,c.map(e=>t(e)).filter(e=>e.length>=3)),d=e(u)<0?u.slice().reverse():u.slice(),f=d.map((e,t)=>t),p=[],m=f.length*f.length;for(;f.length>3&&m-->0;){let e=!1;for(let t=0;t<f.length;t++){let o=f[(t+f.length-1)%f.length],s=f[t],c=f[(t+1)%f.length],l=d[o],u=d[s],m=d[c];if(r(l,u,m)<=n)continue;let h=!1;for(let e of f){if(e===o||e===s||e===c)continue;let t=d[e];if(!(i(t,l)||i(t,u)||i(t,m))&&a(t,l,u,m)){h=!0;break}}if(!h){p.push(o,s,c),f.splice(t,1),e=!0;break}}if(!e)break}return f.length===3&&p.push(f[0],f[1],f[2]),{vertices:d,indices:p}}export{c as triangulate};
2
2
  //# sourceMappingURL=floorPlan.triangulate.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"floorPlan.triangulate.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.triangulate.ts"],"sourcesContent":["/**\n * Ear clipping for simple polygons, with holes bridged into the outer ring first.\n *\n * Written out rather than pulled in: a floor slab is the only thing in this package that\n * needs a triangulator, and a dependency is a poor trade for one file.\n *\n * The rooms this runs on come from face extraction, so they are already closed, already\n * consistently wound, and free of self-intersections. That is what makes plain ear\n * clipping enough: it is O(n²) and it cannot cope with a self-intersecting ring, and\n * neither limit matters for a room with a few dozen corners.\n */\n\nimport { signedArea, stripSpurs } from './floorPlan.geometry';\nimport type { PlanPoint } from './floorPlan.types';\n\nconst EPS = 1e-9;\n\nfunction area2(a: PlanPoint, b: PlanPoint, c: PlanPoint): number {\n return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);\n}\n\nfunction samePoint(a: PlanPoint, b: PlanPoint): boolean {\n return Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS;\n}\n\n/** Inclusive of the edges: a vertex lying on one would still leave a sliver behind. */\nfunction inTriangle(p: PlanPoint, a: PlanPoint, b: PlanPoint, c: PlanPoint): boolean {\n const d1 = area2(p, a, b);\n const d2 = area2(p, b, c);\n const d3 = area2(p, c, a);\n const neg = d1 < -EPS || d2 < -EPS || d3 < -EPS;\n const pos = d1 > EPS || d2 > EPS || d3 > EPS;\n return !(neg && pos);\n}\n\n/**\n * Joins each hole to the outer ring with a pair of coincident edges, leaving one ring\n * that ear clipping can take. The bridge runs from the hole's rightmost vertex to a\n * visible vertex of the outer ring, which is the standard construction: going rightwards\n * from the extreme point of a hole must leave it, so the first outer edge hit is visible\n * from it.\n */\nfunction bridgeHoles(outer: PlanPoint[], holes: PlanPoint[][]): PlanPoint[] {\n let ring = outer.slice();\n\n // Rightmost hole first: bridging it cannot cross a hole further right.\n const queue = holes\n .filter((h) => h.length >= 3)\n .map((h) => ({ ring: h, index: h.reduce((best, p, i) => (p.x > h[best].x ? i : best), 0) }))\n .sort((a, b) => b.ring[b.index].x - a.ring[a.index].x);\n\n for (const hole of queue) {\n const from = hole.ring[hole.index];\n\n // The outer vertex to bridge to: the one that is visible and, among those, closest.\n let best = -1;\n let bestDist = Infinity;\n for (let i = 0; i < ring.length; i++) {\n if (ring[i].x < from.x) continue; // must lie to the right, or the bridge doubles back\n const d = (ring[i].x - from.x) ** 2 + (ring[i].y - from.y) ** 2;\n if (d < bestDist && visible(from, ring[i], ring, hole.ring)) {\n bestDist = d;\n best = i;\n }\n }\n // Nothing to the right was visible, so fall back to the nearest vertex anywhere. The\n // ring stays closed either way; at worst the slab gains a seam nobody can see.\n if (best < 0) {\n for (let i = 0; i < ring.length; i++) {\n const d = (ring[i].x - from.x) ** 2 + (ring[i].y - from.y) ** 2;\n if (d < bestDist) {\n bestDist = d;\n best = i;\n }\n }\n }\n\n // Walk the hole from its bridge vertex, then come back along the bridge.\n const woven: PlanPoint[] = [];\n for (let k = 0; k <= hole.ring.length; k++) {\n woven.push(hole.ring[(hole.index + k) % hole.ring.length]);\n }\n ring = [...ring.slice(0, best + 1), ...woven, ...ring.slice(best)];\n }\n\n return ring;\n}\n\n/** Whether the segment from `a` to `b` crosses any edge of the rings it has to avoid. */\nfunction visible(a: PlanPoint, b: PlanPoint, ring: PlanPoint[], hole: PlanPoint[]): boolean {\n for (const poly of [ring, hole]) {\n for (let i = 0; i < poly.length; i++) {\n const p = poly[i];\n const q = poly[(i + 1) % poly.length];\n if (p === a || q === a || p === b || q === b) continue;\n const d1 = area2(a, b, p);\n const d2 = area2(a, b, q);\n const d3 = area2(p, q, a);\n const d4 = area2(p, q, b);\n if (d1 * d2 < 0 && d3 * d4 < 0) return false;\n }\n }\n return true;\n}\n\n/**\n * Triangulates a ring and its holes into index triples over the returned vertex list.\n *\n * The vertices come back as well as the indices because bridging a hole duplicates the\n * two vertices it joins, so the caller cannot simply index into what it passed in.\n */\nexport function triangulate(\n outer: readonly PlanPoint[],\n holes: readonly (readonly PlanPoint[])[] = [],\n): { vertices: PlanPoint[]; indices: number[] } {\n // A dangling wall inside a room puts the same vertex in the ring twice, which is not a\n // simple polygon and would stall ear clipping on a zero-area ear.\n const cleanOuter = stripSpurs(outer);\n if (cleanOuter.length < 3) return { vertices: [], indices: [] };\n\n const ring = bridgeHoles(\n cleanOuter,\n holes.map((h) => stripSpurs(h)).filter((h) => h.length >= 3),\n );\n\n // Ear clipping wants a counter-clockwise ring under the usual maths convention.\n const vertices = signedArea(ring) < 0 ? ring.slice().reverse() : ring.slice();\n\n const live = vertices.map((_, i) => i);\n const indices: number[] = [];\n let guard = live.length * live.length;\n\n while (live.length > 3 && guard-- > 0) {\n let clipped = false;\n for (let i = 0; i < live.length; i++) {\n const ai = live[(i + live.length - 1) % live.length];\n const bi = live[i];\n const ci = live[(i + 1) % live.length];\n const a = vertices[ai];\n const b = vertices[bi];\n const c = vertices[ci];\n if (area2(a, b, c) <= EPS) continue; // reflex or degenerate: not an ear\n\n let blocked = false;\n for (const vi of live) {\n if (vi === ai || vi === bi || vi === ci) continue;\n const v = vertices[vi];\n // Bridging a hole duplicates the two vertices it joins, so a candidate can sit\n // exactly on a corner of the ear. Counting that as \"inside\" blocks every ear in\n // the ring and the triangulation comes back empty.\n if (samePoint(v, a) || samePoint(v, b) || samePoint(v, c)) continue;\n if (inTriangle(v, a, b, c)) {\n blocked = true;\n break;\n }\n }\n if (blocked) continue;\n\n indices.push(ai, bi, ci);\n live.splice(i, 1);\n clipped = true;\n break;\n }\n // No ear found: the ring is degenerate in a way this cannot reduce further. Stopping\n // beats spinning, and the caller gets whatever was triangulated so far.\n if (!clipped) break;\n }\n if (live.length === 3) indices.push(live[0], live[1], live[2]);\n\n return { vertices, indices };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"floorPlan.triangulate.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.triangulate.ts"],"sourcesContent":["/**\n * Ear clipping for simple polygons, with holes bridged into the outer ring first.\n *\n * Written out rather than pulled in: a floor slab is the only thing in this package that\n * needs a triangulator, and a dependency is a poor trade for one file.\n *\n * The rooms this runs on come from face extraction, so they are already closed, already\n * consistently wound, and free of self-intersections. That is what makes plain ear\n * clipping enough: it is O(n²) and it cannot cope with a self-intersecting ring, and\n * neither limit matters for a room with a few dozen corners.\n */\n\nimport { signedArea, stripSpurs } from './floorPlan.geometry';\nimport type { PlanPoint } from './floorPlan.types';\n\nconst EPS = 1e-9;\n\nfunction area2(a: PlanPoint, b: PlanPoint, c: PlanPoint): number {\n return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);\n}\n\nfunction samePoint(a: PlanPoint, b: PlanPoint): boolean {\n return Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS;\n}\n\n/** Inclusive of the edges: a vertex lying on one would still leave a sliver behind. */\nfunction inTriangle(p: PlanPoint, a: PlanPoint, b: PlanPoint, c: PlanPoint): boolean {\n const d1 = area2(p, a, b);\n const d2 = area2(p, b, c);\n const d3 = area2(p, c, a);\n const neg = d1 < -EPS || d2 < -EPS || d3 < -EPS;\n const pos = d1 > EPS || d2 > EPS || d3 > EPS;\n return !(neg && pos);\n}\n\n/**\n * Joins each hole to the outer ring with a pair of coincident edges, leaving one ring\n * that ear clipping can take. The bridge runs from the hole's rightmost vertex to a\n * visible vertex of the outer ring, which is the standard construction: going rightwards\n * from the extreme point of a hole must leave it, so the first outer edge hit is visible\n * from it.\n */\nfunction bridgeHoles(outer: PlanPoint[], holes: PlanPoint[][]): PlanPoint[] {\n let ring = outer.slice();\n\n // Rightmost hole first: bridging it cannot cross a hole further right.\n const queue = holes\n .filter((h) => h.length >= 3)\n .map((h) => ({ ring: h, index: h.reduce((best, p, i) => (p.x > h[best].x ? i : best), 0) }))\n .sort((a, b) => b.ring[b.index].x - a.ring[a.index].x);\n\n for (const hole of queue) {\n const from = hole.ring[hole.index];\n\n // The outer vertex to bridge to: the one that is visible and, among those, closest.\n let best = -1;\n let bestDist = Infinity;\n for (let i = 0; i < ring.length; i++) {\n if (ring[i].x < from.x) continue; // must lie to the right, or the bridge doubles back\n const d = (ring[i].x - from.x) ** 2 + (ring[i].y - from.y) ** 2;\n if (d < bestDist && visible(from, ring[i], ring, hole.ring)) {\n bestDist = d;\n best = i;\n }\n }\n // Nothing to the right was visible, so fall back to the nearest vertex anywhere. The\n // ring stays closed either way; at worst the slab gains a seam nobody can see.\n if (best < 0) {\n for (let i = 0; i < ring.length; i++) {\n const d = (ring[i].x - from.x) ** 2 + (ring[i].y - from.y) ** 2;\n if (d < bestDist) {\n bestDist = d;\n best = i;\n }\n }\n }\n\n // Walk the hole from its bridge vertex, then come back along the bridge.\n const woven: PlanPoint[] = [];\n for (let k = 0; k <= hole.ring.length; k++) {\n woven.push(hole.ring[(hole.index + k) % hole.ring.length]);\n }\n ring = [...ring.slice(0, best + 1), ...woven, ...ring.slice(best)];\n }\n\n return ring;\n}\n\n/** Whether the segment from `a` to `b` crosses any edge of the rings it has to avoid. */\nfunction visible(a: PlanPoint, b: PlanPoint, ring: PlanPoint[], hole: PlanPoint[]): boolean {\n for (const poly of [ring, hole]) {\n for (let i = 0; i < poly.length; i++) {\n const p = poly[i];\n const q = poly[(i + 1) % poly.length];\n if (p === a || q === a || p === b || q === b) continue;\n const d1 = area2(a, b, p);\n const d2 = area2(a, b, q);\n const d3 = area2(p, q, a);\n const d4 = area2(p, q, b);\n if (d1 * d2 < 0 && d3 * d4 < 0) return false;\n }\n }\n return true;\n}\n\n/**\n * Triangulates a ring and its holes into index triples over the returned vertex list.\n *\n * The vertices come back as well as the indices because bridging a hole duplicates the\n * two vertices it joins, so the caller cannot simply index into what it passed in.\n */\nexport function triangulate(\n outer: readonly PlanPoint[],\n holes: readonly (readonly PlanPoint[])[] = [],\n): { vertices: PlanPoint[]; indices: number[] } {\n // A dangling wall inside a room puts the same vertex in the ring twice, which is not a\n // simple polygon and would stall ear clipping on a zero-area ear.\n const cleanOuter = stripSpurs(outer);\n if (cleanOuter.length < 3) return { vertices: [], indices: [] };\n\n const ring = bridgeHoles(\n cleanOuter,\n holes.map((h) => stripSpurs(h)).filter((h) => h.length >= 3),\n );\n\n // Ear clipping wants a counter-clockwise ring under the usual maths convention.\n const vertices = signedArea(ring) < 0 ? ring.slice().reverse() : ring.slice();\n\n const live = vertices.map((_, i) => i);\n const indices: number[] = [];\n let guard = live.length * live.length;\n\n while (live.length > 3 && guard-- > 0) {\n let clipped = false;\n for (let i = 0; i < live.length; i++) {\n const ai = live[(i + live.length - 1) % live.length];\n const bi = live[i];\n const ci = live[(i + 1) % live.length];\n const a = vertices[ai];\n const b = vertices[bi];\n const c = vertices[ci];\n if (area2(a, b, c) <= EPS) continue; // reflex or degenerate: not an ear\n\n let blocked = false;\n for (const vi of live) {\n if (vi === ai || vi === bi || vi === ci) continue;\n const v = vertices[vi];\n // Bridging a hole duplicates the two vertices it joins, so a candidate can sit\n // exactly on a corner of the ear. Counting that as \"inside\" blocks every ear in\n // the ring and the triangulation comes back empty.\n if (samePoint(v, a) || samePoint(v, b) || samePoint(v, c)) continue;\n if (inTriangle(v, a, b, c)) {\n blocked = true;\n break;\n }\n }\n if (blocked) continue;\n\n indices.push(ai, bi, ci);\n live.splice(i, 1);\n clipped = true;\n break;\n }\n // No ear found: the ring is degenerate in a way this cannot reduce further. Stopping\n // beats spinning, and the caller gets whatever was triangulated so far.\n if (!clipped) break;\n }\n if (live.length === 3) indices.push(live[0], live[1], live[2]);\n\n return { vertices, indices };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- var e=function(e){return e.Metric=`metric`,e.Imperial=`imperial`,e}({}),t=function(e){return e.Exterior=`exterior`,e.Interior=`interior`,e.Partition=`partition`,e.Virtual=`virtual`,e}({}),n=function(e){return e.Door=`door`,e.Window=`window`,e.Archway=`archway`,e.Gap=`gap`,e}({}),r=function(e){return e.None=`none`,e.StartLeft=`startLeft`,e.StartRight=`startRight`,e.EndLeft=`endLeft`,e.EndRight=`endRight`,e.Double=`double`,e.Sliding=`sliding`,e}({}),i=function(e){return e.ZeroLengthWall=`zeroLengthWall`,e.SelfLoopWall=`selfLoopWall`,e.DuplicateWall=`duplicateWall`,e.CoincidentNodes=`coincidentNodes`,e.UnsplitCrossing=`unsplitCrossing`,e.UnsplitNodeOnWall=`unsplitNodeOnWall`,e.OverlappingWalls=`overlappingWalls`,e.OrphanNode=`orphanNode`,e.DanglingOpening=`danglingOpening`,e.UnmatchedRoom=`unmatchedRoom`,e.OpeningDropped=`openingDropped`,e}({});export{r as DoorSwing,n as OpeningKind,i as PlanIssueKind,e as PlanUnitSystem,t as WallType};
1
+ "use client";var e=function(e){return e.Metric=`metric`,e.Imperial=`imperial`,e}({}),t=function(e){return e.Exterior=`exterior`,e.Interior=`interior`,e.Partition=`partition`,e.Virtual=`virtual`,e}({}),n=function(e){return e.Door=`door`,e.Window=`window`,e.Archway=`archway`,e.Gap=`gap`,e}({}),r=function(e){return e.None=`none`,e.StartLeft=`startLeft`,e.StartRight=`startRight`,e.EndLeft=`endLeft`,e.EndRight=`endRight`,e.Double=`double`,e.Sliding=`sliding`,e}({}),i=function(e){return e.ZeroLengthWall=`zeroLengthWall`,e.SelfLoopWall=`selfLoopWall`,e.DuplicateWall=`duplicateWall`,e.CoincidentNodes=`coincidentNodes`,e.UnsplitCrossing=`unsplitCrossing`,e.UnsplitNodeOnWall=`unsplitNodeOnWall`,e.OverlappingWalls=`overlappingWalls`,e.OrphanNode=`orphanNode`,e.DanglingOpening=`danglingOpening`,e.UnmatchedRoom=`unmatchedRoom`,e.OpeningDropped=`openingDropped`,e}({});export{r as DoorSwing,n as OpeningKind,i as PlanIssueKind,e as PlanUnitSystem,t as WallType};
2
2
  //# sourceMappingURL=floorPlan.types.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"floorPlan.types.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.types.ts"],"sourcesContent":["/**\n * Floor plan model: a planar wall graph whose rooms are derived, not stored.\n *\n * Nodes and walls are first-class and shared, so the wall between two rooms is one\n * wall. An opening therefore cuts it exactly once, which is what the previous\n * room-polygon model could not express.\n *\n * GEOMETRY INVARIANT: every coordinate, length, thickness, offset and height in this\n * module is in MILLIMETRES. There is deliberately no per-plan scale factor: one would\n * have to be threaded through every geometry function, and a single missed call site\n * is a silent 1000x error that no type can catch. `unitSystem` below drives display\n * formatting only and is never read by geometry.\n */\n\n/** A point in plan space, in millimetres. y increases downwards, as in SVG. */\nexport interface PlanPoint {\n x: number;\n y: number;\n}\n\nexport enum PlanUnitSystem {\n /** Display as m / cm / mm. */\n Metric = 'metric',\n /** Display as feet and inches. Storage stays millimetres. */\n Imperial = 'imperial',\n}\n\nexport enum WallType {\n /** Load-bearing envelope wall. */\n Exterior = 'exterior',\n /** Load-bearing interior wall. */\n Interior = 'interior',\n /** Non-load-bearing partition. */\n Partition = 'partition',\n /**\n * A room boundary with no physical wall, for dividing open-plan space. It still\n * closes a face, so it produces two rooms, but renders as a thin dashed line and\n * extrudes to nothing in 3D.\n */\n Virtual = 'virtual',\n}\n\nexport enum OpeningKind {\n /** A real door leaf, with a swing and a threshold. */\n Door = 'door',\n /** Glazed opening with a sill above floor level. */\n Window = 'window',\n /** Structural opening with a head but no leaf. */\n Archway = 'archway',\n /**\n * A plain hole in the wall with no jamb detailing. Distinct from \"no wall at all\":\n * a Gap is a marked span of a wall that exists, which is what lets a gap-style\n * rendering stay unambiguous.\n */\n Gap = 'gap',\n}\n\nexport enum DoorSwing {\n None = 'none',\n /** Hinged at the opening's start, opening towards the wall's left normal. */\n StartLeft = 'startLeft',\n StartRight = 'startRight',\n EndLeft = 'endLeft',\n EndRight = 'endRight',\n /** Two leaves, hinged at both ends. */\n Double = 'double',\n Sliding = 'sliding',\n}\n\n/** A graph vertex. Structurally assignable to `PlanPoint`. */\nexport interface PlanNode {\n id: string;\n x: number;\n y: number;\n}\n\n/**\n * An undirected edge between two nodes. `a` and `b` define the wall's own direction,\n * which is what opening offsets and door swings are measured against.\n *\n * The wall is centred on the segment a..b, extending `thickness / 2` to each side.\n * There is no eccentric offset in this version.\n */\nexport interface PlanWall {\n id: string;\n a: string;\n b: string;\n /** Millimetres. Must be greater than 0. */\n thickness: number;\n /** Millimetres above this wall's base. Omit to inherit `PlanFloor.height`. */\n height?: number;\n /** Millimetres above the floor's finished level. Defaults to 0. */\n baseHeight?: number;\n type: WallType;\n}\n\n/**\n * An opening cut into exactly one wall. Because a shared wall is one wall, an opening\n * between two rooms exists exactly once and needs no de-duplication anywhere.\n */\nexport interface PlanOpening {\n id: string;\n wallId: string;\n kind: OpeningKind;\n /**\n * Distance in millimetres from the wall's node `a` to the opening's CENTRE, measured\n * along the centreline.\n *\n * Deliberately absolute rather than a 0..1 parameter: with a parameter, stretching a\n * wall by dragging one of its nodes would rescale every door on it. Absolute offsets\n * are clamped instead when a wall shortens.\n */\n offset: number;\n /** Millimetres, along the wall. */\n width: number;\n /**\n * Height of the opening's BOTTOM above the floor, in millimetres: 0 for a door, around\n * 900 for a window. Nothing in the 2D plan reads it, since a plan is a horizontal cut\n * and every opening looks the same from above. It is here so the same plan can drive a\n * 3D view later without the heights having to be invented or asked for again.\n */\n sill: number;\n /** Height of the opening's TOP above the floor, in millimetres. Typically 2100. */\n head: number;\n /** Only meaningful for `OpeningKind.Door`. */\n swing?: DoorSwing;\n}\n\n/**\n * Room METADATA. This is not a geometric object: the polygon comes from face\n * extraction. The trailing `wallIds` / `area` fields are a match cache maintained by\n * `derivePlan`, and are the reason a room's name survives edits.\n */\nexport interface PlanRoom {\n id: string;\n name: string;\n /** Any CSS colour. Omit to use the theme default. */\n colour?: string;\n /** Free-form usage tag (\"kitchen\", \"bath\"), for later fixture and 3D defaults. */\n usage?: string;\n /**\n * A point that lay inside the room's polygon when it was last written. Used as the\n * label position and as the secondary identity signal during matching.\n */\n anchor: PlanPoint;\n /** Set once the user drags the label; `derivePlan` then stops moving it. */\n anchorPinned: boolean;\n /** Monotonic creation counter, used as a deterministic tie-break when matching. */\n seq: number;\n /** Match cache: the wall ids on this room's boundary at the last successful match. */\n wallIds: string[];\n /** Match cache: area in mm² at the last successful match. */\n area: number;\n}\n\nexport interface PlanFloor {\n id: string;\n label: string;\n /** Finished floor level of this storey, millimetres above datum. */\n elevation: number;\n /** Default storey height in millimetres, used when a wall omits its own `height`. */\n height: number;\n /**\n * Keyed by id for O(1) lookup. Ids must not be numeric strings ('n_12', never '12'),\n * so that JavaScript preserves insertion order and iteration stays reproducible.\n */\n nodes: Record<string, PlanNode>;\n walls: Record<string, PlanWall>;\n openings: Record<string, PlanOpening>;\n rooms: Record<string, PlanRoom>;\n}\n\nexport interface FloorPlan {\n id: string;\n name?: string;\n /** Schema version, for future migrations. */\n version: 1;\n /** Display formatting only. Never read by geometry. */\n unitSystem: PlanUnitSystem;\n floors: PlanFloor[];\n /**\n * Bumped by every mutating operation. Use it as the memo key for `derivePlan`, so a\n * gesture that does not actually change the graph does not trigger a re-derive.\n */\n revision: number;\n}\n\n/**\n * A raw face from the planar walk, before room metadata is attached.\n *\n * WINDING: `outer` is positively wound under the shoelace formula, which in SVG's\n * y-down space means it reads as visually CLOCKWISE. Anything downstream that assumes\n * counter-clockwise (extruded slab normals, most triangulators) must flip it.\n */\nexport interface PlanFace {\n /**\n * Derived from the boundary itself, so it is stable across derives for as long as\n * the geometry is unchanged. Safe to use as a React key.\n */\n id: string;\n /** Outer ring, closed implicitly: the first vertex is not repeated at the end. */\n outer: PlanPoint[];\n /** Inner rings from nested components, wound opposite to `outer`. */\n holes: PlanPoint[][];\n /** mm², always positive, with holes already subtracted. */\n area: number;\n /** mm, outer ring only. */\n perimeter: number;\n /**\n * Wall ids on the boundary, in traversal order. A dangling wall inside this face\n * appears TWICE, once per direction, so consumers needing unique ids must dedupe.\n */\n wallIds: string[];\n nodeIds: string[];\n}\n\n/** A face with its matched metadata. This is what the components render. */\nexport interface DerivedRoom extends PlanFace {\n /** `null` when no `PlanRoom` matched this face during this derive. */\n roomId: string | null;\n meta: PlanRoom | null;\n /** Guaranteed inside `outer` and outside every hole. The label position. */\n anchor: PlanPoint;\n}\n\nexport enum PlanIssueKind {\n ZeroLengthWall = 'zeroLengthWall',\n SelfLoopWall = 'selfLoopWall',\n DuplicateWall = 'duplicateWall',\n CoincidentNodes = 'coincidentNodes',\n /** Two walls cross with no node at the intersection. Corrupts face extraction. */\n UnsplitCrossing = 'unsplitCrossing',\n /** A node sits on a wall's interior but that wall was never split there. */\n UnsplitNodeOnWall = 'unsplitNodeOnWall',\n /** Two walls leave a node in the same direction, tying the angular sort. */\n OverlappingWalls = 'overlappingWalls',\n OrphanNode = 'orphanNode',\n DanglingOpening = 'danglingOpening',\n /** A `PlanRoom` matched no face this derive. It is kept, never deleted. */\n UnmatchedRoom = 'unmatchedRoom',\n /** An opening was moved or dropped because its wall no longer has room for it. */\n OpeningDropped = 'openingDropped',\n}\n\nexport interface PlanIssue {\n kind: PlanIssueKind;\n message: string;\n nodeIds?: string[];\n wallIds?: string[];\n roomIds?: string[];\n openingIds?: string[];\n}\n\nexport interface PlanDerivation {\n /** The floor with room match caches and anchors refreshed. Pure: a new object. */\n floor: PlanFloor;\n rooms: DerivedRoom[];\n issues: PlanIssue[];\n}\n\n/** The uniform result of every graph mutation. All of them are pure. */\nexport interface PlanEdit<T = void> {\n floor: PlanFloor;\n result: T;\n issues: PlanIssue[];\n}\n\n/**\n * The viewport: which world point sits at the canvas's top-left corner, and how many\n * screen pixels one world millimetre occupies.\n *\n * A 6m plan filling a 600px canvas is `zoom: 0.1`. Note this is a transform on a group,\n * not an SVG `viewBox`: handles, labels and hit tolerances are then sized in screen\n * pixels and stay constant as you zoom, instead of scaling with the drawing.\n */\nexport interface PlanView {\n x: number;\n y: number;\n zoom: number;\n}\n\n/** What is currently selected or hovered. */\nexport type PlanSelection =\n | { type: 'room'; id: string }\n | { type: 'wall'; id: string }\n | { type: 'opening'; id: string }\n | { type: 'node'; id: string };\n"],"mappings":"AAoBA,IAAY,EAAL,SAAA,EAAA,OAEL,GAAA,OAAA,SAEA,EAAA,SAAA,YACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OAEL,GAAA,SAAA,WAEA,EAAA,SAAA,WAEA,EAAA,UAAA,YAMA,EAAA,QAAA,WACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OAEL,GAAA,KAAA,OAEA,EAAA,OAAA,SAEA,EAAA,QAAA,UAMA,EAAA,IAAA,OACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OACL,GAAA,KAAA,OAEA,EAAA,UAAA,YACA,EAAA,WAAA,aACA,EAAA,QAAA,UACA,EAAA,SAAA,WAEA,EAAA,OAAA,SACA,EAAA,QAAA,WACF,EAAA,CAAA,CAAA,EA8JY,EAAL,SAAA,EAAA,OACL,GAAA,eAAA,iBACA,EAAA,aAAA,eACA,EAAA,cAAA,gBACA,EAAA,gBAAA,kBAEA,EAAA,gBAAA,kBAEA,EAAA,kBAAA,oBAEA,EAAA,iBAAA,mBACA,EAAA,WAAA,aACA,EAAA,gBAAA,kBAEA,EAAA,cAAA,gBAEA,EAAA,eAAA,kBACF,EAAA,CAAA,CAAA"}
1
+ {"version":3,"file":"floorPlan.types.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.types.ts"],"sourcesContent":["/**\n * Floor plan model: a planar wall graph whose rooms are derived, not stored.\n *\n * Nodes and walls are first-class and shared, so the wall between two rooms is one\n * wall. An opening therefore cuts it exactly once, which is what the previous\n * room-polygon model could not express.\n *\n * GEOMETRY INVARIANT: every coordinate, length, thickness, offset and height in this\n * module is in MILLIMETRES. There is deliberately no per-plan scale factor: one would\n * have to be threaded through every geometry function, and a single missed call site\n * is a silent 1000x error that no type can catch. `unitSystem` below drives display\n * formatting only and is never read by geometry.\n */\n\n/** A point in plan space, in millimetres. y increases downwards, as in SVG. */\nexport interface PlanPoint {\n x: number;\n y: number;\n}\n\nexport enum PlanUnitSystem {\n /** Display as m / cm / mm. */\n Metric = 'metric',\n /** Display as feet and inches. Storage stays millimetres. */\n Imperial = 'imperial',\n}\n\nexport enum WallType {\n /** Load-bearing envelope wall. */\n Exterior = 'exterior',\n /** Load-bearing interior wall. */\n Interior = 'interior',\n /** Non-load-bearing partition. */\n Partition = 'partition',\n /**\n * A room boundary with no physical wall, for dividing open-plan space. It still\n * closes a face, so it produces two rooms, but renders as a thin dashed line and\n * extrudes to nothing in 3D.\n */\n Virtual = 'virtual',\n}\n\nexport enum OpeningKind {\n /** A real door leaf, with a swing and a threshold. */\n Door = 'door',\n /** Glazed opening with a sill above floor level. */\n Window = 'window',\n /** Structural opening with a head but no leaf. */\n Archway = 'archway',\n /**\n * A plain hole in the wall with no jamb detailing. Distinct from \"no wall at all\":\n * a Gap is a marked span of a wall that exists, which is what lets a gap-style\n * rendering stay unambiguous.\n */\n Gap = 'gap',\n}\n\nexport enum DoorSwing {\n None = 'none',\n /** Hinged at the opening's start, opening towards the wall's left normal. */\n StartLeft = 'startLeft',\n StartRight = 'startRight',\n EndLeft = 'endLeft',\n EndRight = 'endRight',\n /** Two leaves, hinged at both ends. */\n Double = 'double',\n Sliding = 'sliding',\n}\n\n/** A graph vertex. Structurally assignable to `PlanPoint`. */\nexport interface PlanNode {\n id: string;\n x: number;\n y: number;\n}\n\n/**\n * An undirected edge between two nodes. `a` and `b` define the wall's own direction,\n * which is what opening offsets and door swings are measured against.\n *\n * The wall is centred on the segment a..b, extending `thickness / 2` to each side.\n * There is no eccentric offset in this version.\n */\nexport interface PlanWall {\n id: string;\n a: string;\n b: string;\n /** Millimetres. Must be greater than 0. */\n thickness: number;\n /** Millimetres above this wall's base. Omit to inherit `PlanFloor.height`. */\n height?: number;\n /** Millimetres above the floor's finished level. Defaults to 0. */\n baseHeight?: number;\n type: WallType;\n}\n\n/**\n * An opening cut into exactly one wall. Because a shared wall is one wall, an opening\n * between two rooms exists exactly once and needs no de-duplication anywhere.\n */\nexport interface PlanOpening {\n id: string;\n wallId: string;\n kind: OpeningKind;\n /**\n * Distance in millimetres from the wall's node `a` to the opening's CENTRE, measured\n * along the centreline.\n *\n * Deliberately absolute rather than a 0..1 parameter: with a parameter, stretching a\n * wall by dragging one of its nodes would rescale every door on it. Absolute offsets\n * are clamped instead when a wall shortens.\n */\n offset: number;\n /** Millimetres, along the wall. */\n width: number;\n /**\n * Height of the opening's BOTTOM above the floor, in millimetres: 0 for a door, around\n * 900 for a window. Nothing in the 2D plan reads it, since a plan is a horizontal cut\n * and every opening looks the same from above. It is here so the same plan can drive a\n * 3D view later without the heights having to be invented or asked for again.\n */\n sill: number;\n /** Height of the opening's TOP above the floor, in millimetres. Typically 2100. */\n head: number;\n /** Only meaningful for `OpeningKind.Door`. */\n swing?: DoorSwing;\n}\n\n/**\n * Room METADATA. This is not a geometric object: the polygon comes from face\n * extraction. The trailing `wallIds` / `area` fields are a match cache maintained by\n * `derivePlan`, and are the reason a room's name survives edits.\n */\nexport interface PlanRoom {\n id: string;\n name: string;\n /** Any CSS colour. Omit to use the theme default. */\n colour?: string;\n /** Free-form usage tag (\"kitchen\", \"bath\"), for later fixture and 3D defaults. */\n usage?: string;\n /**\n * A point that lay inside the room's polygon when it was last written. Used as the\n * label position and as the secondary identity signal during matching.\n */\n anchor: PlanPoint;\n /** Set once the user drags the label; `derivePlan` then stops moving it. */\n anchorPinned: boolean;\n /** Monotonic creation counter, used as a deterministic tie-break when matching. */\n seq: number;\n /** Match cache: the wall ids on this room's boundary at the last successful match. */\n wallIds: string[];\n /** Match cache: area in mm² at the last successful match. */\n area: number;\n}\n\nexport interface PlanFloor {\n id: string;\n label: string;\n /** Finished floor level of this storey, millimetres above datum. */\n elevation: number;\n /** Default storey height in millimetres, used when a wall omits its own `height`. */\n height: number;\n /**\n * Keyed by id for O(1) lookup. Ids must not be numeric strings ('n_12', never '12'),\n * so that JavaScript preserves insertion order and iteration stays reproducible.\n */\n nodes: Record<string, PlanNode>;\n walls: Record<string, PlanWall>;\n openings: Record<string, PlanOpening>;\n rooms: Record<string, PlanRoom>;\n}\n\nexport interface FloorPlan {\n id: string;\n name?: string;\n /** Schema version, for future migrations. */\n version: 1;\n /** Display formatting only. Never read by geometry. */\n unitSystem: PlanUnitSystem;\n floors: PlanFloor[];\n /**\n * Bumped by every mutating operation. Use it as the memo key for `derivePlan`, so a\n * gesture that does not actually change the graph does not trigger a re-derive.\n */\n revision: number;\n}\n\n/**\n * A raw face from the planar walk, before room metadata is attached.\n *\n * WINDING: `outer` is positively wound under the shoelace formula, which in SVG's\n * y-down space means it reads as visually CLOCKWISE. Anything downstream that assumes\n * counter-clockwise (extruded slab normals, most triangulators) must flip it.\n */\nexport interface PlanFace {\n /**\n * Derived from the boundary itself, so it is stable across derives for as long as\n * the geometry is unchanged. Safe to use as a React key.\n */\n id: string;\n /** Outer ring, closed implicitly: the first vertex is not repeated at the end. */\n outer: PlanPoint[];\n /** Inner rings from nested components, wound opposite to `outer`. */\n holes: PlanPoint[][];\n /** mm², always positive, with holes already subtracted. */\n area: number;\n /** mm, outer ring only. */\n perimeter: number;\n /**\n * Wall ids on the boundary, in traversal order. A dangling wall inside this face\n * appears TWICE, once per direction, so consumers needing unique ids must dedupe.\n */\n wallIds: string[];\n nodeIds: string[];\n}\n\n/** A face with its matched metadata. This is what the components render. */\nexport interface DerivedRoom extends PlanFace {\n /** `null` when no `PlanRoom` matched this face during this derive. */\n roomId: string | null;\n meta: PlanRoom | null;\n /** Guaranteed inside `outer` and outside every hole. The label position. */\n anchor: PlanPoint;\n}\n\nexport enum PlanIssueKind {\n ZeroLengthWall = 'zeroLengthWall',\n SelfLoopWall = 'selfLoopWall',\n DuplicateWall = 'duplicateWall',\n CoincidentNodes = 'coincidentNodes',\n /** Two walls cross with no node at the intersection. Corrupts face extraction. */\n UnsplitCrossing = 'unsplitCrossing',\n /** A node sits on a wall's interior but that wall was never split there. */\n UnsplitNodeOnWall = 'unsplitNodeOnWall',\n /** Two walls leave a node in the same direction, tying the angular sort. */\n OverlappingWalls = 'overlappingWalls',\n OrphanNode = 'orphanNode',\n DanglingOpening = 'danglingOpening',\n /** A `PlanRoom` matched no face this derive. It is kept, never deleted. */\n UnmatchedRoom = 'unmatchedRoom',\n /** An opening was moved or dropped because its wall no longer has room for it. */\n OpeningDropped = 'openingDropped',\n}\n\nexport interface PlanIssue {\n kind: PlanIssueKind;\n message: string;\n nodeIds?: string[];\n wallIds?: string[];\n roomIds?: string[];\n openingIds?: string[];\n}\n\nexport interface PlanDerivation {\n /** The floor with room match caches and anchors refreshed. Pure: a new object. */\n floor: PlanFloor;\n rooms: DerivedRoom[];\n issues: PlanIssue[];\n}\n\n/** The uniform result of every graph mutation. All of them are pure. */\nexport interface PlanEdit<T = void> {\n floor: PlanFloor;\n result: T;\n issues: PlanIssue[];\n}\n\n/**\n * The viewport: which world point sits at the canvas's top-left corner, and how many\n * screen pixels one world millimetre occupies.\n *\n * A 6m plan filling a 600px canvas is `zoom: 0.1`. Note this is a transform on a group,\n * not an SVG `viewBox`: handles, labels and hit tolerances are then sized in screen\n * pixels and stay constant as you zoom, instead of scaling with the drawing.\n */\nexport interface PlanView {\n x: number;\n y: number;\n zoom: number;\n}\n\n/** What is currently selected or hovered. */\nexport type PlanSelection =\n | { type: 'room'; id: string }\n | { type: 'wall'; id: string }\n | { type: 'opening'; id: string }\n | { type: 'node'; id: string };\n"],"mappings":"aAoBA,IAAY,EAAL,SAAA,EAAA,OAEL,GAAA,OAAA,SAEA,EAAA,SAAA,YACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OAEL,GAAA,SAAA,WAEA,EAAA,SAAA,WAEA,EAAA,UAAA,YAMA,EAAA,QAAA,WACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OAEL,GAAA,KAAA,OAEA,EAAA,OAAA,SAEA,EAAA,QAAA,UAMA,EAAA,IAAA,OACF,EAAA,CAAA,CAAA,EAEY,EAAL,SAAA,EAAA,OACL,GAAA,KAAA,OAEA,EAAA,UAAA,YACA,EAAA,WAAA,aACA,EAAA,QAAA,UACA,EAAA,SAAA,WAEA,EAAA,OAAA,SACA,EAAA,QAAA,WACF,EAAA,CAAA,CAAA,EA8JY,EAAL,SAAA,EAAA,OACL,GAAA,eAAA,iBACA,EAAA,aAAA,eACA,EAAA,cAAA,gBACA,EAAA,gBAAA,kBAEA,EAAA,gBAAA,kBAEA,EAAA,kBAAA,oBAEA,EAAA,iBAAA,mBACA,EAAA,WAAA,aACA,EAAA,gBAAA,kBAEA,EAAA,cAAA,gBAEA,EAAA,eAAA,kBACF,EAAA,CAAA,CAAA"}
@@ -1,2 +1,2 @@
1
- import{OpeningKind as e,WallType as t}from"./floorPlan.types.mjs";import{DEFAULT_WALL_ATTRS as n,addOpening as r,addWall as i}from"./floorPlan.graph.mjs";import{emptyFloorPlan as a}from"./floorPlan.plan.mjs";import{commitDerivation as o,derivePlan as s}from"./floorPlan.rooms.mjs";function c(e){let t=[];for(let n of e.matchAll(/[ML]\s*(-?[\d.]+)[,\s]+(-?[\d.]+)/gi))t.push({x:parseFloat(n[1]),y:parseFloat(n[2])});return t}function l(e){let t=e.match(/^\s*M\s*(-?[\d.]+),(-?[\d.]+)\s*L\s*(-?[\d.]+),(-?[\d.]+)\s*M\s*(-?[\d.]+),(-?[\d.]+)\s*A\s/i);return t?{p1:{x:parseFloat(t[1]),y:parseFloat(t[2])},p2:{x:parseFloat(t[3]),y:parseFloat(t[4])}}:null}function u(e){let t=new DOMParser().parseFromString(e,`image/svg+xml`),n=[],r=[];return t.querySelectorAll(`[id^="room"]`).forEach(e=>{let t=e.querySelector(`[id="background"]`),i=t?.getAttribute(`d`);if(!i)return;let a=c(i);a.length<3||(n.push({name:e.querySelector(`text`)?.textContent?.trim()??``,points:a}),e.querySelectorAll(`path`).forEach(e=>{if(e===t)return;let n=l(e.getAttribute(`d`)??``);n&&r.push(n)}))}),{rooms:n,doors:r}}function d(c,l={}){let{scale:d=1,thickness:p=n.thickness,weldTolerance:m,label:h}=l,{rooms:g,doors:_}=u(c),v=a({floorLabel:h}),y=v.floors[0],b=m??Math.max(1,d*2);for(let e of g){let n=e.points.map(e=>({x:e.x*d,y:e.y*d}));for(let e=0;e<n.length;e++){let r=n[e],a=n[(e+1)%n.length];y=i(y,r,a,{thickness:p,type:t.Interior},b).floor}}for(let t of _){let n={x:t.p1.x*d,y:t.p1.y*d},i={x:t.p2.x*d,y:t.p2.y*d},a={x:(n.x+i.x)/2,y:(n.y+i.y)/2},o=Math.hypot(i.x-n.x,i.y-n.y);if(o<=0)continue;let s=null;for(let e of Object.values(y.walls)){let t=y.nodes[e.a],n=y.nodes[e.b];if(!t||!n)continue;let r=Math.hypot(n.x-t.x,n.y-t.y);if(r<1e-6)continue;let i=((a.x-t.x)*(n.x-t.x)+(a.y-t.y)*(n.y-t.y))/(r*r),o=Math.min(1,Math.max(0,i)),c=t.x+(n.x-t.x)*o,l=t.y+(n.y-t.y)*o,u=Math.hypot(a.x-c,a.y-l);u<=b&&(!s||u<s.dist)&&(s={wallId:e.id,offset:o*r,dist:u})}s&&(y=r(y,{wallId:s.wallId,kind:e.Door,offset:s.offset,width:o,sill:0,head:2100}).floor)}y=o(y).floor;let x=s(y).rooms;for(let e of g){if(!e.name)continue;let t={x:e.points.reduce((e,t)=>e+t.x,0)/e.points.length*d,y:e.points.reduce((e,t)=>e+t.y,0)/e.points.length*d},n=x.find(e=>e.roomId&&e.outer.length>2&&f(t,e.outer)&&y.rooms[e.roomId]?.name.startsWith(`Room `));n?.roomId&&(y={...y,rooms:{...y.rooms,[n.roomId]:{...y.rooms[n.roomId],name:e.name}}})}return{...v,floors:[y]}}function f(e,t){let n=!1;for(let r=0,i=t.length-1;r<t.length;i=r++){let a=t[r].x,o=t[r].y,s=t[i].x,c=t[i].y;o>e.y!=c>e.y&&e.x<(s-a)*(e.y-o)/(c-o)+a&&(n=!n)}return n}export{d as planFromLegacySvg};
1
+ "use client";import{OpeningKind as e,WallType as t}from"./floorPlan.types.mjs";import{DEFAULT_WALL_ATTRS as n,addOpening as r,addWall as i}from"./floorPlan.graph.mjs";import{emptyFloorPlan as a}from"./floorPlan.plan.mjs";import{commitDerivation as o,derivePlan as s}from"./floorPlan.rooms.mjs";function c(e){let t=[];for(let n of e.matchAll(/[ML]\s*(-?[\d.]+)[,\s]+(-?[\d.]+)/gi))t.push({x:parseFloat(n[1]),y:parseFloat(n[2])});return t}function l(e){let t=e.match(/^\s*M\s*(-?[\d.]+),(-?[\d.]+)\s*L\s*(-?[\d.]+),(-?[\d.]+)\s*M\s*(-?[\d.]+),(-?[\d.]+)\s*A\s/i);return t?{p1:{x:parseFloat(t[1]),y:parseFloat(t[2])},p2:{x:parseFloat(t[3]),y:parseFloat(t[4])}}:null}function u(e){let t=new DOMParser().parseFromString(e,`image/svg+xml`),n=[],r=[];return t.querySelectorAll(`[id^="room"]`).forEach(e=>{let t=e.querySelector(`[id="background"]`),i=t?.getAttribute(`d`);if(!i)return;let a=c(i);a.length<3||(n.push({name:e.querySelector(`text`)?.textContent?.trim()??``,points:a}),e.querySelectorAll(`path`).forEach(e=>{if(e===t)return;let n=l(e.getAttribute(`d`)??``);n&&r.push(n)}))}),{rooms:n,doors:r}}function d(c,l={}){let{scale:d=1,thickness:p=n.thickness,weldTolerance:m,label:h}=l,{rooms:g,doors:_}=u(c),v=a({floorLabel:h}),y=v.floors[0],b=m??Math.max(1,d*2);for(let e of g){let n=e.points.map(e=>({x:e.x*d,y:e.y*d}));for(let e=0;e<n.length;e++){let r=n[e],a=n[(e+1)%n.length];y=i(y,r,a,{thickness:p,type:t.Interior},b).floor}}for(let t of _){let n={x:t.p1.x*d,y:t.p1.y*d},i={x:t.p2.x*d,y:t.p2.y*d},a={x:(n.x+i.x)/2,y:(n.y+i.y)/2},o=Math.hypot(i.x-n.x,i.y-n.y);if(o<=0)continue;let s=null;for(let e of Object.values(y.walls)){let t=y.nodes[e.a],n=y.nodes[e.b];if(!t||!n)continue;let r=Math.hypot(n.x-t.x,n.y-t.y);if(r<1e-6)continue;let i=((a.x-t.x)*(n.x-t.x)+(a.y-t.y)*(n.y-t.y))/(r*r),o=Math.min(1,Math.max(0,i)),c=t.x+(n.x-t.x)*o,l=t.y+(n.y-t.y)*o,u=Math.hypot(a.x-c,a.y-l);u<=b&&(!s||u<s.dist)&&(s={wallId:e.id,offset:o*r,dist:u})}s&&(y=r(y,{wallId:s.wallId,kind:e.Door,offset:s.offset,width:o,sill:0,head:2100}).floor)}y=o(y).floor;let x=s(y).rooms;for(let e of g){if(!e.name)continue;let t={x:e.points.reduce((e,t)=>e+t.x,0)/e.points.length*d,y:e.points.reduce((e,t)=>e+t.y,0)/e.points.length*d},n=x.find(e=>e.roomId&&e.outer.length>2&&f(t,e.outer)&&y.rooms[e.roomId]?.name.startsWith(`Room `));n?.roomId&&(y={...y,rooms:{...y.rooms,[n.roomId]:{...y.rooms[n.roomId],name:e.name}}})}return{...v,floors:[y]}}function f(e,t){let n=!1;for(let r=0,i=t.length-1;r<t.length;i=r++){let a=t[r].x,o=t[r].y,s=t[i].x,c=t[i].y;o>e.y!=c>e.y&&e.x<(s-a)*(e.y-o)/(c-o)+a&&(n=!n)}return n}export{d as planFromLegacySvg};
2
2
  //# sourceMappingURL=legacySvg.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"legacySvg.mjs","names":[],"sources":["../../../../package/common/floorPlan/legacySvg.ts"],"sourcesContent":["/**\n * Imports floor plans exported by the previous `RoomDrawer` into the current model.\n *\n * The old format stored each room as its own closed polygon, so a wall between two\n * rooms appeared twice, once per room. Feeding every polygon edge through `addWall` is\n * what converts that: coincident edges resolve to the same nodes and the second one is\n * recognised as already walled, so the pair collapses into the single shared wall the\n * new model needs.\n *\n * Import only. Nothing writes this format any more. See LEGACY_SVG_FORMAT.md in this\n * folder for the specification of what is accepted.\n */\n\nimport { DEFAULT_WALL_ATTRS, addOpening, addWall, scalePlan } from './floorPlan.graph';\nimport { commitDerivation, derivePlan } from './floorPlan.rooms';\nimport { emptyFloorPlan } from './floorPlan.plan';\nimport { OpeningKind, WallType } from './floorPlan.types';\nimport type { FloorPlan, PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport interface LegacyImportOptions {\n /**\n * Multiplies every imported coordinate, converting the old unitless canvas into the\n * module's millimetre invariant.\n *\n * There is no correct automatic value: the old format records no scale at all. The\n * default of 1 keeps the numbers as they were, which makes a typical 400x300 export\n * a 40cm plan. A 400-unit-wide export meant to be a 12m house wants `scale: 30`.\n */\n scale?: number;\n /** Wall thickness for imported walls, in millimetres after scaling. */\n thickness?: number;\n /** Tolerance for welding coincident endpoints, in millimetres after scaling. */\n weldTolerance?: number;\n label?: string;\n}\n\ninterface LegacyRoom {\n name: string;\n points: PlanPoint[];\n}\n\ninterface LegacyDoor {\n p1: PlanPoint;\n p2: PlanPoint;\n}\n\n/** Reads the absolute M/L commands the old exporter wrote. */\nfunction parseClosedPath(d: string): PlanPoint[] {\n const points: PlanPoint[] = [];\n for (const m of d.matchAll(/[ML]\\s*(-?[\\d.]+)[,\\s]+(-?[\\d.]+)/gi)) {\n points.push({ x: parseFloat(m[1]), y: parseFloat(m[2]) });\n }\n return points;\n}\n\n/**\n * Recovers a door from the old swing-arc path: `M p1 L p2 M p1 A w,w 0 0,1 end`.\n *\n * Position and width follow from p1 and p2. Anything that does not match this shape is\n * skipped and the wall is left solid, rather than guessed at.\n */\nfunction parseDoorPath(d: string): LegacyDoor | null {\n const match = d.match(/^\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*L\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*A\\s/i);\n if (!match) return null;\n return {\n p1: { x: parseFloat(match[1]), y: parseFloat(match[2]) },\n p2: { x: parseFloat(match[3]), y: parseFloat(match[4]) },\n };\n}\n\nfunction parseLegacyDocument(svg: string): { rooms: LegacyRoom[]; doors: LegacyDoor[] } {\n const doc = new DOMParser().parseFromString(svg, 'image/svg+xml');\n const rooms: LegacyRoom[] = [];\n const doors: LegacyDoor[] = [];\n\n doc.querySelectorAll('[id^=\"room\"]').forEach((group) => {\n const background = group.querySelector('[id=\"background\"]');\n const d = background?.getAttribute('d');\n if (!d) return;\n const points = parseClosedPath(d);\n if (points.length < 3) return;\n rooms.push({ name: group.querySelector('text')?.textContent?.trim() ?? '', points });\n\n group.querySelectorAll('path').forEach((path) => {\n if (path === background) return;\n const door = parseDoorPath(path.getAttribute('d') ?? '');\n if (door) doors.push(door);\n });\n });\n\n return { rooms, doors };\n}\n\n/**\n * Parses a legacy exported SVG into a single-floor `FloorPlan`.\n *\n * Rooms keep their names. Doors are recovered where the old swing-arc path is\n * recognisable, as `OpeningKind.Door`. Windows and archways did not exist in that\n * format, so nothing is lost by not looking for them.\n */\nexport function planFromLegacySvg(svg: string, options: LegacyImportOptions = {}): FloorPlan {\n const { scale = 1, thickness = DEFAULT_WALL_ATTRS.thickness, weldTolerance, label } = options;\n const { rooms, doors } = parseLegacyDocument(svg);\n\n const plan = emptyFloorPlan({ floorLabel: label });\n let floor: PlanFloor = plan.floors[0];\n // Generous relative to the old canvas, where coordinates were snapped to a grid of 5.\n const tol = weldTolerance ?? Math.max(1, scale * 2);\n\n for (const room of rooms) {\n const points = room.points.map((p) => ({ x: p.x * scale, y: p.y * scale }));\n for (let i = 0; i < points.length; i++) {\n const a = points[i];\n const b = points[(i + 1) % points.length];\n // The welding step: an edge an adjacent room already drew resolves to the same\n // two nodes, and addWall declines to duplicate it.\n floor = addWall(floor, a, b, { thickness, type: WallType.Interior }, tol).floor;\n }\n }\n\n for (const door of doors) {\n const p1 = { x: door.p1.x * scale, y: door.p1.y * scale };\n const p2 = { x: door.p2.x * scale, y: door.p2.y * scale };\n const centre = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };\n const width = Math.hypot(p2.x - p1.x, p2.y - p1.y);\n if (width <= 0) continue;\n\n // Find the wall the door sits on now that edges have been welded.\n let best: { wallId: string; offset: number; dist: number } | null = null;\n for (const wall of Object.values(floor.walls)) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const len = Math.hypot(b.x - a.x, b.y - a.y);\n if (len < 1e-6) continue;\n const t = ((centre.x - a.x) * (b.x - a.x) + (centre.y - a.y) * (b.y - a.y)) / (len * len);\n const clamped = Math.min(1, Math.max(0, t));\n const px = a.x + (b.x - a.x) * clamped;\n const py = a.y + (b.y - a.y) * clamped;\n const d = Math.hypot(centre.x - px, centre.y - py);\n if (d <= tol && (!best || d < best.dist)) {\n best = { wallId: wall.id, offset: clamped * len, dist: d };\n }\n }\n if (!best) continue;\n\n floor = addOpening(floor, {\n wallId: best.wallId,\n kind: OpeningKind.Door,\n offset: best.offset,\n width,\n sill: 0,\n head: 2100,\n }).floor;\n }\n\n // Name the derived faces from the polygons they came from, matching each face to the\n // legacy room whose own centroid falls inside it.\n const committed = commitDerivation(floor);\n floor = committed.floor;\n\n const derived = derivePlan(floor).rooms;\n for (const legacy of rooms) {\n if (!legacy.name) continue;\n const centroid = {\n x: (legacy.points.reduce((s, p) => s + p.x, 0) / legacy.points.length) * scale,\n y: (legacy.points.reduce((s, p) => s + p.y, 0) / legacy.points.length) * scale,\n };\n const match = derived.find(\n (face) =>\n face.roomId &&\n face.outer.length > 2 &&\n pointInsideRing(centroid, face.outer) &&\n floor.rooms[face.roomId]?.name.startsWith('Room '),\n );\n if (match?.roomId) {\n floor = {\n ...floor,\n rooms: { ...floor.rooms, [match.roomId]: { ...floor.rooms[match.roomId], name: legacy.name } },\n };\n }\n }\n\n return { ...plan, floors: [floor] };\n}\n\nfunction pointInsideRing(p: PlanPoint, ring: PlanPoint[]): boolean {\n let inside = false;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {\n const xi = ring[i].x;\n const yi = ring[i].y;\n const xj = ring[j].x;\n const yj = ring[j].y;\n if (yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi) inside = !inside;\n }\n return inside;\n}\n\nexport { scalePlan };\n"],"mappings":"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"}
1
+ {"version":3,"file":"legacySvg.mjs","names":[],"sources":["../../../../package/common/floorPlan/legacySvg.ts"],"sourcesContent":["/**\n * Imports floor plans exported by the previous `RoomDrawer` into the current model.\n *\n * The old format stored each room as its own closed polygon, so a wall between two\n * rooms appeared twice, once per room. Feeding every polygon edge through `addWall` is\n * what converts that: coincident edges resolve to the same nodes and the second one is\n * recognised as already walled, so the pair collapses into the single shared wall the\n * new model needs.\n *\n * Import only. Nothing writes this format any more. See LEGACY_SVG_FORMAT.md in this\n * folder for the specification of what is accepted.\n */\n\nimport { DEFAULT_WALL_ATTRS, addOpening, addWall, scalePlan } from './floorPlan.graph';\nimport { commitDerivation, derivePlan } from './floorPlan.rooms';\nimport { emptyFloorPlan } from './floorPlan.plan';\nimport { OpeningKind, WallType } from './floorPlan.types';\nimport type { FloorPlan, PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport interface LegacyImportOptions {\n /**\n * Multiplies every imported coordinate, converting the old unitless canvas into the\n * module's millimetre invariant.\n *\n * There is no correct automatic value: the old format records no scale at all. The\n * default of 1 keeps the numbers as they were, which makes a typical 400x300 export\n * a 40cm plan. A 400-unit-wide export meant to be a 12m house wants `scale: 30`.\n */\n scale?: number;\n /** Wall thickness for imported walls, in millimetres after scaling. */\n thickness?: number;\n /** Tolerance for welding coincident endpoints, in millimetres after scaling. */\n weldTolerance?: number;\n label?: string;\n}\n\ninterface LegacyRoom {\n name: string;\n points: PlanPoint[];\n}\n\ninterface LegacyDoor {\n p1: PlanPoint;\n p2: PlanPoint;\n}\n\n/** Reads the absolute M/L commands the old exporter wrote. */\nfunction parseClosedPath(d: string): PlanPoint[] {\n const points: PlanPoint[] = [];\n for (const m of d.matchAll(/[ML]\\s*(-?[\\d.]+)[,\\s]+(-?[\\d.]+)/gi)) {\n points.push({ x: parseFloat(m[1]), y: parseFloat(m[2]) });\n }\n return points;\n}\n\n/**\n * Recovers a door from the old swing-arc path: `M p1 L p2 M p1 A w,w 0 0,1 end`.\n *\n * Position and width follow from p1 and p2. Anything that does not match this shape is\n * skipped and the wall is left solid, rather than guessed at.\n */\nfunction parseDoorPath(d: string): LegacyDoor | null {\n const match = d.match(/^\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*L\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*A\\s/i);\n if (!match) return null;\n return {\n p1: { x: parseFloat(match[1]), y: parseFloat(match[2]) },\n p2: { x: parseFloat(match[3]), y: parseFloat(match[4]) },\n };\n}\n\nfunction parseLegacyDocument(svg: string): { rooms: LegacyRoom[]; doors: LegacyDoor[] } {\n const doc = new DOMParser().parseFromString(svg, 'image/svg+xml');\n const rooms: LegacyRoom[] = [];\n const doors: LegacyDoor[] = [];\n\n doc.querySelectorAll('[id^=\"room\"]').forEach((group) => {\n const background = group.querySelector('[id=\"background\"]');\n const d = background?.getAttribute('d');\n if (!d) return;\n const points = parseClosedPath(d);\n if (points.length < 3) return;\n rooms.push({ name: group.querySelector('text')?.textContent?.trim() ?? '', points });\n\n group.querySelectorAll('path').forEach((path) => {\n if (path === background) return;\n const door = parseDoorPath(path.getAttribute('d') ?? '');\n if (door) doors.push(door);\n });\n });\n\n return { rooms, doors };\n}\n\n/**\n * Parses a legacy exported SVG into a single-floor `FloorPlan`.\n *\n * Rooms keep their names. Doors are recovered where the old swing-arc path is\n * recognisable, as `OpeningKind.Door`. Windows and archways did not exist in that\n * format, so nothing is lost by not looking for them.\n */\nexport function planFromLegacySvg(svg: string, options: LegacyImportOptions = {}): FloorPlan {\n const { scale = 1, thickness = DEFAULT_WALL_ATTRS.thickness, weldTolerance, label } = options;\n const { rooms, doors } = parseLegacyDocument(svg);\n\n const plan = emptyFloorPlan({ floorLabel: label });\n let floor: PlanFloor = plan.floors[0];\n // Generous relative to the old canvas, where coordinates were snapped to a grid of 5.\n const tol = weldTolerance ?? Math.max(1, scale * 2);\n\n for (const room of rooms) {\n const points = room.points.map((p) => ({ x: p.x * scale, y: p.y * scale }));\n for (let i = 0; i < points.length; i++) {\n const a = points[i];\n const b = points[(i + 1) % points.length];\n // The welding step: an edge an adjacent room already drew resolves to the same\n // two nodes, and addWall declines to duplicate it.\n floor = addWall(floor, a, b, { thickness, type: WallType.Interior }, tol).floor;\n }\n }\n\n for (const door of doors) {\n const p1 = { x: door.p1.x * scale, y: door.p1.y * scale };\n const p2 = { x: door.p2.x * scale, y: door.p2.y * scale };\n const centre = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };\n const width = Math.hypot(p2.x - p1.x, p2.y - p1.y);\n if (width <= 0) continue;\n\n // Find the wall the door sits on now that edges have been welded.\n let best: { wallId: string; offset: number; dist: number } | null = null;\n for (const wall of Object.values(floor.walls)) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const len = Math.hypot(b.x - a.x, b.y - a.y);\n if (len < 1e-6) continue;\n const t = ((centre.x - a.x) * (b.x - a.x) + (centre.y - a.y) * (b.y - a.y)) / (len * len);\n const clamped = Math.min(1, Math.max(0, t));\n const px = a.x + (b.x - a.x) * clamped;\n const py = a.y + (b.y - a.y) * clamped;\n const d = Math.hypot(centre.x - px, centre.y - py);\n if (d <= tol && (!best || d < best.dist)) {\n best = { wallId: wall.id, offset: clamped * len, dist: d };\n }\n }\n if (!best) continue;\n\n floor = addOpening(floor, {\n wallId: best.wallId,\n kind: OpeningKind.Door,\n offset: best.offset,\n width,\n sill: 0,\n head: 2100,\n }).floor;\n }\n\n // Name the derived faces from the polygons they came from, matching each face to the\n // legacy room whose own centroid falls inside it.\n const committed = commitDerivation(floor);\n floor = committed.floor;\n\n const derived = derivePlan(floor).rooms;\n for (const legacy of rooms) {\n if (!legacy.name) continue;\n const centroid = {\n x: (legacy.points.reduce((s, p) => s + p.x, 0) / legacy.points.length) * scale,\n y: (legacy.points.reduce((s, p) => s + p.y, 0) / legacy.points.length) * scale,\n };\n const match = derived.find(\n (face) =>\n face.roomId &&\n face.outer.length > 2 &&\n pointInsideRing(centroid, face.outer) &&\n floor.rooms[face.roomId]?.name.startsWith('Room '),\n );\n if (match?.roomId) {\n floor = {\n ...floor,\n rooms: { ...floor.rooms, [match.roomId]: { ...floor.rooms[match.roomId], name: legacy.name } },\n };\n }\n }\n\n return { ...plan, floors: [floor] };\n}\n\nfunction pointInsideRing(p: PlanPoint, ring: PlanPoint[]): boolean {\n let inside = false;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {\n const xi = ring[i].x;\n const yi = ring[i].y;\n const xj = ring[j].x;\n const yj = ring[j].y;\n if (yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi) inside = !inside;\n }\n return inside;\n}\n\nexport { scalePlan };\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{getScrollableAncestors as e}from"../utils/scrollAncestors.mjs";import{useLayoutEffect as t,useRef as n}from"react";function r({isOpen:r,triggerRef:i,elementRefs:a,update:o,resizeRefs:s,teardown:c,deps:l=[]}){let u=n({elementRefs:a,update:o,resizeRefs:s,teardown:c});u.current={elementRefs:a,update:o,resizeRefs:s,teardown:c},t(()=>{if(!r)return;let t,n;function a(){let r=i.current,o=u.current.elementRefs;if(!r||o.some(e=>!e.current)){t=requestAnimationFrame(a);return}let s=e(r),c=new ResizeObserver(()=>d()),l=new Set;function d(){u.current.update(r,s),u.current.resizeRefs?.forEach(({current:e})=>{e&&!l.has(e)&&(l.add(e),c.observe(e))})}d();let f=[window,...s];f.forEach(e=>e.addEventListener(`scroll`,d,{passive:!0})),window.addEventListener(`resize`,d),window.visualViewport?.addEventListener(`resize`,d),window.visualViewport?.addEventListener(`scroll`,d),n=()=>{f.forEach(e=>e.removeEventListener(`scroll`,d)),window.removeEventListener(`resize`,d),window.visualViewport?.removeEventListener(`resize`,d),window.visualViewport?.removeEventListener(`scroll`,d),c.disconnect(),u.current.teardown?.()}}return a(),()=>{t!==void 0&&cancelAnimationFrame(t),n?.()}},[r,i,...l])}export{r as useAnchorTracking};
1
+ "use client";import{getScrollableAncestors as e}from"../utils/scrollAncestors.mjs";import{useLayoutEffect as t,useRef as n}from"react";function r({isOpen:r,triggerRef:i,elementRefs:a,update:o,resizeRefs:s,teardown:c,deps:l=[]}){let u=n({elementRefs:a,update:o,resizeRefs:s,teardown:c});u.current={elementRefs:a,update:o,resizeRefs:s,teardown:c},t(()=>{if(!r)return;let t,n;function a(){let r=i.current,o=u.current.elementRefs;if(!r||o.some(e=>!e.current)){t=requestAnimationFrame(a);return}let s=e(r),c=new ResizeObserver(()=>d()),l=new Set;function d(){u.current.update(r,s),u.current.resizeRefs?.forEach(({current:e})=>{e&&!l.has(e)&&(l.add(e),c.observe(e))})}d();let f=[window,...s];f.forEach(e=>e.addEventListener(`scroll`,d,{passive:!0})),window.addEventListener(`resize`,d),window.visualViewport?.addEventListener(`resize`,d),window.visualViewport?.addEventListener(`scroll`,d),n=()=>{f.forEach(e=>e.removeEventListener(`scroll`,d)),window.removeEventListener(`resize`,d),window.visualViewport?.removeEventListener(`resize`,d),window.visualViewport?.removeEventListener(`scroll`,d),c.disconnect(),u.current.teardown?.()}}return a(),()=>{t!==void 0&&cancelAnimationFrame(t),n?.()}},[r,i,...l])}export{r as useAnchorTracking};
2
2
  //# sourceMappingURL=useAnchorTracking.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAnchorTracking.mjs","names":[],"sources":["../../../../package/common/hooks/useAnchorTracking.ts"],"sourcesContent":["import { useLayoutEffect, useRef, type RefObject } from 'react';\nimport { getScrollableAncestors } from 'components/common/utils/scrollAncestors';\n\nexport interface UseAnchorTrackingArgs {\n isOpen: boolean;\n /** The element the popup is anchored to. Its scrollable ancestors are what get tracked. */\n triggerRef: RefObject<HTMLElement | null>;\n /**\n * Every element the positioning maths needs. Tracking starts only once all of them, plus the\n * trigger, are mounted. Read fresh on each setup, so an inline array literal is fine.\n */\n elementRefs: RefObject<HTMLElement | null>[];\n /**\n * Positions the popup. Called once on setup and then on every scroll, resize and tracked size\n * change. Reads the caller's own refs for everything beyond the two arguments.\n */\n update: (trigger: HTMLElement, ancestors: HTMLElement[]) => void;\n /**\n * Elements whose own size changes must re-run `update`: a popup whose content changes size\n * while open (a filtered list shrinking, an async list loading in) otherwise keeps coordinates\n * computed for its old height and ends up detached from the trigger. Re-read on every update,\n * since a panel can mount a commit later than the element it hangs off.\n */\n resizeRefs?: RefObject<HTMLElement | null>[];\n /** Hands the popup's elements back as they were found. Runs when tracking stops. */\n teardown?: () => void;\n /** Values that must restart tracking, and so re-run `update`, when they change. */\n deps?: readonly unknown[];\n}\n\n/**\n * The lifecycle half of anchoring a portaled, `position: fixed` popup to a trigger: waits for the\n * elements to mount, resolves the trigger's scrolling ancestors once, and re-runs the caller's\n * positioning on every scroll, viewport resize and tracked size change until it closes.\n *\n * Callers supply only the geometry (`update`) and the inline styles to clear again (`teardown`);\n * `usePopoverPosition` is built on this.\n *\n * Position is deliberately written straight to the DOM by the caller rather than through React\n * state: going through setState -> re-render -> commit adds a round-trip that lags behind the\n * browser's own scroll painting by at least a frame.\n */\nexport function useAnchorTracking({\n isOpen,\n triggerRef,\n elementRefs,\n update,\n resizeRefs,\n teardown,\n deps = [],\n}: UseAnchorTrackingArgs) {\n // Latest-value refs, so the effect restarts only for `deps` the caller names, never merely\n // because these were passed as fresh array/function literals this render.\n const latest = useRef({ elementRefs, update, resizeRefs, teardown });\n latest.current = { elementRefs, update, resizeRefs, teardown };\n\n useLayoutEffect(() => {\n if (!isOpen) return;\n\n let rafId: number | undefined;\n let stop: (() => void) | undefined;\n\n // The elements may not be mounted yet on the very first invocation of this effect: a portal\n // target (e.g. BodyEnd's `#bodyEnd` div) can still be missing on the initial render, in which\n // case the portaled content — and its ref — commits one render later. Since refs never change\n // identity, that later commit doesn't re-trigger this effect on its own, so retry each frame.\n function trySetup() {\n const trigger = triggerRef.current;\n const elements = latest.current.elementRefs;\n if (!trigger || elements.some((ref) => !ref.current)) {\n rafId = requestAnimationFrame(trySetup);\n return;\n }\n const ancestors = getScrollableAncestors(trigger);\n\n const resizeObserver = new ResizeObserver(() => runUpdate());\n const observed = new Set<Element>();\n\n function runUpdate() {\n latest.current.update(trigger!, ancestors);\n // After the update, so an element that only mounts during it is picked up too. Once\n // observed this is a handful of Set lookups, so it costs nothing on the scroll path.\n latest.current.resizeRefs?.forEach(({ current }) => {\n if (!current || observed.has(current)) return;\n observed.add(current);\n resizeObserver.observe(current);\n });\n }\n\n runUpdate();\n\n const scrollTargets: (Window | HTMLElement)[] = [window, ...ancestors];\n scrollTargets.forEach((t) => t.addEventListener('scroll', runUpdate, { passive: true }));\n window.addEventListener('resize', runUpdate);\n // `window.resize` doesn't reliably fire when a mobile keyboard opens/closes — visualViewport\n // does, and is what the callers' flip/clamp maths is measured against.\n window.visualViewport?.addEventListener('resize', runUpdate);\n window.visualViewport?.addEventListener('scroll', runUpdate);\n\n stop = () => {\n scrollTargets.forEach((t) => t.removeEventListener('scroll', runUpdate));\n window.removeEventListener('resize', runUpdate);\n window.visualViewport?.removeEventListener('resize', runUpdate);\n window.visualViewport?.removeEventListener('scroll', runUpdate);\n resizeObserver.disconnect();\n latest.current.teardown?.();\n };\n }\n\n trySetup();\n\n return () => {\n if (rafId !== undefined) cancelAnimationFrame(rafId);\n stop?.();\n };\n // Spread rather than listed: `deps` is the caller's own set of geometry inputs, and changing\n // any of them has to restart tracking so the new values get applied immediately.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen, triggerRef, ...deps]);\n}\n"],"mappings":"0HA0CA,SAAgB,EAAkB,CAChC,SACA,aACA,cACA,SACA,aACA,WACA,OAAO,CAAC,GACgB,CAGxB,IAAM,EAAS,EAAO,CAAE,cAAa,SAAQ,aAAY,UAAS,CAAC,EACnE,EAAO,QAAU,CAAE,cAAa,SAAQ,aAAY,UAAS,EAE7D,MAAsB,CACpB,GAAI,CAAC,EAAQ,OAEb,IAAI,EACA,EAMJ,SAAS,GAAW,CAClB,IAAM,EAAU,EAAW,QACrB,EAAW,EAAO,QAAQ,YAChC,GAAI,CAAC,GAAW,EAAS,KAAM,GAAQ,CAAC,EAAI,OAAO,EAAG,CACpD,EAAQ,sBAAsB,CAAQ,EACtC,MACF,CACA,IAAM,EAAY,EAAuB,CAAO,EAE1C,EAAiB,IAAI,mBAAqB,EAAU,CAAC,EACrD,EAAW,IAAI,IAErB,SAAS,GAAY,CACnB,EAAO,QAAQ,OAAO,EAAU,CAAS,EAGzC,EAAO,QAAQ,YAAY,SAAS,CAAE,aAAc,CAC7C,GAAW,GAAS,IAAI,CAAO,IACpC,EAAS,IAAI,CAAO,EACpB,EAAe,QAAQ,CAAO,EAChC,CAAC,CACH,CAEA,EAAU,EAEV,IAAM,EAA0C,CAAC,OAAQ,GAAG,CAAS,EACrE,EAAc,QAAS,GAAM,EAAE,iBAAiB,SAAU,EAAW,CAAE,QAAS,EAAK,CAAC,CAAC,EACvF,OAAO,iBAAiB,SAAU,CAAS,EAG3C,OAAO,gBAAgB,iBAAiB,SAAU,CAAS,EAC3D,OAAO,gBAAgB,iBAAiB,SAAU,CAAS,EAE3D,MAAa,CACX,EAAc,QAAS,GAAM,EAAE,oBAAoB,SAAU,CAAS,CAAC,EACvE,OAAO,oBAAoB,SAAU,CAAS,EAC9C,OAAO,gBAAgB,oBAAoB,SAAU,CAAS,EAC9D,OAAO,gBAAgB,oBAAoB,SAAU,CAAS,EAC9D,EAAe,WAAW,EAC1B,EAAO,QAAQ,WAAW,CAC5B,CACF,CAIA,OAFA,EAAS,MAEI,CACP,IAAU,IAAA,IAAW,qBAAqB,CAAK,EACnD,IAAO,CACT,CAIF,EAAG,CAAC,EAAQ,EAAY,GAAG,CAAI,CAAC,CAClC"}
1
+ {"version":3,"file":"useAnchorTracking.mjs","names":[],"sources":["../../../../package/common/hooks/useAnchorTracking.ts"],"sourcesContent":["import { useLayoutEffect, useRef, type RefObject } from 'react';\nimport { getScrollableAncestors } from 'components/common/utils/scrollAncestors';\n\nexport interface UseAnchorTrackingArgs {\n isOpen: boolean;\n /** The element the popup is anchored to. Its scrollable ancestors are what get tracked. */\n triggerRef: RefObject<HTMLElement | null>;\n /**\n * Every element the positioning maths needs. Tracking starts only once all of them, plus the\n * trigger, are mounted. Read fresh on each setup, so an inline array literal is fine.\n */\n elementRefs: RefObject<HTMLElement | null>[];\n /**\n * Positions the popup. Called once on setup and then on every scroll, resize and tracked size\n * change. Reads the caller's own refs for everything beyond the two arguments.\n */\n update: (trigger: HTMLElement, ancestors: HTMLElement[]) => void;\n /**\n * Elements whose own size changes must re-run `update`: a popup whose content changes size\n * while open (a filtered list shrinking, an async list loading in) otherwise keeps coordinates\n * computed for its old height and ends up detached from the trigger. Re-read on every update,\n * since a panel can mount a commit later than the element it hangs off.\n */\n resizeRefs?: RefObject<HTMLElement | null>[];\n /** Hands the popup's elements back as they were found. Runs when tracking stops. */\n teardown?: () => void;\n /** Values that must restart tracking, and so re-run `update`, when they change. */\n deps?: readonly unknown[];\n}\n\n/**\n * The lifecycle half of anchoring a portaled, `position: fixed` popup to a trigger: waits for the\n * elements to mount, resolves the trigger's scrolling ancestors once, and re-runs the caller's\n * positioning on every scroll, viewport resize and tracked size change until it closes.\n *\n * Callers supply only the geometry (`update`) and the inline styles to clear again (`teardown`);\n * `usePopoverPosition` is built on this.\n *\n * Position is deliberately written straight to the DOM by the caller rather than through React\n * state: going through setState -> re-render -> commit adds a round-trip that lags behind the\n * browser's own scroll painting by at least a frame.\n */\nexport function useAnchorTracking({\n isOpen,\n triggerRef,\n elementRefs,\n update,\n resizeRefs,\n teardown,\n deps = [],\n}: UseAnchorTrackingArgs) {\n // Latest-value refs, so the effect restarts only for `deps` the caller names, never merely\n // because these were passed as fresh array/function literals this render.\n const latest = useRef({ elementRefs, update, resizeRefs, teardown });\n latest.current = { elementRefs, update, resizeRefs, teardown };\n\n useLayoutEffect(() => {\n if (!isOpen) return;\n\n let rafId: number | undefined;\n let stop: (() => void) | undefined;\n\n // The elements may not be mounted yet on the very first invocation of this effect: a portal\n // target (e.g. BodyEnd's `#bodyEnd` div) can still be missing on the initial render, in which\n // case the portaled content — and its ref — commits one render later. Since refs never change\n // identity, that later commit doesn't re-trigger this effect on its own, so retry each frame.\n function trySetup() {\n const trigger = triggerRef.current;\n const elements = latest.current.elementRefs;\n if (!trigger || elements.some((ref) => !ref.current)) {\n rafId = requestAnimationFrame(trySetup);\n return;\n }\n const ancestors = getScrollableAncestors(trigger);\n\n const resizeObserver = new ResizeObserver(() => runUpdate());\n const observed = new Set<Element>();\n\n function runUpdate() {\n latest.current.update(trigger!, ancestors);\n // After the update, so an element that only mounts during it is picked up too. Once\n // observed this is a handful of Set lookups, so it costs nothing on the scroll path.\n latest.current.resizeRefs?.forEach(({ current }) => {\n if (!current || observed.has(current)) return;\n observed.add(current);\n resizeObserver.observe(current);\n });\n }\n\n runUpdate();\n\n const scrollTargets: (Window | HTMLElement)[] = [window, ...ancestors];\n scrollTargets.forEach((t) => t.addEventListener('scroll', runUpdate, { passive: true }));\n window.addEventListener('resize', runUpdate);\n // `window.resize` doesn't reliably fire when a mobile keyboard opens/closes — visualViewport\n // does, and is what the callers' flip/clamp maths is measured against.\n window.visualViewport?.addEventListener('resize', runUpdate);\n window.visualViewport?.addEventListener('scroll', runUpdate);\n\n stop = () => {\n scrollTargets.forEach((t) => t.removeEventListener('scroll', runUpdate));\n window.removeEventListener('resize', runUpdate);\n window.visualViewport?.removeEventListener('resize', runUpdate);\n window.visualViewport?.removeEventListener('scroll', runUpdate);\n resizeObserver.disconnect();\n latest.current.teardown?.();\n };\n }\n\n trySetup();\n\n return () => {\n if (rafId !== undefined) cancelAnimationFrame(rafId);\n stop?.();\n };\n // Spread rather than listed: `deps` is the caller's own set of geometry inputs, and changing\n // any of them has to restart tracking so the new values get applied immediately.\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen, triggerRef, ...deps]);\n}\n"],"mappings":"uIA0CA,SAAgB,EAAkB,CAChC,SACA,aACA,cACA,SACA,aACA,WACA,OAAO,CAAC,GACgB,CAGxB,IAAM,EAAS,EAAO,CAAE,cAAa,SAAQ,aAAY,UAAS,CAAC,EACnE,EAAO,QAAU,CAAE,cAAa,SAAQ,aAAY,UAAS,EAE7D,MAAsB,CACpB,GAAI,CAAC,EAAQ,OAEb,IAAI,EACA,EAMJ,SAAS,GAAW,CAClB,IAAM,EAAU,EAAW,QACrB,EAAW,EAAO,QAAQ,YAChC,GAAI,CAAC,GAAW,EAAS,KAAM,GAAQ,CAAC,EAAI,OAAO,EAAG,CACpD,EAAQ,sBAAsB,CAAQ,EACtC,MACF,CACA,IAAM,EAAY,EAAuB,CAAO,EAE1C,EAAiB,IAAI,mBAAqB,EAAU,CAAC,EACrD,EAAW,IAAI,IAErB,SAAS,GAAY,CACnB,EAAO,QAAQ,OAAO,EAAU,CAAS,EAGzC,EAAO,QAAQ,YAAY,SAAS,CAAE,aAAc,CAC7C,GAAW,GAAS,IAAI,CAAO,IACpC,EAAS,IAAI,CAAO,EACpB,EAAe,QAAQ,CAAO,EAChC,CAAC,CACH,CAEA,EAAU,EAEV,IAAM,EAA0C,CAAC,OAAQ,GAAG,CAAS,EACrE,EAAc,QAAS,GAAM,EAAE,iBAAiB,SAAU,EAAW,CAAE,QAAS,EAAK,CAAC,CAAC,EACvF,OAAO,iBAAiB,SAAU,CAAS,EAG3C,OAAO,gBAAgB,iBAAiB,SAAU,CAAS,EAC3D,OAAO,gBAAgB,iBAAiB,SAAU,CAAS,EAE3D,MAAa,CACX,EAAc,QAAS,GAAM,EAAE,oBAAoB,SAAU,CAAS,CAAC,EACvE,OAAO,oBAAoB,SAAU,CAAS,EAC9C,OAAO,gBAAgB,oBAAoB,SAAU,CAAS,EAC9D,OAAO,gBAAgB,oBAAoB,SAAU,CAAS,EAC9D,EAAe,WAAW,EAC1B,EAAO,QAAQ,WAAW,CAC5B,CACF,CAIA,OAFA,EAAS,MAEI,CACP,IAAU,IAAA,IAAW,qBAAqB,CAAK,EACnD,IAAO,CACT,CAIF,EAAG,CAAC,EAAQ,EAAY,GAAG,CAAI,CAAC,CAClC"}
@@ -1,2 +1,2 @@
1
- import e from"./useMediaQuery.mjs";function t(){return e(`(pointer: coarse)`)}export{t as default,t as useCoarsePointer};
1
+ "use client";import e from"./useMediaQuery.mjs";function t(){return e(`(pointer: coarse)`)}export{t as default,t as useCoarsePointer};
2
2
  //# sourceMappingURL=useCoarsePointer.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useCoarsePointer.mjs","names":[],"sources":["../../../../package/common/hooks/useCoarsePointer.ts"],"sourcesContent":["import { useMediaQuery } from './useMediaQuery';\n\n/** `true` when the primary pointing device is coarse (touch). */\nexport function useCoarsePointer(): boolean {\n return useMediaQuery('(pointer: coarse)');\n}\n\nexport default useCoarsePointer;\n"],"mappings":"mCAGA,SAAgB,GAA4B,CAC1C,OAAO,EAAc,mBAAmB,CAC1C"}
1
+ {"version":3,"file":"useCoarsePointer.mjs","names":[],"sources":["../../../../package/common/hooks/useCoarsePointer.ts"],"sourcesContent":["import { useMediaQuery } from './useMediaQuery';\n\n/** `true` when the primary pointing device is coarse (touch). */\nexport function useCoarsePointer(): boolean {\n return useMediaQuery('(pointer: coarse)');\n}\n\nexport default useCoarsePointer;\n"],"mappings":"gDAGA,SAAgB,GAA4B,CAC1C,OAAO,EAAc,mBAAmB,CAC1C"}
@@ -1,2 +1,2 @@
1
- var e=[`a[href]`,`area[href]`,`button:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`textarea:not([disabled])`,`[tabindex]:not([tabindex="-1"])`,`[contenteditable="true"]`].join(`,`);function t(t){return t?Array.from(t.querySelectorAll(e)).filter(e=>e.getAttribute(`tabindex`)!==`-1`):[]}function n({triggerRef:e,contentRef:n}){function r(){return t(e.current)[0]??e.current}function i(){r()?.focus()}function a(){let e=t(document.body).filter(e=>!n.current?.contains(e)),i=r(),a=i?e.indexOf(i):-1;a>=0&&e[a+1]?.focus()}return{firstTriggerFocusable:r,restoreFocusToTrigger:i,focusAfterTrigger:a}}export{t as tabbablesIn,n as useFocusBoundary};
1
+ "use client";var e=[`a[href]`,`area[href]`,`button:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`textarea:not([disabled])`,`[tabindex]:not([tabindex="-1"])`,`[contenteditable="true"]`].join(`,`);function t(t){return t?Array.from(t.querySelectorAll(e)).filter(e=>e.getAttribute(`tabindex`)!==`-1`):[]}function n({triggerRef:e,contentRef:n}){function r(){return t(e.current)[0]??e.current}function i(){r()?.focus()}function a(){let e=t(document.body).filter(e=>!n.current?.contains(e)),i=r(),a=i?e.indexOf(i):-1;a>=0&&e[a+1]?.focus()}return{firstTriggerFocusable:r,restoreFocusToTrigger:i,focusAfterTrigger:a}}export{t as tabbablesIn,n as useFocusBoundary};
2
2
  //# sourceMappingURL=useFocusBoundary.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useFocusBoundary.mjs","names":[],"sources":["../../../../package/common/hooks/useFocusBoundary.ts"],"sourcesContent":["import type { RefObject } from 'react';\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[contenteditable=\"true\"]',\n].join(',');\n\n/** Tabbable descendants of `root`, in DOM order. */\nexport function tabbablesIn(root: ParentNode | null): HTMLElement[] {\n if (!root) return [];\n return (\n Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n // A button or a link can be taken out of the tab order too, which the\n // selector above only catches for elements whose focusability comes from\n // the attribute. A roving tabindex, where one item of a list is the tab\n // stop and the rest are reachable by arrow key, otherwise reads as a list\n // of tab stops, and the Tab boundary lands on the wrong entry.\n .filter((el) => el.getAttribute('tabindex') !== '-1')\n );\n}\n\nexport interface UseFocusBoundaryArgs {\n /** The trigger element focus returns to on Escape / Shift+Tab out of the content's top. */\n triggerRef: RefObject<HTMLElement | null>;\n /** The portaled content root — tabbables inside it define the Tab boundary. */\n contentRef: RefObject<HTMLElement | null>;\n}\n\n/**\n * Focus helpers for content portaled to the end of the DOM (e.g. via BodyEnd), where Tab can't\n * reach it in visual document order on its own. Used by Popover and by Menu, which both need to\n * hand focus back to the trigger on Escape / Shift+Tab out of the top, and continue focus\n * naturally past the trigger when Tab exits the bottom.\n */\nexport function useFocusBoundary({ triggerRef, contentRef }: UseFocusBoundaryArgs) {\n function firstTriggerFocusable(): HTMLElement | null {\n return tabbablesIn(triggerRef.current)[0] ?? triggerRef.current;\n }\n\n function restoreFocusToTrigger() {\n firstTriggerFocusable()?.focus();\n }\n\n // Move focus to the tabbable element that follows the trigger in the document, skipping the\n // portaled content — the \"natural next\" stop when Tab exits the bottom of the content.\n function focusAfterTrigger() {\n const all = tabbablesIn(document.body).filter((el) => !contentRef.current?.contains(el));\n const trig = firstTriggerFocusable();\n const idx = trig ? all.indexOf(trig) : -1;\n if (idx >= 0) all[idx + 1]?.focus();\n }\n\n return { firstTriggerFocusable, restoreFocusToTrigger, focusAfterTrigger };\n}\n"],"mappings":"AAEA,IAAM,EAAqB,CACzB,UACA,aACA,yBACA,wBACA,yBACA,2BACA,kCACA,0BACF,CAAC,CAAC,KAAK,GAAG,EAGV,SAAgB,EAAY,EAAwC,CAElE,OADK,EAEH,MAAM,KAAK,EAAK,iBAA8B,CAAkB,CAAC,CAAC,CAM/D,OAAQ,GAAO,EAAG,aAAa,UAAU,IAAM,IAAI,EARtC,CAAC,CAUrB,CAeA,SAAgB,EAAiB,CAAE,aAAY,cAAoC,CACjF,SAAS,GAA4C,CACnD,OAAO,EAAY,EAAW,OAAO,CAAC,CAAC,IAAM,EAAW,OAC1D,CAEA,SAAS,GAAwB,CAC/B,EAAsB,CAAC,EAAE,MAAM,CACjC,CAIA,SAAS,GAAoB,CAC3B,IAAM,EAAM,EAAY,SAAS,IAAI,CAAC,CAAC,OAAQ,GAAO,CAAC,EAAW,SAAS,SAAS,CAAE,CAAC,EACjF,EAAO,EAAsB,EAC7B,EAAM,EAAO,EAAI,QAAQ,CAAI,EAAI,GACnC,GAAO,GAAG,EAAI,EAAM,EAAE,EAAE,MAAM,CACpC,CAEA,MAAO,CAAE,wBAAuB,wBAAuB,mBAAkB,CAC3E"}
1
+ {"version":3,"file":"useFocusBoundary.mjs","names":[],"sources":["../../../../package/common/hooks/useFocusBoundary.ts"],"sourcesContent":["import type { RefObject } from 'react';\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'area[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[contenteditable=\"true\"]',\n].join(',');\n\n/** Tabbable descendants of `root`, in DOM order. */\nexport function tabbablesIn(root: ParentNode | null): HTMLElement[] {\n if (!root) return [];\n return (\n Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n // A button or a link can be taken out of the tab order too, which the\n // selector above only catches for elements whose focusability comes from\n // the attribute. A roving tabindex, where one item of a list is the tab\n // stop and the rest are reachable by arrow key, otherwise reads as a list\n // of tab stops, and the Tab boundary lands on the wrong entry.\n .filter((el) => el.getAttribute('tabindex') !== '-1')\n );\n}\n\nexport interface UseFocusBoundaryArgs {\n /** The trigger element focus returns to on Escape / Shift+Tab out of the content's top. */\n triggerRef: RefObject<HTMLElement | null>;\n /** The portaled content root — tabbables inside it define the Tab boundary. */\n contentRef: RefObject<HTMLElement | null>;\n}\n\n/**\n * Focus helpers for content portaled to the end of the DOM (e.g. via BodyEnd), where Tab can't\n * reach it in visual document order on its own. Used by Popover and by Menu, which both need to\n * hand focus back to the trigger on Escape / Shift+Tab out of the top, and continue focus\n * naturally past the trigger when Tab exits the bottom.\n */\nexport function useFocusBoundary({ triggerRef, contentRef }: UseFocusBoundaryArgs) {\n function firstTriggerFocusable(): HTMLElement | null {\n return tabbablesIn(triggerRef.current)[0] ?? triggerRef.current;\n }\n\n function restoreFocusToTrigger() {\n firstTriggerFocusable()?.focus();\n }\n\n // Move focus to the tabbable element that follows the trigger in the document, skipping the\n // portaled content — the \"natural next\" stop when Tab exits the bottom of the content.\n function focusAfterTrigger() {\n const all = tabbablesIn(document.body).filter((el) => !contentRef.current?.contains(el));\n const trig = firstTriggerFocusable();\n const idx = trig ? all.indexOf(trig) : -1;\n if (idx >= 0) all[idx + 1]?.focus();\n }\n\n return { firstTriggerFocusable, restoreFocusToTrigger, focusAfterTrigger };\n}\n"],"mappings":"aAEA,IAAM,EAAqB,CACzB,UACA,aACA,yBACA,wBACA,yBACA,2BACA,kCACA,0BACF,CAAC,CAAC,KAAK,GAAG,EAGV,SAAgB,EAAY,EAAwC,CAElE,OADK,EAEH,MAAM,KAAK,EAAK,iBAA8B,CAAkB,CAAC,CAAC,CAM/D,OAAQ,GAAO,EAAG,aAAa,UAAU,IAAM,IAAI,EARtC,CAAC,CAUrB,CAeA,SAAgB,EAAiB,CAAE,aAAY,cAAoC,CACjF,SAAS,GAA4C,CACnD,OAAO,EAAY,EAAW,OAAO,CAAC,CAAC,IAAM,EAAW,OAC1D,CAEA,SAAS,GAAwB,CAC/B,EAAsB,CAAC,EAAE,MAAM,CACjC,CAIA,SAAS,GAAoB,CAC3B,IAAM,EAAM,EAAY,SAAS,IAAI,CAAC,CAAC,OAAQ,GAAO,CAAC,EAAW,SAAS,SAAS,CAAE,CAAC,EACjF,EAAO,EAAsB,EAC7B,EAAM,EAAO,EAAI,QAAQ,CAAI,EAAI,GACnC,GAAO,GAAG,EAAI,EAAM,EAAE,EAAE,MAAM,CACpC,CAEA,MAAO,CAAE,wBAAuB,wBAAuB,mBAAkB,CAC3E"}
@@ -1,2 +1,2 @@
1
- import{useCallback as e,useSyncExternalStore as t}from"react";function n(){return typeof window<`u`&&typeof window.matchMedia==`function`}function r(r){let i=e(e=>{if(!n())return()=>{};let t=window.matchMedia(r);return t.addEventListener(`change`,e),()=>t.removeEventListener(`change`,e)},[r]),a=e(()=>n()&&window.matchMedia(r).matches,[r]),o=e(()=>!1,[]);return t(i,a,o)}export{r as default,r as useMediaQuery};
1
+ "use client";import{useCallback as e,useSyncExternalStore as t}from"react";function n(){return typeof window<`u`&&typeof window.matchMedia==`function`}function r(r){let i=e(e=>{if(!n())return()=>{};let t=window.matchMedia(r);return t.addEventListener(`change`,e),()=>t.removeEventListener(`change`,e)},[r]),a=e(()=>n()&&window.matchMedia(r).matches,[r]),o=e(()=>!1,[]);return t(i,a,o)}export{r as default,r as useMediaQuery};
2
2
  //# sourceMappingURL=useMediaQuery.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useMediaQuery.mjs","names":[],"sources":["../../../../package/common/hooks/useMediaQuery.ts"],"sourcesContent":["import { useCallback, useSyncExternalStore } from 'react';\n\nfunction supported(): boolean {\n return typeof window !== 'undefined' && typeof window.matchMedia === 'function';\n}\n\n/**\n * `true` while `query` matches, kept current so a resize or an input-mode change re-renders.\n *\n * `useSyncExternalStore` rather than `useState` + `useEffect`: callers pick *markup* with this,\n * so hydration has to start from the server's answer. Reading `matchMedia` in a `useState`\n * initialiser makes the client's first render disagree with the server's HTML.\n */\nexport function useMediaQuery(query: string): boolean {\n const subscribe = useCallback(\n (onChange: () => void) => {\n if (!supported()) return () => {};\n const mql = window.matchMedia(query);\n mql.addEventListener('change', onChange);\n return () => mql.removeEventListener('change', onChange);\n },\n [query],\n );\n\n const getSnapshot = useCallback(() => supported() && window.matchMedia(query).matches, [query]);\n\n /** No media query exists on the server, so the tree renders as if nothing matched. */\n const getServerSnapshot = useCallback(() => false, []);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nexport default useMediaQuery;\n"],"mappings":"8DAEA,SAAS,GAAqB,CAC5B,OAAO,OAAO,OAAW,KAAe,OAAO,OAAO,YAAe,UACvE,CASA,SAAgB,EAAc,EAAwB,CACpD,IAAM,EAAY,EACf,GAAyB,CACxB,GAAI,CAAC,EAAU,EAAG,UAAa,CAAC,EAChC,IAAM,EAAM,OAAO,WAAW,CAAK,EAEnC,OADA,EAAI,iBAAiB,SAAU,CAAQ,MAC1B,EAAI,oBAAoB,SAAU,CAAQ,CACzD,EACA,CAAC,CAAK,CACR,EAEM,EAAc,MAAkB,EAAU,GAAK,OAAO,WAAW,CAAK,CAAC,CAAC,QAAS,CAAC,CAAK,CAAC,EAGxF,EAAoB,MAAkB,GAAO,CAAC,CAAC,EAErD,OAAO,EAAqB,EAAW,EAAa,CAAiB,CACvE"}
1
+ {"version":3,"file":"useMediaQuery.mjs","names":[],"sources":["../../../../package/common/hooks/useMediaQuery.ts"],"sourcesContent":["import { useCallback, useSyncExternalStore } from 'react';\n\nfunction supported(): boolean {\n return typeof window !== 'undefined' && typeof window.matchMedia === 'function';\n}\n\n/**\n * `true` while `query` matches, kept current so a resize or an input-mode change re-renders.\n *\n * `useSyncExternalStore` rather than `useState` + `useEffect`: callers pick *markup* with this,\n * so hydration has to start from the server's answer. Reading `matchMedia` in a `useState`\n * initialiser makes the client's first render disagree with the server's HTML.\n */\nexport function useMediaQuery(query: string): boolean {\n const subscribe = useCallback(\n (onChange: () => void) => {\n if (!supported()) return () => {};\n const mql = window.matchMedia(query);\n mql.addEventListener('change', onChange);\n return () => mql.removeEventListener('change', onChange);\n },\n [query],\n );\n\n const getSnapshot = useCallback(() => supported() && window.matchMedia(query).matches, [query]);\n\n /** No media query exists on the server, so the tree renders as if nothing matched. */\n const getServerSnapshot = useCallback(() => false, []);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nexport default useMediaQuery;\n"],"mappings":"2EAEA,SAAS,GAAqB,CAC5B,OAAO,OAAO,OAAW,KAAe,OAAO,OAAO,YAAe,UACvE,CASA,SAAgB,EAAc,EAAwB,CACpD,IAAM,EAAY,EACf,GAAyB,CACxB,GAAI,CAAC,EAAU,EAAG,UAAa,CAAC,EAChC,IAAM,EAAM,OAAO,WAAW,CAAK,EAEnC,OADA,EAAI,iBAAiB,SAAU,CAAQ,MAC1B,EAAI,oBAAoB,SAAU,CAAQ,CACzD,EACA,CAAC,CAAK,CACR,EAEM,EAAc,MAAkB,EAAU,GAAK,OAAO,WAAW,CAAK,CAAC,CAAC,QAAS,CAAC,CAAK,CAAC,EAGxF,EAAoB,MAAkB,GAAO,CAAC,CAAC,EAErD,OAAO,EAAqB,EAAW,EAAa,CAAiB,CACvE"}
@@ -1,2 +1,2 @@
1
- import{tabbablesIn as e}from"./useFocusBoundary.mjs";import{useEffect as t,useRef as n}from"react";function r({isActive:r,containerRef:o,onEscape:s,trapFocus:c=!0,lockScroll:d=!0}){let f=n(s);t(()=>{f.current=s});let p=n(0);r&&p.current===0&&(p.current=i++),!r&&p.current!==0&&(p.current=0),t(()=>{if(!r)return;let t=p.current;a.add(t);let n=()=>t===Math.max(...a),i=document.activeElement;if(c){let t=o.current;(e(t)[0]??t)?.focus()}function s(t){if(!n())return;if(t.key===`Escape`&&f.current){t.preventDefault(),f.current();return}if(t.key!==`Tab`||!c)return;let r=e(o.current);if(r.length===0){t.preventDefault();return}let i=r[0],a=r[r.length-1],s=document.activeElement;o.current?.contains(s)?!t.shiftKey&&s===a?(t.preventDefault(),i.focus()):t.shiftKey&&s===i&&(t.preventDefault(),a.focus()):(t.preventDefault(),i.focus())}return window.addEventListener(`keydown`,s),d&&l(),()=>{window.removeEventListener(`keydown`,s),d&&u(),a.delete(t),c&&i?.focus()}},[r,c,d,o])}var i=1,a=new Set,o=0,s=``,c=``;function l(){let{body:e}=document;if(o===0){s=e.style.overflow,c=e.style.paddingRight;let t=window.innerWidth-document.documentElement.clientWidth;if(t>0){let n=parseFloat(window.getComputedStyle(e).paddingRight)||0;e.style.paddingRight=`${n+t}px`}e.style.overflow=`hidden`}o+=1}function u(){--o,!(o>0)&&(o=0,document.body.style.overflow=s,document.body.style.paddingRight=c)}export{r as useOverlay};
1
+ "use client";import{tabbablesIn as e}from"./useFocusBoundary.mjs";import{useEffect as t,useRef as n}from"react";function r({isActive:r,containerRef:o,onEscape:s,trapFocus:c=!0,lockScroll:d=!0}){let f=n(s);t(()=>{f.current=s});let p=n(0);r&&p.current===0&&(p.current=i++),!r&&p.current!==0&&(p.current=0),t(()=>{if(!r)return;let t=p.current;a.add(t);let n=()=>t===Math.max(...a),i=document.activeElement;if(c){let t=o.current;(e(t)[0]??t)?.focus()}function s(t){if(!n())return;if(t.key===`Escape`&&f.current){t.preventDefault(),f.current();return}if(t.key!==`Tab`||!c)return;let r=e(o.current);if(r.length===0){t.preventDefault();return}let i=r[0],a=r[r.length-1],s=document.activeElement;o.current?.contains(s)?!t.shiftKey&&s===a?(t.preventDefault(),i.focus()):t.shiftKey&&s===i&&(t.preventDefault(),a.focus()):(t.preventDefault(),i.focus())}return window.addEventListener(`keydown`,s),d&&l(),()=>{window.removeEventListener(`keydown`,s),d&&u(),a.delete(t),c&&i?.focus()}},[r,c,d,o])}var i=1,a=new Set,o=0,s=``,c=``;function l(){let{body:e}=document;if(o===0){s=e.style.overflow,c=e.style.paddingRight;let t=window.innerWidth-document.documentElement.clientWidth;if(t>0){let n=parseFloat(window.getComputedStyle(e).paddingRight)||0;e.style.paddingRight=`${n+t}px`}e.style.overflow=`hidden`}o+=1}function u(){--o,!(o>0)&&(o=0,document.body.style.overflow=s,document.body.style.paddingRight=c)}export{r as useOverlay};
2
2
  //# sourceMappingURL=useOverlay.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useOverlay.mjs","names":[],"sources":["../../../../package/common/hooks/useOverlay.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\nimport { tabbablesIn } from './useFocusBoundary';\n\nexport interface UseOverlayArgs {\n /** Whether the overlay is currently visible. Everything below is set up while this is true. */\n isActive: boolean;\n /** The overlay panel. Focus is trapped inside it and Escape is scoped to it. */\n containerRef: RefObject<HTMLElement | null>;\n /** Called when Escape is pressed while this is the topmost overlay. Omit to not handle Escape. */\n onEscape?: () => void;\n /** Keep Tab inside the panel and restore focus to the previously focused element on close. */\n trapFocus?: boolean;\n /** Prevent the page behind the overlay from scrolling. */\n lockScroll?: boolean;\n}\n\n/**\n * The three behaviours every blocking overlay needs: Escape to dismiss, a focus trap, and a\n * scroll lock on the page behind it.\n *\n * They share one module-level stack because they share one question: overlays nest (a\n * ConfirmModal opened from inside a Modal), every instance listens on `document`, and only the\n * innermost one may react. Without the stack, Escape would close every open layer at once and\n * the outer trap would yank focus out of the inner panel on the next Tab.\n *\n * The scroll lock is reference-counted for the same reason: the page stays locked until the last\n * overlay closes, and the body's original inline styles are restored rather than cleared.\n */\nexport function useOverlay({ isActive, containerRef, onEscape, trapFocus = true, lockScroll = true }: UseOverlayArgs) {\n const onEscapeRef = useRef(onEscape);\n useEffect(() => {\n onEscapeRef.current = onEscape;\n });\n\n // The layer's position in the stack is claimed during render, not in the effect below, because\n // effects run child-first: an overlay opened from *inside* another one (a menu in a modal) has\n // its effect run before its own parent's and would come out as the lower layer. Render runs\n // parent-first, which is the order the layers are actually nested in. DOM order is no help\n // either — React commits the inner portal ahead of the outer one.\n const orderRef = useRef(0);\n if (isActive && orderRef.current === 0) orderRef.current = nextLayerOrder++;\n if (!isActive && orderRef.current !== 0) orderRef.current = 0;\n\n useEffect(() => {\n if (!isActive) return;\n\n const order = orderRef.current;\n activeLayers.add(order);\n const isTopmost = () => order === Math.max(...activeLayers);\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n if (trapFocus) {\n const container = containerRef.current;\n (tabbablesIn(container)[0] ?? container)?.focus();\n }\n\n function onKeyDown(event: KeyboardEvent) {\n if (!isTopmost()) return;\n\n if (event.key === 'Escape' && onEscapeRef.current) {\n event.preventDefault();\n onEscapeRef.current();\n return;\n }\n\n if (event.key !== 'Tab' || !trapFocus) return;\n const focusables = tabbablesIn(containerRef.current);\n // Nothing to move to inside the panel, but Tab must still not escape it.\n if (focusables.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = document.activeElement;\n if (!containerRef.current?.contains(active)) {\n event.preventDefault();\n first.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first.focus();\n } else if (event.shiftKey && active === first) {\n event.preventDefault();\n last.focus();\n }\n }\n\n // `window`, not `document`: a key event on a focused element reaches both, but a synthetic\n // one dispatched straight at `window` (as tests and some consumers do) never reaches\n // `document`, since it sits above it in the propagation path.\n window.addEventListener('keydown', onKeyDown);\n if (lockScroll) acquireScrollLock();\n\n return () => {\n window.removeEventListener('keydown', onKeyDown);\n if (lockScroll) releaseScrollLock();\n activeLayers.delete(order);\n if (trapFocus) previouslyFocused?.focus();\n };\n }, [isActive, trapFocus, lockScroll, containerRef]);\n}\n\n/** Monotonic, so a layer opened later always outranks one opened earlier. */\nlet nextLayerOrder = 1;\nconst activeLayers = new Set<number>();\n\nlet scrollLockCount = 0;\nlet previousOverflow = '';\nlet previousPaddingRight = '';\n\nfunction acquireScrollLock() {\n const { body } = document;\n if (scrollLockCount === 0) {\n previousOverflow = body.style.overflow;\n previousPaddingRight = body.style.paddingRight;\n // Removing the page's scrollbar reclaims its width, so everything behind the overlay would\n // shift right by that amount without the compensating padding.\n const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;\n if (scrollbarWidth > 0) {\n const current = parseFloat(window.getComputedStyle(body).paddingRight) || 0;\n body.style.paddingRight = `${current + scrollbarWidth}px`;\n }\n body.style.overflow = 'hidden';\n }\n scrollLockCount += 1;\n}\n\nfunction releaseScrollLock() {\n scrollLockCount -= 1;\n if (scrollLockCount > 0) return;\n scrollLockCount = 0;\n document.body.style.overflow = previousOverflow;\n document.body.style.paddingRight = previousPaddingRight;\n}\n"],"mappings":"mGA4BA,SAAgB,EAAW,CAAE,WAAU,eAAc,WAAU,YAAY,GAAM,aAAa,IAAwB,CACpH,IAAM,EAAc,EAAO,CAAQ,EACnC,MAAgB,CACd,EAAY,QAAU,CACxB,CAAC,EAOD,IAAM,EAAW,EAAO,CAAC,EACrB,GAAY,EAAS,UAAY,IAAG,EAAS,QAAU,KACvD,CAAC,GAAY,EAAS,UAAY,IAAG,EAAS,QAAU,GAE5D,MAAgB,CACd,GAAI,CAAC,EAAU,OAEf,IAAM,EAAQ,EAAS,QACvB,EAAa,IAAI,CAAK,EACtB,IAAM,MAAkB,IAAU,KAAK,IAAI,GAAG,CAAY,EAEpD,EAAoB,SAAS,cACnC,GAAI,EAAW,CACb,IAAM,EAAY,EAAa,SAC9B,EAAY,CAAS,CAAC,CAAC,IAAM,EAAA,EAAY,MAAM,CAClD,CAEA,SAAS,EAAU,EAAsB,CACvC,GAAI,CAAC,EAAU,EAAG,OAElB,GAAI,EAAM,MAAQ,UAAY,EAAY,QAAS,CACjD,EAAM,eAAe,EACrB,EAAY,QAAQ,EACpB,MACF,CAEA,GAAI,EAAM,MAAQ,OAAS,CAAC,EAAW,OACvC,IAAM,EAAa,EAAY,EAAa,OAAO,EAEnD,GAAI,EAAW,SAAW,EAAG,CAC3B,EAAM,eAAe,EACrB,MACF,CACA,IAAM,EAAQ,EAAW,GACnB,EAAO,EAAW,EAAW,OAAS,GACtC,EAAS,SAAS,cACnB,EAAa,SAAS,SAAS,CAAM,EAG/B,CAAC,EAAM,UAAY,IAAW,GACvC,EAAM,eAAe,EACrB,EAAM,MAAM,GACH,EAAM,UAAY,IAAW,IACtC,EAAM,eAAe,EACrB,EAAK,MAAM,IAPX,EAAM,eAAe,EACrB,EAAM,MAAM,EAQhB,CAQA,OAHA,OAAO,iBAAiB,UAAW,CAAS,EACxC,GAAY,EAAkB,MAErB,CACX,OAAO,oBAAoB,UAAW,CAAS,EAC3C,GAAY,EAAkB,EAClC,EAAa,OAAO,CAAK,EACrB,GAAW,GAAmB,MAAM,CAC1C,CACF,EAAG,CAAC,EAAU,EAAW,EAAY,CAAY,CAAC,CACpD,CAGA,IAAI,EAAiB,EACf,EAAe,IAAI,IAErB,EAAkB,EAClB,EAAmB,GACnB,EAAuB,GAE3B,SAAS,GAAoB,CAC3B,GAAM,CAAE,QAAS,SACjB,GAAI,IAAoB,EAAG,CACzB,EAAmB,EAAK,MAAM,SAC9B,EAAuB,EAAK,MAAM,aAGlC,IAAM,EAAiB,OAAO,WAAa,SAAS,gBAAgB,YACpE,GAAI,EAAiB,EAAG,CACtB,IAAM,EAAU,WAAW,OAAO,iBAAiB,CAAI,CAAC,CAAC,YAAY,GAAK,EAC1E,EAAK,MAAM,aAAe,GAAG,EAAU,EAAe,GACxD,CACA,EAAK,MAAM,SAAW,QACxB,CACA,GAAmB,CACrB,CAEA,SAAS,GAAoB,CAC3B,IACI,IAAkB,KACtB,EAAkB,EAClB,SAAS,KAAK,MAAM,SAAW,EAC/B,SAAS,KAAK,MAAM,aAAe,EACrC"}
1
+ {"version":3,"file":"useOverlay.mjs","names":[],"sources":["../../../../package/common/hooks/useOverlay.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\nimport { tabbablesIn } from './useFocusBoundary';\n\nexport interface UseOverlayArgs {\n /** Whether the overlay is currently visible. Everything below is set up while this is true. */\n isActive: boolean;\n /** The overlay panel. Focus is trapped inside it and Escape is scoped to it. */\n containerRef: RefObject<HTMLElement | null>;\n /** Called when Escape is pressed while this is the topmost overlay. Omit to not handle Escape. */\n onEscape?: () => void;\n /** Keep Tab inside the panel and restore focus to the previously focused element on close. */\n trapFocus?: boolean;\n /** Prevent the page behind the overlay from scrolling. */\n lockScroll?: boolean;\n}\n\n/**\n * The three behaviours every blocking overlay needs: Escape to dismiss, a focus trap, and a\n * scroll lock on the page behind it.\n *\n * They share one module-level stack because they share one question: overlays nest (a\n * ConfirmModal opened from inside a Modal), every instance listens on `document`, and only the\n * innermost one may react. Without the stack, Escape would close every open layer at once and\n * the outer trap would yank focus out of the inner panel on the next Tab.\n *\n * The scroll lock is reference-counted for the same reason: the page stays locked until the last\n * overlay closes, and the body's original inline styles are restored rather than cleared.\n */\nexport function useOverlay({ isActive, containerRef, onEscape, trapFocus = true, lockScroll = true }: UseOverlayArgs) {\n const onEscapeRef = useRef(onEscape);\n useEffect(() => {\n onEscapeRef.current = onEscape;\n });\n\n // The layer's position in the stack is claimed during render, not in the effect below, because\n // effects run child-first: an overlay opened from *inside* another one (a menu in a modal) has\n // its effect run before its own parent's and would come out as the lower layer. Render runs\n // parent-first, which is the order the layers are actually nested in. DOM order is no help\n // either — React commits the inner portal ahead of the outer one.\n const orderRef = useRef(0);\n if (isActive && orderRef.current === 0) orderRef.current = nextLayerOrder++;\n if (!isActive && orderRef.current !== 0) orderRef.current = 0;\n\n useEffect(() => {\n if (!isActive) return;\n\n const order = orderRef.current;\n activeLayers.add(order);\n const isTopmost = () => order === Math.max(...activeLayers);\n\n const previouslyFocused = document.activeElement as HTMLElement | null;\n if (trapFocus) {\n const container = containerRef.current;\n (tabbablesIn(container)[0] ?? container)?.focus();\n }\n\n function onKeyDown(event: KeyboardEvent) {\n if (!isTopmost()) return;\n\n if (event.key === 'Escape' && onEscapeRef.current) {\n event.preventDefault();\n onEscapeRef.current();\n return;\n }\n\n if (event.key !== 'Tab' || !trapFocus) return;\n const focusables = tabbablesIn(containerRef.current);\n // Nothing to move to inside the panel, but Tab must still not escape it.\n if (focusables.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = document.activeElement;\n if (!containerRef.current?.contains(active)) {\n event.preventDefault();\n first.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first.focus();\n } else if (event.shiftKey && active === first) {\n event.preventDefault();\n last.focus();\n }\n }\n\n // `window`, not `document`: a key event on a focused element reaches both, but a synthetic\n // one dispatched straight at `window` (as tests and some consumers do) never reaches\n // `document`, since it sits above it in the propagation path.\n window.addEventListener('keydown', onKeyDown);\n if (lockScroll) acquireScrollLock();\n\n return () => {\n window.removeEventListener('keydown', onKeyDown);\n if (lockScroll) releaseScrollLock();\n activeLayers.delete(order);\n if (trapFocus) previouslyFocused?.focus();\n };\n }, [isActive, trapFocus, lockScroll, containerRef]);\n}\n\n/** Monotonic, so a layer opened later always outranks one opened earlier. */\nlet nextLayerOrder = 1;\nconst activeLayers = new Set<number>();\n\nlet scrollLockCount = 0;\nlet previousOverflow = '';\nlet previousPaddingRight = '';\n\nfunction acquireScrollLock() {\n const { body } = document;\n if (scrollLockCount === 0) {\n previousOverflow = body.style.overflow;\n previousPaddingRight = body.style.paddingRight;\n // Removing the page's scrollbar reclaims its width, so everything behind the overlay would\n // shift right by that amount without the compensating padding.\n const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;\n if (scrollbarWidth > 0) {\n const current = parseFloat(window.getComputedStyle(body).paddingRight) || 0;\n body.style.paddingRight = `${current + scrollbarWidth}px`;\n }\n body.style.overflow = 'hidden';\n }\n scrollLockCount += 1;\n}\n\nfunction releaseScrollLock() {\n scrollLockCount -= 1;\n if (scrollLockCount > 0) return;\n scrollLockCount = 0;\n document.body.style.overflow = previousOverflow;\n document.body.style.paddingRight = previousPaddingRight;\n}\n"],"mappings":"gHA4BA,SAAgB,EAAW,CAAE,WAAU,eAAc,WAAU,YAAY,GAAM,aAAa,IAAwB,CACpH,IAAM,EAAc,EAAO,CAAQ,EACnC,MAAgB,CACd,EAAY,QAAU,CACxB,CAAC,EAOD,IAAM,EAAW,EAAO,CAAC,EACrB,GAAY,EAAS,UAAY,IAAG,EAAS,QAAU,KACvD,CAAC,GAAY,EAAS,UAAY,IAAG,EAAS,QAAU,GAE5D,MAAgB,CACd,GAAI,CAAC,EAAU,OAEf,IAAM,EAAQ,EAAS,QACvB,EAAa,IAAI,CAAK,EACtB,IAAM,MAAkB,IAAU,KAAK,IAAI,GAAG,CAAY,EAEpD,EAAoB,SAAS,cACnC,GAAI,EAAW,CACb,IAAM,EAAY,EAAa,SAC9B,EAAY,CAAS,CAAC,CAAC,IAAM,EAAA,EAAY,MAAM,CAClD,CAEA,SAAS,EAAU,EAAsB,CACvC,GAAI,CAAC,EAAU,EAAG,OAElB,GAAI,EAAM,MAAQ,UAAY,EAAY,QAAS,CACjD,EAAM,eAAe,EACrB,EAAY,QAAQ,EACpB,MACF,CAEA,GAAI,EAAM,MAAQ,OAAS,CAAC,EAAW,OACvC,IAAM,EAAa,EAAY,EAAa,OAAO,EAEnD,GAAI,EAAW,SAAW,EAAG,CAC3B,EAAM,eAAe,EACrB,MACF,CACA,IAAM,EAAQ,EAAW,GACnB,EAAO,EAAW,EAAW,OAAS,GACtC,EAAS,SAAS,cACnB,EAAa,SAAS,SAAS,CAAM,EAG/B,CAAC,EAAM,UAAY,IAAW,GACvC,EAAM,eAAe,EACrB,EAAM,MAAM,GACH,EAAM,UAAY,IAAW,IACtC,EAAM,eAAe,EACrB,EAAK,MAAM,IAPX,EAAM,eAAe,EACrB,EAAM,MAAM,EAQhB,CAQA,OAHA,OAAO,iBAAiB,UAAW,CAAS,EACxC,GAAY,EAAkB,MAErB,CACX,OAAO,oBAAoB,UAAW,CAAS,EAC3C,GAAY,EAAkB,EAClC,EAAa,OAAO,CAAK,EACrB,GAAW,GAAmB,MAAM,CAC1C,CACF,EAAG,CAAC,EAAU,EAAW,EAAY,CAAY,CAAC,CACpD,CAGA,IAAI,EAAiB,EACf,EAAe,IAAI,IAErB,EAAkB,EAClB,EAAmB,GACnB,EAAuB,GAE3B,SAAS,GAAoB,CAC3B,GAAM,CAAE,QAAS,SACjB,GAAI,IAAoB,EAAG,CACzB,EAAmB,EAAK,MAAM,SAC9B,EAAuB,EAAK,MAAM,aAGlC,IAAM,EAAiB,OAAO,WAAa,SAAS,gBAAgB,YACpE,GAAI,EAAiB,EAAG,CACtB,IAAM,EAAU,WAAW,OAAO,iBAAiB,CAAI,CAAC,CAAC,YAAY,GAAK,EAC1E,EAAK,MAAM,aAAe,GAAG,EAAU,EAAe,GACxD,CACA,EAAK,MAAM,SAAW,QACxB,CACA,GAAmB,CACrB,CAEA,SAAS,GAAoB,CAC3B,IACI,IAAkB,KACtB,EAAkB,EAClB,SAAS,KAAK,MAAM,SAAW,EAC/B,SAAS,KAAK,MAAM,aAAe,EACrC"}
@@ -1,2 +1,2 @@
1
- import{Presentation as e}from"../configProvider/presentation.types.mjs";import t from"./useMediaQuery.mjs";import n from"./useCoarsePointer.mjs";import{useVisualViewportSize as r}from"./useVisualViewportSize.mjs";var i=`(max-width: 767px)`,a=8;function o(o=e.Default){let s=n(),c=t(i),{height:l,offsetTop:u}=r();return o===e.Sheet||o===e.Auto&&s&&c?{isSheet:!0,sheetStyle:{bottom:typeof window>`u`?0:Math.max(0,window.innerHeight-(u+l)),"--sheet-available-height":`${Math.max(0,l-a)}px`}}:{isSheet:!1,sheetStyle:void 0}}export{o as default,o as useSheet};
1
+ "use client";import{Presentation as e}from"../configProvider/presentation.types.mjs";import t from"./useMediaQuery.mjs";import n from"./useCoarsePointer.mjs";import{useVisualViewportSize as r}from"./useVisualViewportSize.mjs";var i=`(max-width: 767px)`,a=8;function o(o=e.Default){let s=n(),c=t(i),{height:l,offsetTop:u}=r();return o===e.Sheet||o===e.Auto&&s&&c?{isSheet:!0,sheetStyle:{bottom:typeof window>`u`?0:Math.max(0,window.innerHeight-(u+l)),"--sheet-available-height":`${Math.max(0,l-a)}px`}}:{isSheet:!1,sheetStyle:void 0}}export{o as default,o as useSheet};
2
2
  //# sourceMappingURL=useSheet.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSheet.mjs","names":[],"sources":["../../../../package/common/hooks/useSheet.ts"],"sourcesContent":["import type { CSSProperties } from 'react';\nimport { Presentation } from '../configProvider/presentation.types';\nimport { useCoarsePointer } from './useCoarsePointer';\nimport { useMediaQuery } from './useMediaQuery';\nimport { useVisualViewportSize } from './useVisualViewportSize';\n\n/** Below this, `Presentation.Auto` switches a touch device over to a sheet. */\nconst SMALL_SCREEN_QUERY = '(max-width: 767px)';\n\n/** Sliver kept above a full-height sheet, so it still reads as a sheet rather than a full screen. */\nconst TOP_GAP = 8;\n\nexport interface Sheet {\n /** Whether the popup should render as a bottom sheet instead of anchored to its trigger. */\n isSheet: boolean;\n /**\n * Inline style for the sheet root: keeps it above an open on-screen keyboard, and hands the CSS\n * the height actually left on screen as `--sheet-available-height`.\n */\n sheetStyle: CSSProperties | undefined;\n}\n\n/**\n * Resolves whether a popup presents as a bottom sheet.\n *\n * `Auto` asks for a coarse pointer *and* a small screen: pointer alone turns a touchscreen laptop\n * into a phone, and width alone turns a narrow desktop window into one.\n */\nexport function useSheet(presentation: Presentation = Presentation.Default): Sheet {\n const isCoarsePointer = useCoarsePointer();\n const isSmallScreen = useMediaQuery(SMALL_SCREEN_QUERY);\n const { height: viewportHeight, offsetTop } = useVisualViewportSize();\n\n const isSheet =\n presentation === Presentation.Sheet || (presentation === Presentation.Auto && isCoarsePointer && isSmallScreen);\n\n if (!isSheet) return { isSheet: false, sheetStyle: undefined };\n\n // `position: fixed` resolves against the layout viewport, which an open keyboard doesn't\n // shrink — so a sheet at `bottom: 0` sits behind the keyboard. Lift it by the difference.\n const keyboardHeight =\n typeof window === 'undefined' ? 0 : Math.max(0, window.innerHeight - (offsetTop + viewportHeight));\n\n // Lifting the sheet is only half of it: it also has to *fit* in what's left. No viewport unit\n // can express that, because `dvh` (like `vh`) measures the layout viewport, which the keyboard\n // doesn't shrink either — so a tall sheet capped at, say, `90dvh` and lifted by the keyboard\n // height ends up with its top edge above the screen, taking the header and the first part of\n // the body with it, out of reach of the body's own scrolling. The visual viewport is the only\n // thing that knows the real number, so it's measured here and handed to the CSS to clamp with.\n const availableHeight = Math.max(0, viewportHeight - TOP_GAP);\n\n const sheetStyle = {\n bottom: keyboardHeight,\n '--sheet-available-height': `${availableHeight}px`,\n } as CSSProperties;\n\n return { isSheet: true, sheetStyle };\n}\n\nexport default useSheet;\n"],"mappings":"qNAOA,IAAM,EAAqB,qBAGrB,EAAU,EAkBhB,SAAgB,EAAS,EAA6B,EAAa,QAAgB,CACjF,IAAM,EAAkB,EAAiB,EACnC,EAAgB,EAAc,CAAkB,EAChD,CAAE,OAAQ,EAAgB,aAAc,EAAsB,EAyBpE,OAtBE,IAAiB,EAAa,OAAU,IAAiB,EAAa,MAAQ,GAAmB,EAsB5F,CAAE,QAAS,GAAM,WAAA,CAJtB,OAXA,OAAO,OAAW,IAAc,EAAI,KAAK,IAAI,EAAG,OAAO,aAAe,EAAY,EAAe,EAYjG,2BAA4B,GAJN,KAAK,IAAI,EAAG,EAAiB,CAIpB,EAAgB,GAGzB,CAAW,EApBd,CAAE,QAAS,GAAO,WAAY,IAAA,EAAU,CAqB/D"}
1
+ {"version":3,"file":"useSheet.mjs","names":[],"sources":["../../../../package/common/hooks/useSheet.ts"],"sourcesContent":["import type { CSSProperties } from 'react';\nimport { Presentation } from '../configProvider/presentation.types';\nimport { useCoarsePointer } from './useCoarsePointer';\nimport { useMediaQuery } from './useMediaQuery';\nimport { useVisualViewportSize } from './useVisualViewportSize';\n\n/** Below this, `Presentation.Auto` switches a touch device over to a sheet. */\nconst SMALL_SCREEN_QUERY = '(max-width: 767px)';\n\n/** Sliver kept above a full-height sheet, so it still reads as a sheet rather than a full screen. */\nconst TOP_GAP = 8;\n\nexport interface Sheet {\n /** Whether the popup should render as a bottom sheet instead of anchored to its trigger. */\n isSheet: boolean;\n /**\n * Inline style for the sheet root: keeps it above an open on-screen keyboard, and hands the CSS\n * the height actually left on screen as `--sheet-available-height`.\n */\n sheetStyle: CSSProperties | undefined;\n}\n\n/**\n * Resolves whether a popup presents as a bottom sheet.\n *\n * `Auto` asks for a coarse pointer *and* a small screen: pointer alone turns a touchscreen laptop\n * into a phone, and width alone turns a narrow desktop window into one.\n */\nexport function useSheet(presentation: Presentation = Presentation.Default): Sheet {\n const isCoarsePointer = useCoarsePointer();\n const isSmallScreen = useMediaQuery(SMALL_SCREEN_QUERY);\n const { height: viewportHeight, offsetTop } = useVisualViewportSize();\n\n const isSheet =\n presentation === Presentation.Sheet || (presentation === Presentation.Auto && isCoarsePointer && isSmallScreen);\n\n if (!isSheet) return { isSheet: false, sheetStyle: undefined };\n\n // `position: fixed` resolves against the layout viewport, which an open keyboard doesn't\n // shrink — so a sheet at `bottom: 0` sits behind the keyboard. Lift it by the difference.\n const keyboardHeight =\n typeof window === 'undefined' ? 0 : Math.max(0, window.innerHeight - (offsetTop + viewportHeight));\n\n // Lifting the sheet is only half of it: it also has to *fit* in what's left. No viewport unit\n // can express that, because `dvh` (like `vh`) measures the layout viewport, which the keyboard\n // doesn't shrink either — so a tall sheet capped at, say, `90dvh` and lifted by the keyboard\n // height ends up with its top edge above the screen, taking the header and the first part of\n // the body with it, out of reach of the body's own scrolling. The visual viewport is the only\n // thing that knows the real number, so it's measured here and handed to the CSS to clamp with.\n const availableHeight = Math.max(0, viewportHeight - TOP_GAP);\n\n const sheetStyle = {\n bottom: keyboardHeight,\n '--sheet-available-height': `${availableHeight}px`,\n } as CSSProperties;\n\n return { isSheet: true, sheetStyle };\n}\n\nexport default useSheet;\n"],"mappings":"kOAOA,IAAM,EAAqB,qBAGrB,EAAU,EAkBhB,SAAgB,EAAS,EAA6B,EAAa,QAAgB,CACjF,IAAM,EAAkB,EAAiB,EACnC,EAAgB,EAAc,CAAkB,EAChD,CAAE,OAAQ,EAAgB,aAAc,EAAsB,EAyBpE,OAtBE,IAAiB,EAAa,OAAU,IAAiB,EAAa,MAAQ,GAAmB,EAsB5F,CAAE,QAAS,GAAM,WAAA,CAJtB,OAXA,OAAO,OAAW,IAAc,EAAI,KAAK,IAAI,EAAG,OAAO,aAAe,EAAY,EAAe,EAYjG,2BAA4B,GAJN,KAAK,IAAI,EAAG,EAAiB,CAIpB,EAAgB,GAGzB,CAAW,EApBd,CAAE,QAAS,GAAO,WAAY,IAAA,EAAU,CAqB/D"}
@@ -1,2 +1,2 @@
1
- import e from"./useCoarsePointer.mjs";import{useEffect as t,useRef as n,useState as r}from"react";var i=180,a=10;function o({onDismiss:o,axis:s=`x`,distanceThreshold:c=80,velocityThreshold:l=.5,disabled:u=!1,mobileOnly:d=!0}){let f=n(null),[p,m]=r(!1),h=e(),g=u||d&&!h,_=n(null),v=n(o);return t(()=>{v.current=o}),t(()=>{let e=f.current;if(!e||g)return;function t(t,n){if(!e)return;n!==`none`&&e.offsetHeight;let r=s===`x`?`translateX(${t}px)`:`translateY(${t}px)`;e.style.transition=n,e.style.transform=r;let i=Math.max(0,1-Math.abs(t)/(c*2.5));e.style.opacity=String(i)}function n(e){let n=_.current;if(!n||e.pointerId!==n.pointerId)return;let r=e.clientX-n.startX,i=e.clientY-n.startY;if(!n.axisLocked){let e=s===`x`?r:i,t=s===`x`?i:r;if(Math.abs(e)<a&&Math.abs(t)<a)return;if(Math.abs(t)>Math.abs(e)){n.active=!1;return}n.axisLocked=!0,m(!0)}n.active&&(e.preventDefault(),n.offset=s===`x`?r:i,t(n.offset,`none`))}function r(){let a=_.current;if(_.current=null,window.removeEventListener(`pointermove`,n),window.removeEventListener(`pointerup`,r),window.removeEventListener(`pointercancel`,r),!a||!a.active||!a.axisLocked){m(!1);return}let o=Math.max(1,Date.now()-a.startTime),s=a.offset/o;if(Math.abs(a.offset)>=c||Math.abs(s)>=l){let n=e?.getBoundingClientRect().width??0;t(Math.sign(a.offset||1)*(n+40),`transform ${i}ms ease-out, opacity ${i}ms ease-out`),setTimeout(()=>v.current(),i)}else t(0,`transform ${i}ms ease-out, opacity ${i}ms ease-out`);m(!1)}function o(t){!_.current&&e&&(e.style.animation=`none`,_.current={pointerId:t.pointerId,startX:t.clientX,startY:t.clientY,startTime:Date.now(),offset:0,axisLocked:!1,active:!0},window.addEventListener(`pointermove`,n,{passive:!1}),window.addEventListener(`pointerup`,r),window.addEventListener(`pointercancel`,r))}return e.addEventListener(`pointerdown`,o),()=>{e.removeEventListener(`pointerdown`,o),window.removeEventListener(`pointermove`,n),window.removeEventListener(`pointerup`,r),window.removeEventListener(`pointercancel`,r)}},[s,c,l,g]),{ref:f,isDragging:p}}export{o as useSwipeDismiss};
1
+ "use client";import e from"./useCoarsePointer.mjs";import{useEffect as t,useRef as n,useState as r}from"react";var i=180,a=10;function o({onDismiss:o,axis:s=`x`,distanceThreshold:c=80,velocityThreshold:l=.5,disabled:u=!1,mobileOnly:d=!0}){let f=n(null),[p,m]=r(!1),h=e(),g=u||d&&!h,_=n(null),v=n(o);return t(()=>{v.current=o}),t(()=>{let e=f.current;if(!e||g)return;function t(t,n){if(!e)return;n!==`none`&&e.offsetHeight;let r=s===`x`?`translateX(${t}px)`:`translateY(${t}px)`;e.style.transition=n,e.style.transform=r;let i=Math.max(0,1-Math.abs(t)/(c*2.5));e.style.opacity=String(i)}function n(e){let n=_.current;if(!n||e.pointerId!==n.pointerId)return;let r=e.clientX-n.startX,i=e.clientY-n.startY;if(!n.axisLocked){let e=s===`x`?r:i,t=s===`x`?i:r;if(Math.abs(e)<a&&Math.abs(t)<a)return;if(Math.abs(t)>Math.abs(e)){n.active=!1;return}n.axisLocked=!0,m(!0)}n.active&&(e.preventDefault(),n.offset=s===`x`?r:i,t(n.offset,`none`))}function r(){let a=_.current;if(_.current=null,window.removeEventListener(`pointermove`,n),window.removeEventListener(`pointerup`,r),window.removeEventListener(`pointercancel`,r),!a||!a.active||!a.axisLocked){m(!1);return}let o=Math.max(1,Date.now()-a.startTime),s=a.offset/o;if(Math.abs(a.offset)>=c||Math.abs(s)>=l){let n=e?.getBoundingClientRect().width??0;t(Math.sign(a.offset||1)*(n+40),`transform ${i}ms ease-out, opacity ${i}ms ease-out`),setTimeout(()=>v.current(),i)}else t(0,`transform ${i}ms ease-out, opacity ${i}ms ease-out`);m(!1)}function o(t){!_.current&&e&&(e.style.animation=`none`,_.current={pointerId:t.pointerId,startX:t.clientX,startY:t.clientY,startTime:Date.now(),offset:0,axisLocked:!1,active:!0},window.addEventListener(`pointermove`,n,{passive:!1}),window.addEventListener(`pointerup`,r),window.addEventListener(`pointercancel`,r))}return e.addEventListener(`pointerdown`,o),()=>{e.removeEventListener(`pointerdown`,o),window.removeEventListener(`pointermove`,n),window.removeEventListener(`pointerup`,r),window.removeEventListener(`pointercancel`,r)}},[s,c,l,g]),{ref:f,isDragging:p}}export{o as useSwipeDismiss};
2
2
  //# sourceMappingURL=useSwipeDismiss.mjs.map