@leavepulse/ui 0.43.4 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (437) hide show
  1. package/README.md +121 -7
  2. package/dist/assets/{LpCommandPalette-B-0Est4Z.css → LpCommandPalette-nGNnzXqQ.css} +21 -21
  3. package/dist/assets/{LpDropzone-BBoWvi-J.css → LpDropzone-i4BdMi1H.css} +12 -12
  4. package/dist/assets/{LpImageEditor-Ct7Y3aNu.css → LpImageEditor-CbMA82g8.css} +6 -6
  5. package/dist/assets/{LpRollingText-CR1mb0Vt.css → LpRollingText-BGnE6q9j.css} +3 -0
  6. package/dist/assets/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-CqYX3yy6.css} +28 -7
  7. package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
  8. package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-CPwWxTJS.css} +49 -9
  9. package/dist/chunks/ContextMenuItems-BtKU7074.js +124 -0
  10. package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-BkaGXTjx.js} +26 -9
  11. package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-B3_9E1Xa.js} +28 -12
  12. package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-CnfFVIwC.js} +26 -12
  13. package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BwX8uo4_.js} +54 -49
  14. package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-B6ilrM7T.js} +16 -9
  15. package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-C53_RaGF.js} +53 -26
  16. package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-C8IgSMsi.js} +52 -24
  17. package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-C1XStdOo.js} +19 -10
  18. package/dist/chunks/LpBreadcrumbs-CQJd73Gm.js +95 -0
  19. package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-CKSwC6Mu.js} +45 -15
  20. package/dist/chunks/{LpCalendar-B7Ko5RdR.js → LpCalendar-DrA2McTU.js} +1 -1
  21. package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-C2IvucmX.js} +1 -1
  22. package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-Bkj13CbW.js} +8 -4
  23. package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-ca7ANFyW.js} +49 -17
  24. package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-zz4VGkG4.js} +75 -26
  25. package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-DCw5jR1t.js} +59 -17
  26. package/dist/chunks/LpConfirmHost-CbywMpMP.js +49 -0
  27. package/dist/chunks/LpContextMenu-BqkAYa9X.js +69 -0
  28. package/dist/chunks/LpDatePicker-DuusgT69.js +195 -0
  29. package/dist/chunks/LpDateRangePicker-DHeuznfC.js +208 -0
  30. package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
  31. package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-Dt4aB4Fr.js} +65 -28
  32. package/dist/chunks/{LpDropdownMenu-DJpoqRCb.js → LpDropdownMenu-DdJGQzPn.js} +2 -1
  33. package/dist/chunks/{LpEmptyState-Dsdqz1Wj.js → LpEmptyState-BJbZXC8D.js} +1 -1
  34. package/dist/chunks/LpFieldNotice-wtieLUTj.js +64 -0
  35. package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CNurOvkV.js} +25 -10
  36. package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-D_qRRdsH.js} +3 -3
  37. package/dist/chunks/LpFormField-CtVYj6tO.js +101 -0
  38. package/dist/chunks/{LpIcon-CCnX5_2j.js → LpIcon-DYtaid0K.js} +20 -6
  39. package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-D-YGQbEv.js} +58 -20
  40. package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-DSDahF23.js} +148 -61
  41. package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
  42. package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-KypbgOyo.js} +300 -72
  43. package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-Cc84LaM3.js} +39 -14
  44. package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-YKr-cQPN.js} +7 -4
  45. package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DMMDQHAu.js} +52 -29
  46. package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-BVPDyIup.js} +10 -7
  47. package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-CCwyJP5C.js} +79 -33
  48. package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-B77O-z28.js} +21 -8
  49. package/dist/chunks/{LpNumberFlow-amWYaeuA.js → LpNumberFlow-z4WPZcZu.js} +22 -12
  50. package/dist/chunks/{LpOtpInput-y2v-pmiN.js → LpOtpInput-CziPF3Oq.js} +37 -8
  51. package/dist/chunks/{LpPagination-5FxjmmB5.js → LpPagination-BcDlo3r_.js} +31 -20
  52. package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-eKzQhbQP.js} +11 -8
  53. package/dist/chunks/LpPhoneInput-BCbrrhgE.js +336 -0
  54. package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BEJ2SfI6.js} +84 -28
  55. package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-DCKeqiVn.js} +5 -2
  56. package/dist/chunks/{LpProgress-Btq5W4Ut.js → LpProgress-CBU1iI69.js} +5 -4
  57. package/dist/chunks/{LpRadioGroup-xPIuVDO_.js → LpRadioGroup-DmEW0l78.js} +9 -2
  58. package/dist/chunks/LpRangeCalendar-DZuaRV0W.js +331 -0
  59. package/dist/chunks/{LpRollingText-CM5gkSeF.js → LpRollingText-DB7upf-5.js} +1 -1
  60. package/dist/chunks/{LpScrollArea-D8xyKXcF.js → LpScrollArea-Bry1fdOM.js} +14 -2
  61. package/dist/chunks/{LpSection-aM6J_u51.js → LpSection-tKEH0jYf.js} +1 -1
  62. package/dist/chunks/{LpSegmented-fV0Sk92p.js → LpSegmented-C09EEam2.js} +8 -4
  63. package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DNIL6MB9.js} +50 -31
  64. package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-CQNB0-yU.js} +30 -22
  65. package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-kx-LdqNU.js} +16 -11
  66. package/dist/chunks/{LpSkeleton-CTI-vBgW.js → LpSkeleton-BB6XIV-w.js} +1 -1
  67. package/dist/chunks/{LpSlider-BwNIBkAK.js → LpSlider-Cuzmrd7p.js} +7 -2
  68. package/dist/chunks/{LpSparkline-C6xQ2dwv.js → LpSparkline-CB80N8SJ.js} +11 -4
  69. package/dist/chunks/{LpStat-CQaW8R--.js → LpStat-B-7udBRP.js} +2 -2
  70. package/dist/chunks/{LpStepper-Bk-XaQYU.js → LpStepper-C7XiVKZj.js} +34 -19
  71. package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-OrcOJX9m.js} +41 -27
  72. package/dist/chunks/LpTableFilter-BV8PpLM9.js +158 -0
  73. package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
  74. package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-29iW1XOx.js} +20 -6
  75. package/dist/chunks/LpTagsInput-D1SuqbhB.js +300 -0
  76. package/dist/chunks/LpTextarea-uuScWo6-.js +77 -0
  77. package/dist/chunks/LpThemeProvider-Be4r2TQX.js +35 -0
  78. package/dist/chunks/LpThemeSwitcher-B4aVPbNC.js +166 -0
  79. package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
  80. package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-Cqe7fXnh.js} +14 -8
  81. package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster-DpJvoCOh.js} +94 -31
  82. package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CXVKcOXT.js} +14 -6
  83. package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
  84. package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-6ozNrQKh.js} +8 -2
  85. package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-ugcv25xj.js} +44 -15
  86. package/dist/chunks/icons-CkA0njzm.js +43 -0
  87. package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-DJm1JSsY.js} +215 -260
  88. package/dist/component-names.d.ts +1 -1
  89. package/dist/components/ContextMenuItems.vue.d.ts +8 -0
  90. package/dist/components/ContextMenuItems.vue.js +4 -0
  91. package/dist/components/LayoutCanvas.vue.js +1 -1
  92. package/dist/components/LayoutNode.vue.d.ts +4 -0
  93. package/dist/components/LayoutNode.vue.js +1 -1
  94. package/dist/components/LpActionBar.vue.d.ts +3 -0
  95. package/dist/components/LpActionBar.vue.js +1 -1
  96. package/dist/components/LpAddressInput.vue.d.ts +1 -1
  97. package/dist/components/LpAddressInput.vue.js +1 -1
  98. package/dist/components/LpAlert.vue.d.ts +9 -0
  99. package/dist/components/LpAlert.vue.js +1 -1
  100. package/dist/components/LpAppShell.vue.d.ts +10 -0
  101. package/dist/components/LpAppShell.vue.js +1 -1
  102. package/dist/components/LpAutocomplete.vue.d.ts +31 -6
  103. package/dist/components/LpAutocomplete.vue.js +1 -1
  104. package/dist/components/LpAvatar.vue.d.ts +3 -3
  105. package/dist/components/LpAvatar.vue.js +1 -1
  106. package/dist/components/LpBadge.vue.d.ts +2 -2
  107. package/dist/components/LpBreadcrumbs.vue.d.ts +13 -9
  108. package/dist/components/LpBreadcrumbs.vue.js +1 -1
  109. package/dist/components/LpButton.vue.d.ts +35 -98
  110. package/dist/components/LpButton.vue.js +1 -1
  111. package/dist/components/LpCalendar.vue.js +1 -1
  112. package/dist/components/LpCard.vue.js +1 -1
  113. package/dist/components/LpChart.vue.d.ts +47 -3
  114. package/dist/components/LpChart.vue.js +377 -97
  115. package/dist/components/LpCheckbox.vue.js +1 -1
  116. package/dist/components/LpCodeBlock.vue.d.ts +2 -0
  117. package/dist/components/LpCodeBlock.vue.js +1 -1
  118. package/dist/components/LpColorPicker.vue.d.ts +2 -2
  119. package/dist/components/LpColorPicker.vue.js +1 -1
  120. package/dist/components/LpCommandPalette.vue.d.ts +14 -3
  121. package/dist/components/LpCommandPalette.vue.js +142 -77
  122. package/dist/components/LpConfirmDialog.vue.d.ts +15 -3
  123. package/dist/components/LpConfirmDialog.vue.js +1 -1
  124. package/dist/components/LpConfirmHost.vue.d.ts +9 -0
  125. package/dist/components/LpConfirmHost.vue.js +4 -0
  126. package/dist/components/LpContextMenu.vue.d.ts +3 -5
  127. package/dist/components/LpContextMenu.vue.js +1 -1
  128. package/dist/components/LpDatePicker.vue.d.ts +24 -6
  129. package/dist/components/LpDatePicker.vue.js +1 -1
  130. package/dist/components/LpDateRangePicker.vue.d.ts +60 -0
  131. package/dist/components/LpDateRangePicker.vue.js +4 -0
  132. package/dist/components/LpDisclosure.vue.js +1 -1
  133. package/dist/components/LpDrawer.vue.d.ts +10 -0
  134. package/dist/components/LpDrawer.vue.js +1 -1
  135. package/dist/components/LpDropdownMenu.vue.js +1 -1
  136. package/dist/components/LpDropzone.vue.js +23 -7
  137. package/dist/components/LpEmptyState.vue.js +1 -1
  138. package/dist/components/LpFieldNotice.vue.d.ts +8 -0
  139. package/dist/components/LpFieldNotice.vue.js +4 -0
  140. package/dist/components/LpFileTree.vue.d.ts +11 -2
  141. package/dist/components/LpFileTree.vue.js +1 -1
  142. package/dist/components/LpFileTreeNode.vue.js +1 -1
  143. package/dist/components/LpFormField.vue.d.ts +13 -1
  144. package/dist/components/LpFormField.vue.js +1 -1
  145. package/dist/components/LpIcon.vue.d.ts +0 -1
  146. package/dist/components/LpIcon.vue.js +1 -1
  147. package/dist/components/LpImageEditor.vue.js +93 -58
  148. package/dist/components/LpInput.vue.d.ts +22 -3
  149. package/dist/components/LpInput.vue.js +1 -1
  150. package/dist/components/LpLightbox.vue.d.ts +17 -0
  151. package/dist/components/LpLightbox.vue.js +1 -1
  152. package/dist/components/LpLink.vue.d.ts +11 -11
  153. package/dist/components/LpLink.vue.js +1 -1
  154. package/dist/components/LpLogViewer.vue.d.ts +72 -3
  155. package/dist/components/LpLogViewer.vue.js +1 -1
  156. package/dist/components/LpMapPicker.vue.d.ts +4 -4
  157. package/dist/components/LpMapPicker.vue.js +1 -1
  158. package/dist/components/LpMediaGallery.vue.js +1 -1
  159. package/dist/components/LpModal.vue.d.ts +16 -6
  160. package/dist/components/LpModal.vue.js +1 -1
  161. package/dist/components/LpModalNav.vue.d.ts +0 -1
  162. package/dist/components/LpModalNav.vue.js +1 -1
  163. package/dist/components/LpNotificationBell.vue.d.ts +11 -2
  164. package/dist/components/LpNotificationBell.vue.js +1 -1
  165. package/dist/components/LpNumberField.vue.d.ts +2 -0
  166. package/dist/components/LpNumberField.vue.js +1 -1
  167. package/dist/components/LpNumberFlow.vue.js +1 -1
  168. package/dist/components/LpOtpInput.vue.js +1 -1
  169. package/dist/components/LpPagination.vue.d.ts +2 -0
  170. package/dist/components/LpPagination.vue.js +1 -1
  171. package/dist/components/LpPasswordInput.vue.js +1 -1
  172. package/dist/components/LpPhoneInput.vue.d.ts +13 -0
  173. package/dist/components/LpPhoneInput.vue.js +1 -1
  174. package/dist/components/LpPie.vue.d.ts +21 -2
  175. package/dist/components/LpPie.vue.js +1 -1
  176. package/dist/components/LpPopover.vue.d.ts +2 -0
  177. package/dist/components/LpPopover.vue.js +1 -1
  178. package/dist/components/LpProgress.vue.d.ts +1 -1
  179. package/dist/components/LpProgress.vue.js +1 -1
  180. package/dist/components/LpRadioGroup.vue.js +1 -1
  181. package/dist/components/LpRangeCalendar.vue.d.ts +51 -0
  182. package/dist/components/LpRangeCalendar.vue.js +4 -0
  183. package/dist/components/LpRollingText.vue.d.ts +1 -1
  184. package/dist/components/LpRollingText.vue.js +1 -1
  185. package/dist/components/LpScrollArea.vue.d.ts +2 -2
  186. package/dist/components/LpScrollArea.vue.js +1 -1
  187. package/dist/components/LpSection.vue.d.ts +1 -1
  188. package/dist/components/LpSection.vue.js +1 -1
  189. package/dist/components/LpSegmented.vue.d.ts +5 -0
  190. package/dist/components/LpSegmented.vue.js +1 -1
  191. package/dist/components/LpSelect.vue.d.ts +1 -0
  192. package/dist/components/LpSelect.vue.js +1 -1
  193. package/dist/components/LpShortcutsDialog.vue.d.ts +7 -1
  194. package/dist/components/LpShortcutsDialog.vue.js +60 -15
  195. package/dist/components/LpSidebar.vue.d.ts +6 -2
  196. package/dist/components/LpSidebar.vue.js +1 -1
  197. package/dist/components/LpSidebarNav.vue.js +1 -1
  198. package/dist/components/LpSkeleton.vue.js +1 -1
  199. package/dist/components/LpSlider.vue.js +1 -1
  200. package/dist/components/LpSparkline.vue.d.ts +1 -0
  201. package/dist/components/LpSparkline.vue.js +1 -1
  202. package/dist/components/LpStat.vue.js +1 -1
  203. package/dist/components/LpStepper.vue.js +1 -1
  204. package/dist/components/LpSwitch.vue.js +1 -1
  205. package/dist/components/LpTable.vue.d.ts +107 -3
  206. package/dist/components/LpTable.vue.js +540 -128
  207. package/dist/components/LpTableFilter.vue.d.ts +31 -0
  208. package/dist/components/LpTableFilter.vue.js +4 -0
  209. package/dist/components/LpTableOfContents.vue.d.ts +4 -2
  210. package/dist/components/LpTableOfContents.vue.js +1 -1
  211. package/dist/components/LpTabs.vue.d.ts +2 -0
  212. package/dist/components/LpTabs.vue.js +1 -1
  213. package/dist/components/LpTagsInput.vue.d.ts +77 -0
  214. package/dist/components/LpTagsInput.vue.js +4 -0
  215. package/dist/components/LpTextarea.vue.d.ts +19 -6
  216. package/dist/components/LpTextarea.vue.js +1 -1
  217. package/dist/components/LpThemeProvider.vue.d.ts +22 -0
  218. package/dist/components/LpThemeProvider.vue.js +4 -0
  219. package/dist/components/LpThemeSwitcher.vue.d.ts +15 -2
  220. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  221. package/dist/components/LpTilt.vue.js +1 -1
  222. package/dist/components/LpTimeField.vue.js +1 -1
  223. package/dist/components/LpToaster.vue.d.ts +6 -1
  224. package/dist/components/LpToaster.vue.js +1 -1
  225. package/dist/components/LpTooltip.vue.d.ts +1 -2
  226. package/dist/components/LpTooltip.vue.js +1 -1
  227. package/dist/components/LpTooltipProvider.vue.d.ts +22 -0
  228. package/dist/components/LpTooltipProvider.vue.js +4 -0
  229. package/dist/components/LpTopologyCanvas.vue.js +1 -1
  230. package/dist/components/LpUptimeBar.vue.d.ts +4 -0
  231. package/dist/components/LpUptimeBar.vue.js +1 -1
  232. package/dist/components/chart.d.ts +32 -1
  233. package/dist/components/chart.js +42 -6
  234. package/dist/components/codeHighlight.d.ts +6 -0
  235. package/dist/components/codeHighlight.js +92 -20
  236. package/dist/components/color.d.ts +1 -1
  237. package/dist/components/color.js +20 -9
  238. package/dist/components/commandSearch.js +3 -0
  239. package/dist/components/confirm.d.ts +25 -0
  240. package/dist/components/confirm.js +26 -0
  241. package/dist/components/countries.d.ts +6 -0
  242. package/dist/components/countries.js +7 -0
  243. package/dist/components/dateRange.d.ts +43 -0
  244. package/dist/components/dateRange.js +76 -0
  245. package/dist/components/exportData.d.ts +58 -0
  246. package/dist/components/exportData.js +165 -0
  247. package/dist/components/fileTree.js +16 -7
  248. package/dist/components/imageCrop.d.ts +42 -0
  249. package/dist/components/imageCrop.js +47 -0
  250. package/dist/components/link.d.ts +12 -0
  251. package/dist/components/link.js +19 -0
  252. package/dist/components/logSearch.d.ts +18 -0
  253. package/dist/components/logSearch.js +36 -0
  254. package/dist/components/modalDepth.d.ts +18 -2
  255. package/dist/components/modalDepth.js +17 -5
  256. package/dist/components/otp.d.ts +5 -0
  257. package/dist/components/otp.js +10 -0
  258. package/dist/components/phone.d.ts +41 -0
  259. package/dist/components/phone.js +80 -0
  260. package/dist/components/sidebar.d.ts +7 -1
  261. package/dist/components/tableFilter.d.ts +36 -0
  262. package/dist/components/tableFilter.js +84 -0
  263. package/dist/components/tableSort.d.ts +8 -0
  264. package/dist/components/tableSort.js +31 -0
  265. package/dist/components/tags.d.ts +20 -0
  266. package/dist/components/tags.js +37 -0
  267. package/dist/composables/useField.d.ts +38 -0
  268. package/dist/composables/useField.js +45 -0
  269. package/dist/composables/useFieldNotice.d.ts +17 -0
  270. package/dist/composables/useFieldNotice.js +28 -0
  271. package/dist/composables/useForm.d.ts +53 -0
  272. package/dist/composables/useForm.js +207 -0
  273. package/dist/composables/useFormDraft.d.ts +10 -3
  274. package/dist/composables/useFormDraft.js +9 -14
  275. package/dist/composables/useHotkeys.d.ts +81 -5
  276. package/dist/composables/useHotkeys.js +223 -25
  277. package/dist/composables/useInputFilter.d.ts +17 -0
  278. package/dist/composables/useInputFilter.js +47 -10
  279. package/dist/composables/useInputMask.d.ts +59 -0
  280. package/dist/composables/useInputMask.js +170 -0
  281. package/dist/composables/useLpConfig.d.ts +24 -0
  282. package/dist/composables/useLpConfig.js +44 -0
  283. package/dist/composables/usePaintSelect.d.ts +22 -0
  284. package/dist/composables/usePaintSelect.js +169 -0
  285. package/dist/composables/useRovingFocus.js +7 -0
  286. package/dist/composables/useShift.js +7 -2
  287. package/dist/composables/useTilt.d.ts +2 -2
  288. package/dist/composables/useTilt.js +5 -6
  289. package/dist/composables/useToast.d.ts +20 -0
  290. package/dist/composables/useToast.js +28 -4
  291. package/dist/composables/useZoomPan.d.ts +3 -3
  292. package/dist/composables/useZoomPan.js +4 -4
  293. package/dist/icons-all.d.ts +1 -0
  294. package/dist/icons-all.js +3 -0
  295. package/dist/icons.d.ts +14 -1
  296. package/dist/index.d.ts +60 -4
  297. package/dist/index.js +245 -141
  298. package/dist/kit-icons.d.ts +2 -0
  299. package/dist/layout/tree.d.ts +2 -0
  300. package/dist/layout/tree.js +16 -1
  301. package/dist/layout/useLayout.d.ts +15 -0
  302. package/dist/layout/useLayout.js +41 -14
  303. package/dist/theme/backdrop.d.ts +12 -0
  304. package/dist/theme/presets.d.ts +0 -2
  305. package/dist/theme/useTheme.d.ts +32 -9
  306. package/package.json +18 -8
  307. package/src/component-names.ts +9 -7
  308. package/src/components/ContextMenuItems.vue +88 -0
  309. package/src/components/LayoutCanvas.vue +26 -6
  310. package/src/components/LayoutNode.vue +30 -4
  311. package/src/components/LpActionBar.vue +31 -8
  312. package/src/components/LpAddressInput.vue +57 -41
  313. package/src/components/LpAlert.vue +16 -3
  314. package/src/components/LpAppShell.vue +45 -12
  315. package/src/components/LpAutocomplete.vue +47 -10
  316. package/src/components/LpAvatar.vue +20 -6
  317. package/src/components/LpBreadcrumbs.vue +65 -41
  318. package/src/components/LpButton.vue +60 -14
  319. package/src/components/LpChart.vue +351 -28
  320. package/src/components/LpCheckbox.vue +7 -3
  321. package/src/components/LpCodeBlock.vue +50 -14
  322. package/src/components/LpColorPicker.vue +73 -10
  323. package/src/components/LpCommandPalette.vue +115 -20
  324. package/src/components/LpConfirmDialog.vue +68 -17
  325. package/src/components/LpConfirmHost.vue +59 -0
  326. package/src/components/LpContextMenu.vue +15 -116
  327. package/src/components/LpDatePicker.vue +104 -68
  328. package/src/components/LpDateRangePicker.vue +224 -0
  329. package/src/components/LpDrawer.vue +56 -19
  330. package/src/components/LpDropdownMenu.vue +1 -0
  331. package/src/components/LpDropzone.vue +21 -4
  332. package/src/components/LpFieldNotice.vue +65 -0
  333. package/src/components/LpFileTree.vue +33 -7
  334. package/src/components/LpFormField.vue +59 -7
  335. package/src/components/LpIcon.vue +22 -4
  336. package/src/components/LpImageEditor.vue +96 -57
  337. package/src/components/LpInput.vue +62 -11
  338. package/src/components/LpLightbox.vue +140 -29
  339. package/src/components/LpLink.vue +24 -13
  340. package/src/components/LpLogViewer.vue +311 -39
  341. package/src/components/LpMapPicker.vue +36 -13
  342. package/src/components/LpMediaGallery.vue +8 -2
  343. package/src/components/LpModal.vue +54 -28
  344. package/src/components/LpModalNav.vue +5 -2
  345. package/src/components/LpNotificationBell.vue +77 -23
  346. package/src/components/LpNumberField.vue +21 -6
  347. package/src/components/LpNumberFlow.vue +26 -10
  348. package/src/components/LpOtpInput.vue +43 -8
  349. package/src/components/LpPagination.vue +23 -7
  350. package/src/components/LpPasswordInput.vue +4 -1
  351. package/src/components/LpPhoneInput.vue +224 -39
  352. package/src/components/LpPie.vue +102 -20
  353. package/src/components/LpPopover.vue +4 -0
  354. package/src/components/LpProgress.vue +5 -3
  355. package/src/components/LpRadioGroup.vue +9 -2
  356. package/src/components/LpRangeCalendar.vue +365 -0
  357. package/src/components/LpRollingText.vue +3 -0
  358. package/src/components/LpScrollArea.vue +22 -2
  359. package/src/components/LpSegmented.vue +12 -2
  360. package/src/components/LpSelect.vue +0 -0
  361. package/src/components/LpShortcutsDialog.vue +83 -9
  362. package/src/components/LpSidebar.vue +14 -9
  363. package/src/components/LpSidebarNav.vue +17 -5
  364. package/src/components/LpSkeleton.vue +4 -1
  365. package/src/components/LpSlider.vue +7 -1
  366. package/src/components/LpSparkline.vue +11 -3
  367. package/src/components/LpStepper.vue +23 -10
  368. package/src/components/LpSwitch.vue +31 -8
  369. package/src/components/LpTable.vue +668 -90
  370. package/src/components/LpTableFilter.vue +175 -0
  371. package/src/components/LpTableOfContents.vue +50 -32
  372. package/src/components/LpTabs.vue +23 -6
  373. package/src/components/LpTagsInput.vue +364 -0
  374. package/src/components/LpTextarea.vue +44 -6
  375. package/src/components/LpThemeProvider.vue +41 -0
  376. package/src/components/LpThemeSwitcher.vue +76 -33
  377. package/src/components/LpTilt.vue +2 -2
  378. package/src/components/LpTimeField.vue +14 -3
  379. package/src/components/LpToaster.vue +115 -27
  380. package/src/components/LpTooltip.vue +29 -5
  381. package/src/components/LpTooltipProvider.vue +24 -0
  382. package/src/components/LpTopologyCanvas.vue +19 -2
  383. package/src/components/LpUptimeBar.vue +46 -8
  384. package/src/components/chart.ts +82 -8
  385. package/src/components/codeHighlight.ts +111 -15
  386. package/src/components/color.ts +30 -9
  387. package/src/components/commandSearch.ts +6 -0
  388. package/src/components/confirm.ts +58 -0
  389. package/src/components/countries.ts +12 -0
  390. package/src/components/dateRange.ts +134 -0
  391. package/src/components/exportData.ts +228 -0
  392. package/src/components/fileTree.ts +19 -7
  393. package/src/components/imageCrop.ts +107 -0
  394. package/src/components/link.ts +34 -0
  395. package/src/components/logSearch.ts +46 -0
  396. package/src/components/modalDepth.ts +48 -6
  397. package/src/components/otp.ts +16 -0
  398. package/src/components/phone.ts +140 -0
  399. package/src/components/sidebar.ts +7 -1
  400. package/src/components/tableFilter.ts +122 -0
  401. package/src/components/tableSort.ts +37 -0
  402. package/src/components/tags.ts +62 -0
  403. package/src/composables/useField.ts +83 -0
  404. package/src/composables/useFieldNotice.ts +59 -0
  405. package/src/composables/useForm.ts +314 -0
  406. package/src/composables/useFormDraft.ts +25 -20
  407. package/src/composables/useHotkeys.ts +355 -28
  408. package/src/composables/useInputFilter.ts +77 -14
  409. package/src/composables/useInputMask.ts +254 -0
  410. package/src/composables/useLpConfig.ts +87 -0
  411. package/src/composables/usePaintSelect.ts +243 -0
  412. package/src/composables/useRovingFocus.ts +11 -0
  413. package/src/composables/useShift.ts +13 -2
  414. package/src/composables/useTilt.ts +5 -6
  415. package/src/composables/useToast.ts +56 -4
  416. package/src/composables/useZoomPan.ts +6 -6
  417. package/src/iconSubset.d.mts +10 -0
  418. package/src/iconSubset.mjs +51 -0
  419. package/src/icons-all.ts +10 -0
  420. package/src/icons.ts +59 -8
  421. package/src/index.ts +73 -4
  422. package/src/kit-icons.ts +5 -0
  423. package/src/layout/tree.ts +30 -0
  424. package/src/layout/useLayout.ts +68 -18
  425. package/src/nuxt.ts +87 -17
  426. package/src/runtime/config.plugin.ts +14 -0
  427. package/src/runtime/theme.plugin.ts +43 -23
  428. package/src/theme/backdrop.ts +69 -0
  429. package/src/theme/presets.ts +6 -64
  430. package/src/theme/useTheme.ts +206 -100
  431. package/src/tokens/tokens.css +46 -0
  432. package/dist/chunks/LpBreadcrumbs-p4jHe_V-.js +0 -62
  433. package/dist/chunks/LpContextMenu-CwW1QXe_.js +0 -217
  434. package/dist/chunks/LpDatePicker-CwOToxGq.js +0 -164
  435. package/dist/chunks/LpFormField-CazBc2-a.js +0 -62
  436. package/dist/chunks/LpPhoneInput-BDSKVl_z.js +0 -209
  437. package/dist/chunks/LpTextarea-t7F6gv8E.js +0 -47
