@leavepulse/ui 0.44.0 → 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 (265) hide show
  1. package/README.md +49 -3
  2. package/dist/assets/{LpCommandPalette-DCP5g2ik.css → LpCommandPalette-nGNnzXqQ.css} +21 -21
  3. package/dist/assets/{LpRollingText-CR1mb0Vt.css → LpRollingText-BGnE6q9j.css} +3 -0
  4. package/dist/assets/{LpShortcutsDialog-ChUyU7oV.css → LpShortcutsDialog-CqYX3yy6.css} +28 -7
  5. package/dist/assets/{LpTable-DDQNdf86.css → LpTable-CPwWxTJS.css} +49 -9
  6. package/dist/chunks/{ContextMenuItems-CrfeNPWT.js → ContextMenuItems-BtKU7074.js} +2 -1
  7. package/dist/chunks/{LayoutCanvas-D-jPOAvg.js → LayoutCanvas-BkaGXTjx.js} +1 -1
  8. package/dist/chunks/{LayoutNode-D8Xy9N-o.js → LayoutNode-B3_9E1Xa.js} +9 -5
  9. package/dist/chunks/{LpActionBar-BbwCF6l5.js → LpActionBar-CnfFVIwC.js} +3 -3
  10. package/dist/chunks/{LpAddressInput-BrOqkZqg.js → LpAddressInput-BwX8uo4_.js} +7 -7
  11. package/dist/chunks/{LpAlert-DkOIjsbq.js → LpAlert-B6ilrM7T.js} +1 -1
  12. package/dist/chunks/{LpAppShell-BUKPcHco.js → LpAppShell-C53_RaGF.js} +4 -4
  13. package/dist/chunks/{LpAutocomplete-DxV3Y9h4.js → LpAutocomplete-C8IgSMsi.js} +2 -1
  14. package/dist/chunks/{LpAvatar-ClBbEYZN.js → LpAvatar-C1XStdOo.js} +3 -3
  15. package/dist/chunks/{LpBreadcrumbs-B8IJgFIc.js → LpBreadcrumbs-CQJd73Gm.js} +1 -1
  16. package/dist/chunks/{LpButton-kqJBHIIb.js → LpButton-CKSwC6Mu.js} +1 -1
  17. package/dist/chunks/{LpCalendar-B7Ko5RdR.js → LpCalendar-DrA2McTU.js} +1 -1
  18. package/dist/chunks/{LpCard-CpRByR0q.js → LpCard-C2IvucmX.js} +1 -1
  19. package/dist/chunks/{LpCheckbox-DpqchzH4.js → LpCheckbox-Bkj13CbW.js} +1 -1
  20. package/dist/chunks/{LpCodeBlock-DeHJPq7p.js → LpCodeBlock-ca7ANFyW.js} +1 -1
  21. package/dist/chunks/{LpColorPicker-Bhf1qxhB.js → LpColorPicker-zz4VGkG4.js} +46 -20
  22. package/dist/chunks/{LpConfirmDialog-CIUOSnWo.js → LpConfirmDialog-DCw5jR1t.js} +3 -3
  23. package/dist/chunks/{LpConfirmHost-CDm0Wtpv.js → LpConfirmHost-CbywMpMP.js} +1 -1
  24. package/dist/chunks/{LpContextMenu-DQw5dGfG.js → LpContextMenu-BqkAYa9X.js} +2 -1
  25. package/dist/chunks/{LpDatePicker-Drirx1AK.js → LpDatePicker-DuusgT69.js} +13 -9
  26. package/dist/chunks/LpDateRangePicker-DHeuznfC.js +208 -0
  27. package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
  28. package/dist/chunks/{LpDrawer-bScx-KMO.js → LpDrawer-Dt4aB4Fr.js} +2 -2
  29. package/dist/chunks/{LpDropdownMenu-DJpoqRCb.js → LpDropdownMenu-DdJGQzPn.js} +2 -1
  30. package/dist/chunks/{LpEmptyState-Dsdqz1Wj.js → LpEmptyState-BJbZXC8D.js} +1 -1
  31. package/dist/chunks/LpFieldNotice-wtieLUTj.js +64 -0
  32. package/dist/chunks/{LpFileTree-CSI_5SK2.js → LpFileTree-CNurOvkV.js} +3 -3
  33. package/dist/chunks/{LpFileTreeNode-B4MiSQX2.js → LpFileTreeNode-D_qRRdsH.js} +3 -3
  34. package/dist/chunks/{LpFormField-CIk5qosh.js → LpFormField-CtVYj6tO.js} +29 -15
  35. package/dist/chunks/{LpIcon-CCnX5_2j.js → LpIcon-DYtaid0K.js} +20 -6
  36. package/dist/chunks/{LpInput-C0e8y8_d.js → LpInput-D-YGQbEv.js} +39 -16
  37. package/dist/chunks/{LpLightbox-Mr0W_6HK.js → LpLightbox-DSDahF23.js} +1 -1
  38. package/dist/chunks/{LpLogViewer-YBOaFnc_.js → LpLogViewer-KypbgOyo.js} +252 -58
  39. package/dist/chunks/{LpMapPicker-NUzS9e37.js → LpMapPicker-Cc84LaM3.js} +1 -1
  40. package/dist/chunks/{LpMediaGallery-DfJmMW0I.js → LpMediaGallery-YKr-cQPN.js} +2 -2
  41. package/dist/chunks/{LpModal-DPwGKBPv.js → LpModal-DMMDQHAu.js} +3 -3
  42. package/dist/chunks/{LpModalNav-DPg5H9f3.js → LpModalNav-BVPDyIup.js} +4 -4
  43. package/dist/chunks/{LpNotificationBell-t5yn-siX.js → LpNotificationBell-CCwyJP5C.js} +26 -14
  44. package/dist/chunks/{LpNumberField-DWnQKktS.js → LpNumberField-B77O-z28.js} +1 -1
  45. package/dist/chunks/{LpPagination-DFngX7Xo.js → LpPagination-BcDlo3r_.js} +1 -1
  46. package/dist/chunks/{LpPasswordInput-CWVDuBJ5.js → LpPasswordInput-eKzQhbQP.js} +2 -2
  47. package/dist/chunks/{LpPhoneInput-CD-ZTeSd.js → LpPhoneInput-BCbrrhgE.js} +126 -23
  48. package/dist/chunks/{LpPie-BLco31Nx.js → LpPie-BEJ2SfI6.js} +9 -10
  49. package/dist/chunks/{LpPopover-p_e_shN3.js → LpPopover-DCKeqiVn.js} +1 -0
  50. package/dist/chunks/LpRangeCalendar-DZuaRV0W.js +331 -0
  51. package/dist/chunks/{LpRollingText-CM5gkSeF.js → LpRollingText-DB7upf-5.js} +1 -1
  52. package/dist/chunks/{LpScrollArea-D8xyKXcF.js → LpScrollArea-Bry1fdOM.js} +14 -2
  53. package/dist/chunks/{LpSection-aM6J_u51.js → LpSection-tKEH0jYf.js} +1 -1
  54. package/dist/chunks/{LpSegmented-Dejd7cIx.js → LpSegmented-C09EEam2.js} +1 -1
  55. package/dist/chunks/{LpSelect-DrRD9I7L.js → LpSelect-DNIL6MB9.js} +20 -23
  56. package/dist/chunks/{LpSidebar-BgwUNWpJ.js → LpSidebar-CQNB0-yU.js} +3 -3
  57. package/dist/chunks/{LpSidebarNav-Bqxz8aWT.js → LpSidebarNav-kx-LdqNU.js} +3 -3
  58. package/dist/chunks/{LpStat-dXtyrdX9.js → LpStat-B-7udBRP.js} +1 -1
  59. package/dist/chunks/{LpStepper-Bk-XaQYU.js → LpStepper-C7XiVKZj.js} +34 -19
  60. package/dist/chunks/{LpSwitch-B1aLKulj.js → LpSwitch-OrcOJX9m.js} +1 -1
  61. package/dist/chunks/LpTableFilter-BV8PpLM9.js +158 -0
  62. package/dist/chunks/{LpTabs-_qMVaZJ6.js → LpTabs-29iW1XOx.js} +1 -1
  63. package/dist/chunks/LpTagsInput-D1SuqbhB.js +300 -0
  64. package/dist/chunks/{LpTextarea-yP8hGedR.js → LpTextarea-uuScWo6-.js} +1 -1
  65. package/dist/chunks/{LpThemeProvider-Wjm7Tuit.js → LpThemeProvider-Be4r2TQX.js} +1 -1
  66. package/dist/chunks/{LpThemeSwitcher-CKLc10KZ.js → LpThemeSwitcher-B4aVPbNC.js} +19 -22
  67. package/dist/chunks/{LpTimeField-C5HRgS5H.js → LpTimeField-Cqe7fXnh.js} +1 -1
  68. package/dist/chunks/{LpToaster--oT6lfhL.js → LpToaster-DpJvoCOh.js} +37 -3
  69. package/dist/chunks/{LpTooltip-CBdzeKNf.js → LpTooltip-CXVKcOXT.js} +3 -2
  70. package/dist/chunks/{LpTopologyCanvas-DocsLal3.js → LpTopologyCanvas-6ozNrQKh.js} +2 -1
  71. package/dist/chunks/{LpUptimeBar-DLtxOMRy.js → LpUptimeBar-ugcv25xj.js} +2 -2
  72. package/dist/chunks/icons-CkA0njzm.js +43 -0
  73. package/dist/chunks/{useTheme-C8JHUlPM.js → useTheme-DJm1JSsY.js} +47 -50
  74. package/dist/component-names.d.ts +1 -1
  75. package/dist/components/ContextMenuItems.vue.js +1 -1
  76. package/dist/components/LayoutCanvas.vue.js +1 -1
  77. package/dist/components/LayoutNode.vue.js +1 -1
  78. package/dist/components/LpActionBar.vue.js +1 -1
  79. package/dist/components/LpAddressInput.vue.js +1 -1
  80. package/dist/components/LpAlert.vue.js +1 -1
  81. package/dist/components/LpAppShell.vue.js +1 -1
  82. package/dist/components/LpAutocomplete.vue.js +1 -1
  83. package/dist/components/LpAvatar.vue.js +1 -1
  84. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  85. package/dist/components/LpButton.vue.d.ts +2 -2
  86. package/dist/components/LpButton.vue.js +1 -1
  87. package/dist/components/LpCalendar.vue.js +1 -1
  88. package/dist/components/LpCard.vue.js +1 -1
  89. package/dist/components/LpChart.vue.d.ts +16 -1
  90. package/dist/components/LpChart.vue.js +63 -5
  91. package/dist/components/LpCheckbox.vue.js +1 -1
  92. package/dist/components/LpCodeBlock.vue.js +1 -1
  93. package/dist/components/LpColorPicker.vue.d.ts +2 -2
  94. package/dist/components/LpColorPicker.vue.js +1 -1
  95. package/dist/components/LpCommandPalette.vue.d.ts +6 -0
  96. package/dist/components/LpCommandPalette.vue.js +61 -10
  97. package/dist/components/LpConfirmDialog.vue.js +1 -1
  98. package/dist/components/LpConfirmHost.vue.js +1 -1
  99. package/dist/components/LpContextMenu.vue.js +1 -1
  100. package/dist/components/LpDatePicker.vue.js +1 -1
  101. package/dist/components/LpDateRangePicker.vue.d.ts +60 -0
  102. package/dist/components/LpDateRangePicker.vue.js +4 -0
  103. package/dist/components/LpDisclosure.vue.js +1 -1
  104. package/dist/components/LpDrawer.vue.js +1 -1
  105. package/dist/components/LpDropdownMenu.vue.js +1 -1
  106. package/dist/components/LpDropzone.vue.js +1 -1
  107. package/dist/components/LpEmptyState.vue.js +1 -1
  108. package/dist/components/LpFieldNotice.vue.d.ts +8 -0
  109. package/dist/components/LpFieldNotice.vue.js +4 -0
  110. package/dist/components/LpFileTree.vue.js +1 -1
  111. package/dist/components/LpFileTreeNode.vue.js +1 -1
  112. package/dist/components/LpFormField.vue.d.ts +4 -1
  113. package/dist/components/LpFormField.vue.js +1 -1
  114. package/dist/components/LpIcon.vue.d.ts +0 -1
  115. package/dist/components/LpIcon.vue.js +1 -1
  116. package/dist/components/LpImageEditor.vue.js +2 -2
  117. package/dist/components/LpInput.vue.d.ts +19 -0
  118. package/dist/components/LpInput.vue.js +1 -1
  119. package/dist/components/LpLightbox.vue.js +1 -1
  120. package/dist/components/LpLogViewer.vue.d.ts +52 -1
  121. package/dist/components/LpLogViewer.vue.js +1 -1
  122. package/dist/components/LpMapPicker.vue.js +1 -1
  123. package/dist/components/LpMediaGallery.vue.js +1 -1
  124. package/dist/components/LpModal.vue.js +1 -1
  125. package/dist/components/LpModalNav.vue.js +1 -1
  126. package/dist/components/LpNotificationBell.vue.js +1 -1
  127. package/dist/components/LpNumberField.vue.js +1 -1
  128. package/dist/components/LpPagination.vue.js +1 -1
  129. package/dist/components/LpPasswordInput.vue.js +1 -1
  130. package/dist/components/LpPhoneInput.vue.d.ts +13 -0
  131. package/dist/components/LpPhoneInput.vue.js +1 -1
  132. package/dist/components/LpPie.vue.js +1 -1
  133. package/dist/components/LpPopover.vue.js +1 -1
  134. package/dist/components/LpProgress.vue.d.ts +1 -1
  135. package/dist/components/LpRangeCalendar.vue.d.ts +51 -0
  136. package/dist/components/LpRangeCalendar.vue.js +4 -0
  137. package/dist/components/LpRollingText.vue.d.ts +1 -1
  138. package/dist/components/LpRollingText.vue.js +1 -1
  139. package/dist/components/LpScrollArea.vue.d.ts +2 -2
  140. package/dist/components/LpScrollArea.vue.js +1 -1
  141. package/dist/components/LpSection.vue.js +1 -1
  142. package/dist/components/LpSegmented.vue.js +1 -1
  143. package/dist/components/LpSelect.vue.js +1 -1
  144. package/dist/components/LpShortcutsDialog.vue.d.ts +6 -0
  145. package/dist/components/LpShortcutsDialog.vue.js +56 -14
  146. package/dist/components/LpSidebar.vue.js +1 -1
  147. package/dist/components/LpSidebarNav.vue.js +1 -1
  148. package/dist/components/LpStat.vue.js +1 -1
  149. package/dist/components/LpStepper.vue.js +1 -1
  150. package/dist/components/LpSwitch.vue.js +1 -1
  151. package/dist/components/LpTable.vue.d.ts +84 -2
  152. package/dist/components/LpTable.vue.js +481 -107
  153. package/dist/components/LpTableFilter.vue.d.ts +31 -0
  154. package/dist/components/LpTableFilter.vue.js +4 -0
  155. package/dist/components/LpTabs.vue.js +1 -1
  156. package/dist/components/LpTagsInput.vue.d.ts +77 -0
  157. package/dist/components/LpTagsInput.vue.js +4 -0
  158. package/dist/components/LpTextarea.vue.js +1 -1
  159. package/dist/components/LpThemeProvider.vue.js +1 -1
  160. package/dist/components/LpThemeSwitcher.vue.d.ts +2 -2
  161. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  162. package/dist/components/LpTimeField.vue.js +1 -1
  163. package/dist/components/LpToaster.vue.js +1 -1
  164. package/dist/components/LpTooltip.vue.js +1 -1
  165. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  166. package/dist/components/LpUptimeBar.vue.js +1 -1
  167. package/dist/components/chart.js +3 -2
  168. package/dist/components/commandSearch.js +3 -0
  169. package/dist/components/dateRange.d.ts +43 -0
  170. package/dist/components/dateRange.js +76 -0
  171. package/dist/components/exportData.d.ts +58 -0
  172. package/dist/components/exportData.js +165 -0
  173. package/dist/components/logSearch.d.ts +18 -0
  174. package/dist/components/logSearch.js +36 -0
  175. package/dist/components/phone.d.ts +41 -0
  176. package/dist/components/phone.js +80 -0
  177. package/dist/components/tableFilter.d.ts +36 -0
  178. package/dist/components/tableFilter.js +84 -0
  179. package/dist/components/tags.d.ts +20 -0
  180. package/dist/components/tags.js +37 -0
  181. package/dist/composables/useField.d.ts +4 -0
  182. package/dist/composables/useFieldNotice.d.ts +17 -0
  183. package/dist/composables/useFieldNotice.js +28 -0
  184. package/dist/composables/useForm.d.ts +53 -0
  185. package/dist/composables/useForm.js +207 -0
  186. package/dist/composables/useHotkeys.d.ts +65 -5
  187. package/dist/composables/useHotkeys.js +170 -9
  188. package/dist/composables/useInputMask.d.ts +59 -0
  189. package/dist/composables/useInputMask.js +170 -0
  190. package/dist/composables/usePaintSelect.d.ts +22 -0
  191. package/dist/composables/usePaintSelect.js +169 -0
  192. package/dist/composables/useShift.js +7 -2
  193. package/dist/icons-all.d.ts +1 -0
  194. package/dist/icons-all.js +3 -0
  195. package/dist/icons.d.ts +14 -1
  196. package/dist/index.d.ts +29 -3
  197. package/dist/index.js +195 -138
  198. package/dist/kit-icons.d.ts +2 -0
  199. package/dist/theme/backdrop.d.ts +12 -0
  200. package/dist/theme/presets.d.ts +0 -2
  201. package/package.json +13 -4
  202. package/src/component-names.ts +4 -0
  203. package/src/components/ContextMenuItems.vue +1 -1
  204. package/src/components/LayoutNode.vue +11 -2
  205. package/src/components/LpActionBar.vue +1 -1
  206. package/src/components/LpAutocomplete.vue +1 -0
  207. package/src/components/LpAvatar.vue +2 -2
  208. package/src/components/LpButton.vue +1 -1
  209. package/src/components/LpChart.vue +80 -1
  210. package/src/components/LpColorPicker.vue +34 -1
  211. package/src/components/LpCommandPalette.vue +81 -7
  212. package/src/components/LpContextMenu.vue +1 -1
  213. package/src/components/LpDatePicker.vue +8 -3
  214. package/src/components/LpDateRangePicker.vue +224 -0
  215. package/src/components/LpDropdownMenu.vue +1 -0
  216. package/src/components/LpFieldNotice.vue +65 -0
  217. package/src/components/LpFormField.vue +33 -4
  218. package/src/components/LpIcon.vue +22 -4
  219. package/src/components/LpInput.vue +46 -6
  220. package/src/components/LpLogViewer.vue +225 -22
  221. package/src/components/LpModal.vue +1 -1
  222. package/src/components/LpNotificationBell.vue +15 -7
  223. package/src/components/LpPhoneInput.vue +179 -20
  224. package/src/components/LpPie.vue +8 -5
  225. package/src/components/LpPopover.vue +1 -0
  226. package/src/components/LpRangeCalendar.vue +365 -0
  227. package/src/components/LpRollingText.vue +3 -0
  228. package/src/components/LpScrollArea.vue +22 -2
  229. package/src/components/LpSelect.vue +0 -0
  230. package/src/components/LpShortcutsDialog.vue +77 -8
  231. package/src/components/LpStepper.vue +23 -10
  232. package/src/components/LpSwitch.vue +1 -1
  233. package/src/components/LpTable.vue +584 -65
  234. package/src/components/LpTableFilter.vue +175 -0
  235. package/src/components/LpTagsInput.vue +364 -0
  236. package/src/components/LpThemeSwitcher.vue +19 -23
  237. package/src/components/LpToaster.vue +49 -1
  238. package/src/components/LpTooltip.vue +9 -2
  239. package/src/components/LpTopologyCanvas.vue +7 -1
  240. package/src/components/chart.ts +5 -3
  241. package/src/components/commandSearch.ts +6 -0
  242. package/src/components/dateRange.ts +134 -0
  243. package/src/components/exportData.ts +228 -0
  244. package/src/components/logSearch.ts +46 -0
  245. package/src/components/phone.ts +140 -0
  246. package/src/components/tableFilter.ts +122 -0
  247. package/src/components/tags.ts +62 -0
  248. package/src/composables/useField.ts +4 -0
  249. package/src/composables/useFieldNotice.ts +59 -0
  250. package/src/composables/useForm.ts +314 -0
  251. package/src/composables/useHotkeys.ts +269 -13
  252. package/src/composables/useInputMask.ts +254 -0
  253. package/src/composables/usePaintSelect.ts +243 -0
  254. package/src/composables/useShift.ts +13 -2
  255. package/src/iconSubset.d.mts +10 -0
  256. package/src/iconSubset.mjs +51 -0
  257. package/src/icons-all.ts +10 -0
  258. package/src/icons.ts +59 -8
  259. package/src/index.ts +37 -3
  260. package/src/kit-icons.ts +5 -0
  261. package/src/nuxt.ts +30 -2
  262. package/src/runtime/theme.plugin.ts +2 -2
  263. package/src/theme/backdrop.ts +69 -0
  264. package/src/theme/presets.ts +6 -64
  265. package/src/tokens/tokens.css +46 -0
