@leavepulse/ui 0.44.0 → 0.45.1

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 (271) hide show
  1. package/README.md +49 -3
  2. package/dist/assets/{LpCommandPalette-DCP5g2ik.css → LpCommandPalette-HzAA4gri.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-CJOYGZlI.js} +7 -7
  11. package/dist/chunks/{LpAlert-DkOIjsbq.js → LpAlert-B6ilrM7T.js} +1 -1
  12. package/dist/chunks/{LpAppShell-BUKPcHco.js → LpAppShell-B07Lf48e.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-BTVAR4tF.js} +3 -3
  23. package/dist/chunks/{LpConfirmHost-CDm0Wtpv.js → LpConfirmHost-Ci_uCHgQ.js} +1 -1
  24. package/dist/chunks/{LpContextMenu-DQw5dGfG.js → LpContextMenu-BqkAYa9X.js} +2 -1
  25. package/dist/chunks/{LpDatePicker-Drirx1AK.js → LpDatePicker-BrdFtSTy.js} +16 -10
  26. package/dist/chunks/LpDateRangePicker-Drd7EbIz.js +210 -0
  27. package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
  28. package/dist/chunks/{LpDrawer-bScx-KMO.js → LpDrawer-CfRhHwmE.js} +3 -3
  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-BzTVeNqe.js} +3 -3
  42. package/dist/chunks/{LpModalNav-DPg5H9f3.js → LpModalNav-CjABSGCu.js} +14 -8
  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-Bwws_X29.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-DTtWuKvq.js} +76 -9
  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 +62 -11
  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.d.ts +2 -0
  164. package/dist/components/LpToaster.vue.js +1 -1
  165. package/dist/components/LpTooltip.vue.js +1 -1
  166. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  167. package/dist/components/LpUptimeBar.vue.js +1 -1
  168. package/dist/components/chart.js +3 -2
  169. package/dist/components/commandSearch.js +3 -0
  170. package/dist/components/dateRange.d.ts +43 -0
  171. package/dist/components/dateRange.js +76 -0
  172. package/dist/components/exportData.d.ts +58 -0
  173. package/dist/components/exportData.js +165 -0
  174. package/dist/components/logSearch.d.ts +18 -0
  175. package/dist/components/logSearch.js +36 -0
  176. package/dist/components/phone.d.ts +41 -0
  177. package/dist/components/phone.js +80 -0
  178. package/dist/components/tableFilter.d.ts +36 -0
  179. package/dist/components/tableFilter.js +84 -0
  180. package/dist/components/tags.d.ts +20 -0
  181. package/dist/components/tags.js +37 -0
  182. package/dist/composables/useField.d.ts +4 -0
  183. package/dist/composables/useFieldNotice.d.ts +17 -0
  184. package/dist/composables/useFieldNotice.js +28 -0
  185. package/dist/composables/useForm.d.ts +53 -0
  186. package/dist/composables/useForm.js +207 -0
  187. package/dist/composables/useHotkeys.d.ts +65 -5
  188. package/dist/composables/useHotkeys.js +170 -9
  189. package/dist/composables/useInputMask.d.ts +59 -0
  190. package/dist/composables/useInputMask.js +170 -0
  191. package/dist/composables/useLocale.d.ts +2 -0
  192. package/dist/composables/useLocale.js +9 -0
  193. package/dist/composables/usePaintSelect.d.ts +22 -0
  194. package/dist/composables/usePaintSelect.js +169 -0
  195. package/dist/composables/useShift.js +7 -2
  196. package/dist/icons-all.d.ts +1 -0
  197. package/dist/icons-all.js +3 -0
  198. package/dist/icons.d.ts +14 -1
  199. package/dist/index.d.ts +29 -3
  200. package/dist/index.js +195 -138
  201. package/dist/kit-icons.d.ts +2 -0
  202. package/dist/theme/backdrop.d.ts +12 -0
  203. package/dist/theme/presets.d.ts +0 -2
  204. package/package.json +13 -4
  205. package/src/component-names.ts +4 -0
  206. package/src/components/ContextMenuItems.vue +1 -1
  207. package/src/components/LayoutNode.vue +11 -2
  208. package/src/components/LpActionBar.vue +1 -1
  209. package/src/components/LpAutocomplete.vue +1 -0
  210. package/src/components/LpAvatar.vue +2 -2
  211. package/src/components/LpButton.vue +1 -1
  212. package/src/components/LpChart.vue +80 -1
  213. package/src/components/LpColorPicker.vue +34 -1
  214. package/src/components/LpCommandPalette.vue +82 -8
  215. package/src/components/LpContextMenu.vue +1 -1
  216. package/src/components/LpDatePicker.vue +11 -4
  217. package/src/components/LpDateRangePicker.vue +226 -0
  218. package/src/components/LpDrawer.vue +1 -1
  219. package/src/components/LpDropdownMenu.vue +1 -0
  220. package/src/components/LpFieldNotice.vue +65 -0
  221. package/src/components/LpFormField.vue +33 -4
  222. package/src/components/LpIcon.vue +22 -4
  223. package/src/components/LpInput.vue +46 -6
  224. package/src/components/LpLogViewer.vue +225 -22
  225. package/src/components/LpModal.vue +2 -2
  226. package/src/components/LpModalNav.vue +13 -2
  227. package/src/components/LpNotificationBell.vue +15 -7
  228. package/src/components/LpPhoneInput.vue +179 -20
  229. package/src/components/LpPie.vue +8 -5
  230. package/src/components/LpPopover.vue +1 -0
  231. package/src/components/LpRangeCalendar.vue +365 -0
  232. package/src/components/LpRollingText.vue +3 -0
  233. package/src/components/LpScrollArea.vue +22 -2
  234. package/src/components/LpSelect.vue +0 -0
  235. package/src/components/LpShortcutsDialog.vue +77 -8
  236. package/src/components/LpStepper.vue +23 -10
  237. package/src/components/LpSwitch.vue +1 -1
  238. package/src/components/LpTable.vue +584 -65
  239. package/src/components/LpTableFilter.vue +175 -0
  240. package/src/components/LpTagsInput.vue +364 -0
  241. package/src/components/LpThemeSwitcher.vue +19 -23
  242. package/src/components/LpToaster.vue +91 -5
  243. package/src/components/LpTooltip.vue +9 -2
  244. package/src/components/LpTopologyCanvas.vue +7 -1
  245. package/src/components/chart.ts +5 -3
  246. package/src/components/commandSearch.ts +6 -0
  247. package/src/components/dateRange.ts +134 -0
  248. package/src/components/exportData.ts +228 -0
  249. package/src/components/logSearch.ts +46 -0
  250. package/src/components/phone.ts +140 -0
  251. package/src/components/tableFilter.ts +122 -0
  252. package/src/components/tags.ts +62 -0
  253. package/src/composables/useField.ts +4 -0
  254. package/src/composables/useFieldNotice.ts +59 -0
  255. package/src/composables/useForm.ts +314 -0
  256. package/src/composables/useHotkeys.ts +269 -13
  257. package/src/composables/useInputMask.ts +254 -0
  258. package/src/composables/useLocale.ts +14 -0
  259. package/src/composables/usePaintSelect.ts +243 -0
  260. package/src/composables/useShift.ts +13 -2
  261. package/src/iconSubset.d.mts +10 -0
  262. package/src/iconSubset.mjs +51 -0
  263. package/src/icons-all.ts +10 -0
  264. package/src/icons.ts +59 -8
  265. package/src/index.ts +37 -3
  266. package/src/kit-icons.ts +5 -0
  267. package/src/nuxt.ts +30 -2
  268. package/src/runtime/theme.plugin.ts +2 -2
  269. package/src/theme/backdrop.ts +69 -0
  270. package/src/theme/presets.ts +6 -64
  271. package/src/tokens/tokens.css +53 -4
