@leavepulse/ui 0.43.4 → 0.45.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 (437) hide show
  1. package/README.md +121 -7
  2. package/dist/assets/{LpCommandPalette-B-0Est4Z.css → LpCommandPalette-nGNnzXqQ.css} +21 -21
  3. package/dist/assets/{LpDropzone-BBoWvi-J.css → LpDropzone-i4BdMi1H.css} +12 -12
  4. package/dist/assets/{LpImageEditor-Ct7Y3aNu.css → LpImageEditor-CbMA82g8.css} +6 -6
  5. package/dist/assets/{LpRollingText-CR1mb0Vt.css → LpRollingText-BGnE6q9j.css} +3 -0
  6. package/dist/assets/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-CqYX3yy6.css} +28 -7
  7. package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
  8. package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-CPwWxTJS.css} +49 -9
  9. package/dist/chunks/ContextMenuItems-BtKU7074.js +124 -0
  10. package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-BkaGXTjx.js} +26 -9
  11. package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-B3_9E1Xa.js} +28 -12
  12. package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-CnfFVIwC.js} +26 -12
  13. package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BwX8uo4_.js} +54 -49
  14. package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-B6ilrM7T.js} +16 -9
  15. package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-C53_RaGF.js} +53 -26
  16. package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-C8IgSMsi.js} +52 -24
  17. package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-C1XStdOo.js} +19 -10
  18. package/dist/chunks/LpBreadcrumbs-CQJd73Gm.js +95 -0
  19. package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-CKSwC6Mu.js} +45 -15
  20. package/dist/chunks/{LpCalendar-B7Ko5RdR.js → LpCalendar-DrA2McTU.js} +1 -1
  21. package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-C2IvucmX.js} +1 -1
  22. package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-Bkj13CbW.js} +8 -4
  23. package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-ca7ANFyW.js} +49 -17
  24. package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-zz4VGkG4.js} +75 -26
  25. package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-DCw5jR1t.js} +59 -17
  26. package/dist/chunks/LpConfirmHost-CbywMpMP.js +49 -0
  27. package/dist/chunks/LpContextMenu-BqkAYa9X.js +69 -0
  28. package/dist/chunks/LpDatePicker-DuusgT69.js +195 -0
  29. package/dist/chunks/LpDateRangePicker-DHeuznfC.js +208 -0
  30. package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
  31. package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-Dt4aB4Fr.js} +65 -28
  32. package/dist/chunks/{LpDropdownMenu-DJpoqRCb.js → LpDropdownMenu-DdJGQzPn.js} +2 -1
  33. package/dist/chunks/{LpEmptyState-Dsdqz1Wj.js → LpEmptyState-BJbZXC8D.js} +1 -1
  34. package/dist/chunks/LpFieldNotice-wtieLUTj.js +64 -0
  35. package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CNurOvkV.js} +25 -10
  36. package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-D_qRRdsH.js} +3 -3
  37. package/dist/chunks/LpFormField-CtVYj6tO.js +101 -0
  38. package/dist/chunks/{LpIcon-CCnX5_2j.js → LpIcon-DYtaid0K.js} +20 -6
  39. package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-D-YGQbEv.js} +58 -20
  40. package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-DSDahF23.js} +148 -61
  41. package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
  42. package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-KypbgOyo.js} +300 -72
  43. package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-Cc84LaM3.js} +39 -14
  44. package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-YKr-cQPN.js} +7 -4
  45. package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DMMDQHAu.js} +52 -29
  46. package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-BVPDyIup.js} +10 -7
  47. package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-CCwyJP5C.js} +79 -33
  48. package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-B77O-z28.js} +21 -8
  49. package/dist/chunks/{LpNumberFlow-amWYaeuA.js → LpNumberFlow-z4WPZcZu.js} +22 -12
  50. package/dist/chunks/{LpOtpInput-y2v-pmiN.js → LpOtpInput-CziPF3Oq.js} +37 -8
  51. package/dist/chunks/{LpPagination-5FxjmmB5.js → LpPagination-BcDlo3r_.js} +31 -20
  52. package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-eKzQhbQP.js} +11 -8
  53. package/dist/chunks/LpPhoneInput-BCbrrhgE.js +336 -0
  54. package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BEJ2SfI6.js} +84 -28
  55. package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-DCKeqiVn.js} +5 -2
  56. package/dist/chunks/{LpProgress-Btq5W4Ut.js → LpProgress-CBU1iI69.js} +5 -4
  57. package/dist/chunks/{LpRadioGroup-xPIuVDO_.js → LpRadioGroup-DmEW0l78.js} +9 -2
  58. package/dist/chunks/LpRangeCalendar-DZuaRV0W.js +331 -0
  59. package/dist/chunks/{LpRollingText-CM5gkSeF.js → LpRollingText-DB7upf-5.js} +1 -1
  60. package/dist/chunks/{LpScrollArea-D8xyKXcF.js → LpScrollArea-Bry1fdOM.js} +14 -2
  61. package/dist/chunks/{LpSection-aM6J_u51.js → LpSection-tKEH0jYf.js} +1 -1
  62. package/dist/chunks/{LpSegmented-fV0Sk92p.js → LpSegmented-C09EEam2.js} +8 -4
  63. package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DNIL6MB9.js} +50 -31
  64. package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-CQNB0-yU.js} +30 -22
  65. package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-kx-LdqNU.js} +16 -11
  66. package/dist/chunks/{LpSkeleton-CTI-vBgW.js → LpSkeleton-BB6XIV-w.js} +1 -1
  67. package/dist/chunks/{LpSlider-BwNIBkAK.js → LpSlider-Cuzmrd7p.js} +7 -2
  68. package/dist/chunks/{LpSparkline-C6xQ2dwv.js → LpSparkline-CB80N8SJ.js} +11 -4
  69. package/dist/chunks/{LpStat-CQaW8R--.js → LpStat-B-7udBRP.js} +2 -2
  70. package/dist/chunks/{LpStepper-Bk-XaQYU.js → LpStepper-C7XiVKZj.js} +34 -19
  71. package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-OrcOJX9m.js} +41 -27
  72. package/dist/chunks/LpTableFilter-BV8PpLM9.js +158 -0
  73. package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
  74. package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-29iW1XOx.js} +20 -6
  75. package/dist/chunks/LpTagsInput-D1SuqbhB.js +300 -0
  76. package/dist/chunks/LpTextarea-uuScWo6-.js +77 -0
  77. package/dist/chunks/LpThemeProvider-Be4r2TQX.js +35 -0
  78. package/dist/chunks/LpThemeSwitcher-B4aVPbNC.js +166 -0
  79. package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
  80. package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-Cqe7fXnh.js} +14 -8
  81. package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster-DpJvoCOh.js} +94 -31
  82. package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CXVKcOXT.js} +14 -6
  83. package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
  84. package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-6ozNrQKh.js} +8 -2
  85. package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-ugcv25xj.js} +44 -15
  86. package/dist/chunks/icons-CkA0njzm.js +43 -0
  87. package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-DJm1JSsY.js} +215 -260
  88. package/dist/component-names.d.ts +1 -1
  89. package/dist/components/ContextMenuItems.vue.d.ts +8 -0
  90. package/dist/components/ContextMenuItems.vue.js +4 -0
  91. package/dist/components/LayoutCanvas.vue.js +1 -1
  92. package/dist/components/LayoutNode.vue.d.ts +4 -0
  93. package/dist/components/LayoutNode.vue.js +1 -1
  94. package/dist/components/LpActionBar.vue.d.ts +3 -0
  95. package/dist/components/LpActionBar.vue.js +1 -1
  96. package/dist/components/LpAddressInput.vue.d.ts +1 -1
  97. package/dist/components/LpAddressInput.vue.js +1 -1
  98. package/dist/components/LpAlert.vue.d.ts +9 -0
  99. package/dist/components/LpAlert.vue.js +1 -1
  100. package/dist/components/LpAppShell.vue.d.ts +10 -0
  101. package/dist/components/LpAppShell.vue.js +1 -1
  102. package/dist/components/LpAutocomplete.vue.d.ts +31 -6
  103. package/dist/components/LpAutocomplete.vue.js +1 -1
  104. package/dist/components/LpAvatar.vue.d.ts +3 -3
  105. package/dist/components/LpAvatar.vue.js +1 -1
  106. package/dist/components/LpBadge.vue.d.ts +2 -2
  107. package/dist/components/LpBreadcrumbs.vue.d.ts +13 -9
  108. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  109. package/dist/components/LpButton.vue.d.ts +35 -98
  110. package/dist/components/LpButton.vue.js +1 -1
  111. package/dist/components/LpCalendar.vue.js +1 -1
  112. package/dist/components/LpCard.vue.js +1 -1
  113. package/dist/components/LpChart.vue.d.ts +47 -3
  114. package/dist/components/LpChart.vue.js +377 -97
  115. package/dist/components/LpCheckbox.vue.js +1 -1
  116. package/dist/components/LpCodeBlock.vue.d.ts +2 -0
  117. package/dist/components/LpCodeBlock.vue.js +1 -1
  118. package/dist/components/LpColorPicker.vue.d.ts +2 -2
  119. package/dist/components/LpColorPicker.vue.js +1 -1
  120. package/dist/components/LpCommandPalette.vue.d.ts +14 -3
  121. package/dist/components/LpCommandPalette.vue.js +142 -77
  122. package/dist/components/LpConfirmDialog.vue.d.ts +15 -3
  123. package/dist/components/LpConfirmDialog.vue.js +1 -1
  124. package/dist/components/LpConfirmHost.vue.d.ts +9 -0
  125. package/dist/components/LpConfirmHost.vue.js +4 -0
  126. package/dist/components/LpContextMenu.vue.d.ts +3 -5
  127. package/dist/components/LpContextMenu.vue.js +1 -1
  128. package/dist/components/LpDatePicker.vue.d.ts +24 -6
  129. package/dist/components/LpDatePicker.vue.js +1 -1
  130. package/dist/components/LpDateRangePicker.vue.d.ts +60 -0
  131. package/dist/components/LpDateRangePicker.vue.js +4 -0
  132. package/dist/components/LpDisclosure.vue.js +1 -1
  133. package/dist/components/LpDrawer.vue.d.ts +10 -0
  134. package/dist/components/LpDrawer.vue.js +1 -1
  135. package/dist/components/LpDropdownMenu.vue.js +1 -1
  136. package/dist/components/LpDropzone.vue.js +23 -7
  137. package/dist/components/LpEmptyState.vue.js +1 -1
  138. package/dist/components/LpFieldNotice.vue.d.ts +8 -0
  139. package/dist/components/LpFieldNotice.vue.js +4 -0
  140. package/dist/components/LpFileTree.vue.d.ts +11 -2
  141. package/dist/components/LpFileTree.vue.js +1 -1
  142. package/dist/components/LpFileTreeNode.vue.js +1 -1
  143. package/dist/components/LpFormField.vue.d.ts +13 -1
  144. package/dist/components/LpFormField.vue.js +1 -1
  145. package/dist/components/LpIcon.vue.d.ts +0 -1
  146. package/dist/components/LpIcon.vue.js +1 -1
  147. package/dist/components/LpImageEditor.vue.js +93 -58
  148. package/dist/components/LpInput.vue.d.ts +22 -3
  149. package/dist/components/LpInput.vue.js +1 -1
  150. package/dist/components/LpLightbox.vue.d.ts +17 -0
  151. package/dist/components/LpLightbox.vue.js +1 -1
  152. package/dist/components/LpLink.vue.d.ts +11 -11
  153. package/dist/components/LpLink.vue.js +1 -1
  154. package/dist/components/LpLogViewer.vue.d.ts +72 -3
  155. package/dist/components/LpLogViewer.vue.js +1 -1
  156. package/dist/components/LpMapPicker.vue.d.ts +4 -4
  157. package/dist/components/LpMapPicker.vue.js +1 -1
  158. package/dist/components/LpMediaGallery.vue.js +1 -1
  159. package/dist/components/LpModal.vue.d.ts +16 -6
  160. package/dist/components/LpModal.vue.js +1 -1
  161. package/dist/components/LpModalNav.vue.d.ts +0 -1
  162. package/dist/components/LpModalNav.vue.js +1 -1
  163. package/dist/components/LpNotificationBell.vue.d.ts +11 -2
  164. package/dist/components/LpNotificationBell.vue.js +1 -1
  165. package/dist/components/LpNumberField.vue.d.ts +2 -0
  166. package/dist/components/LpNumberField.vue.js +1 -1
  167. package/dist/components/LpNumberFlow.vue.js +1 -1
  168. package/dist/components/LpOtpInput.vue.js +1 -1
  169. package/dist/components/LpPagination.vue.d.ts +2 -0
  170. package/dist/components/LpPagination.vue.js +1 -1
  171. package/dist/components/LpPasswordInput.vue.js +1 -1
  172. package/dist/components/LpPhoneInput.vue.d.ts +13 -0
  173. package/dist/components/LpPhoneInput.vue.js +1 -1
  174. package/dist/components/LpPie.vue.d.ts +21 -2
  175. package/dist/components/LpPie.vue.js +1 -1
  176. package/dist/components/LpPopover.vue.d.ts +2 -0
  177. package/dist/components/LpPopover.vue.js +1 -1
  178. package/dist/components/LpProgress.vue.d.ts +1 -1
  179. package/dist/components/LpProgress.vue.js +1 -1
  180. package/dist/components/LpRadioGroup.vue.js +1 -1
  181. package/dist/components/LpRangeCalendar.vue.d.ts +51 -0
  182. package/dist/components/LpRangeCalendar.vue.js +4 -0
  183. package/dist/components/LpRollingText.vue.d.ts +1 -1
  184. package/dist/components/LpRollingText.vue.js +1 -1
  185. package/dist/components/LpScrollArea.vue.d.ts +2 -2
  186. package/dist/components/LpScrollArea.vue.js +1 -1
  187. package/dist/components/LpSection.vue.d.ts +1 -1
  188. package/dist/components/LpSection.vue.js +1 -1
  189. package/dist/components/LpSegmented.vue.d.ts +5 -0
  190. package/dist/components/LpSegmented.vue.js +1 -1
  191. package/dist/components/LpSelect.vue.d.ts +1 -0
  192. package/dist/components/LpSelect.vue.js +1 -1
  193. package/dist/components/LpShortcutsDialog.vue.d.ts +7 -1
  194. package/dist/components/LpShortcutsDialog.vue.js +60 -15
  195. package/dist/components/LpSidebar.vue.d.ts +6 -2
  196. package/dist/components/LpSidebar.vue.js +1 -1
  197. package/dist/components/LpSidebarNav.vue.js +1 -1
  198. package/dist/components/LpSkeleton.vue.js +1 -1
  199. package/dist/components/LpSlider.vue.js +1 -1
  200. package/dist/components/LpSparkline.vue.d.ts +1 -0
  201. package/dist/components/LpSparkline.vue.js +1 -1
  202. package/dist/components/LpStat.vue.js +1 -1
  203. package/dist/components/LpStepper.vue.js +1 -1
  204. package/dist/components/LpSwitch.vue.js +1 -1
  205. package/dist/components/LpTable.vue.d.ts +107 -3
  206. package/dist/components/LpTable.vue.js +540 -128
  207. package/dist/components/LpTableFilter.vue.d.ts +31 -0
  208. package/dist/components/LpTableFilter.vue.js +4 -0
  209. package/dist/components/LpTableOfContents.vue.d.ts +4 -2
  210. package/dist/components/LpTableOfContents.vue.js +1 -1
  211. package/dist/components/LpTabs.vue.d.ts +2 -0
  212. package/dist/components/LpTabs.vue.js +1 -1
  213. package/dist/components/LpTagsInput.vue.d.ts +77 -0
  214. package/dist/components/LpTagsInput.vue.js +4 -0
  215. package/dist/components/LpTextarea.vue.d.ts +19 -6
  216. package/dist/components/LpTextarea.vue.js +1 -1
  217. package/dist/components/LpThemeProvider.vue.d.ts +22 -0
  218. package/dist/components/LpThemeProvider.vue.js +4 -0
  219. package/dist/components/LpThemeSwitcher.vue.d.ts +15 -2
  220. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  221. package/dist/components/LpTilt.vue.js +1 -1
  222. package/dist/components/LpTimeField.vue.js +1 -1
  223. package/dist/components/LpToaster.vue.d.ts +6 -1
  224. package/dist/components/LpToaster.vue.js +1 -1
  225. package/dist/components/LpTooltip.vue.d.ts +1 -2
  226. package/dist/components/LpTooltip.vue.js +1 -1
  227. package/dist/components/LpTooltipProvider.vue.d.ts +22 -0
  228. package/dist/components/LpTooltipProvider.vue.js +4 -0
  229. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  230. package/dist/components/LpUptimeBar.vue.d.ts +4 -0
  231. package/dist/components/LpUptimeBar.vue.js +1 -1
  232. package/dist/components/chart.d.ts +32 -1
  233. package/dist/components/chart.js +42 -6
  234. package/dist/components/codeHighlight.d.ts +6 -0
  235. package/dist/components/codeHighlight.js +92 -20
  236. package/dist/components/color.d.ts +1 -1
  237. package/dist/components/color.js +20 -9
  238. package/dist/components/commandSearch.js +3 -0
  239. package/dist/components/confirm.d.ts +25 -0
  240. package/dist/components/confirm.js +26 -0
  241. package/dist/components/countries.d.ts +6 -0
  242. package/dist/components/countries.js +7 -0
  243. package/dist/components/dateRange.d.ts +43 -0
  244. package/dist/components/dateRange.js +76 -0
  245. package/dist/components/exportData.d.ts +58 -0
  246. package/dist/components/exportData.js +165 -0
  247. package/dist/components/fileTree.js +16 -7
  248. package/dist/components/imageCrop.d.ts +42 -0
  249. package/dist/components/imageCrop.js +47 -0
  250. package/dist/components/link.d.ts +12 -0
  251. package/dist/components/link.js +19 -0
  252. package/dist/components/logSearch.d.ts +18 -0
  253. package/dist/components/logSearch.js +36 -0
  254. package/dist/components/modalDepth.d.ts +18 -2
  255. package/dist/components/modalDepth.js +17 -5
  256. package/dist/components/otp.d.ts +5 -0
  257. package/dist/components/otp.js +10 -0
  258. package/dist/components/phone.d.ts +41 -0
  259. package/dist/components/phone.js +80 -0
  260. package/dist/components/sidebar.d.ts +7 -1
  261. package/dist/components/tableFilter.d.ts +36 -0
  262. package/dist/components/tableFilter.js +84 -0
  263. package/dist/components/tableSort.d.ts +8 -0
  264. package/dist/components/tableSort.js +31 -0
  265. package/dist/components/tags.d.ts +20 -0
  266. package/dist/components/tags.js +37 -0
  267. package/dist/composables/useField.d.ts +38 -0
  268. package/dist/composables/useField.js +45 -0
  269. package/dist/composables/useFieldNotice.d.ts +17 -0
  270. package/dist/composables/useFieldNotice.js +28 -0
  271. package/dist/composables/useForm.d.ts +53 -0
  272. package/dist/composables/useForm.js +207 -0
  273. package/dist/composables/useFormDraft.d.ts +10 -3
  274. package/dist/composables/useFormDraft.js +9 -14
  275. package/dist/composables/useHotkeys.d.ts +81 -5
  276. package/dist/composables/useHotkeys.js +223 -25
  277. package/dist/composables/useInputFilter.d.ts +17 -0
  278. package/dist/composables/useInputFilter.js +47 -10
  279. package/dist/composables/useInputMask.d.ts +59 -0
  280. package/dist/composables/useInputMask.js +170 -0
  281. package/dist/composables/useLpConfig.d.ts +24 -0
  282. package/dist/composables/useLpConfig.js +44 -0
  283. package/dist/composables/usePaintSelect.d.ts +22 -0
  284. package/dist/composables/usePaintSelect.js +169 -0
  285. package/dist/composables/useRovingFocus.js +7 -0
  286. package/dist/composables/useShift.js +7 -2
  287. package/dist/composables/useTilt.d.ts +2 -2
  288. package/dist/composables/useTilt.js +5 -6
  289. package/dist/composables/useToast.d.ts +20 -0
  290. package/dist/composables/useToast.js +28 -4
  291. package/dist/composables/useZoomPan.d.ts +3 -3
  292. package/dist/composables/useZoomPan.js +4 -4
  293. package/dist/icons-all.d.ts +1 -0
  294. package/dist/icons-all.js +3 -0
  295. package/dist/icons.d.ts +14 -1
  296. package/dist/index.d.ts +60 -4
  297. package/dist/index.js +245 -141
  298. package/dist/kit-icons.d.ts +2 -0
  299. package/dist/layout/tree.d.ts +2 -0
  300. package/dist/layout/tree.js +16 -1
  301. package/dist/layout/useLayout.d.ts +15 -0
  302. package/dist/layout/useLayout.js +41 -14
  303. package/dist/theme/backdrop.d.ts +12 -0
  304. package/dist/theme/presets.d.ts +0 -2
  305. package/dist/theme/useTheme.d.ts +32 -9
  306. package/package.json +18 -8
  307. package/src/component-names.ts +9 -7
  308. package/src/components/ContextMenuItems.vue +88 -0
  309. package/src/components/LayoutCanvas.vue +26 -6
  310. package/src/components/LayoutNode.vue +30 -4
  311. package/src/components/LpActionBar.vue +31 -8
  312. package/src/components/LpAddressInput.vue +57 -41
  313. package/src/components/LpAlert.vue +16 -3
  314. package/src/components/LpAppShell.vue +45 -12
  315. package/src/components/LpAutocomplete.vue +47 -10
  316. package/src/components/LpAvatar.vue +20 -6
  317. package/src/components/LpBreadcrumbs.vue +65 -41
  318. package/src/components/LpButton.vue +60 -14
  319. package/src/components/LpChart.vue +351 -28
  320. package/src/components/LpCheckbox.vue +7 -3
  321. package/src/components/LpCodeBlock.vue +50 -14
  322. package/src/components/LpColorPicker.vue +73 -10
  323. package/src/components/LpCommandPalette.vue +115 -20
  324. package/src/components/LpConfirmDialog.vue +68 -17
  325. package/src/components/LpConfirmHost.vue +59 -0
  326. package/src/components/LpContextMenu.vue +15 -116
  327. package/src/components/LpDatePicker.vue +104 -68
  328. package/src/components/LpDateRangePicker.vue +224 -0
  329. package/src/components/LpDrawer.vue +56 -19
  330. package/src/components/LpDropdownMenu.vue +1 -0
  331. package/src/components/LpDropzone.vue +21 -4
  332. package/src/components/LpFieldNotice.vue +65 -0
  333. package/src/components/LpFileTree.vue +33 -7
  334. package/src/components/LpFormField.vue +59 -7
  335. package/src/components/LpIcon.vue +22 -4
  336. package/src/components/LpImageEditor.vue +96 -57
  337. package/src/components/LpInput.vue +62 -11
  338. package/src/components/LpLightbox.vue +140 -29
  339. package/src/components/LpLink.vue +24 -13
  340. package/src/components/LpLogViewer.vue +311 -39
  341. package/src/components/LpMapPicker.vue +36 -13
  342. package/src/components/LpMediaGallery.vue +8 -2
  343. package/src/components/LpModal.vue +54 -28
  344. package/src/components/LpModalNav.vue +5 -2
  345. package/src/components/LpNotificationBell.vue +77 -23
  346. package/src/components/LpNumberField.vue +21 -6
  347. package/src/components/LpNumberFlow.vue +26 -10
  348. package/src/components/LpOtpInput.vue +43 -8
  349. package/src/components/LpPagination.vue +23 -7
  350. package/src/components/LpPasswordInput.vue +4 -1
  351. package/src/components/LpPhoneInput.vue +224 -39
  352. package/src/components/LpPie.vue +102 -20
  353. package/src/components/LpPopover.vue +4 -0
  354. package/src/components/LpProgress.vue +5 -3
  355. package/src/components/LpRadioGroup.vue +9 -2
  356. package/src/components/LpRangeCalendar.vue +365 -0
  357. package/src/components/LpRollingText.vue +3 -0
  358. package/src/components/LpScrollArea.vue +22 -2
  359. package/src/components/LpSegmented.vue +12 -2
  360. package/src/components/LpSelect.vue +0 -0
  361. package/src/components/LpShortcutsDialog.vue +83 -9
  362. package/src/components/LpSidebar.vue +14 -9
  363. package/src/components/LpSidebarNav.vue +17 -5
  364. package/src/components/LpSkeleton.vue +4 -1
  365. package/src/components/LpSlider.vue +7 -1
  366. package/src/components/LpSparkline.vue +11 -3
  367. package/src/components/LpStepper.vue +23 -10
  368. package/src/components/LpSwitch.vue +31 -8
  369. package/src/components/LpTable.vue +668 -90
  370. package/src/components/LpTableFilter.vue +175 -0
  371. package/src/components/LpTableOfContents.vue +50 -32
  372. package/src/components/LpTabs.vue +23 -6
  373. package/src/components/LpTagsInput.vue +364 -0
  374. package/src/components/LpTextarea.vue +44 -6
  375. package/src/components/LpThemeProvider.vue +41 -0
  376. package/src/components/LpThemeSwitcher.vue +76 -33
  377. package/src/components/LpTilt.vue +2 -2
  378. package/src/components/LpTimeField.vue +14 -3
  379. package/src/components/LpToaster.vue +115 -27
  380. package/src/components/LpTooltip.vue +29 -5
  381. package/src/components/LpTooltipProvider.vue +24 -0
  382. package/src/components/LpTopologyCanvas.vue +19 -2
  383. package/src/components/LpUptimeBar.vue +46 -8
  384. package/src/components/chart.ts +82 -8
  385. package/src/components/codeHighlight.ts +111 -15
  386. package/src/components/color.ts +30 -9
  387. package/src/components/commandSearch.ts +6 -0
  388. package/src/components/confirm.ts +58 -0
  389. package/src/components/countries.ts +12 -0
  390. package/src/components/dateRange.ts +134 -0
  391. package/src/components/exportData.ts +228 -0
  392. package/src/components/fileTree.ts +19 -7
  393. package/src/components/imageCrop.ts +107 -0
  394. package/src/components/link.ts +34 -0
  395. package/src/components/logSearch.ts +46 -0
  396. package/src/components/modalDepth.ts +48 -6
  397. package/src/components/otp.ts +16 -0
  398. package/src/components/phone.ts +140 -0
  399. package/src/components/sidebar.ts +7 -1
  400. package/src/components/tableFilter.ts +122 -0
  401. package/src/components/tableSort.ts +37 -0
  402. package/src/components/tags.ts +62 -0
  403. package/src/composables/useField.ts +83 -0
  404. package/src/composables/useFieldNotice.ts +59 -0
  405. package/src/composables/useForm.ts +314 -0
  406. package/src/composables/useFormDraft.ts +25 -20
  407. package/src/composables/useHotkeys.ts +355 -28
  408. package/src/composables/useInputFilter.ts +77 -14
  409. package/src/composables/useInputMask.ts +254 -0
  410. package/src/composables/useLpConfig.ts +87 -0
  411. package/src/composables/usePaintSelect.ts +243 -0
  412. package/src/composables/useRovingFocus.ts +11 -0
  413. package/src/composables/useShift.ts +13 -2
  414. package/src/composables/useTilt.ts +5 -6
  415. package/src/composables/useToast.ts +56 -4
  416. package/src/composables/useZoomPan.ts +6 -6
  417. package/src/iconSubset.d.mts +10 -0
  418. package/src/iconSubset.mjs +51 -0
  419. package/src/icons-all.ts +10 -0
  420. package/src/icons.ts +59 -8
  421. package/src/index.ts +73 -4
  422. package/src/kit-icons.ts +5 -0
  423. package/src/layout/tree.ts +30 -0
  424. package/src/layout/useLayout.ts +68 -18
  425. package/src/nuxt.ts +87 -17
  426. package/src/runtime/config.plugin.ts +14 -0
  427. package/src/runtime/theme.plugin.ts +43 -23
  428. package/src/theme/backdrop.ts +69 -0
  429. package/src/theme/presets.ts +6 -64
  430. package/src/theme/useTheme.ts +206 -100
  431. package/src/tokens/tokens.css +46 -0
  432. package/dist/chunks/LpBreadcrumbs-p4jHe_V-.js +0 -62
  433. package/dist/chunks/LpContextMenu-CwW1QXe_.js +0 -217
  434. package/dist/chunks/LpDatePicker-CwOToxGq.js +0 -164
  435. package/dist/chunks/LpFormField-CazBc2-a.js +0 -62
  436. package/dist/chunks/LpPhoneInput-BDSKVl_z.js +0 -209
  437. package/dist/chunks/LpTextarea-t7F6gv8E.js +0 -47
