@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,226 @@
1
+ <script lang="ts">
2
+ export default { inheritAttrs: false }
3
+ </script>
4
+
5
+ <script setup lang="ts">
6
+ /*
7
+ * Range field + popover: presets on one side, a range calendar on the other.
8
+ * The model is `{ start, end }` of ISO days, like LpDatePicker's single day.
9
+ * Picking the second end or a preset fills the field and closes the popover.
10
+ * Two months sit side by side where there is room, one on a phone.
11
+ */
12
+ import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from "reka-ui"
13
+ import { computed, onBeforeUnmount, onMounted, ref, useAttrs } from "vue"
14
+ import { useFieldControl } from "../composables/useField"
15
+ import { useLocale } from "../composables/useLocale"
16
+ import { useLabels } from "../composables/useLpConfig"
17
+ import { mergeClass } from "../composables/useMergedClass"
18
+ import {
19
+ clampRange,
20
+ type DateRange,
21
+ type DateRangePreset,
22
+ defaultRangePresets,
23
+ formatRange,
24
+ normalizeRange,
25
+ sameRange,
26
+ } from "./dateRange"
27
+ import { POPOVER_PANEL } from "./dropdown"
28
+ import LpIcon from "./LpIcon.vue"
29
+ import LpRollingText from "./LpRollingText.vue"
30
+ import LpRangeCalendar, { type RangeChange } from "./LpRangeCalendar.vue"
31
+
32
+ export interface DateRangePickerLabels extends Record<string, string> {
33
+ placeholder: string
34
+ clear: string
35
+ presets: string
36
+ today: string
37
+ last7: string
38
+ last30: string
39
+ thisMonth: string
40
+ lastMonth: string
41
+ }
42
+
43
+ const props = withDefaults(
44
+ defineProps<{
45
+ /** Selected range (v-model). */
46
+ modelValue?: DateRange
47
+ placeholder?: string
48
+ min?: string
49
+ max?: string
50
+ isDisabled?: (iso: string) => boolean
51
+ isDateUnavailable?: (iso: string) => boolean
52
+ /** Longest allowed range, in days. */
53
+ maximumDays?: number
54
+ /** Shortcut ranges; an empty list hides them. Defaults to the built-in set. */
55
+ presets?: DateRangePreset[]
56
+ clearable?: boolean
57
+ disabled?: boolean
58
+ invalid?: boolean
59
+ size?: "sm" | "md" | "lg"
60
+ locale?: string
61
+ /** Intl format for the displayed ends. */
62
+ format?: Intl.DateTimeFormatOptions
63
+ labels?: Partial<DateRangePickerLabels>
64
+ }>(),
65
+ { size: "md" },
66
+ )
67
+
68
+ const emit = defineEmits<{
69
+ (e: "update:modelValue", value: DateRange | undefined): void
70
+ }>()
71
+
72
+ const text = useLabels<DateRangePickerLabels>(
73
+ "dateRangePicker",
74
+ {
75
+ placeholder: "Pick a date range",
76
+ clear: "Clear",
77
+ presets: "Presets",
78
+ today: "Today",
79
+ last7: "Last 7 days",
80
+ last30: "Last 30 days",
81
+ thisMonth: "This month",
82
+ lastMonth: "Last month",
83
+ },
84
+ () => props.labels,
85
+ )
86
+
87
+ const open = ref(false)
88
+ const value = computed(() => normalizeRange(props.modelValue))
89
+ const locale = useLocale(() => props.locale)
90
+ const display = computed(() => (value.value ? formatRange(value.value, locale.value, props.format) : ""))
91
+
92
+ const presetList = computed<DateRangePreset[]>(() => {
93
+ return props.presets ?? defaultRangePresets(text.value)
94
+ })
95
+
96
+ /*
97
+ * A range picked on the calendar keeps the popover open: the field already
98
+ * shows it, and the user can still pull either end before clicking away.
99
+ * Closing on the second click left no moment to adjust. A preset is a whole
100
+ * answer, so it closes.
101
+ */
102
+ function pick(range: DateRange, change?: RangeChange) {
103
+ emit("update:modelValue", range)
104
+ if (!change) open.value = false
105
+ }
106
+
107
+ function pickPreset(preset: DateRangePreset) {
108
+ const range = clampRange(preset.range(), props.min, props.max)
109
+ if (range) pick(range)
110
+ }
111
+
112
+ function presetFits(preset: DateRangePreset): boolean {
113
+ return !!clampRange(preset.range(), props.min, props.max)
114
+ }
115
+
116
+ const wide = ref(false)
117
+ let query: MediaQueryList | undefined
118
+ const onQuery = () => {
119
+ wide.value = !!query?.matches
120
+ }
121
+ onMounted(() => {
122
+ query = window.matchMedia("(min-width: 640px)")
123
+ onQuery()
124
+ query.addEventListener("change", onQuery)
125
+ })
126
+ onBeforeUnmount(() => query?.removeEventListener("change", onQuery))
127
+
128
+ const shellSize = {
129
+ sm: "h-(--size-control-sm) text-xs",
130
+ md: "h-(--size-control-md) text-sm",
131
+ lg: "h-(--size-control-lg) text-sm",
132
+ }
133
+
134
+ const attrs = useAttrs()
135
+ const field = useFieldControl(() => ({
136
+ wrapped: false,
137
+ id: typeof attrs.id === "string" ? attrs.id : undefined,
138
+ invalid: props.invalid,
139
+ disabled: props.disabled,
140
+ }))
141
+ const showClear = computed(() => props.clearable && !!value.value && !field.value.disabled)
142
+ </script>
143
+
144
+ <template>
145
+ <div :class="mergeClass('relative w-full', attrs.class as string)" :style="attrs.style as never">
146
+ <PopoverRoot v-model:open="open">
147
+ <PopoverTrigger as-child>
148
+ <button
149
+ v-bind="{ ...attrs, class: undefined, style: undefined }"
150
+ :id="field.id"
151
+ type="button"
152
+ :disabled="field.disabled"
153
+ :aria-invalid="field.invalid || undefined"
154
+ :aria-describedby="field.describedBy"
155
+ class="group flex w-full items-center gap-2 rounded-control border bg-surface-soft px-3 text-left text-ink outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:border-brand disabled:cursor-not-allowed disabled:opacity-55"
156
+ :class="[
157
+ shellSize[size],
158
+ field.invalid ? 'border-danger focus-visible:ring-danger-soft' : 'border-line focus-visible:border-brand',
159
+ ]"
160
+ >
161
+ <LpIcon name="lucide:calendar-range" :size="16" class="shrink-0 text-muted" />
162
+ <LpRollingText
163
+ :value="display || placeholder || text.placeholder"
164
+ align="start"
165
+ class="min-w-0 flex-1"
166
+ :class="display ? '' : 'text-muted'"
167
+ />
168
+ <span v-if="showClear" class="w-[15px] shrink-0" aria-hidden="true" />
169
+ <LpIcon
170
+ name="lucide:chevron-down"
171
+ :size="15"
172
+ class="shrink-0 text-muted transition-transform duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] group-data-[state=open]:rotate-180 motion-reduce:transition-none"
173
+ />
174
+ </button>
175
+ </PopoverTrigger>
176
+ <PopoverPortal>
177
+ <PopoverContent
178
+ :side-offset="6"
179
+ align="start"
180
+ :collision-padding="8"
181
+ :class="[POPOVER_PANEL, 'z-(--z-popover) flex max-w-[calc(100vw-1rem)] flex-col rounded-card p-0 outline-none sm:flex-row']"
182
+ >
183
+ <div
184
+ v-if="presetList.length"
185
+ role="group"
186
+ :aria-label="text.presets"
187
+ class="flex gap-1 overflow-x-auto border-b border-line p-2 sm:w-40 sm:flex-col sm:overflow-visible sm:border-r sm:border-b-0"
188
+ >
189
+ <button
190
+ v-for="(preset, i) in presetList"
191
+ :key="`${i}:${preset.label}`"
192
+ type="button"
193
+ :disabled="!presetFits(preset)"
194
+ :aria-pressed="sameRange(value, preset.range())"
195
+ class="shrink-0 rounded-control px-2.5 py-1.5 text-left text-sm whitespace-nowrap text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:bg-surface-soft hover:text-ink focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40 aria-pressed:bg-brand-soft aria-pressed:text-ink"
196
+ @click="pickPreset(preset)"
197
+ >
198
+ {{ preset.label }}
199
+ </button>
200
+ </div>
201
+ <LpRangeCalendar
202
+ :model-value="value"
203
+ :min="min"
204
+ :max="max"
205
+ :is-disabled="isDisabled"
206
+ :is-date-unavailable="isDateUnavailable"
207
+ :maximum-days="maximumDays"
208
+ :months="wide ? 2 : 1"
209
+ :locale="locale"
210
+ class="border-0 bg-transparent"
211
+ @update:model-value="pick"
212
+ />
213
+ </PopoverContent>
214
+ </PopoverPortal>
215
+ </PopoverRoot>
216
+ <button
217
+ v-if="showClear"
218
+ type="button"
219
+ :aria-label="text.clear"
220
+ class="absolute top-1/2 right-[calc(0.75rem+15px+0.5rem)] flex -translate-y-1/2 items-center rounded-sm text-muted outline-none transition-colors hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
221
+ @click="emit('update:modelValue', undefined)"
222
+ >
223
+ <LpIcon name="lucide:x" :size="15" />
224
+ </button>
225
+ </div>
226
+ </template>
@@ -162,7 +162,7 @@ watch(() => props.open, (open) => (open ? claim() : release()), { immediate: tru
162
162
  onBeforeUnmount(release)
163
163
 
164
164
  const contentClass = computed(() => {
165
- const surface = frosted.value ? (layer.value.nested ? "lp-glass-overlay--nested" : "lp-glass-overlay") : "bg-surface-raised"
165
+ const surface = frosted.value ? (layer.value.nested ? "lp-glass-overlay lp-glass-overlay--nested" : "lp-glass-overlay") : "bg-surface-raised"
166
166
  const base = `fixed z-(--z-modal) flex flex-col border-line ${surface} shadow-panel outline-none`
167
167
  const map: Record<Direction, string> = {
168
168
  left: "inset-y-0 left-0 border-r rounded-r-card",
@@ -28,6 +28,7 @@ defineProps<{ items: MenuItem[] }>()
28
28
  </DropdownMenuTrigger>
29
29
  <DropdownMenuPortal>
30
30
  <DropdownMenuContent
31
+ :collision-padding="8"
31
32
  :side-offset="6"
32
33
  align="end"
33
34
  :class="[POPOVER_PANEL, 'z-(--z-popover) min-w-44 p-1 outline-none']"
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * The floating note from useFieldNotice: anchored under the nearest
4
+ * `relative` box, over whatever sits below it (usually the hint), so it never
5
+ * moves the layout. It slides in, slides out, and nudges sideways when the
6
+ * same note is raised again while showing.
7
+ */
8
+ import { ref, watch } from "vue"
9
+ import { prefersReducedMotion } from "../composables/easing"
10
+ import type { FieldNotice } from "../composables/useFieldNotice"
11
+ import LpIcon from "./LpIcon.vue"
12
+
13
+ const props = defineProps<{
14
+ notice: FieldNotice
15
+ id?: string
16
+ }>()
17
+
18
+ const el = ref<HTMLElement | null>(null)
19
+
20
+ watch(
21
+ () => props.notice.seq.value,
22
+ (_, before) => {
23
+ if (before === undefined || !el.value || prefersReducedMotion()) return
24
+ el.value.animate(
25
+ [
26
+ { transform: "translateX(0)" },
27
+ { transform: "translateX(-3px)" },
28
+ { transform: "translateX(3px)" },
29
+ { transform: "translateX(0)" },
30
+ ],
31
+ { duration: 240, easing: "ease-out" },
32
+ )
33
+ },
34
+ )
35
+ </script>
36
+
37
+ <template>
38
+ <Transition
39
+ enter-active-class="transition-[opacity,translate] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none"
40
+ leave-active-class="transition-[opacity,translate] duration-fast ease-out motion-reduce:transition-none"
41
+ enter-from-class="-translate-y-1 opacity-0"
42
+ leave-to-class="-translate-y-1 opacity-0"
43
+ >
44
+ <div
45
+ v-if="notice.message.value"
46
+ :id="id"
47
+ ref="el"
48
+ role="status"
49
+ class="pointer-events-none absolute top-full left-0 z-20 mt-1.5 flex max-w-full items-center gap-1.5 rounded-control border px-2 py-1 text-xs shadow-sm"
50
+ :class="
51
+ notice.tone.value === 'danger'
52
+ ? 'border-danger/40 bg-surface-raised bg-[linear-gradient(var(--color-danger-soft),var(--color-danger-soft))] text-danger'
53
+ : 'border-line bg-surface-raised text-ink'
54
+ "
55
+ >
56
+ <LpIcon
57
+ :name="notice.tone.value === 'danger' ? 'lucide:circle-alert' : 'lucide:info'"
58
+ :size="13"
59
+ class="shrink-0"
60
+ :class="notice.tone.value === 'danger' ? '' : 'text-muted'"
61
+ />
62
+ <span class="truncate">{{ notice.message.value }}</span>
63
+ </div>
64
+ </Transition>
65
+ </template>
@@ -17,8 +17,10 @@ export default { inheritAttrs: false }
17
17
  */
18
18
  import { Label } from "reka-ui"
19
19
  import { computed, useId, useSlots } from "vue"
20
+ import LpFieldNotice from "./LpFieldNotice.vue"
20
21
  import LpShift from "./LpShift.vue"
21
22
  import { fieldMessageId, provideField } from "../composables/useField"
23
+ import { useFieldNotice } from "../composables/useFieldNotice"
22
24
  import { useMergedAttrs } from "../composables/useMergedClass"
23
25
 
24
26
  const props = defineProps<{
@@ -37,6 +39,8 @@ const id = computed(() => props.id ?? generated)
37
39
  const messageId = computed(() => fieldMessageId(id.value))
38
40
  const slots = useSlots()
39
41
  const hasMessage = computed(() => !!props.error || !!props.hint || !!slots.hint)
42
+ const notice = useFieldNotice()
43
+ const noticeId = computed(() => `${id.value}-notice`)
40
44
 
41
45
  // The control inside reads its id, description and states from here, so the
42
46
  // label, the message line and aria-invalid stay tied to it without the
@@ -45,13 +49,20 @@ provideField(
45
49
  computed(() => ({
46
50
  id: id.value,
47
51
  labelId: props.label ? `${id.value}-label` : undefined,
48
- describedBy: hasMessage.value ? messageId.value : undefined,
52
+ describedBy:
53
+ [hasMessage.value ? messageId.value : "", notice.message.value ? noticeId.value : ""]
54
+ .filter(Boolean)
55
+ .join(" ") || undefined,
49
56
  invalid: !!props.invalid || !!props.error,
50
57
  required: !!props.required,
51
58
  disabled: !!props.disabled,
59
+ notify: notice.show,
60
+ clearNotice: notice.clear,
52
61
  })),
53
62
  )
54
63
 
64
+ defineExpose({ notify: notice.show, clearNotice: notice.clear })
65
+
55
66
  // The field stacks label and control with a fixed gap; a denser form needs that gap smaller.
56
67
  const { class: rootClass, attrs: rest } = useMergedAttrs(
57
68
  "flex min-w-0 flex-col gap-1.5",
@@ -72,7 +83,13 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
72
83
  <span v-if="required" class="text-danger">*</span>
73
84
  </Label>
74
85
 
75
- <slot :id="id" :describedBy="hasMessage ? messageId : undefined" />
86
+ <!-- The note from `notify` floats from here, over the message line below,
87
+ so raising one never moves the form; the line fades out under it
88
+ rather than showing through. -->
89
+ <div class="relative">
90
+ <slot :id="id" :describedBy="hasMessage ? messageId : undefined" />
91
+ <LpFieldNotice :notice="notice" :id="noticeId" />
92
+ </div>
76
93
 
77
94
  <!-- Swapping hint→error changes the text but not the height, so the eased
78
95
  box only ever moves when a message appears or clears.
@@ -83,12 +100,24 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
83
100
  leave every message-less field a row taller than before — and would be
84
101
  a step the easing could not cover, since it exists even at height 0. -->
85
102
  <LpShift axis="height">
86
- <p v-if="error" :id="messageId" class="pt-1.5 text-xs text-danger">{{ error }}</p>
103
+ <p
104
+ v-if="error"
105
+ :id="messageId"
106
+ class="pt-1.5 text-xs text-danger transition-opacity duration-fast"
107
+ :class="notice.message.value ? 'opacity-0' : ''"
108
+ >
109
+ {{ error }}
110
+ </p>
87
111
  <!-- The hint is a slot as well as a prop: most are a sentence, but some
88
112
  carry a link or a unit in <code>, and a prop can only be text. Callers
89
113
  had been reaching for `#hint` on the assumption it existed — it
90
114
  rendered nothing, so the explanation silently went missing. -->
91
- <p v-else-if="hint || $slots.hint" :id="messageId" class="pt-1.5 text-xs text-muted">
115
+ <p
116
+ v-else-if="hint || $slots.hint"
117
+ :id="messageId"
118
+ class="pt-1.5 text-xs text-muted transition-opacity duration-fast"
119
+ :class="notice.message.value ? 'opacity-0' : ''"
120
+ >
92
121
  <slot name="hint">{{ hint }}</slot>
93
122
  </p>
94
123
  </LpShift>
@@ -8,11 +8,13 @@
8
8
  * inline <svg> synchronously from getIcon() so the server and client produce
9
9
  * identical markup. The <Icon> component only resolves icons after mount, which
10
10
  * left empty SSR placeholders and caused hydration mismatches. We fall back to
11
- * <Icon> only for icons that aren't registered yet (e.g. fetched on demand).
11
+ * <Icon> only for non-lucide icons that aren't registered (fetched on demand).
12
+ * A lucide icon outside the kit's own set loads from the bundled collection
13
+ * (see icons.ts); until then an empty box of the same size holds its place.
12
14
  */
13
- import { computed } from "vue"
15
+ import { computed, watchEffect } from "vue"
14
16
  import { Icon, getIcon } from "@iconify/vue"
15
- import "../icons" // registers the lucide collection offline
17
+ import { iconsVersion, requestIcon } from "../icons"
16
18
 
17
19
  const props = defineProps<{
18
20
  /** iconify name, e.g. "lucide:check". Bare names assume the lucide set. */
@@ -25,7 +27,15 @@ function resolve(name: string): string {
25
27
  }
26
28
 
27
29
  const fullName = computed(() => resolve(props.name))
28
- const data = computed(() => getIcon(fullName.value))
30
+ const data = computed(() => {
31
+ void iconsVersion.value
32
+ return getIcon(fullName.value)
33
+ })
34
+ const lucide = computed(() => fullName.value.startsWith("lucide:"))
35
+
36
+ watchEffect(() => {
37
+ if (!data.value && lucide.value) requestIcon(fullName.value)
38
+ })
29
39
  const dim = computed(() => props.size ?? "1em")
30
40
  </script>
31
41
 
@@ -39,6 +49,14 @@ const dim = computed(() => props.size ?? "1em")
39
49
  aria-hidden="true"
40
50
  v-html="data.body"
41
51
  />
52
+ <svg
53
+ v-else-if="lucide"
54
+ xmlns="http://www.w3.org/2000/svg"
55
+ :width="dim"
56
+ :height="dim"
57
+ viewBox="0 0 24 24"
58
+ aria-hidden="true"
59
+ />
42
60
  <Icon
43
61
  v-else
44
62
  :icon="fullName"
@@ -7,6 +7,7 @@ import { tv, type VariantProps } from "tailwind-variants"
7
7
  import { computed, ref, useAttrs, useSlots } from "vue"
8
8
  import { useFieldControl } from "../composables/useField"
9
9
  import { useInputFilter } from "../composables/useInputFilter"
10
+ import { DEFAULT_MASK_TOKENS, useInputMask, type MaskSpec, type MaskTokens } from "../composables/useInputMask"
10
11
  import { mergeClass } from "../composables/useMergedClass"
11
12
  import LpFormField from "./LpFormField.vue"
12
13
  import LpIcon from "./LpIcon.vue"
@@ -73,17 +74,56 @@ const props = withDefaults(
73
74
  * disallowed input/paste at the source — cursor- and IME-safe.
74
75
  */
75
76
  restrict?: RegExp
77
+ /**
78
+ * Format-as-you-type mask ("+0 (000) 000-00-00", a list, or a function of
79
+ * the raw value). The model holds the masked text; `update:raw` carries
80
+ * the accepted characters alone.
81
+ */
82
+ mask?: MaskSpec
83
+ /** Extra or replacement mask tokens, one-character RegExps by key. */
84
+ maskTokens?: MaskTokens
76
85
  }>(),
77
86
  { type: "text", size: "md" },
78
87
  )
79
88
 
80
- defineEmits<{ (e: "update:modelValue", value: string): void }>()
89
+ const emit = defineEmits<{
90
+ (e: "update:modelValue", value: string): void
91
+ (e: "update:raw", value: string): void
92
+ }>()
81
93
 
82
94
  const { isInvalid, onBeforeInput, onInput: filterInput, onCompositionEnd, onPaste } = useInputFilter({
83
95
  pattern: () => props.pattern,
84
96
  restrict: () => props.restrict,
85
97
  })
86
98
 
99
+ const masking = useInputMask({
100
+ mask: () => props.mask,
101
+ tokens: () => (props.maskTokens ? { ...DEFAULT_MASK_TOKENS, ...props.maskTokens } : undefined),
102
+ onChange: (_masked, raw) => emit("update:raw", raw),
103
+ })
104
+ const shown = computed(() => (masking.active.value ? masking.format(props.modelValue).masked : props.modelValue))
105
+
106
+ function onBefore(event: InputEvent): void {
107
+ onBeforeInput(event)
108
+ if (!event.defaultPrevented) masking.onBeforeInput(event)
109
+ if (event.defaultPrevented && masking.active.value && event.target instanceof HTMLInputElement) {
110
+ emit("update:modelValue", event.target.value)
111
+ }
112
+ }
113
+
114
+ function onInputEvent(event: Event): void {
115
+ if (masking.active.value) masking.onInput(event)
116
+ else filterInput(event)
117
+ emit("update:modelValue", (event.target as HTMLInputElement).value)
118
+ }
119
+
120
+ function onComposed(event: CompositionEvent): void {
121
+ if (masking.active.value) {
122
+ masking.onCompositionEnd(event)
123
+ emit("update:modelValue", (event.target as HTMLInputElement).value)
124
+ } else onCompositionEnd(event)
125
+ }
126
+
87
127
  // Pattern failure folds into the same invalid state as the explicit prop/error.
88
128
  const patternInvalid = computed(() => isInvalid(props.modelValue))
89
129
 
@@ -134,7 +174,7 @@ defineExpose({
134
174
  :error="hasField ? error : undefined"
135
175
  :required="hasField ? required : undefined"
136
176
  :id="hasField ? field.id : undefined"
137
- :class="hasField ? rootClass : undefined"
177
+ :class="hasField ? rootClass : 'contents'"
138
178
  :style="hasField ? rootStyle : undefined"
139
179
  >
140
180
  <div
@@ -150,7 +190,7 @@ defineExpose({
150
190
  v-bind="inputAttrs"
151
191
  :id="field.id"
152
192
  :type="type"
153
- :value="modelValue"
193
+ :value="shown"
154
194
  :placeholder="placeholder"
155
195
  :disabled="field.disabled"
156
196
  :required="field.required || undefined"
@@ -161,10 +201,10 @@ defineExpose({
161
201
  hasLeading ? 'pl-2' : padX[size ?? 'md'],
162
202
  hasTrailing ? '' : padX[size ?? 'md'],
163
203
  ]"
164
- @beforeinput="onBeforeInput"
204
+ @beforeinput="onBefore"
165
205
  @paste="onPaste"
166
- @compositionend="onCompositionEnd"
167
- @input="filterInput($event); $emit('update:modelValue', ($event.target as HTMLInputElement).value)"
206
+ @compositionend="onComposed"
207
+ @input="onInputEvent"
168
208
  />
169
209
  <slot name="trailing" />
170
210
  </div>