@@ -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
+ }
@@ -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,62 @@
1
+ /*
2
+ * The add pipeline behind LpTagsInput, kept pure so its rules are testable:
3
+ * split what was typed or pasted, normalise each part, then accept or reject
4
+ * it against the limit, the duplicate policy and the consumer's validator.
5
+ */
6
+
7
+ export type TagDuplicates = "reject" | "ignore" | "allow"
8
+
9
+ /** An error message for an invalid tag; anything else accepts it. */
10
+ export type TagValidator = (tag: string, tags: readonly string[]) => string | null | undefined | void
11
+
12
+ export type TagRejection = { value: string; reason: "max" | "duplicate" | "invalid"; message?: string }
13
+
14
+ export interface AddTagsOptions {
15
+ max?: number
16
+ duplicates?: TagDuplicates
17
+ validate?: TagValidator
18
+ normalize?: (tag: string) => string
19
+ }
20
+
21
+ export function splitTags(text: string, delimiter: string | RegExp | readonly string[]): string[] {
22
+ const parts =
23
+ delimiter instanceof RegExp
24
+ ? text.split(new RegExp(delimiter.source, delimiter.flags.includes("g") ? delimiter.flags : `${delimiter.flags}g`))
25
+ : (typeof delimiter === "string" ? [delimiter] : delimiter).reduce<string[]>(
26
+ (acc, d) => (d ? acc.flatMap((p) => p.split(d)) : acc),
27
+ [text],
28
+ )
29
+ return parts.map((p) => p.trim()).filter(Boolean)
30
+ }
31
+
32
+ export function addTags(
33
+ current: readonly string[],
34
+ candidates: readonly string[],
35
+ options: AddTagsOptions = {},
36
+ ): { tags: string[]; rejected: TagRejection[]; ignored: string[] } {
37
+ const tags = [...current]
38
+ const rejected: TagRejection[] = []
39
+ /** Repeats dropped under the "ignore" policy, as stored, so the UI can point at the original. */
40
+ const ignored: string[] = []
41
+ const duplicates = options.duplicates ?? "reject"
42
+ for (const candidate of candidates) {
43
+ const value = options.normalize ? options.normalize(candidate) : candidate
44
+ if (!value) continue
45
+ if (tags.includes(value) && duplicates !== "allow") {
46
+ if (duplicates === "reject") rejected.push({ value: candidate, reason: "duplicate" })
47
+ else ignored.push(value)
48
+ continue
49
+ }
50
+ if (options.max && tags.length >= options.max) {
51
+ rejected.push({ value: candidate, reason: "max" })
52
+ continue
53
+ }
54
+ const message = options.validate?.(value, tags)
55
+ if (typeof message === "string" && message) {
56
+ rejected.push({ value: candidate, reason: "invalid", message })
57
+ continue
58
+ }
59
+ tags.push(value)
60
+ }
61
+ return { tags, rejected, ignored }
62
+ }
@@ -1,4 +1,5 @@
1
1
  import { computed, inject, provide, useId, type ComputedRef, type InjectionKey } from "vue"