@@ -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
+ }
@@ -0,0 +1,83 @@
1
+ import { computed, inject, provide, useId, type ComputedRef, type InjectionKey } from "vue"
2
+ import type { FieldNoticeOptions } from "./useFieldNotice"
3
+
4
+ /*
5
+ * What a form field tells the control inside it: the id its label points at,
6
+ * the id of the hint/error line the control should be described by, and the
7
+ * states the control should reflect. LpFormField provides it; every kit control
8
+ * consumes it, and so can a consumer's own control placed inside a field.
9
+ */
10
+ export interface FieldContext {
11
+ id: string
12
+ /** Id of the visible label, for controls a `<label for>` cannot name (groups). */
13
+ labelId?: string
14
+ describedBy?: string
15
+ invalid: boolean
16
+ required: boolean
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
21
+ }
22
+
23
+ export const LP_FIELD: InjectionKey<ComputedRef<FieldContext>> = Symbol.for("lp-field")
24
+
25
+ export function provideField(context: ComputedRef<FieldContext>): void {
26
+ provide(LP_FIELD, context)
27
+ }
28
+
29
+ /** The nearest LpFormField's context, or null outside one. */
30
+ export function useField(): ComputedRef<FieldContext> | null {
31
+ return inject(LP_FIELD, null)
32
+ }
33
+
34
+ export function fieldMessageId(id: string): string {
35
+ return `${id}-message`
36
+ }
37
+
38
+ export interface FieldControlState {
39
+ /** The control wraps itself in its own LpFormField (it has a label, hint or error prop). */
40
+ wrapped: boolean
41
+ /** That own field renders a hint or error line. */
42
+ hasMessage?: boolean
43
+ /** An id the consumer set on the control, which must win over a generated one. */
44
+ id?: string
45
+ invalid?: boolean
46
+ required?: boolean
47
+ disabled?: boolean
48
+ }
49
+
50
+ /**
51
+ * The field state a control renders with: its own wrapping field when it has
52
+ * one, otherwise the field around it, with the control's own flags or-ed in.
53
+ * `id` is undefined only for a bare control outside any field.
54
+ */
55
+ export function useFieldControl(
56
+ own: () => FieldControlState,
57
+ ): ComputedRef<Omit<FieldContext, "id"> & { id?: string }> {
58
+ const outer = useField()
59
+ const generated = useId()
60
+ return computed(() => {
61
+ const state = own()
62
+ if (state.wrapped) {
63
+ const id = state.id ?? generated
64
+ return {
65
+ id,
66
+ labelId: undefined,
67
+ describedBy: state.hasMessage ? fieldMessageId(id) : undefined,
68
+ invalid: !!state.invalid,
69
+ required: !!state.required,
70
+ disabled: !!state.disabled,
71
+ }
72
+ }
73
+ const field = outer?.value
74
+ return {
75
+ id: state.id ?? field?.id,
76
+ labelId: field?.labelId,
77
+ describedBy: field?.describedBy,
78
+ invalid: !!state.invalid || !!field?.invalid,
79
+ required: !!state.required || !!field?.required,
80
+ disabled: !!state.disabled || !!field?.disabled,
81
+ }
82
+ })
83
+ }
@@ -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
+ }
@@ -0,0 +1,314 @@
1
+ import {
2
+ computed,
3
+ reactive,
4
+ ref,
5
+ toRaw,
6
+ toValue,
7
+ watch,
8
+ type ComputedRef,
9
+ type MaybeRefOrGetter,
10
+ type Ref,
11
+ type UnwrapRef,
12
+ } from "vue"
13
+
14
+ /*
15
+ * Form state that every form ends up writing by hand: the values, which
16
+ * fields were touched, whether anything differs from what was loaded, the
17
+ * errors, and a submit that cannot run twice.
18
+ *
19
+ * Errors are computed for every field but shown only once a field was touched
20
+ * (blurred, or edited after a blur) or a submit was attempted, so nobody is
21
+ * told off for a field they have not reached yet.
22
+ *
23
+ * `dirty` is the same signal useFormDraft asks for, so the two pair directly:
24
+ *
25
+ * ```ts
26
+ * const form = useForm({ initial: () => record.value, rules: { email: required } })
27
+ * const draft = useFormDraft({ key: () => `draft:${id}`, value: () => form.values, dirty: form.dirty })
28
+ * function restore() { form.setValues(draft.read() ?? {}); draft.discard() }
29
+ * const save = form.handleSubmit(async (values) => { await api.save(values); draft.discard() })
30
+ * ```
31
+ *
32
+ * A reactive `initial` (a record that loads after mount) is adopted while the
33
+ * form is untouched, and becomes the new baseline for `dirty`; once the user
34
+ * has changed something, a reload of the record no longer overwrites it.
35
+ */
36
+
37
+ export type FormErrorMessage = string | null | undefined | void | false
38
+ export type FieldRule<V, T> = (value: V, values: T) => FormErrorMessage | Promise<FormErrorMessage>
39
+ export type FormRules<T> = { [K in keyof T]?: FieldRule<T[K], T> | FieldRule<T[K], T>[] }
40
+ export type FormErrors<T> = Partial<Record<keyof T | "form", string>>
41
+
42
+ export interface UseFormOptions<T extends object> {
43
+ initial: MaybeRefOrGetter<T>
44
+ rules?: FormRules<T>
45
+ /** Cross-field checks; return messages by field, or under `form` for the whole form. */
46
+ validate?: (values: T) => FormErrors<T> | void | Promise<FormErrors<T> | void>
47
+ }
48
+
49
+ export interface FieldBinding<V> {
50
+ modelValue: V
51
+ "onUpdate:modelValue": (value: V) => void
52
+ onBlur: () => void
53
+ error: string | undefined
54
+ invalid: boolean
55
+ }
56
+
57
+ export interface UseForm<T extends object> {
58
+ values: UnwrapRef<T>
59
+ /** Errors as currently computed, shown or not. */
60
+ errors: ComputedRef<FormErrors<T>>
61
+ /** Errors the user should see now. */
62
+ visibleErrors: ComputedRef<FormErrors<T>>
63
+ touched: Partial<Record<keyof T, boolean>>
64
+ dirty: ComputedRef<boolean>
65
+ valid: ComputedRef<boolean>
66
+ validating: Ref<boolean>
67
+ submitting: Ref<boolean>
68
+ submitCount: Ref<number>
69
+ isDirty: (name: keyof T) => boolean
70
+ /** Props for a kit control: `<LpInput v-bind="form.field('email')" />`. */
71
+ field: <K extends keyof T>(name: K) => FieldBinding<T[K]>
72
+ set: <K extends keyof T>(name: K, value: T[K]) => void
73
+ setValues: (values: Partial<T>) => void
74
+ touch: (name: keyof T) => void
75
+ /** Run the checks for one field, or all of them; resolves to whether it passed. */
76
+ validate: (name?: keyof T) => Promise<boolean>
77
+ /** Errors from elsewhere, a server reply most often. Cleared field by field on edit. */
78
+ setErrors: (errors: FormErrors<T>) => void
79
+ /** Back to `values` (or the current initial), which also become the new baseline. */
80
+ reset: (values?: T) => void
81
+ /**
82
+ * Wrap the submit handler: touches everything, validates, and calls `fn`
83
+ * only when valid and not already submitting. A successful `fn` makes the
84
+ * submitted values the new baseline.
85
+ */
86
+ handleSubmit: (fn: (values: T) => unknown) => (event?: Event) => Promise<void>
87
+ }
88
+
89
+ function clone<T>(value: T): T {
90
+ try {
91
+ return structuredClone(toRaw(value))
92
+ } catch {
93
+ return JSON.parse(JSON.stringify(value)) as T
94
+ }
95
+ }
96
+
97
+ export function deepEqual(a: unknown, b: unknown): boolean {
98
+ if (Object.is(a, b)) return true
99
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
100
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false
101
+ if (Array.isArray(a) !== Array.isArray(b)) return false
102
+ const ka = Object.keys(a)
103
+ const kb = Object.keys(b)
104
+ if (ka.length !== kb.length) return false
105
+ return ka.every((k) => deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]))
106
+ }
107
+
108
+ function message(result: FormErrorMessage): string | undefined {
109
+ return typeof result === "string" && result ? result : undefined
110
+ }
111
+
112
+ export function useForm<T extends object>(options: UseFormOptions<T>): UseForm<T> {
113
+ let baseline = clone(toValue(options.initial))
114
+ const values = reactive(clone(baseline)) as UnwrapRef<T>
115
+ const plain = () => values as unknown as T
116
+ const errors = reactive<FormErrors<T>>({}) as FormErrors<T>
117
+ const external = reactive<FormErrors<T>>({}) as FormErrors<T>
118
+ const touched = reactive<Partial<Record<keyof T, boolean>>>({}) as Partial<Record<keyof T, boolean>>
119
+ const validating = ref(false)
120
+ const submitting = ref(false)
121
+ const submitCount = ref(0)
122
+ const runs = new Map<keyof T | "form", number>()
123
+ const baseVersion = ref(0)
124
+
125
+ const keys = (): (keyof T)[] => Object.keys(plain()) as (keyof T)[]
126
+
127
+ const dirty = computed(() => {
128
+ void baseVersion.value
129
+ return !deepEqual(plain(), baseline)
130
+ })
131
+
132
+ function isDirty(name: keyof T): boolean {
133
+ void baseVersion.value
134
+ return !deepEqual(plain()[name], baseline[name])
135
+ }
136
+
137
+ async function checkField(name: keyof T): Promise<string | undefined> {
138
+ const rule = options.rules?.[name]
139
+ const list = (Array.isArray(rule) ? rule : rule ? [rule] : []) as FieldRule<unknown, T>[]
140
+ for (const r of list) {
141
+ const found = message(await r(plain()[name], plain()))
142
+ if (found) return found
143
+ }
144
+ return undefined
145
+ }
146
+
147
+ async function runField(name: keyof T): Promise<boolean> {
148
+ const run = (runs.get(name) ?? 0) + 1
149
+ runs.set(name, run)
150
+ const found = await checkField(name)
151
+ if (runs.get(name) !== run) return !errors[name]
152
+ if (found) errors[name] = found
153
+ else delete errors[name]
154
+ return !found && !external[name]
155
+ }
156
+
157
+ async function runForm(): Promise<boolean> {
158
+ if (!options.validate) return true
159
+ const run = (runs.get("form") ?? 0) + 1
160
+ runs.set("form", run)
161
+ const found = (await options.validate(plain())) ?? {}
162
+ if (runs.get("form") !== run) return true
163
+ let ok = true
164
+ for (const [name, text] of Object.entries(found) as [keyof T | "form", string | undefined][]) {
165
+ if (!text) continue
166
+ ok = false
167
+ if (!errors[name]) errors[name] = text
168
+ }
169
+ return ok
170
+ }
171
+
172
+ async function validate(name?: keyof T): Promise<boolean> {
173
+ validating.value = true
174
+ try {
175
+ if (name !== undefined) return await runField(name)
176
+ if (errors.form) delete errors.form
177
+ const fields = await Promise.all(keys().map(runField))
178
+ const form = await runForm()
179
+ return fields.every(Boolean) && form && !Object.values(external).some(Boolean)
180
+ } finally {
181
+ validating.value = false
182
+ }
183
+ }
184
+
185
+ function touch(name: keyof T): void {
186
+ touched[name] = true
187
+ void validate(name)
188
+ }
189
+
190
+ function set<K extends keyof T>(name: K, value: T[K]): void {
191
+ ;(values as unknown as T)[name] = value
192
+ }
193
+
194
+ function setValues(next: Partial<T>): void {
195
+ for (const [k, v] of Object.entries(next) as [keyof T, T[keyof T]][]) set(k, v)
196
+ }
197
+
198
+ function clearState(): void {
199
+ for (const k of Object.keys(errors)) delete errors[k as keyof T]
200
+ for (const k of Object.keys(external)) delete external[k as keyof T]
201
+ for (const k of Object.keys(touched)) delete touched[k as keyof T]
202
+ runs.clear()
203
+ submitCount.value = 0
204
+ }
205
+
206
+ function adopt(next: T): void {
207
+ baseline = clone(next)
208
+ const target = values as unknown as Record<string, unknown>
209
+ for (const k of Object.keys(target)) if (!(k in (baseline as object))) delete target[k]
210
+ Object.assign(target, clone(baseline))
211
+ baseVersion.value++
212
+ }
213
+
214
+ function reset(next?: T): void {
215
+ adopt(next ?? toValue(options.initial))
216
+ clearState()
217
+ }
218
+
219
+ function setErrors(next: FormErrors<T>): void {
220
+ for (const [k, v] of Object.entries(next) as [keyof T | "form", string | undefined][]) {
221
+ if (v) external[k] = v
222
+ else delete external[k]
223
+ }
224
+ }
225
+
226
+ watch(
227
+ () => toValue(options.initial),
228
+ (next) => {
229
+ if (!dirty.value && !deepEqual(next, baseline)) adopt(next)
230
+ },
231
+ { deep: true },
232
+ )
233
+
234
+ // An edit re-checks a field already touched, and drops an external error on it.
235
+ let snapshot = clone(plain())
236
+ watch(
237
+ plain,
238
+ () => {
239
+ const now = clone(plain())
240
+ for (const name of Object.keys(now) as (keyof T)[]) {
241
+ if (deepEqual(now[name], snapshot[name])) continue
242
+ if (external[name]) delete external[name]
243
+ if (touched[name] || submitCount.value) void validate(name)
244
+ }
245
+ snapshot = now
246
+ },
247
+ { deep: true },
248
+ )
249
+
250
+ const merged = computed<FormErrors<T>>(() => ({ ...errors, ...external }))
251
+ const visibleErrors = computed<FormErrors<T>>(() => {
252
+ const all = merged.value
253
+ if (submitCount.value) return all
254
+ const shown: FormErrors<T> = {}
255
+ for (const [k, v] of Object.entries(all) as [keyof T, string][]) {
256
+ if (touched[k] || external[k]) shown[k] = v
257
+ }
258
+ return shown
259
+ })
260
+ const valid = computed(() => !Object.values(merged.value).some(Boolean))
261
+
262
+ function field<K extends keyof T>(name: K): FieldBinding<T[K]> {
263
+ const error = visibleErrors.value[name]
264
+ return {
265
+ modelValue: (values as unknown as T)[name],
266
+ "onUpdate:modelValue": (value: T[K]) => set(name, value),
267
+ onBlur: () => touch(name),
268
+ error,
269
+ invalid: !!error,
270
+ }
271
+ }
272
+
273
+ function handleSubmit(fn: (values: T) => unknown) {
274
+ return async (event?: Event) => {
275
+ event?.preventDefault()
276
+ if (submitting.value) return
277
+ submitting.value = true
278
+ try {
279
+ submitCount.value++
280
+ for (const name of keys()) touched[name] = true
281
+ if (!(await validate())) return
282
+ const submitted = clone(plain())
283
+ await fn(submitted)
284
+ if (deepEqual(plain(), submitted)) {
285
+ baseline = submitted
286
+ baseVersion.value++
287
+ }
288
+ } finally {
289
+ submitting.value = false
290
+ }
291
+ }
292
+ }
293
+
294
+ return {
295
+ values,
296
+ errors: merged,
297
+ visibleErrors,
298
+ touched,
299
+ dirty,
300
+ valid,
301
+ validating,
302
+ submitting,
303
+ submitCount,
304
+ isDirty,
305
+ field,
306
+ set,
307
+ setValues,
308
+ touch,
309
+ validate,
310
+ setErrors,
311
+ reset,
312
+ handleSubmit,
313
+ }
314
+ }
@@ -69,9 +69,16 @@ export interface UseFormDraftOptions<T> {
69
69
  }