@@ -126,7 +126,8 @@ export function formatSize(bytes: number): string {
126
126
  const units = ["KB", "MB", "GB", "TB", "PB"]
127
127
  let value = bytes / 1024
128
128
  let i = 0
129
- while (value >= 1024 && i < units.length - 1) {
129
+ // Compare the value as it will print, so 1 048 575 B is "1.0 MB", not "1024 KB".
130
+ while (Math.round(value) >= 1024 && i < units.length - 1) {
130
131
  value /= 1024
131
132
  i++
132
133
  }
@@ -191,14 +192,20 @@ export function checkStateOf(node: FileNode, checked: ReadonlySet<string>): Chec
191
192
  }
192
193
  let all = true
193
194
  let any = false
195
+ let considered = 0
194
196
  for (const child of node.children) {
195
197
  const state = checkStateOf(child, checked)
198
+ // A locked, unticked entry can never be ticked, so it must not hold its
199
+ // folder at "indeterminate" forever.
200
+ if (child.disabled && state === "unchecked") continue
201
+ considered++
196
202
  if (state === "checked") any = true
197
203
  else if (state === "indeterminate") {
198
204
  any = true
199
205
  all = false
200
206
  } else all = false
201
207
  }
208
+ if (!considered) return checked.has(node.id) ? "checked" : "unchecked"
202
209
  if (all) return "checked"
203
210
  return any ? "indeterminate" : "unchecked"
204
211
  }
