@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
@@ -13,8 +13,14 @@ export default { inheritAttrs: false }
13
13
  * match wins). The model is the full string ("+380501234567"); a `change` event
14
14
  * also carries the parsed { country, dialCode, number } for forms that want it.
15
15
  *
16
- * Deliberately NOT a full phone validator (no libphonenumber) — it stays free
17
- * text so any number is allowed; `restrict` only blocks obviously non-phone keys.
16
+ * Numbers are formatted as they are typed ("+380 50 123 4567") with
17
+ * libphonenumber-js, loaded on first use (see phone.ts). Digits past the
18
+ * longest number the country allows are dropped with a passing note, and a
19
+ * number that cannot be right is marked once the field is left, with the reason
20
+ * floating under it. v-model stays the formatted text the field shows; `change`
21
+ * also carries `e164` ("+380501234567"), `possible` and `valid`, which
22
+ * is what a form should store. Until the library arrives the field takes digits
23
+ * as before, unformatted.
18
24
  */
19
25
  import {
20
26
  ComboboxAnchor,
@@ -27,13 +33,24 @@ import {
27
33
  ComboboxTrigger,
28
34
  ComboboxViewport,
29
35
  } from "reka-ui"
30
- import { computed, onMounted, ref, useAttrs } from "vue"
31
- import { useFieldControl } from "../composables/useField"
36
+ import { computed, onMounted, ref, shallowRef, useAttrs } from "vue"
37
+ import { useField, useFieldControl } from "../composables/useField"
38
+ import { useFieldNotice, type FieldNoticeOptions } from "../composables/useFieldNotice"
32
39
  import { useInputFilter } from "../composables/useInputFilter"
33
40
  import { useLabels } from "../composables/useLpConfig"
34
41
  import { type Country, filterCountries, flagEmoji, loadCountries, matchCountryByValue } from "./countries"
35
42
  import { OPTION_ITEM, POPOVER_PANEL } from "./dropdown"
43
+ import LpFieldNotice from "./LpFieldNotice.vue"
36
44
  import LpIcon from "./LpIcon.vue"
45
+ import {
46
+ caretAfterDigits,
47
+ digitsBefore,
48
+ digitsOf,
49
+ formatPhone,
50
+ loadPhoneExamples,
51
+ loadPhoneLib,
52
+ type PhoneLib,
53
+ } from "./phone"
37
54
  import { useMergedAttrs } from "../composables/useMergedClass"
38
55
 
39
56
  export interface PhoneDetail {
@@ -43,6 +60,17 @@ export interface PhoneDetail {
43
60
  dialCode: string
44
61
  /** National part (digits after the dial code). */
45
62
  number: string
63
+ /** Normalised "+380501234567"; empty when there are no digits. */
64
+ e164: string
65
+ /** The length fits the country. False until the formatter has loaded. */
66
+ possible: boolean
67
+ /** A real number for the country. False until the formatter has loaded. */
68
+ valid: boolean
69
+ /**
70
+ * "MOBILE", "FIXED_LINE"… when known. The `min` metadata the field loads does
71
+ * not carry number types, so this stays undefined unless that changes.
72
+ */
73
+ type?: string
46
74
  }
47
75
 
48
76
  const props = withDefaults(
@@ -53,6 +81,7 @@ const props = withDefaults(
53
81
  only?: string[]
54
82
  /** Default country (ISO2) used when the field is empty. */
55
83
  defaultCountry?: string
84
+ /** Unset shows an example number for the active country. */
56
85
  placeholder?: string
57
86
  disabled?: boolean
58
87
  invalid?: boolean
@@ -61,7 +90,7 @@ const props = withDefaults(
61
90
  searchPlaceholder?: string
62
91
  }>(),
63
92
  {
64
- placeholder: "Phone number",
93
+ placeholder: undefined,
65
94
  size: "md",
66
95
  emptyText: "No country",
67
96
  searchPlaceholder: "Search country…",
@@ -106,13 +135,17 @@ const query = ref("")
106
135
 
107
136
  const filtered = computed(() => filterCountries(countries.value, query.value))
108
137
 
138
+ const phoneLib = shallowRef<PhoneLib>()
139
+
109
140
  function parse(value: string): PhoneDetail {
110
- const country = matchCountryByValue(value, countries.value)
111
- const digits = value.replace(/[^\d]/g, "")
112
- if (country) {
113
- return { country, dialCode: country.dial, number: digits.slice(country.dial.length) }
114
- }
115
- return { country: undefined, dialCode: "", number: digits }
141
+ const formatted = formatPhone(value, phoneLib.value)
142
+ const country =
143
+ (formatted.country && countries.value.find((c) => c.iso2 === formatted.country)) ??
144
+ matchCountryByValue(value, countries.value)
145
+ const digits = digitsOf(formatted.e164)
146
+ const base = { e164: formatted.e164, possible: formatted.possible, valid: formatted.valid, type: formatted.type }
147
+ if (country) return { ...base, country, dialCode: country.dial, number: digits.slice(country.dial.length) }
148
+ return { ...base, country: undefined, dialCode: "", number: digits }
116
149
  }
117
150
 
118
151
  function setValue(value: string) {
@@ -120,9 +153,85 @@ function setValue(value: string) {
120
153
  emit("change", parse(value))
121
154
  }
122
155
 
156
+ /*
157
+ * Notes float under the field: the LpFormField around it shows them when there
158
+ * is one, otherwise the field shows its own.
159
+ */
160
+ const notice = useFieldNotice()
161
+ const outerField = useField()
162
+ const ownNotice = computed(() => !outerField?.value.notify)
163
+ function say(message: string, options?: FieldNoticeOptions) {
164
+ if (outerField?.value.notify) outerField.value.notify(message, options)
165
+ else notice.show(message, options)
166
+ }
167
+ function unsay() {
168
+ if (outerField?.value.clearNotice) outerField.value.clearNotice()
169
+ else notice.clear()
170
+ }
171
+
172
+ /** Shown invalid after the field was left with a number that cannot be right. */
173
+ const flagged = ref(false)
174
+ const countryName = (iso?: string) =>
175
+ (iso && countries.value.find((c) => c.iso2 === iso)?.name) || ""
176
+
177
+ /*
178
+ * Reformat after every edit and put the caret back after the same number of
179
+ * digits it followed, so spaces the formatter adds or moves never push it
180
+ * around.
181
+ */
123
182
  function onInput(e: Event) {
124
183
  filterInput(e)
125
- setValue((e.target as HTMLInputElement).value)
184
+ const el = e.target as HTMLInputElement
185
+ const before = digitsBefore(el.value, el.selectionStart ?? el.value.length)
186
+ const result = formatPhone(el.value, phoneLib.value)
187
+ if (el.value !== result.display) {
188
+ el.value = result.display
189
+ const caret = caretAfterDigits(result.display, Math.min(before, digitsOf(result.display).length))
190
+ el.setSelectionRange(caret, caret)
191
+ }
192
+ if (flagged.value) {
193
+ flagged.value = false
194
+ unsay()
195
+ }
196
+ if (result.dropped) {
197
+ const name = countryName(result.country)
198
+ say(
199
+ (name ? text.value.tooLongFor : text.value.tooLong)
200
+ .replace("{country}", name)
201
+ .replace("{max}", String(digitsOf(result.e164).length - (result.callingCode?.length ?? 0))),
202
+ )
203
+ }
204
+ setValue(result.display)
205
+ }
206
+
207
+ /*
208
+ * Backspace or Delete next to a space the formatter put there would only remove
209
+ * the space, and formatting would put it straight back: the key would seem
210
+ * dead. The digit beside it goes instead.
211
+ */
212
+ function onKeyEdit(e: InputEvent) {
213
+ const el = e.target as HTMLInputElement
214
+ const start = el.selectionStart ?? 0
215
+ const end = el.selectionEnd ?? 0
216
+ const backward = e.inputType === "deleteContentBackward"
217
+ const forward = e.inputType === "deleteContentForward"
218
+ if ((!backward && !forward) || start !== end) return
219
+ const value = el.value
220
+ const at = backward ? start - 1 : start
221
+ if (at < 0 || at >= value.length || /\d/.test(value[at]!)) return
222
+ let i = at
223
+ while (i >= 0 && i < value.length && !/\d/.test(value[i]!)) i += backward ? -1 : 1
224
+ if (i < 0 || i >= value.length) return
225
+ e.preventDefault()
226
+ el.value = value.slice(0, i) + value.slice(i + 1)
227
+ const caret = backward ? i : start
228
+ el.setSelectionRange(caret, caret)
229
+ el.dispatchEvent(new Event("input", { bubbles: true }))
230
+ }
231
+
232
+ function onBeforeEdit(e: InputEvent) {
233
+ onKeyEdit(e)
234
+ if (!e.defaultPrevented) onBeforeInput(e)
126
235
  }
127
236
 
128
237
  /**
@@ -139,7 +248,31 @@ function chooseCountry(c: Country) {
139
248
  }
140
249
 
141
250
  onMounted(async () => {
142
- allCountries.value = await loadCountries()
251
+ const [list, phone] = await Promise.all([loadCountries(), loadPhoneLib().catch(() => undefined)])
252
+ allCountries.value = list
253
+ phoneLib.value = phone
254
+ // A value set before the formatter arrived is shown formatted from now on.
255
+ if (phone && props.modelValue) {
256
+ const display = formatPhone(props.modelValue, phone).display
257
+ if (display !== props.modelValue) setValue(display)
258
+ }
259
+ if (props.placeholder === undefined) examplesFor.value = await loadPhoneExamples().catch(() => undefined)
260
+ })
261
+
262
+ /* An example number for the active country, when the consumer gave no text. */
263
+ const examplesFor = shallowRef<Awaited<ReturnType<typeof loadPhoneExamples>>>()
264
+ const shownPlaceholder = computed(() => {
265
+ if (props.placeholder !== undefined) return props.placeholder
266
+ const phone = phoneLib.value
267
+ const iso = activeCountry.value?.iso2
268
+ if (phone && examplesFor.value && iso) {
269
+ try {
270
+ return phone.getExampleNumber(iso as never, examplesFor.value)?.formatInternational() ?? text.value.placeholder
271
+ } catch {
272
+ return text.value.placeholder
273
+ }
274
+ }
275
+ return text.value.placeholder
143
276
  })
144
277
 
145
278
  // The default country's "+code" is put in place when the user enters an empty
@@ -151,7 +284,21 @@ function onFocus() {
151
284
  if (!props.modelValue && seeded.value) setValue(seeded.value)
152
285
  }
153
286
  function onBlur() {
154
- if (seeded.value && props.modelValue?.trim() === seeded.value.trim()) setValue("")
287
+ if (seeded.value && props.modelValue?.trim() === seeded.value.trim()) {
288
+ setValue("")
289
+ return
290
+ }
291
+ const result = phoneLib.value && props.modelValue ? formatPhone(props.modelValue, phoneLib.value) : undefined
292
+ if (!result || result.valid || digitsOf(result.e164).length <= (activeCountry.value?.dial.length ?? 0)) return
293
+ flagged.value = true
294
+ const name = countryName(result.country) || activeCountry.value?.name || ""
295
+ const reason =
296
+ result.problem === "too-short"
297
+ ? (name ? text.value.tooShortFor : text.value.tooShort)
298
+ : result.problem === "invalid-country"
299
+ ? text.value.unknownCountry
300
+ : text.value.invalidNumber
301
+ say(reason.replace("{country}", name), { tone: "danger", duration: 0 })
155
302
  }
156
303
 
157
304
  const sizeClass = {
@@ -168,13 +315,23 @@ const attrs = useAttrs()
168
315
  const field = useFieldControl(() => ({
169
316
  wrapped: false,
170
317
  id: typeof attrs.id === "string" ? attrs.id : undefined,
171
- invalid: props.invalid,
318
+ invalid: props.invalid || flagged.value,
172
319
  disabled: props.disabled,
173
320
  }))
174
- const text = useLabels("phoneInput", { selectCountry: "Select country" })
321
+ const text = useLabels("phoneInput", {
322
+ selectCountry: "Select country",
323
+ placeholder: "Phone number",
324
+ tooLong: "A phone number has at most {max} digits",
325
+ tooLongFor: "Numbers in {country} have at most {max} digits after the code",
326
+ tooShort: "This number is too short",
327
+ tooShortFor: "Too short for {country}",
328
+ invalidNumber: "Not a valid phone number",
329
+ unknownCountry: "Unknown country code",
330
+ })
331
+ const noticeId = `${field.value.id ?? "lp-phone"}-notice`
175
332
  const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
176
333
  [
177
- "flex w-full items-stretch rounded-control border bg-surface-soft text-ink transition-colors duration-[var(--duration-fast)] focus-within:ring-2 focus-within:ring-ring",
334
+ "relative flex w-full items-stretch rounded-control border bg-surface-soft text-ink transition-colors duration-[var(--duration-fast)] focus-within:ring-2 focus-within:ring-ring",
178
335
  sizeClass[props.size],
179
336
  field.value.invalid
180
337
  ? "border-danger focus-within:border-danger focus-within:ring-danger-soft"
@@ -224,6 +381,7 @@ const inputAttrs = computed(() => {
224
381
 
225
382
  <ComboboxPortal>
226
383
  <ComboboxContent
384
+ :collision-padding="8"
227
385
  position="popper"
228
386
  align="start"
229
387
  :side-offset="6"
@@ -272,18 +430,19 @@ const inputAttrs = computed(() => {
272
430
  v-bind="inputAttrs"
273
431
  :id="field.id"
274
432
  :value="modelValue"
275
- :placeholder="placeholder"
433
+ :placeholder="shownPlaceholder"
276
434
  :disabled="field.disabled"
277
435
  :required="field.required || undefined"
278
436
  :aria-invalid="field.invalid || undefined"
279
- :aria-describedby="field.describedBy"
437
+ :aria-describedby="[field.describedBy, notice.message.value ? noticeId : ''].filter(Boolean).join(' ') || undefined"
280
438
  class="lp-autofill min-w-0 flex-1 self-stretch bg-transparent px-3 outline-none placeholder:text-muted"
281
- @beforeinput="onBeforeInput"
439
+ @beforeinput="onBeforeEdit"
282
440
  @paste="onPaste"
283
441
  @compositionend="onCompositionEnd"
284
442
  @input="onInput"
285
443
  @focus="onFocus"
286
444
  @blur="onBlur"
287
445
  />
446
+ <LpFieldNotice v-if="ownNotice" :notice="notice" :id="noticeId" />
288
447
  </div>
289
448
  </template>
@@ -697,16 +697,19 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
697
697
  </span>
698
698
  </LpShift>
699
699
  </template>
700
+ <!-- The canvas keeps its natural size where there is room and scales
701
+ down as a whole where there is not (a phone, a narrow column):
702
+ the callout labels widen the box well past the circle, and at a
703
+ fixed width they ran off the screen. -->
700
704
  <div
701
- class="relative shrink-0"
702
- :style="{ width: `${canvasWidth}px`, height: `${canvasHeight}px` }"
705
+ class="relative max-w-full shrink-0 max-sm:mx-auto"
706
+ :style="{ width: `${canvasWidth}px`, aspectRatio: `${canvasWidth} / ${canvasHeight}` }"
703
707
  >
704
708
  <!-- Clicking the empty middle (or the gaps) clears, so a selection can
705
709
  be dropped without hunting for somewhere outside the chart. -->
706
710
  <svg
707
- :width="canvasWidth"
708
- :height="canvasHeight"
709
- class="touch-none select-none"
711
+ :viewBox="`0 0 ${canvasWidth} ${canvasHeight}`"
712
+ class="block size-full touch-none select-none"
710
713
  role="img"
711
714
  :aria-label="summary"
712
715
  @pointerleave="leaveChart"
@@ -32,6 +32,7 @@ const open = defineModel<boolean>("open")
32
32
  </PopoverTrigger>
33
33
  <PopoverPortal>
34
34
  <PopoverContent
35
+ :collision-padding="8"
35
36
  :side="side"
36
37
  :align="align"
37
38
  :side-offset="sideOffset"