70
70
 
71
71
  export interface UseFormDraft<T> {
72
- /** A draft was found for this key, and has not been restored or discarded. */
72
+ /**
73
+ * A draft was found for this key, and has not been restored or discarded.
74
+ * Only a draft found on reading counts: the writes this composable makes as
75
+ * the user types never raise it. While it is up, automatic writes are held
76
+ * back, so the draft on offer cannot be overwritten by the form's own state
77
+ * (an async load of the record, or typing past the banner) before the user
78
+ * has answered.
79
+ */
73
80
  hasDraft: Ref<boolean>
74
- /** When that draft was written, ISO-8601, or `null` when there is none. */
81
+ /** When the found draft was written, ISO-8601, or `null` when there is none. */
75
82
  savedAt: Ref<string | null>
76
83
  /**
77
84
  * The stored value, or `null`. Reading does not apply it — the caller decides
@@ -80,7 +87,7 @@ export interface UseFormDraft<T> {
80
87
  read: () => T | null
81
88
  /** Forget the draft. Call after applying it, and after a successful save. */
82
89
  discard: () => void
83
- /** Write now, ignoring `dirty`. For "save and come back to this later". */
90
+ /** Write now, ignoring `dirty` and a pending draft. For "save and come back to this later". */
84
91
  save: () => void
85
92
  }