@@ -271,14 +278,18 @@ export function computeStats(
271
278
  let total = 0
272
279
  let bytes = 0
273
280
  let sized = node.size !== undefined
281
+ let considered = 0
274
282
 
275
283
  for (const child of node.children) {
276
284
  const childStats = visit(child)
277
- if (childStats.state === "checked") any = true
278
- else if (childStats.state === "indeterminate") {
279
- any = true
280
- all = false
281
- } else all = false
285
+ if (!child.disabled || childStats.state !== "unchecked") {
286
+ considered++
287
+ if (childStats.state === "checked") any = true
288
+ else if (childStats.state === "indeterminate") {
289
+ any = true
290
+ all = false
291
+ } else all = false
292
+ }
282
293
  count += childStats.checked
283
294
  total += childStats.total
284
295
  if (childStats.size !== undefined) {
@@ -287,8 +298,9 @@ export function computeStats(
287
298
  }
288
299
  }
289
300
 
301
+ const own: CheckState = checked.has(node.id) ? "checked" : "unchecked"
290
302
  const self: NodeStats = {
291
- state: all ? "checked" : any ? "indeterminate" : "unchecked",
303
+ state: !considered ? own : all ? "checked" : any ? "indeterminate" : "unchecked",
292
304
  checked: count,
293
305
  total,
294
306
  // An explicit size on the directory wins: a server-side listing usually
@@ -0,0 +1,107 @@
1
+ /*
2
+ * Geometry shared by LpImageEditor's preview and its export, so what is drawn
3
+ * on screen and what is written to the Blob come from one calculation.
4
+ *
5
+ * Three spaces are involved:
6
+ * - source: the picture's own pixels, origin at its centre;
7
+ * - upright: the source after the rotation, as the person sees it, origin at
8
+ * its centre, still in source-pixel units;
9
+ * - frame: the crop box on screen, origin at its centre, in CSS pixels.
10
+ *
11
+ * A turn of θ (clockwise, as CSS and canvas both rotate in a y-down space)
12
+ * maps source to upright by u = R(θ)·q. The picture is drawn at `cover` CSS
13
+ * pixels per source pixel, then panned by `offset` and zoomed by `scale`, so a
14
+ * frame point f shows the upright point (f / scale − offset) / cover.
15
+ */
16
+
17
+ export interface Point {
18
+ x: number
19
+ y: number
20
+ }
21
+
22
+ export interface CropInput {
23
+ sourceWidth: number
24
+ sourceHeight: number
25
+ frameWidth: number
26
+ frameHeight: number
27
+ /** Accumulated rotation in degrees; any angle, not only quarter turns. */
28
+ rotation: number
29
+ /** Zoom factor, 1 = the picture just covers the frame. */
30
+ scale: number
31
+ /** Pan in frame pixels before the zoom, as useZoomPan reports it. */
32
+ offset: Point
33
+ }
34
+
35
+ export interface CropGeometry {
36
+ /** CSS pixels per source pixel at zoom 1. */
37
+ cover: number
38
+ /** Crop size in upright (source-pixel) units. */
39
+ width: number
40
+ height: number
41
+ /** Crop centre in upright space. */
42
+ centre: Point
43
+ }
44
+
45
+ const radians = (deg: number) => (deg * Math.PI) / 180
46
+
47
+ /** Rotate a point clockwise by `deg` in a y-down space. */
48
+ export function rotatePoint(p: Point, deg: number): Point {
49
+ const a = radians(deg)
50
+ const cos = Math.cos(a)
51
+ const sin = Math.sin(a)
52
+ return { x: p.x * cos - p.y * sin, y: p.x * sin + p.y * cos }
53
+ }
54
+
55
+ /** The source pixel (from the source's top-left) under an upright-space point. */
56
+ export function uprightToSource(u: Point, rotation: number, sourceWidth: number, sourceHeight: number): Point {
57
+ const q = rotatePoint(u, -rotation)
58
+ return { x: q.x + sourceWidth / 2, y: q.y + sourceHeight / 2 }
59
+ }
60
+
61
+ /**
62
+ * Smallest scale at which the turned picture still covers the whole frame.
63
+ *
64
+ * Covering the bounding box of a turned picture is not enough at a free angle:
65
+ * its corners are empty. The frame, turned back by the same angle, has to fit
66
+ * inside the unturned picture; for a quarter turn this reduces to the usual
67
+ * object-cover of the swapped dimensions.
68
+ */
69
+ export function coverScale(
70
+ sourceWidth: number,
71
+ sourceHeight: number,
72
+ frameWidth: number,
73
+ frameHeight: number,
74
+ rotation: number,
75
+ ): number {
76
+ const a = radians(rotation)
77
+ const cos = Math.abs(Math.cos(a))
78
+ const sin = Math.abs(Math.sin(a))
79
+ return Math.max(
80
+ (frameWidth * cos + frameHeight * sin) / sourceWidth,
81
+ (frameWidth * sin + frameHeight * cos) / sourceHeight,
82
+ )
83
+ }
84
+
85
+ /** What the frame holds, in upright space. */
86
+ export function cropGeometry(input: CropInput): CropGeometry {
87
+ const cover = coverScale(
88
+ input.sourceWidth,
89
+ input.sourceHeight,
90
+ input.frameWidth,
91
+ input.frameHeight,
92
+ input.rotation,
93
+ )
94
+ const shown = cover * input.scale
95
+ return {
96
+ cover,
97
+ width: input.frameWidth / shown,
98
+ height: input.frameHeight / shown,
99
+ centre: { x: -input.offset.x / cover, y: -input.offset.y / cover },
100
+ }
101
+ }
102
+
103
+ /** Whether the turn is closer to sideways than to upright. */
104
+ export function isSideways(rotation: number): boolean {
105
+ const quarters = Math.round(rotation / 90)
106
+ return ((quarters % 2) + 2) % 2 === 1
107
+ }
@@ -0,0 +1,34 @@
1
+ import type { Component } from "vue"
2
+
3
+ /*
4
+ * How a navigation target renders: an internal destination goes through the
5
+ * configured link component (RouterLink, NuxtLink) so it navigates client-side,
6
+ * anything else is a plain anchor. Shared by every kit piece that links.
7
+ */
8
+ export type LinkTarget = string | Record<string, unknown>
9
+
10
+ export interface ResolvedLink {
11
+ is: Component | string
12
+ props: Record<string, unknown>
13
+ }
14
+
15
+ /** An app-relative path: "/x", not "//host" or "https://…". */
16
+ export function isInternalHref(href: string): boolean {
17
+ return href.startsWith("/") && !href.startsWith("//")
18
+ }
19
+
20
+ export function resolveLink(
21
+ linkComponent: Component | string,
22
+ target: { to?: LinkTarget; href?: string },
23
+ ): ResolvedLink | null {
24
+ const router = linkComponent !== "a"
25
+ if (target.to != null) {
26
+ if (router) return { is: linkComponent, props: { to: target.to } }
27
+ return typeof target.to === "string" ? { is: "a", props: { href: target.to } } : null
28
+ }
29
+ if (target.href != null) {
30
+ if (router && isInternalHref(target.href)) return { is: linkComponent, props: { to: target.href } }
31
+ return { is: "a", props: { href: target.href } }
32
+ }
33
+ return null
34
+ }
@@ -0,0 +1,46 @@
1
+ /*
2
+ * The log viewer's search and filtering, kept free of the component so the
3
+ * rules (what counts as a match, how the cursor wraps) can be tested alone.
4
+ */
5
+
6
+ /** The last `max` lines, and how many were cut from the front. */
7
+ export function ringWindow<T>(lines: readonly T[], max: number | undefined): { lines: readonly T[]; offset: number } {
8
+ if (!max || max <= 0 || lines.length <= max) return { lines, offset: 0 }
9
+ return { lines: lines.slice(lines.length - max), offset: lines.length - max }
10
+ }
11
+
12
+ /** Lines per level, a line with no level counting as `fallback`. */
13
+ export function countLevels<L extends string>(
14
+ lines: readonly { level?: L }[],
15
+ fallback: L,
16
+ ): Map<L, number> {
17
+ const counts = new Map<L, number>()
18
+ for (const line of lines) {
19
+ const level = line.level ?? fallback
20
+ counts.set(level, (counts.get(level) ?? 0) + 1)
21
+ }
22
+ return counts
23
+ }
24
+
25
+ /** Case-insensitive substring test; an empty term matches nothing. */
26
+ export function matchesTerm(message: string, term: string): boolean {
27
+ const needle = term.trim().toLowerCase()
28
+ return needle.length > 0 && message.toLowerCase().includes(needle)
29
+ }
30
+
31
+ /** Positions (into `rows`) of the rows whose message or source holds the term. */
32
+ export function matchPositions(rows: readonly { message: string; source?: string }[], term: string): number[] {
33
+ if (!term.trim()) return []
34
+ const out: number[] = []
35
+ rows.forEach((row, i) => {
36
+ if (matchesTerm(row.message, term) || (row.source !== undefined && matchesTerm(row.source, term))) out.push(i)
37
+ })
38
+ return out
39
+ }
40
+
41
+ /** The next match cursor, wrapping at both ends; -1 when there are none. */
42
+ export function stepMatch(current: number, total: number, direction: 1 | -1): number {
43
+ if (total <= 0) return -1
44
+ if (current < 0 || current >= total) return direction === 1 ? 0 : total - 1
45
+ return (current + direction + total) % total
46
+ }
@@ -1,16 +1,47 @@
1
- import { computed, ref, type ComputedRef } from "vue"
1
+ import { computed, ref, type ComputedRef, type Ref } from "vue"
2
2
 
3
3
  const STEP = 20
4
4
 
5
- const stack = ref<number[]>([])
6
- let nextId = 0
5
+ /*
6
+ * One opening-order stack for every modal surface. Pinned to a global symbol
7
+ * like the toast queue: a barrel import and a deep import can load this module
8
+ * twice, and two stacks would put a deep-imported dialog's scrim under a
9
+ * barrel-imported drawer's panel.
10
+ */
11
+ const STATE_KEY = Symbol.for("@leavepulse/ui:modal-stack")
12
+ type GlobalWithStack = typeof globalThis & {
13
+ [STATE_KEY]?: {
14
+ stack: Ref<number[]>
15
+ nextId: number
16
+ elements: Map<number, () => HTMLElement | null | undefined>
17
+ }
18
+ }
19
+ const g = globalThis as GlobalWithStack
20
+ const state = (g[STATE_KEY] ??= { stack: ref<number[]>([]), nextId: 0, elements: new Map() })
21
+ const stack = state.stack
7
22
 
8
- export function useModalLayer(): {
23
+ export interface ModalLayer {
24
+ /** Join the stack on top. Ignored on the server, where nothing would ever release it. */
9
25
  claim: () => void
10
26
  release: () => void
27
+ /** z-index values for this surface's scrim and panel, and whether another modal is under it. */
11
28
  layer: ComputedRef<{ scrim: string; panel: string; nested: boolean }>
12
- } {
13
- const id = nextId++
29
+ /** Whether this surface is the topmost open one. */
30
+ top: ComputedRef<boolean>
31
+ }
32
+
33
+ export interface ModalLayerOptions {
34
+ /** The surface's panel, so keyboard handling can tell what sits inside the top modal. */
35
+ element?: () => HTMLElement | null | undefined
36
+ }
37
+
38
+ /**
39
+ * Put an overlay on the shared modal stack, so a surface opened from another
40
+ * one (a dialog from a drawer, a lightbox from a dialog) lands above it with
41
+ * its scrim, whatever component either of them is.
42
+ */
43
+ export function useModalLayer(options: ModalLayerOptions = {}): ModalLayer {
44
+ const id = state.nextId++
14
45
 
15
46
  const fromTop = computed(() => {
16
47
  const at = stack.value.indexOf(id)
@@ -19,9 +50,12 @@ export function useModalLayer(): {
19
50
 
20
51
  return {
21
52
  claim: () => {
53
+ if (typeof window === "undefined") return
54
+ if (options.element) state.elements.set(id, options.element)
22
55
  if (!stack.value.includes(id)) stack.value = [...stack.value, id]
23
56
  },
24
57
  release: () => {
58
+ state.elements.delete(id)
25
59
  stack.value = stack.value.filter((x) => x !== id)
26
60
  },
27
61
  layer: computed(() => {
@@ -32,9 +66,17 @@ export function useModalLayer(): {
32
66
  nested: stack.value.indexOf(id) > 0,
33
67
  }
34
68
  }),
69
+ top: computed(() => stack.value[stack.value.length - 1] === id),
35
70
  }
36
71
  }
37
72
 
38
73
  export function anyModalOpen(): boolean {
39
74
  return stack.value.length > 0
40
75
  }
76
+
77
+ /** The panel of the topmost modal, when it registered one. */
78
+ export function topModalElement(): HTMLElement | null {
79
+ const id = stack.value[stack.value.length - 1]
80
+ if (id === undefined) return null
81
+ return state.elements.get(id)?.() ?? null
82
+ }
@@ -0,0 +1,16 @@
1
+ /*
2
+ * The bridge between reka PinInput's per-cell array and LpOtpInput's string
3
+ * model. reka empties a cell by deleting its slot (numeric mode) or setting it
4
+ * to "", so the array can have holes in the middle.
5
+ */
6
+ export type OtpCells = (string | number | undefined)[]
7
+
8
+ /** The code typed so far: the filled cells in order. */
9
+ export function joinCells(cells: OtpCells): string {
10
+ return Array.from(cells, (c) => (c === undefined ? "" : String(c))).join("")
11
+ }
12
+
13
+ /** Cells for a code given from outside, one character each. */
14
+ export function splitCode(code: string | undefined, length: number): string[] {
15
+ return (code ?? "").split("").slice(0, length)
16
+ }
@@ -0,0 +1,140 @@
1
+ /*
2
+ * The phone-number engine behind LpPhoneInput, split from the component so its
3
+ * rules are testable. libphonenumber-js (the `min` metadata build) is loaded
4
+ * on first use, the way the country table is: an app that never renders a
5
+ * phone field never downloads it, and until it arrives the field still takes
6
+ * digits, just without formatting or checks.
7
+ */
8
+ import type { CountryCode } from "libphonenumber-js/min"
9
+
10
+ export type PhoneLib = typeof import("libphonenumber-js/min")
11
+ type Examples = typeof import("libphonenumber-js/examples.mobile.json")["default"]
12
+
13
+ let lib: Promise<PhoneLib> | undefined
14
+ let examples: Promise<Examples> | undefined
15
+
16
+ export function loadPhoneLib(): Promise<PhoneLib> {
17
+ lib ??= import("libphonenumber-js/min")
18
+ return lib
19
+ }
20
+
21
+ export function loadPhoneExamples(): Promise<Examples> {
22
+ examples ??= import("libphonenumber-js/examples.mobile.json").then((m) => m.default)
23
+ return examples
24
+ }
25
+
26
+ /** E.164 allows at most 15 digits, country code included. */
27
+ export const E164_MAX_DIGITS = 15
28
+
29
+ export function digitsOf(text: string): string {
30
+ return text.replace(/\D/g, "")
31
+ }
32
+
33
+ /** How many digits sit before `caret` in `text`. */
34
+ export function digitsBefore(text: string, caret: number): number {
35
+ return digitsOf(text.slice(0, caret)).length
36
+ }
37
+
38
+ /**
39
+ * The caret position in `formatted` that has `count` digits before it, placed
40
+ * right after the last of them so typing continues from there. Zero digits
41
+ * puts it after a leading "+".
42
+ */
43
+ export function caretAfterDigits(formatted: string, count: number): number {
44
+ if (count <= 0) return formatted.startsWith("+") ? 1 : 0
45
+ let seen = 0
46
+ for (let i = 0; i < formatted.length; i++) {
47
+ if (/\d/.test(formatted[i]!)) {
48
+ seen++
49
+ if (seen === count) return i + 1
50
+ }
51
+ }
52
+ return formatted.length
53
+ }
54
+
55
+ export type PhoneProblem = "too-short" | "too-long" | "invalid" | "invalid-country"
56
+
57
+ export interface FormattedPhone {
58
+ /** What the field shows: "+380 50 123 4567". */
59
+ display: string
60
+ /** Normalised "+380501234567", or "" when there are no digits. */
61
+ e164: string
62
+ /** Digits that were typed past the longest number the country allows. */
63
+ dropped: number
64
+ /** The calling code the number starts with ("380"), once recognised. */
65
+ callingCode?: string
66
+ /** Country libphonenumber recognised from the number, when it did. */
67
+ country?: CountryCode
68
+ possible: boolean
69
+ valid: boolean
70
+ type?: string
71
+ problem?: PhoneProblem
72
+ }
73
+
74
+ /**
75
+ * Format what was typed, dropping digits beyond what the number may hold:
76
+ * the country's own maximum once libphonenumber knows the country, the E.164
77
+ * cap otherwise. The value always starts with "+", since the field holds a
78
+ * full international number.
79
+ */
80
+ export function formatPhone(input: string, phone: PhoneLib | undefined): FormattedPhone {
81
+ let digits = digitsOf(input)
82
+ let dropped = 0
83
+ if (digits.length > E164_MAX_DIGITS) {
84
+ dropped += digits.length - E164_MAX_DIGITS
85
+ digits = digits.slice(0, E164_MAX_DIGITS)
86
+ }
87
+ if (!digits) return { display: input.trim().startsWith("+") ? "+" : "", e164: "", dropped, possible: false, valid: false }
88
+ if (!phone) return { display: `+${digits}`, e164: `+${digits}`, dropped, possible: false, valid: false }
89
+
90
+ while (digits.length > 1 && phone.validatePhoneNumberLength(`+${digits}`) === "TOO_LONG") {
91
+ digits = digits.slice(0, -1)
92
+ dropped++
93
+ }
94
+
95
+ // A number that was already complete and valid does not take one more
96
+ // digit that would make it neither: a Ukrainian mobile ends at nine digits
97
+ // after +380 although the country's longest number type allows ten, and
98
+ // that tenth digit is almost always a slip rather than another kind of line.
99
+ const isValid = (d: string) => phone.parsePhoneNumberFromString(`+${d}`)?.isValid() ?? false
100
+ while (digits.length > 1 && !isValid(digits) && isValid(digits.slice(0, -1))) {
101
+ digits = digits.slice(0, -1)
102
+ dropped++
103
+ }
104
+
105
+ const typer = new phone.AsYouType()
106
+ let display = typer.input(`+${digits}`)
107
+ // A length the country allows but no format covers (a 10-digit Ukrainian
108
+ // national number) comes back as one run of digits; group it by threes after
109
+ // the calling code so it still reads as a phone number.
110
+ const code = typer.getCallingCode()
111
+ if (code && !/\s/.test(display.slice(code.length + 2))) {
112
+ const national = digits.slice(code.length)
113
+ display = `+${code}${national ? " " + (national.match(/.{1,3}/g) ?? []).join(" ") : ""}`
114
+ }
115
+ const parsed = phone.parsePhoneNumberFromString(`+${digits}`)
116
+ const country = typer.getCountry() ?? parsed?.country
117
+ const possible = parsed?.isPossible() ?? false
118
+ const valid = parsed?.isValid() ?? false
119
+ const length = phone.validatePhoneNumberLength(`+${digits}`)
120
+ const problem: PhoneProblem | undefined = valid
121
+ ? undefined
122
+ : length === "TOO_SHORT" || length === "NOT_A_NUMBER"
123
+ ? "too-short"
124
+ : length === "INVALID_COUNTRY"
125
+ ? "invalid-country"
126
+ : length === "TOO_LONG"
127
+ ? "too-long"
128
+ : "invalid"
129
+ return {
130
+ display,
131
+ e164: `+${digits}`,
132
+ dropped,
133
+ callingCode: code,
134
+ country,
135
+ possible,
136
+ valid,
137
+ type: valid ? parsed?.getType() : undefined,
138
+ problem,
139
+ }
140
+ }
@@ -10,8 +10,14 @@ export interface SidebarItem {
10
10
  id: string
11
11
  label: string
12
12
  icon?: string
13
- /** Render as a link instead of a button. */
13
+ /**
14
+ * Render as a link instead of a button. An app-relative path goes through
15
+ * the link component from provideLpConfig (NuxtLink under the Nuxt module),
16
+ * so it navigates client-side; other URLs render a plain anchor.
17
+ */
14
18
  href?: string
19
+ /** Router destination (rendered through the configured link component). */
20
+ to?: string | Record<string, unknown>
15
21
  /** Count chip on the right (number or short string). */
16
22
  badge?: number | string
17
23
  disabled?: boolean
@@ -0,0 +1,122 @@
1
+ /*
2
+ * The rules behind LpTable's value filters and remembered state, kept pure so
3
+ * they can be tested. A column's filter is the set of HIDDEN values (see
4
+ * LpTable): a value nobody has seen before shows up rather than staying out.
5
+ */
6
+
7
+ /** Hide everything except `keep`: "show only this" in one step. */
8
+ export function onlyValues(values: readonly unknown[], keep: readonly unknown[]): Set<unknown> {
9
+ const kept = new Set(keep)
10
+ return new Set(values.filter((v) => !kept.has(v)))
11
+ }
12
+
13
+ /** Hide every value: the "None" step before ticking the few that matter. */
14
+ export function hideAllValues(values: readonly unknown[]): Set<unknown> {
15
+ return new Set(values)
16
+ }
17
+
18
+ /** Swap shown and hidden among the values the column holds. */
19
+ export function invertHidden(values: readonly unknown[], hidden: ReadonlySet<unknown>): Set<unknown> {
20
+ return new Set(values.filter((v) => !hidden.has(v)))
21
+ }
22
+
23
+ /** Show these values again, leaving the rest of the filter as it was. */
24
+ export function unhideValues(hidden: ReadonlySet<unknown>, values: readonly unknown[]): Set<unknown> {
25
+ const next = new Set(hidden)
26
+ for (const v of values) next.delete(v)
27
+ return next
28
+ }
29
+
30
+ /** Lower-cased and stripped of accents, so "cafe" finds "Café". */
31
+ export function foldText(text: string): string {
32
+ return text.normalize("NFKD").replace(/\p{M}/gu, "").toLocaleLowerCase()
33
+ }
34
+
35
+ /** Every word of the query appears somewhere in the label, in any order. */
36
+ export function matchesQuery(label: string, query: string): boolean {
37
+ const words = foldText(query).split(/\s+/).filter(Boolean)
38
+ if (!words.length) return true
39
+ const hay = foldText(label)
40
+ return words.every((w) => hay.includes(w))
41
+ }
42
+
43
+ /** How many rows hold each value of a column. */
44
+ export function countValues<Row extends Record<string, unknown>>(
45
+ rows: readonly Row[],
46
+ key: string,
47
+ ): Map<unknown, number> {
48
+ const counts = new Map<unknown, number>()
49
+ for (const row of rows) counts.set(row[key], (counts.get(row[key]) ?? 0) + 1)
50
+ return counts
51
+ }
52
+
53
+ /**
54
+ * A chip's text for one column's filter: the values still shown, the first
55
+ * `listed` by name and the rest as "+N", which says more than "3 of 12".
56
+ */
57
+ export function summarizeShown(shownLabels: readonly string[], listed = 2): string {
58
+ if (shownLabels.length <= listed) return shownLabels.join(", ")
59
+ return `${shownLabels.slice(0, listed).join(", ")} +${shownLabels.length - listed}`
60
+ }
61
+
62
+ // ── remembered state ───────────────────────────────────────────
63
+
64
+ export interface PersistedTableState {
65
+ sort?: { key: string; dir: "asc" | "desc" } | null
66
+ hidden?: Record<string, unknown[]>
67
+ columns?: string[]
68
+ }
69
+
70
+ type Plain = string | number | boolean | null
71
+
72
+ function isPlain(value: unknown): value is Plain {
73
+ return value === null || ["string", "number", "boolean"].includes(typeof value)
74
+ }
75
+
76
+ /**
77
+ * The state worth keeping across visits, as JSON. Only plain values survive:
78
+ * a Date or an object hidden in a filter cannot come back as the same value
79
+ * (the filter compares by identity), so it is left out rather than restored
80
+ * as something that matches nothing.
81
+ */
82
+ export function serializeTableState(state: PersistedTableState): string {
83
+ const hidden: Record<string, Plain[]> = {}
84
+ for (const [key, values] of Object.entries(state.hidden ?? {})) {
85
+ const plain = values.filter(isPlain)
86
+ if (plain.length) hidden[key] = plain
87
+ }
88
+ return JSON.stringify({ sort: state.sort ?? null, hidden, columns: state.columns ?? [] })
89
+ }
90
+
91
+ /** Read it back, dropping anything about columns the table no longer has. */
92
+ export function parseTableState(raw: string | null, columnKeys: readonly string[]): PersistedTableState | null {
93
+ if (!raw) return null
94
+ let data: unknown
95
+ try {
96
+ data = JSON.parse(raw)
97
+ } catch {
98
+ return null
99
+ }
100
+ if (!data || typeof data !== "object") return null
101
+ const known = new Set(columnKeys)
102
+ const record = data as Record<string, unknown>
103
+ const out: PersistedTableState = {}
104
+
105
+ const sort = record.sort as { key?: unknown; dir?: unknown } | null | undefined
106
+ if (sort === null) out.sort = null
107
+ else if (sort && typeof sort.key === "string" && known.has(sort.key) && (sort.dir === "asc" || sort.dir === "desc")) {
108
+ out.sort = { key: sort.key, dir: sort.dir }
109
+ }
110
+
111
+ if (record.hidden && typeof record.hidden === "object") {
112
+ out.hidden = {}
113
+ for (const [key, values] of Object.entries(record.hidden as Record<string, unknown>)) {
114
+ if (known.has(key) && Array.isArray(values)) out.hidden[key] = values.filter(isPlain)
115
+ }
116
+ }
117
+
118
+ if (Array.isArray(record.columns)) {
119
+ out.columns = record.columns.filter((c): c is string => typeof c === "string" && known.has(c))
120
+ }
121
+ return out
122
+ }
@@ -0,0 +1,37 @@
1
+ const COLLATOR = new Intl.Collator(undefined, { numeric: true })
2
+
3
+ const rank = (v: unknown): number => {
4
+ if (v == null || (typeof v === "number" && Number.isNaN(v))) return 0
5
+ if (typeof v === "boolean") return 1
6
+ if (typeof v === "number" || typeof v === "bigint") return 2
7
+ if (v instanceof Date) return Number.isNaN(v.getTime()) ? 0 : 3
8
+ return 4
9
+ }
10
+
11
+ /**
12
+ * Ascending order for raw cell values. Empty values (null, undefined, NaN, an
13
+ * invalid Date) come first; numbers, bigints and Dates compare by magnitude,
14
+ * booleans false before true, everything else as text with numeric runs read as
15
+ * numbers ("file 2" before "file 10"). Mixed kinds group by kind so the order
16
+ * is total and a sort is stable from one run to the next.
17
+ */
18
+ export function compareValues(a: unknown, b: unknown): number {
19
+ const ra = rank(a)
20
+ const rb = rank(b)
21
+ if (ra !== rb) return ra - rb
22
+ switch (ra) {
23
+ case 0:
24
+ return 0
25
+ case 1:
26
+ return Number(a) - Number(b)
27
+ case 2: {
28
+ const x = a as number | bigint
29
+ const y = b as number | bigint
30
+ return x < y ? -1 : x > y ? 1 : 0
31
+ }
32
+ case 3:
33
+ return (a as Date).getTime() - (b as Date).getTime()
34
+ default:
35
+ return COLLATOR.compare(String(a), String(b))
36
+ }
37
+ }