@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.
- package/README.md +121 -7
- package/dist/assets/{LpCommandPalette-B-0Est4Z.css → LpCommandPalette-nGNnzXqQ.css} +21 -21
- package/dist/assets/{LpDropzone-BBoWvi-J.css → LpDropzone-i4BdMi1H.css} +12 -12
- package/dist/assets/{LpImageEditor-Ct7Y3aNu.css → LpImageEditor-CbMA82g8.css} +6 -6
- package/dist/assets/{LpRollingText-CR1mb0Vt.css → LpRollingText-BGnE6q9j.css} +3 -0
- package/dist/assets/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-CqYX3yy6.css} +28 -7
- package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
- package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-CPwWxTJS.css} +49 -9
- package/dist/chunks/ContextMenuItems-BtKU7074.js +124 -0
- package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-BkaGXTjx.js} +26 -9
- package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-B3_9E1Xa.js} +28 -12
- package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-CnfFVIwC.js} +26 -12
- package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BwX8uo4_.js} +54 -49
- package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-B6ilrM7T.js} +16 -9
- package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-C53_RaGF.js} +53 -26
- package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-C8IgSMsi.js} +52 -24
- package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-C1XStdOo.js} +19 -10
- package/dist/chunks/LpBreadcrumbs-CQJd73Gm.js +95 -0
- package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-CKSwC6Mu.js} +45 -15
- package/dist/chunks/{LpCalendar-B7Ko5RdR.js → LpCalendar-DrA2McTU.js} +1 -1
- package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-C2IvucmX.js} +1 -1
- package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-Bkj13CbW.js} +8 -4
- package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-ca7ANFyW.js} +49 -17
- package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-zz4VGkG4.js} +75 -26
- package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-DCw5jR1t.js} +59 -17
- package/dist/chunks/LpConfirmHost-CbywMpMP.js +49 -0
- package/dist/chunks/LpContextMenu-BqkAYa9X.js +69 -0
- package/dist/chunks/LpDatePicker-DuusgT69.js +195 -0
- package/dist/chunks/LpDateRangePicker-DHeuznfC.js +208 -0
- package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
- package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-Dt4aB4Fr.js} +65 -28
- package/dist/chunks/{LpDropdownMenu-DJpoqRCb.js → LpDropdownMenu-DdJGQzPn.js} +2 -1
- package/dist/chunks/{LpEmptyState-Dsdqz1Wj.js → LpEmptyState-BJbZXC8D.js} +1 -1
- package/dist/chunks/LpFieldNotice-wtieLUTj.js +64 -0
- package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CNurOvkV.js} +25 -10
- package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-D_qRRdsH.js} +3 -3
- package/dist/chunks/LpFormField-CtVYj6tO.js +101 -0
- package/dist/chunks/{LpIcon-CCnX5_2j.js → LpIcon-DYtaid0K.js} +20 -6
- package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-D-YGQbEv.js} +58 -20
- package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-DSDahF23.js} +148 -61
- package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
- package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-KypbgOyo.js} +300 -72
- package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-Cc84LaM3.js} +39 -14
- package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-YKr-cQPN.js} +7 -4
- package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DMMDQHAu.js} +52 -29
- package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-BVPDyIup.js} +10 -7
- package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-CCwyJP5C.js} +79 -33
- package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-B77O-z28.js} +21 -8
- package/dist/chunks/{LpNumberFlow-amWYaeuA.js → LpNumberFlow-z4WPZcZu.js} +22 -12
- package/dist/chunks/{LpOtpInput-y2v-pmiN.js → LpOtpInput-CziPF3Oq.js} +37 -8
- package/dist/chunks/{LpPagination-5FxjmmB5.js → LpPagination-BcDlo3r_.js} +31 -20
- package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-eKzQhbQP.js} +11 -8
- package/dist/chunks/LpPhoneInput-BCbrrhgE.js +336 -0
- package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BEJ2SfI6.js} +84 -28
- package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-DCKeqiVn.js} +5 -2
- package/dist/chunks/{LpProgress-Btq5W4Ut.js → LpProgress-CBU1iI69.js} +5 -4
- package/dist/chunks/{LpRadioGroup-xPIuVDO_.js → LpRadioGroup-DmEW0l78.js} +9 -2
- package/dist/chunks/LpRangeCalendar-DZuaRV0W.js +331 -0
- package/dist/chunks/{LpRollingText-CM5gkSeF.js → LpRollingText-DB7upf-5.js} +1 -1
- package/dist/chunks/{LpScrollArea-D8xyKXcF.js → LpScrollArea-Bry1fdOM.js} +14 -2
- package/dist/chunks/{LpSection-aM6J_u51.js → LpSection-tKEH0jYf.js} +1 -1
- package/dist/chunks/{LpSegmented-fV0Sk92p.js → LpSegmented-C09EEam2.js} +8 -4
- package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DNIL6MB9.js} +50 -31
- package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-CQNB0-yU.js} +30 -22
- package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-kx-LdqNU.js} +16 -11
- package/dist/chunks/{LpSkeleton-CTI-vBgW.js → LpSkeleton-BB6XIV-w.js} +1 -1
- package/dist/chunks/{LpSlider-BwNIBkAK.js → LpSlider-Cuzmrd7p.js} +7 -2
- package/dist/chunks/{LpSparkline-C6xQ2dwv.js → LpSparkline-CB80N8SJ.js} +11 -4
- package/dist/chunks/{LpStat-CQaW8R--.js → LpStat-B-7udBRP.js} +2 -2
- package/dist/chunks/{LpStepper-Bk-XaQYU.js → LpStepper-C7XiVKZj.js} +34 -19
- package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-OrcOJX9m.js} +41 -27
- package/dist/chunks/LpTableFilter-BV8PpLM9.js +158 -0
- package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
- package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-29iW1XOx.js} +20 -6
- package/dist/chunks/LpTagsInput-D1SuqbhB.js +300 -0
- package/dist/chunks/LpTextarea-uuScWo6-.js +77 -0
- package/dist/chunks/LpThemeProvider-Be4r2TQX.js +35 -0
- package/dist/chunks/LpThemeSwitcher-B4aVPbNC.js +166 -0
- package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
- package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-Cqe7fXnh.js} +14 -8
- package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster-DpJvoCOh.js} +94 -31
- package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CXVKcOXT.js} +14 -6
- package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
- package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-6ozNrQKh.js} +8 -2
- package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-ugcv25xj.js} +44 -15
- package/dist/chunks/icons-CkA0njzm.js +43 -0
- package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-DJm1JSsY.js} +215 -260
- package/dist/component-names.d.ts +1 -1
- package/dist/components/ContextMenuItems.vue.d.ts +8 -0
- package/dist/components/ContextMenuItems.vue.js +4 -0
- package/dist/components/LayoutCanvas.vue.js +1 -1
- package/dist/components/LayoutNode.vue.d.ts +4 -0
- package/dist/components/LayoutNode.vue.js +1 -1
- package/dist/components/LpActionBar.vue.d.ts +3 -0
- package/dist/components/LpActionBar.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.d.ts +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.d.ts +9 -0
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.d.ts +10 -0
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.d.ts +31 -6
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.d.ts +3 -3
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBadge.vue.d.ts +2 -2
- package/dist/components/LpBreadcrumbs.vue.d.ts +13 -9
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpButton.vue.d.ts +35 -98
- package/dist/components/LpButton.vue.js +1 -1
- package/dist/components/LpCalendar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.d.ts +47 -3
- package/dist/components/LpChart.vue.js +377 -97
- package/dist/components/LpCheckbox.vue.js +1 -1
- package/dist/components/LpCodeBlock.vue.d.ts +2 -0
- package/dist/components/LpCodeBlock.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.d.ts +2 -2
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.d.ts +14 -3
- package/dist/components/LpCommandPalette.vue.js +142 -77
- package/dist/components/LpConfirmDialog.vue.d.ts +15 -3
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.d.ts +9 -0
- package/dist/components/LpConfirmHost.vue.js +4 -0
- package/dist/components/LpContextMenu.vue.d.ts +3 -5
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpDatePicker.vue.d.ts +24 -6
- package/dist/components/LpDatePicker.vue.js +1 -1
- package/dist/components/LpDateRangePicker.vue.d.ts +60 -0
- package/dist/components/LpDateRangePicker.vue.js +4 -0
- package/dist/components/LpDisclosure.vue.js +1 -1
- package/dist/components/LpDrawer.vue.d.ts +10 -0
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +23 -7
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFieldNotice.vue.d.ts +8 -0
- package/dist/components/LpFieldNotice.vue.js +4 -0
- package/dist/components/LpFileTree.vue.d.ts +11 -2
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.d.ts +13 -1
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpIcon.vue.d.ts +0 -1
- package/dist/components/LpIcon.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +93 -58
- package/dist/components/LpInput.vue.d.ts +22 -3
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpLightbox.vue.d.ts +17 -0
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLink.vue.d.ts +11 -11
- package/dist/components/LpLink.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.d.ts +72 -3
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.d.ts +4 -4
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.d.ts +16 -6
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.d.ts +0 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.d.ts +11 -2
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNumberField.vue.d.ts +2 -0
- package/dist/components/LpNumberField.vue.js +1 -1
- package/dist/components/LpNumberFlow.vue.js +1 -1
- package/dist/components/LpOtpInput.vue.js +1 -1
- package/dist/components/LpPagination.vue.d.ts +2 -0
- package/dist/components/LpPagination.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpPhoneInput.vue.d.ts +13 -0
- package/dist/components/LpPhoneInput.vue.js +1 -1
- package/dist/components/LpPie.vue.d.ts +21 -2
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpPopover.vue.d.ts +2 -0
- package/dist/components/LpPopover.vue.js +1 -1
- package/dist/components/LpProgress.vue.d.ts +1 -1
- package/dist/components/LpProgress.vue.js +1 -1
- package/dist/components/LpRadioGroup.vue.js +1 -1
- package/dist/components/LpRangeCalendar.vue.d.ts +51 -0
- package/dist/components/LpRangeCalendar.vue.js +4 -0
- package/dist/components/LpRollingText.vue.d.ts +1 -1
- package/dist/components/LpRollingText.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.d.ts +2 -2
- package/dist/components/LpScrollArea.vue.js +1 -1
- package/dist/components/LpSection.vue.d.ts +1 -1
- package/dist/components/LpSection.vue.js +1 -1
- package/dist/components/LpSegmented.vue.d.ts +5 -0
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.d.ts +1 -0
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.d.ts +7 -1
- package/dist/components/LpShortcutsDialog.vue.js +60 -15
- package/dist/components/LpSidebar.vue.d.ts +6 -2
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpSkeleton.vue.js +1 -1
- package/dist/components/LpSlider.vue.js +1 -1
- package/dist/components/LpSparkline.vue.d.ts +1 -0
- package/dist/components/LpSparkline.vue.js +1 -1
- package/dist/components/LpStat.vue.js +1 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpSwitch.vue.js +1 -1
- package/dist/components/LpTable.vue.d.ts +107 -3
- package/dist/components/LpTable.vue.js +540 -128
- package/dist/components/LpTableFilter.vue.d.ts +31 -0
- package/dist/components/LpTableFilter.vue.js +4 -0
- package/dist/components/LpTableOfContents.vue.d.ts +4 -2
- package/dist/components/LpTableOfContents.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +2 -0
- package/dist/components/LpTabs.vue.js +1 -1
- package/dist/components/LpTagsInput.vue.d.ts +77 -0
- package/dist/components/LpTagsInput.vue.js +4 -0
- package/dist/components/LpTextarea.vue.d.ts +19 -6
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeProvider.vue.d.ts +22 -0
- package/dist/components/LpThemeProvider.vue.js +4 -0
- package/dist/components/LpThemeSwitcher.vue.d.ts +15 -2
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTilt.vue.js +1 -1
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpToaster.vue.d.ts +6 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTooltip.vue.d.ts +1 -2
- package/dist/components/LpTooltip.vue.js +1 -1
- package/dist/components/LpTooltipProvider.vue.d.ts +22 -0
- package/dist/components/LpTooltipProvider.vue.js +4 -0
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpUptimeBar.vue.d.ts +4 -0
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/chart.d.ts +32 -1
- package/dist/components/chart.js +42 -6
- package/dist/components/codeHighlight.d.ts +6 -0
- package/dist/components/codeHighlight.js +92 -20
- package/dist/components/color.d.ts +1 -1
- package/dist/components/color.js +20 -9
- package/dist/components/commandSearch.js +3 -0
- package/dist/components/confirm.d.ts +25 -0
- package/dist/components/confirm.js +26 -0
- package/dist/components/countries.d.ts +6 -0
- package/dist/components/countries.js +7 -0
- package/dist/components/dateRange.d.ts +43 -0
- package/dist/components/dateRange.js +76 -0
- package/dist/components/exportData.d.ts +58 -0
- package/dist/components/exportData.js +165 -0
- package/dist/components/fileTree.js +16 -7
- package/dist/components/imageCrop.d.ts +42 -0
- package/dist/components/imageCrop.js +47 -0
- package/dist/components/link.d.ts +12 -0
- package/dist/components/link.js +19 -0
- package/dist/components/logSearch.d.ts +18 -0
- package/dist/components/logSearch.js +36 -0
- package/dist/components/modalDepth.d.ts +18 -2
- package/dist/components/modalDepth.js +17 -5
- package/dist/components/otp.d.ts +5 -0
- package/dist/components/otp.js +10 -0
- package/dist/components/phone.d.ts +41 -0
- package/dist/components/phone.js +80 -0
- package/dist/components/sidebar.d.ts +7 -1
- package/dist/components/tableFilter.d.ts +36 -0
- package/dist/components/tableFilter.js +84 -0
- package/dist/components/tableSort.d.ts +8 -0
- package/dist/components/tableSort.js +31 -0
- package/dist/components/tags.d.ts +20 -0
- package/dist/components/tags.js +37 -0
- package/dist/composables/useField.d.ts +38 -0
- package/dist/composables/useField.js +45 -0
- package/dist/composables/useFieldNotice.d.ts +17 -0
- package/dist/composables/useFieldNotice.js +28 -0
- package/dist/composables/useForm.d.ts +53 -0
- package/dist/composables/useForm.js +207 -0
- package/dist/composables/useFormDraft.d.ts +10 -3
- package/dist/composables/useFormDraft.js +9 -14
- package/dist/composables/useHotkeys.d.ts +81 -5
- package/dist/composables/useHotkeys.js +223 -25
- package/dist/composables/useInputFilter.d.ts +17 -0
- package/dist/composables/useInputFilter.js +47 -10
- package/dist/composables/useInputMask.d.ts +59 -0
- package/dist/composables/useInputMask.js +170 -0
- package/dist/composables/useLpConfig.d.ts +24 -0
- package/dist/composables/useLpConfig.js +44 -0
- package/dist/composables/usePaintSelect.d.ts +22 -0
- package/dist/composables/usePaintSelect.js +169 -0
- package/dist/composables/useRovingFocus.js +7 -0
- package/dist/composables/useShift.js +7 -2
- package/dist/composables/useTilt.d.ts +2 -2
- package/dist/composables/useTilt.js +5 -6
- package/dist/composables/useToast.d.ts +20 -0
- package/dist/composables/useToast.js +28 -4
- package/dist/composables/useZoomPan.d.ts +3 -3
- package/dist/composables/useZoomPan.js +4 -4
- package/dist/icons-all.d.ts +1 -0
- package/dist/icons-all.js +3 -0
- package/dist/icons.d.ts +14 -1
- package/dist/index.d.ts +60 -4
- package/dist/index.js +245 -141
- package/dist/kit-icons.d.ts +2 -0
- package/dist/layout/tree.d.ts +2 -0
- package/dist/layout/tree.js +16 -1
- package/dist/layout/useLayout.d.ts +15 -0
- package/dist/layout/useLayout.js +41 -14
- package/dist/theme/backdrop.d.ts +12 -0
- package/dist/theme/presets.d.ts +0 -2
- package/dist/theme/useTheme.d.ts +32 -9
- package/package.json +18 -8
- package/src/component-names.ts +9 -7
- package/src/components/ContextMenuItems.vue +88 -0
- package/src/components/LayoutCanvas.vue +26 -6
- package/src/components/LayoutNode.vue +30 -4
- package/src/components/LpActionBar.vue +31 -8
- package/src/components/LpAddressInput.vue +57 -41
- package/src/components/LpAlert.vue +16 -3
- package/src/components/LpAppShell.vue +45 -12
- package/src/components/LpAutocomplete.vue +47 -10
- package/src/components/LpAvatar.vue +20 -6
- package/src/components/LpBreadcrumbs.vue +65 -41
- package/src/components/LpButton.vue +60 -14
- package/src/components/LpChart.vue +351 -28
- package/src/components/LpCheckbox.vue +7 -3
- package/src/components/LpCodeBlock.vue +50 -14
- package/src/components/LpColorPicker.vue +73 -10
- package/src/components/LpCommandPalette.vue +115 -20
- package/src/components/LpConfirmDialog.vue +68 -17
- package/src/components/LpConfirmHost.vue +59 -0
- package/src/components/LpContextMenu.vue +15 -116
- package/src/components/LpDatePicker.vue +104 -68
- package/src/components/LpDateRangePicker.vue +224 -0
- package/src/components/LpDrawer.vue +56 -19
- package/src/components/LpDropdownMenu.vue +1 -0
- package/src/components/LpDropzone.vue +21 -4
- package/src/components/LpFieldNotice.vue +65 -0
- package/src/components/LpFileTree.vue +33 -7
- package/src/components/LpFormField.vue +59 -7
- package/src/components/LpIcon.vue +22 -4
- package/src/components/LpImageEditor.vue +96 -57
- package/src/components/LpInput.vue +62 -11
- package/src/components/LpLightbox.vue +140 -29
- package/src/components/LpLink.vue +24 -13
- package/src/components/LpLogViewer.vue +311 -39
- package/src/components/LpMapPicker.vue +36 -13
- package/src/components/LpMediaGallery.vue +8 -2
- package/src/components/LpModal.vue +54 -28
- package/src/components/LpModalNav.vue +5 -2
- package/src/components/LpNotificationBell.vue +77 -23
- package/src/components/LpNumberField.vue +21 -6
- package/src/components/LpNumberFlow.vue +26 -10
- package/src/components/LpOtpInput.vue +43 -8
- package/src/components/LpPagination.vue +23 -7
- package/src/components/LpPasswordInput.vue +4 -1
- package/src/components/LpPhoneInput.vue +224 -39
- package/src/components/LpPie.vue +102 -20
- package/src/components/LpPopover.vue +4 -0
- package/src/components/LpProgress.vue +5 -3
- package/src/components/LpRadioGroup.vue +9 -2
- package/src/components/LpRangeCalendar.vue +365 -0
- package/src/components/LpRollingText.vue +3 -0
- package/src/components/LpScrollArea.vue +22 -2
- package/src/components/LpSegmented.vue +12 -2
- package/src/components/LpSelect.vue +0 -0
- package/src/components/LpShortcutsDialog.vue +83 -9
- package/src/components/LpSidebar.vue +14 -9
- package/src/components/LpSidebarNav.vue +17 -5
- package/src/components/LpSkeleton.vue +4 -1
- package/src/components/LpSlider.vue +7 -1
- package/src/components/LpSparkline.vue +11 -3
- package/src/components/LpStepper.vue +23 -10
- package/src/components/LpSwitch.vue +31 -8
- package/src/components/LpTable.vue +668 -90
- package/src/components/LpTableFilter.vue +175 -0
- package/src/components/LpTableOfContents.vue +50 -32
- package/src/components/LpTabs.vue +23 -6
- package/src/components/LpTagsInput.vue +364 -0
- package/src/components/LpTextarea.vue +44 -6
- package/src/components/LpThemeProvider.vue +41 -0
- package/src/components/LpThemeSwitcher.vue +76 -33
- package/src/components/LpTilt.vue +2 -2
- package/src/components/LpTimeField.vue +14 -3
- package/src/components/LpToaster.vue +115 -27
- package/src/components/LpTooltip.vue +29 -5
- package/src/components/LpTooltipProvider.vue +24 -0
- package/src/components/LpTopologyCanvas.vue +19 -2
- package/src/components/LpUptimeBar.vue +46 -8
- package/src/components/chart.ts +82 -8
- package/src/components/codeHighlight.ts +111 -15
- package/src/components/color.ts +30 -9
- package/src/components/commandSearch.ts +6 -0
- package/src/components/confirm.ts +58 -0
- package/src/components/countries.ts +12 -0
- package/src/components/dateRange.ts +134 -0
- package/src/components/exportData.ts +228 -0
- package/src/components/fileTree.ts +19 -7
- package/src/components/imageCrop.ts +107 -0
- package/src/components/link.ts +34 -0
- package/src/components/logSearch.ts +46 -0
- package/src/components/modalDepth.ts +48 -6
- package/src/components/otp.ts +16 -0
- package/src/components/phone.ts +140 -0
- package/src/components/sidebar.ts +7 -1
- package/src/components/tableFilter.ts +122 -0
- package/src/components/tableSort.ts +37 -0
- package/src/components/tags.ts +62 -0
- package/src/composables/useField.ts +83 -0
- package/src/composables/useFieldNotice.ts +59 -0
- package/src/composables/useForm.ts +314 -0
- package/src/composables/useFormDraft.ts +25 -20
- package/src/composables/useHotkeys.ts +355 -28
- package/src/composables/useInputFilter.ts +77 -14
- package/src/composables/useInputMask.ts +254 -0
- package/src/composables/useLpConfig.ts +87 -0
- package/src/composables/usePaintSelect.ts +243 -0
- package/src/composables/useRovingFocus.ts +11 -0
- package/src/composables/useShift.ts +13 -2
- package/src/composables/useTilt.ts +5 -6
- package/src/composables/useToast.ts +56 -4
- package/src/composables/useZoomPan.ts +6 -6
- package/src/iconSubset.d.mts +10 -0
- package/src/iconSubset.mjs +51 -0
- package/src/icons-all.ts +10 -0
- package/src/icons.ts +59 -8
- package/src/index.ts +73 -4
- package/src/kit-icons.ts +5 -0
- package/src/layout/tree.ts +30 -0
- package/src/layout/useLayout.ts +68 -18
- package/src/nuxt.ts +87 -17
- package/src/runtime/config.plugin.ts +14 -0
- package/src/runtime/theme.plugin.ts +43 -23
- package/src/theme/backdrop.ts +69 -0
- package/src/theme/presets.ts +6 -64
- package/src/theme/useTheme.ts +206 -100
- package/src/tokens/tokens.css +46 -0
- package/dist/chunks/LpBreadcrumbs-p4jHe_V-.js +0 -62
- package/dist/chunks/LpContextMenu-CwW1QXe_.js +0 -217
- package/dist/chunks/LpDatePicker-CwOToxGq.js +0 -164
- package/dist/chunks/LpFormField-CazBc2-a.js +0 -62
- package/dist/chunks/LpPhoneInput-BDSKVl_z.js +0 -209
- 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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|