86
93
 
@@ -132,6 +139,17 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
132
139
  return String(toValue(options.key) || "").trim()
133
140
  }
134
141
 
142
+ // An envelope still worth offering, or null. No side effects: the key watcher
143
+ // only looks, and `read` decides what to clear.
144
+ function fresh(parsed: DraftEnvelope<T> | null): boolean {
145
+ if (!parsed || parsed.version !== ENVELOPE_VERSION) return false
146
+ if (maxAge <= 0) return true
147
+ // NaN when the stamp is unreadable, and NaN > maxAge is false — an
148
+ // unreadable stamp is treated as "no idea how old", which errs towards
149
+ // offering the draft rather than throwing away somebody's typing.
150
+ return !(Date.now() - Date.parse(parsed.savedAt) > maxAge)
151
+ }
152
+
135
153
  function read(): T | null {
136
154
  const store = storage()
137
155
  const key = currentKey()
@@ -145,20 +163,10 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
145
163
  if (!raw) return null
146
164
  try {
147
165
  const parsed = JSON.parse(raw) as DraftEnvelope<T>
148
- if (!parsed || parsed.version !== ENVELOPE_VERSION) {
166
+ if (!fresh(parsed)) {
149
167
  discard()
150
168
  return null
151
169
  }
152
- if (maxAge > 0) {
153
- const age = Date.now() - Date.parse(parsed.savedAt)
154
- // NaN when the stamp is unreadable, and NaN > maxAge is false — an
155
- // unreadable stamp is treated as "no idea how old", which errs towards
156
- // offering the draft rather than throwing away somebody's typing.
157
- if (age > maxAge) {
158
- discard()
159
- return null
160
- }
161
- }
162
170
  return parsed.value
163
171
  } catch {
164
172
  // Corrupt or hand-edited. Nothing to rescue, and leaving it means asking
@@ -172,13 +180,12 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
172
180
  const store = storage()
173
181
  const key = currentKey()
174
182
  if (!store || !key) return
175
- const stamp = new Date().toISOString()
176
183
  try {
177
184
  store.setItem(
178
185
  key,
179
186
  JSON.stringify({
180
187
  version: ENVELOPE_VERSION,
181
- savedAt: stamp,
188
+ savedAt: new Date().toISOString(),
182
189
  value,
183
190
  } satisfies DraftEnvelope<T>),
184
191
  )
@@ -187,8 +194,6 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
187
194
  // unharmed; it simply has no safety net.
188
195
  return
189
196
  }
190
- hasDraft.value = true
191
- savedAt.value = stamp
192
197
  }
193
198
 
194
199
  function discard() {
@@ -226,7 +231,7 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
226
231
  if (!raw) return
227
232
  try {
228
233
  const parsed = JSON.parse(raw) as DraftEnvelope<T>
229
- if (parsed?.version !== ENVELOPE_VERSION) return
234
+ if (!fresh(parsed)) return
230
235
  hasDraft.value = true
231
236
  savedAt.value = parsed.savedAt
232
237
  } catch {
@@ -240,7 +245,7 @@ export function useFormDraft<T>(options: UseFormDraftOptions<T>): UseFormDraft<T
240
245
  watch(
241
246
  () => [toValue(options.value), toValue(options.dirty), toValue(options.disabled)],
242
247
  ([value, dirty, disabled]) => {
243
- if (disabled) return
248
+ if (disabled || hasDraft.value) return
244
249
  // `dirty` unset means "every change is worth keeping"; the option exists
245
250
  // so a form that knows it is untouched leaves no trace.
246
251
  if (options.dirty !== undefined && !dirty) return