2
+ import type { FieldNoticeOptions } from "./useFieldNotice"
2
3
 
3
4
  /*
4
5
  * What a form field tells the control inside it: the id its label points at,
@@ -14,6 +15,9 @@ export interface FieldContext {
14
15
  invalid: boolean
15
16
  required: boolean
16
17
  disabled: boolean
18
+ /** Float a short note under the control without moving the layout (see useFieldNotice). */
19
+ notify?: (text: string, options?: FieldNoticeOptions) => void
20
+ clearNotice?: () => void
17
21
  }
18
22
 
19
23
  export const LP_FIELD: InjectionKey<ComputedRef<FieldContext>> = Symbol.for("lp-field")
@@ -0,0 +1,59 @@
1
+ import { getCurrentScope, onScopeDispose, ref, type Ref } from "vue"
2
+
3
+ /*
4
+ * A short note about what just happened to a field ("already added", "up to 5
5
+ * items") that floats under it instead of taking a line in the layout. A
6
+ * validation message belongs in the field's message line and may push the
7
+ * form down; a passing remark about one keystroke should not, or every refused
8
+ * paste would jolt the fields below.
9
+ *
10
+ * Render it with LpFieldNotice inside a `relative` box around the control.
11
+ * LpFormField does that for its control and hands `notify` down the field
12
+ * context, so a control inside a field (or the app) can raise one without
13
+ * owning any markup.
14
+ */
15
+ export type FieldNoticeTone = "info" | "danger"
16
+
17
+ export interface FieldNoticeOptions {
18
+ tone?: FieldNoticeTone
19
+ /** How long it stays, in ms. 0 keeps it until `clear()`. */
20
+ duration?: number
21
+ }
22
+
23
+ export interface FieldNotice {
24
+ message: Ref<string>
25
+ tone: Ref<FieldNoticeTone>
26
+ /** Bumped on every `show`, so a repeat of the same note can nudge rather than sit still. */
27
+ seq: Ref<number>
28
+ show: (text: string, options?: FieldNoticeOptions) => void
29
+ clear: () => void
30
+ }
31
+
32
+ export const FIELD_NOTICE_MS = 2600
33
+
34
+ export function useFieldNotice(defaultDuration = FIELD_NOTICE_MS): FieldNotice {
35
+ const message = ref("")
36
+ const tone = ref<FieldNoticeTone>("info")
37
+ const seq = ref(0)
38
+ let timer: ReturnType<typeof setTimeout> | undefined
39
+
40
+ function clear() {
41
+ if (timer) clearTimeout(timer)
42
+ timer = undefined
43
+ message.value = ""
44
+ }
45
+
46
+ function show(text: string, options: FieldNoticeOptions = {}) {
47
+ if (timer) clearTimeout(timer)
48
+ timer = undefined
49
+ message.value = text
50
+ tone.value = options.tone ?? "info"
51
+ seq.value++
52
+ const duration = options.duration ?? defaultDuration
53
+ if (duration > 0 && text) timer = setTimeout(clear, duration)
54
+ }
55
+
56
+ if (getCurrentScope()) onScopeDispose(clear)
57
+
58
+ return { message, tone, seq, show, clear }
59
+ }