@@ -35,7 +35,7 @@ import {
35
35
  TooltipRoot,
36
36
  TooltipTrigger,
37
37
  } from "reka-ui"
38
- import { Fragment, onBeforeUnmount, ref, watch } from "vue"
38
+ import { onBeforeUnmount, ref, watch, type FunctionalComponent } from "vue"
39
39
  import {
40
40
  springSettled,
41
41
  springStep,
@@ -108,6 +108,13 @@ const props = withDefaults(
108
108
  * shared one (LpTooltipProvider at the root) the next tip opens at once.
109
109
  */
110
110
  const shared = injectTooltipProviderContext(null)
111
+ /*
112
+ * Under a shared provider the tip renders straight into it. `Fragment` cannot
113
+ * stand in for the wrapper: given through `<component :is>` it takes slots,
114
+ * not children, and rendered nothing at all, so every tip inside an
115
+ * LpTooltipProvider vanished together with its trigger.
116
+ */
117
+ const PassThrough: FunctionalComponent = (_, { slots }) => slots.default?.()
111
118
 
112
119
  const reducedMotion = () =>
113
120
  typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
@@ -221,7 +228,7 @@ onBeforeUnmount(() => {
221
228
  </script>
222
229
 
223
230
  <template>
224
- <component :is="shared ? Fragment : TooltipProvider" v-bind="shared ? {} : { delayDuration: delay ?? 200 }">
231
+ <component :is="shared ? PassThrough : TooltipProvider" v-bind="shared ? {} : { delayDuration: delay ?? 200 }">
225
232
  <!--
226
233
  `disable-closing-trigger` in follow mode: reka dismisses a tip when its
227
234
  trigger is clicked, which is right for a button (the tip explained the
@@ -417,8 +417,12 @@ defineExpose({ fitView, setViewport: applyViewport })
417
417
  class="lp-topology"
418
418
  >
419
419
  <Background :gap="22" :size="1.4" pattern-color="var(--color-line-strong)" />
420
+ <!-- On a phone the minimap, the zoom controls and the legend would all
421
+ share the bottom of a canvas a few hundred pixels wide and cover each
422
+ other and the graph; pinch-zoom and panning do the minimap's job there. -->
420
423
  <MiniMap
421
424
  v-if="minimap"
425
+ class="max-sm:!hidden"
422
426
  pannable
423
427
  zoomable
424
428
  node-color="var(--color-surface-soft)"
@@ -427,7 +431,9 @@ defineExpose({ fitView, setViewport: applyViewport })
427
431
  <Controls v-if="controls" />
428
432
 
429
433
  <!-- Edge-type legend: makes the colour/dash encoding self-explanatory. -->
430
- <Panel v-if="legend" :position="legendPosition" :class="legendOffset">
434
+ <!-- On a phone the legend would sit on the graph itself; the edge styles
435
+ stay readable without it there. -->
436
+ <Panel v-if="legend" :position="legendPosition" :class="legendOffset" class="max-sm:hidden">
431
437
  <div
432
438
  class="flex flex-col gap-1.5 rounded-card border border-line bg-surface-overlay/90 px-3 py-2 text-[11px] backdrop-blur"
433
439
  >
@@ -157,10 +157,12 @@ export const niceScale = (
157
157
  ): { min: number; max: number; step: number } => {
158
158
  if (!Number.isFinite(min) || !Number.isFinite(max)) return { min: 0, max: 1, step: 1 }
159
159
  if (min === max) {
160
- // A flat series still needs a band to sit in.
160
+ // A flat series still needs a band to sit in, but the band must not cross
161
+ // zero: an all-zero count would otherwise read as dipping to -0.5.
161
162
  const pad = Math.abs(min) || 1
162
- min -= pad / 2
163
- max += pad / 2
163
+ min = min >= 0 ? Math.max(0, min - pad / 2) : min - pad / 2
164
+ max = max <= 0 ? Math.min(0, max + pad / 2) : max + pad / 2
165
+ if (min === max) max = min + pad
164
166
  }
165
167
  const raw = (max - min) / Math.max(1, ticks)
166
168
  const mag = 10 ** Math.floor(Math.log10(raw))
@@ -63,6 +63,12 @@ function score(command: SearchableCommand, terms: CommandSearchTerms): number {
63
63
  if (!hit) return 0
64
64
  total += hit
65
65
  }
66
+ // A label that IS what was typed, or starts with it, is what the person is
67
+ // naming: "theme" must put the Theme command above ThemeSwitcher, which only
68
+ // contains the word but also matches its keywords and description.
69
+ const typed = terms.include.join(" ")
70
+ if (typed && label === typed) total += 6
71
+ else if (typed && label.startsWith(typed)) total += 2
66
72
  return total
67
73
  }
68
74
 
@@ -0,0 +1,134 @@
1
+ import {
2
+ type CalendarDate,
3
+ endOfMonth,
4
+ getLocalTimeZone,
5
+ parseDate,
6
+ startOfMonth,
7
+ today,
8
+ } from "@internationalized/date"
9
+
10
+ /*
11
+ * Date ranges as the kit models them: two ISO "YYYY-MM-DD" strings, inclusive
12
+ * on both ends, the same shape LpDatePicker uses for a single day. Presets are
13
+ * plain functions so a consumer can add "This quarter" or "Year to date"
14
+ * without the kit knowing about it.
15
+ */
16
+ export interface DateRange {
17
+ start: string
18
+ end: string
19
+ }
20
+
21
+ export interface DateRangePreset {
22
+ label: string
23
+ range: () => DateRange
24
+ }
25
+
26
+ export interface DateRangePresetLabels {
27
+ today: string
28
+ last7: string
29
+ last30: string
30
+ thisMonth: string
31
+ lastMonth: string
32
+ }
33
+
34
+ function span(start: CalendarDate, end: CalendarDate): DateRange {
35
+ return { start: start.toString(), end: end.toString() }
36
+ }
37
+
38
+ /** The default preset list. `now` is injectable so the set can be tested. */
39
+ export function defaultRangePresets(
40
+ labels: DateRangePresetLabels,
41
+ now: () => CalendarDate = () => today(getLocalTimeZone()),
42
+ ): DateRangePreset[] {
43
+ return [
44
+ { label: labels.today, range: () => span(now(), now()) },
45
+ { label: labels.last7, range: () => span(now().subtract({ days: 6 }), now()) },
46
+ { label: labels.last30, range: () => span(now().subtract({ days: 29 }), now()) },
47
+ { label: labels.thisMonth, range: () => span(startOfMonth(now()), endOfMonth(now())) },
48
+ {
49
+ label: labels.lastMonth,
50
+ range: () => {
51
+ const previous = now().subtract({ months: 1 })
52
+ return span(startOfMonth(previous), endOfMonth(previous))
53
+ },
54
+ },
55
+ ]
56
+ }
57
+
58
+ export function parseIsoDay(iso: string | undefined): CalendarDate | undefined {
59
+ if (!iso) return undefined
60
+ try {
61
+ return parseDate(iso.slice(0, 10))
62
+ } catch {
63
+ return undefined
64
+ }
65
+ }
66
+
67
+ /** A range with both ends parsed and in order, or undefined. */
68
+ export function normalizeRange(range: Partial<DateRange> | undefined): DateRange | undefined {
69
+ const start = parseIsoDay(range?.start)
70
+ const end = parseIsoDay(range?.end)
71
+ if (!start || !end) return undefined
72
+ return start.compare(end) <= 0 ? span(start, end) : span(end, start)
73
+ }
74
+
75
+ export function sameRange(a: Partial<DateRange> | undefined, b: Partial<DateRange> | undefined): boolean {
76
+ const x = normalizeRange(a)
77
+ const y = normalizeRange(b)
78
+ return !!x && !!y && x.start === y.start && x.end === y.end
79
+ }
80
+
81
+ /** Clip a preset to the allowed bounds; undefined when nothing of it is left. */
82
+ export function clampRange(range: DateRange, min?: string, max?: string): DateRange | undefined {
83
+ const r = normalizeRange(range)
84
+ if (!r) return undefined
85
+ const start = min && r.start < min ? min : r.start
86
+ const end = max && r.end > max ? max : r.end
87
+ return start <= end ? { start, end } : undefined
88
+ }
89
+
90
+ /**
91
+ * "Jun 3 – 18, 2026" style text: Intl's own range formatting collapses the
92
+ * shared month and year, which is how a range reads naturally in every locale.
93
+ */
94
+ export function formatRange(
95
+ range: DateRange,
96
+ locale?: string,
97
+ format: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric" },
98
+ ): string {
99
+ const start = parseIsoDay(range.start)
100
+ const end = parseIsoDay(range.end)
101
+ if (!start || !end) return ""
102
+ const zone = "UTC"
103
+ const fmt = new Intl.DateTimeFormat(locale, { ...format, timeZone: zone })
104
+ return fmt.formatRange(start.toDate(zone), end.toDate(zone))
105
+ }
106
+
107
+ export interface ReshapeRules {
108
+ /** Whether the moving end may land on this day (min, max, disabled). */
109
+ allowed?: (iso: string) => boolean
110
+ /** Days a range may not include. */
111
+ unavailable?: (iso: string) => boolean
112
+ /** Longest range, in days, counting both ends. */
113
+ maximumDays?: number
114
+ }
115
+
116
+ /**
117
+ * The range one end makes when it is dragged to `moving` while the other stays
118
+ * at `anchor`; passing the anchor flips which end is which. Null when the result
119
+ * would break a rule, so a drag simply does not follow onto that day.
120
+ */
121
+ export function reshapeRange(anchor: string, moving: string, rules: ReshapeRules = {}): DateRange | null {
122
+ const a = parseIsoDay(anchor)
123
+ const m = parseIsoDay(moving)
124
+ if (!a || !m) return null
125
+ if (rules.allowed && !rules.allowed(m.toString())) return null
126
+ const [start, end] = a.compare(m) <= 0 ? [a, m] : [m, a]
127
+ if (rules.maximumDays && end.compare(start) + 1 > rules.maximumDays) return null
128
+ if (rules.unavailable) {
129
+ for (let d = start; d.compare(end) <= 0; d = d.add({ days: 1 })) {
130
+ if (rules.unavailable(d.toString())) return null
131
+ }
132
+ }
133
+ return { start: start.toString(), end: end.toString() }
134
+ }
@@ -0,0 +1,228 @@
1
+ /*
2
+ * Data export shared by the table and the chart: CSV text, a file download,
3
+ * and an SVG rasterised with its theme resolved.
4
+ */
5
+
6
+ export interface CsvColumn<Row> {
7
+ key: string
8
+ /** Header cell. Defaults to the key. */
9
+ label?: string
10
+ /** The value written for a row; the row's `key` field when omitted. */
11
+ value?: (row: Row) => unknown
12
+ }
13
+
14
+ export interface CsvOptions {
15
+ /** Field separator. Default ",". Some locales' spreadsheets expect ";". */
16
+ separator?: string
17
+ /** Prefix a UTF-8 byte order mark so Excel reads non-ASCII text correctly. */
18
+ bom?: boolean
19
+ /**
20
+ * Neutralise cells a spreadsheet would run as a formula (leading = + - @,
21
+ * tab or CR) by prefixing a quote. On by default: exported data is often
22
+ * user-supplied, and opening it is enough to trigger a formula.
23
+ */
24
+ guardFormulas?: boolean
25
+ /** Line ending. Default "\r\n", as RFC 4180 specifies. */
26
+ newline?: string
27
+ }
28
+
29
+ const FORMULA_START = /^[=+\-@\t\r]/
30
+
31
+ function cellText(value: unknown): string {
32
+ if (value == null) return ""
33
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toISOString()
34
+ if (typeof value === "object") return JSON.stringify(value)
35
+ return String(value)
36
+ }
37
+
38
+ /** One CSV field, quoted when it holds a separator, a quote or a line break. */
39
+ export function csvField(value: unknown, options: CsvOptions = {}): string {
40
+ const separator = options.separator ?? ","
41
+ let text = cellText(value)
42
+ if ((options.guardFormulas ?? true) && typeof value === "string" && FORMULA_START.test(text)) {
43
+ text = `'${text}`
44
+ }
45
+ const needsQuotes = text.includes(separator) || /["\r\n]/.test(text) || /^\s|\s$/.test(text)
46
+ return needsQuotes ? `"${text.replace(/"/g, '""')}"` : text
47
+ }
48
+
49
+ /** Rows as CSV text, a header line first. */
50
+ export function toCsv<Row>(rows: readonly Row[], columns: readonly CsvColumn<Row>[], options: CsvOptions = {}): string {
51
+ const separator = options.separator ?? ","
52
+ const newline = options.newline ?? "\r\n"
53
+ const line = (cells: unknown[]) => cells.map((c) => csvField(c, options)).join(separator)
54
+ const out = [line(columns.map((c) => c.label ?? c.key))]
55
+ for (const row of rows) {
56
+ out.push(
57
+ line(columns.map((c) => (c.value ? c.value(row) : (row as Record<string, unknown>)[c.key]))),
58
+ )
59
+ }
60
+ return (options.bom ? "" : "") + out.join(newline) + newline
61
+ }
62
+
63
+ /**
64
+ * Hand the viewer a file to save. Does nothing outside a browser, so a call
65
+ * that runs during SSR is harmless.
66
+ */
67
+ export function downloadFile(name: string, data: Blob | string, type = "text/plain;charset=utf-8"): void {
68
+ if (typeof document === "undefined" || typeof URL === "undefined") return
69
+ const blob = typeof data === "string" ? new Blob([data], { type }) : data
70
+ const url = URL.createObjectURL(blob)
71
+ const a = document.createElement("a")
72
+ a.href = url
73
+ a.download = name
74
+ a.rel = "noopener"
75
+ a.style.display = "none"
76
+ document.body.append(a)
77
+ a.click()
78
+ a.remove()
79
+ setTimeout(() => URL.revokeObjectURL(url), 0)
80
+ }
81
+
82
+ const PAINT_PROPS = [
83
+ "fill",
84
+ "fill-opacity",
85
+ "fill-rule",
86
+ "stroke",
87
+ "stroke-opacity",
88
+ "stroke-width",
89
+ "stroke-dasharray",
90
+ "stroke-dashoffset",
91
+ "stroke-linecap",
92
+ "stroke-linejoin",
93
+ "opacity",
94
+ "stop-color",
95
+ "stop-opacity",
96
+ "color",
97
+ "font-family",
98
+ "font-size",
99
+ "font-weight",
100
+ "letter-spacing",
101
+ "text-anchor",
102
+ "dominant-baseline",
103
+ "visibility",
104
+ "display",
105
+ ] as const
106
+
107
+ /**
108
+ * A standalone copy of a live SVG: every element carries its computed paint as
109
+ * an inline style, so CSS custom properties and classes from the page's
110
+ * stylesheet survive being rendered as an image, and running SMIL animations
111
+ * are frozen at their end state.
112
+ */
113
+ export function inlineSvg(svg: SVGSVGElement): SVGSVGElement {
114
+ const clone = svg.cloneNode(true) as SVGSVGElement
115
+ const source = [svg, ...svg.querySelectorAll("*")]
116
+ const target = [clone, ...clone.querySelectorAll("*")]
117
+ source.forEach((el, i) => {
118
+ const copy = target[i]
119
+ if (!(copy instanceof Element)) return
120
+ const computed = getComputedStyle(el)
121
+ const style = PAINT_PROPS.map((p) => `${p}:${computed.getPropertyValue(p)}`)
122
+ .filter((d) => !d.endsWith(":"))
123
+ .join(";")
124
+ copy.setAttribute("style", style)
125
+ copy.removeAttribute("class")
126
+ })
127
+ // Comment nodes (Vue leaves them in development builds) may hold "--", which
128
+ // is invalid inside an XML comment and makes the whole image fail to load.
129
+ const comments = document.createTreeWalker(clone, NodeFilter.SHOW_COMMENT)
130
+ const dead: Node[] = []
131
+ while (comments.nextNode()) dead.push(comments.currentNode)
132
+ for (const node of dead) node.parentNode?.removeChild(node)
133
+ for (const anim of clone.querySelectorAll("animate")) {
134
+ const attr = anim.getAttribute("attributeName")
135
+ const to = anim.getAttribute("to")
136
+ if (attr && to != null) anim.parentElement?.setAttribute(attr, to)
137
+ anim.remove()
138
+ }
139
+ clone.setAttribute("xmlns", "http://www.w3.org/2000/svg")
140
+ const { width, height } = svg.getBoundingClientRect()
141
+ clone.setAttribute("width", String(width))
142
+ clone.setAttribute("height", String(height))
143
+ clone.style.overflow = "visible"
144
+ return clone
145
+ }
146
+
147
+ export interface RasterOptions {
148
+ /** Device pixels per CSS pixel. Default: the screen's ratio, at least 2. */
149
+ scale?: number
150
+ /** Fill behind the drawing; transparent when omitted. */
151
+ background?: string
152
+ /** Extra space around the drawing, in CSS pixels. */
153
+ padding?: number
154
+ /** Drawn above the SVG, in the page's own font. */
155
+ header?: (ctx: CanvasRenderingContext2D, width: number) => number
156
+ }
157
+
158
+ /** Rasterise an SVG element to a PNG blob with its theme resolved. */
159
+ export async function svgToPng(svg: SVGSVGElement, options: RasterOptions = {}): Promise<Blob> {
160
+ const copy = inlineSvg(svg)
161
+ const width = Number(copy.getAttribute("width"))
162
+ const height = Number(copy.getAttribute("height"))
163
+ const markup = new XMLSerializer().serializeToString(copy)
164
+ const url = URL.createObjectURL(new Blob([markup], { type: "image/svg+xml;charset=utf-8" }))
165
+ try {
166
+ const img = new Image()
167
+ img.decoding = "async"
168
+ const loaded = new Promise<void>((resolve, reject) => {
169
+ img.onload = () => resolve()
170
+ img.onerror = () => reject(new Error("SVG could not be rasterised"))
171
+ })
172
+ img.src = url
173
+ await loaded
174
+
175
+ const scale = options.scale ?? Math.max(2, globalThis.devicePixelRatio || 1)
176
+ const pad = options.padding ?? 0
177
+ const measure = document.createElement("canvas").getContext("2d")
178
+ const headerHeight = measure && options.header ? options.header(measure, width) : 0
179
+ const canvas = document.createElement("canvas")
180
+ canvas.width = Math.ceil((width + pad * 2) * scale)
181
+ canvas.height = Math.ceil((height + headerHeight + pad * 2) * scale)
182
+ const ctx = canvas.getContext("2d")
183
+ if (!ctx) throw new Error("Canvas 2D is unavailable")
184
+ ctx.scale(scale, scale)
185
+ if (options.background) {
186
+ ctx.fillStyle = options.background
187
+ ctx.fillRect(0, 0, width + pad * 2, height + headerHeight + pad * 2)
188
+ }
189
+ ctx.translate(pad, pad)
190
+ if (options.header) options.header(ctx, width)
191
+ ctx.drawImage(img, 0, headerHeight, width, height)
192
+ return await new Promise<Blob>((resolve, reject) =>
193
+ canvas.toBlob((b) => (b ? resolve(b) : reject(new Error("PNG encoding failed"))), "image/png"),
194
+ )
195
+ } finally {
196
+ URL.revokeObjectURL(url)
197
+ }
198
+ }
199
+
200
+ /** The first non-transparent background at or above an element. */
201
+ export function backgroundOf(el: Element | null): string | undefined {
202
+ for (let node = el; node; node = node.parentElement) {
203
+ const bg = getComputedStyle(node).backgroundColor
204
+ if (bg && bg !== "transparent" && !/rgba\([^)]*,\s*0\)$/.test(bg)) return bg
205
+ }
206
+ return undefined
207
+ }
208
+
209
+ /**
210
+ * A colour as the browser resolves it where `within` sits, for APIs that cannot
211
+ * read custom properties (canvas, an SVG drawn as an image).
212
+ */
213
+ export function resolveCssColor(value: string, within: Element): string {
214
+ if (!value.includes("var(") && !value.toLowerCase().includes("currentcolor")) return value
215
+ const probe = document.createElement("span")
216
+ probe.style.color = value
217
+ probe.style.display = "none"
218
+ within.append(probe)
219
+ const resolved = getComputedStyle(probe).color
220
+ probe.remove()
221
+ return resolved || value
222
+ }
223
+
224
+ /** A filesystem-safe name from a title. */
225
+ export function fileStem(title: string | undefined, fallback: string): string {
226
+ const stem = (title ?? "").trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g, "").replace(/\s+/g, "-")
227
+ return stem || fallback
228
+ }
@@ -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
+ }
@@ -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
+ }