@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
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
getCurrentInstance,
|
|
4
|
+
onBeforeUnmount,
|
|
5
|
+
onMounted,
|
|
6
|
+
ref,
|
|
7
|
+
shallowReactive,
|
|
8
|
+
toValue,
|
|
9
|
+
type MaybeRefOrGetter,
|
|
10
|
+
} from "vue"
|
|
11
|
+
import { topModalElement } from "../components/modalDepth"
|
|
2
12
|
|
|
3
13
|
/*
|
|
4
14
|
* One keydown listener over a registry of bindings, so a shortcut is declared
|
|
@@ -20,11 +30,30 @@ import { computed, onBeforeUnmount, onMounted, shallowReactive, type MaybeRefOrG
|
|
|
20
30
|
*
|
|
21
31
|
* Modifier note: Ctrl and Meta are one modifier here, since macOS binds ⌘ where
|
|
22
32
|
* the rest bind Ctrl. Label the difference in the UI, not in the binding.
|
|
33
|
+
*
|
|
34
|
+
* Sequences ("g i", as on GitHub and Gmail) are bindings with `sequence`. A key
|
|
35
|
+
* that starts a sequence is held while the next one is awaited: if it
|
|
36
|
+
* continues a sequence, the sequence runs; if not, or after the timeout, the
|
|
37
|
+
* single-key binding on the held key (if any) runs, and the new key is handled
|
|
38
|
+
* as a fresh press. A key that starts no sequence runs at once, so sequences
|
|
39
|
+
* cost nothing to the bindings they do not collide with.
|
|
23
40
|
*/
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
/** One key of a sequence: a bare key ("g") or a full chord. */
|
|
43
|
+
export interface HotkeyStep {
|
|
27
44
|
key: string
|
|
45
|
+
code?: string
|
|
46
|
+
ctrl?: boolean
|
|
47
|
+
shift?: boolean
|
|
48
|
+
alt?: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface HotkeyBinding {
|
|
52
|
+
/**
|
|
53
|
+
* Layout-produced key, compared case-insensitively ("l", "f2", "escape").
|
|
54
|
+
* Required unless the binding is a `sequence`.
|
|
55
|
+
*/
|
|
56
|
+
key?: string
|
|
28
57
|
/**
|
|
29
58
|
* Physical key position ("KeyL", "F2", "Digit1"). Matches on its own, which
|
|
30
59
|
* is what makes non-Latin layouts work.
|
|
@@ -34,11 +63,31 @@ export interface HotkeyBinding {
|
|
|
34
63
|
ctrl?: boolean
|
|
35
64
|
shift?: boolean
|
|
36
65
|
alt?: boolean
|
|
66
|
+
/**
|
|
67
|
+
* Keys pressed one after another: `"g i"`, or steps for chords and physical
|
|
68
|
+
* positions (`[{ key: "g", code: "KeyG" }, { key: "i", code: "KeyI" }]`).
|
|
69
|
+
* The string form splits on spaces, so a Space step needs the array form.
|
|
70
|
+
*/
|
|
71
|
+
sequence?: string | (string | HotkeyStep)[]
|
|
72
|
+
/** How long a sequence waits for its next key, in ms. Default 1000. */
|
|
73
|
+
timeout?: number
|
|
74
|
+
/** Extra condition checked on every press; `false` leaves the key alone. */
|
|
75
|
+
when?: (event: KeyboardEvent) => boolean
|
|
37
76
|
/**
|
|
38
77
|
* Runs even while a text field has focus. Only for shortcuts that mean
|
|
39
78
|
* something there — Escape, and the palette the user reaches from anywhere.
|
|
40
79
|
*/
|
|
41
80
|
allowInInput?: boolean
|
|
81
|
+
/**
|
|
82
|
+
* Runs even while an overlay owns the keyboard. By default a binding declared
|
|
83
|
+
* outside the topmost modal stays quiet while that modal is open, and an
|
|
84
|
+
* Escape binding declared outside the topmost dismissable layer (dialog,
|
|
85
|
+
* popover, menu) leaves Escape to that layer, so a page-level shortcut can
|
|
86
|
+
* neither act behind a dialog nor stop the dialog from closing.
|
|
87
|
+
*/
|
|
88
|
+
overLayers?: boolean
|
|
89
|
+
/** Set `false` to ignore auto-repeat while the key is held. */
|
|
90
|
+
repeat?: boolean
|
|
42
91
|
/**
|
|
43
92
|
* How the key reference lists this binding while it is mounted. Bindings
|
|
44
93
|
* without it still work, they just stay out of `activeShortcuts`.
|
|
@@ -62,15 +111,20 @@ export interface HotkeyHelp {
|
|
|
62
111
|
/**
|
|
63
112
|
* Written the way it reads, "+"-separated. Defaults to one built from the
|
|
64
113
|
* binding; set it where the binding is not the whole story, such as the
|
|
65
|
-
*
|
|
114
|
+
* key that needs Shift to type ("?"). Sequences read with spaces: "G I".
|
|
66
115
|
*/
|
|
67
116
|
combo?: string
|
|
68
117
|
}
|
|
69
118
|
|
|
70
119
|
export interface ShortcutEntry {
|
|
71
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Rendered one <kbd> per "+"-separated key, so write it the way it reads.
|
|
122
|
+
* Spaces separate the steps of a sequence ("G I", "Ctrl+K Ctrl+S").
|
|
123
|
+
*/
|
|
72
124
|
combo: string
|
|
73
125
|
label: string
|
|
126
|
+
/** Another mounted binding answers the same keys at the same priority. */
|
|
127
|
+
conflict?: boolean
|
|
74
128
|
}
|
|
75
129
|
|
|
76
130
|
export interface ShortcutGroup {
|
|
@@ -87,12 +141,22 @@ export const isAppleKeyboard =
|
|
|
87
141
|
typeof navigator !== "undefined"
|
|
88
142
|
&& /Mac|iPhone|iPad|iPod/.test(navigator.platform || "")
|
|
89
143
|
|
|
144
|
+
/**
|
|
145
|
+
* `isAppleKeyboard` for render paths: false until mounted, so the server and
|
|
146
|
+
* the hydrating client print the same label and the client switches after.
|
|
147
|
+
*/
|
|
148
|
+
export function useAppleKeyboard() {
|
|
149
|
+
const apple = ref(false)
|
|
150
|
+
onMounted(() => (apple.value = isAppleKeyboard))
|
|
151
|
+
return apple
|
|
152
|
+
}
|
|
153
|
+
|
|
90
154
|
/**
|
|
91
155
|
* The symbol to print for a modifier on this platform. Bindings treat Ctrl and
|
|
92
156
|
* Meta as one modifier, so only the label may differ — never the binding.
|
|
93
157
|
*/
|
|
94
|
-
export function appleModifierLabel(key: string): string {
|
|
95
|
-
if (!
|
|
158
|
+
export function appleModifierLabel(key: string, apple = isAppleKeyboard): string {
|
|
159
|
+
if (!apple) return key
|
|
96
160
|
if (key === "Ctrl") return "⌘"
|
|
97
161
|
if (key === "Alt") return "⌥"
|
|
98
162
|
return key
|
|
@@ -123,17 +187,54 @@ function isTextEntry(target: EventTarget | null): boolean {
|
|
|
123
187
|
return false
|
|
124
188
|
}
|
|
125
189
|
|
|
126
|
-
|
|
190
|
+
const MODIFIER_KEYS = new Set(["control", "meta", "shift", "alt"])
|
|
191
|
+
|
|
192
|
+
type KeyLike = Pick<KeyboardEvent, "key" | "code" | "ctrlKey" | "metaKey" | "shiftKey" | "altKey">
|
|
193
|
+
|
|
194
|
+
export function matchesHotkey(event: KeyLike, binding: HotkeyBinding | HotkeyStep): boolean {
|
|
195
|
+
if (binding.key === undefined) return false
|
|
196
|
+
const key = binding.key.toLowerCase()
|
|
127
197
|
const ctrlHeld = event.ctrlKey || event.metaKey
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
|
|
198
|
+
const wantsCtrl = Boolean(binding.ctrl) || key === "control" || key === "meta"
|
|
199
|
+
if (ctrlHeld !== wantsCtrl) return false
|
|
200
|
+
// Shift is part of typing a symbol on most layouts ("?", "+"), so a binding
|
|
201
|
+
// on a symbol that does not mention Shift accepts it either way.
|
|
202
|
+
const symbol = key.length === 1 && key.toLowerCase() === key.toUpperCase()
|
|
203
|
+
const shiftFree = key === "shift" || (binding.shift === undefined && symbol)
|
|
204
|
+
if (!shiftFree && event.shiftKey !== Boolean(binding.shift)) return false
|
|
205
|
+
if (key !== "alt" && event.altKey !== Boolean(binding.alt)) return false
|
|
206
|
+
if (MODIFIER_KEYS.has(key)) return event.key.toLowerCase() === key
|
|
131
207
|
return (
|
|
132
|
-
event.key.toLowerCase() ===
|
|
208
|
+
event.key.toLowerCase() === key
|
|
133
209
|
|| (binding.code !== undefined && event.code === binding.code)
|
|
134
210
|
)
|
|
135
211
|
}
|
|
136
212
|
|
|
213
|
+
/** The keys a binding waits for, in order: one step for a plain binding. */
|
|
214
|
+
export function hotkeySteps(binding: HotkeyBinding): HotkeyStep[] {
|
|
215
|
+
const raw = binding.sequence
|
|
216
|
+
if (raw === undefined) {
|
|
217
|
+
if (binding.key === undefined) return []
|
|
218
|
+
const { key, code, ctrl, shift, alt } = binding
|
|
219
|
+
return [{ key, code, ctrl, shift, alt }]
|
|
220
|
+
}
|
|
221
|
+
const list = typeof raw === "string" ? raw.trim().split(/\s+/) : raw
|
|
222
|
+
return list.map((step) => (typeof step === "string" ? { key: step } : step))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Where a run of presses stands against a binding: "full" when they spell it
|
|
227
|
+
* out, "prefix" when more keys would, otherwise null.
|
|
228
|
+
*/
|
|
229
|
+
export function sequenceProgress(presses: KeyLike[], binding: HotkeyBinding): "full" | "prefix" | null {
|
|
230
|
+
const steps = hotkeySteps(binding)
|
|
231
|
+
if (!steps.length || presses.length > steps.length) return null
|
|
232
|
+
for (let i = 0; i < presses.length; i++) {
|
|
233
|
+
if (!matchesHotkey(presses[i]!, steps[i]!)) return null
|
|
234
|
+
}
|
|
235
|
+
return presses.length === steps.length ? "full" : "prefix"
|
|
236
|
+
}
|
|
237
|
+
|
|
137
238
|
/**
|
|
138
239
|
* Bind a list of hotkeys for as long as the calling component is mounted.
|
|
139
240
|
*
|
|
@@ -165,11 +266,23 @@ interface MountedHotkeys {
|
|
|
165
266
|
enabled: MaybeRefOrGetter<boolean> | undefined
|
|
166
267
|
priority: MaybeRefOrGetter<number> | undefined
|
|
167
268
|
order: number
|
|
269
|
+
/** Where the declaring component sits in the DOM, for the overlay check. */
|
|
270
|
+
host: () => Node | null
|
|
168
271
|
}
|
|
169
272
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
273
|
+
// Pinned to a global symbol for the same reason as the toast queue: a barrel
|
|
274
|
+
// and a deep import must not end up with two registries and two listeners.
|
|
275
|
+
const STATE_KEY = Symbol.for("@leavepulse/ui:hotkeys")
|
|
276
|
+
type GlobalWithHotkeys = typeof globalThis & {
|
|
277
|
+
[STATE_KEY]?: { mounted: Set<MountedHotkeys>; order: number; listening: boolean }
|
|
278
|
+
}
|
|
279
|
+
const g = globalThis as GlobalWithHotkeys
|
|
280
|
+
const state = (g[STATE_KEY] ??= {
|
|
281
|
+
mounted: shallowReactive(new Set<MountedHotkeys>()),
|
|
282
|
+
order: 0,
|
|
283
|
+
listening: false,
|
|
284
|
+
})
|
|
285
|
+
const mounted = state.mounted
|
|
173
286
|
|
|
174
287
|
function inScope(target: EventTarget | null, scope: MountedHotkeys["scope"]): boolean {
|
|
175
288
|
const el = toValue(scope)
|
|
@@ -178,47 +291,159 @@ function inScope(target: EventTarget | null, scope: MountedHotkeys["scope"]): bo
|
|
|
178
291
|
return !!node && (el === node || el.contains(node))
|
|
179
292
|
}
|
|
180
293
|
|
|
181
|
-
function
|
|
182
|
-
|
|
294
|
+
function topLayer(): Element | null {
|
|
295
|
+
const layers = document.querySelectorAll("[data-dismissable-layer]")
|
|
296
|
+
return layers[layers.length - 1] ?? null
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function insideOf(entry: MountedHotkeys, layer: Element): boolean {
|
|
300
|
+
const scope = toValue(entry.scope)
|
|
301
|
+
const node = scope ?? entry.host()
|
|
302
|
+
return !!node && layer.contains(node)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
interface Candidate {
|
|
306
|
+
binding: HotkeyBinding
|
|
307
|
+
event: KeyboardEvent
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const DEFAULT_TIMEOUT = 1000
|
|
311
|
+
const pressed: KeyLike[] = []
|
|
312
|
+
let held: Candidate[] = []
|
|
313
|
+
let holdTimer: ReturnType<typeof setTimeout> | undefined
|
|
314
|
+
const pendingKeys = ref<string[]>([])
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The keys of a sequence typed so far ("G" after the first key of "g i"),
|
|
318
|
+
* empty when nothing is pending. For a "G…" hint while the next key is awaited.
|
|
319
|
+
*/
|
|
320
|
+
export const pendingHotkey = computed(() => pendingKeys.value)
|
|
321
|
+
|
|
322
|
+
function snapshot(event: KeyboardEvent): KeyLike {
|
|
323
|
+
const { key, code, ctrlKey, metaKey, shiftKey, altKey } = event
|
|
324
|
+
return { key, code, ctrlKey, metaKey, shiftKey, altKey }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function eligible(event: KeyboardEvent): HotkeyBinding[] {
|
|
183
328
|
const inTextEntry = isTextEntry(event.target)
|
|
329
|
+
const blocker = event.key === "Escape" ? topLayer() : topModalElement()
|
|
184
330
|
const entries = [...mounted].sort(
|
|
185
331
|
(a, b) => Number(toValue(b.priority) || 0) - Number(toValue(a.priority) || 0) || b.order - a.order,
|
|
186
332
|
)
|
|
333
|
+
const out: HotkeyBinding[] = []
|
|
187
334
|
for (const entry of entries) {
|
|
188
335
|
if (toValue(entry.enabled) === false || !inScope(event.target, entry.scope)) continue
|
|
336
|
+
const covered = !!blocker && !insideOf(entry, blocker)
|
|
189
337
|
for (const binding of entry.read()) {
|
|
190
338
|
if (inTextEntry && !binding.allowInInput) continue
|
|
191
|
-
if (
|
|
192
|
-
if (binding.
|
|
339
|
+
if (event.repeat && binding.repeat === false) continue
|
|
340
|
+
if (covered && !binding.overLayers) continue
|
|
341
|
+
if (binding.when && !binding.when(event)) continue
|
|
342
|
+
out.push(binding)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return out
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function run(candidates: Candidate[]): boolean {
|
|
349
|
+
for (const { binding, event } of candidates) {
|
|
350
|
+
if (binding.handler(event) !== false) return true
|
|
351
|
+
}
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function resetSequence() {
|
|
356
|
+
clearTimeout(holdTimer)
|
|
357
|
+
holdTimer = undefined
|
|
358
|
+
pressed.length = 0
|
|
359
|
+
held = []
|
|
360
|
+
pendingKeys.value = []
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function flushHeld() {
|
|
364
|
+
const waiting = held
|
|
365
|
+
resetSequence()
|
|
366
|
+
run(waiting)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function onKeydown(event: KeyboardEvent) {
|
|
370
|
+
if (event.isComposing || event.defaultPrevented) return
|
|
371
|
+
if (pressed.length && MODIFIER_KEYS.has(event.key.toLowerCase())) return
|
|
372
|
+
if (pressed.length && event.key === "Escape") {
|
|
373
|
+
resetSequence()
|
|
374
|
+
}
|
|
375
|
+
const presses = [...pressed, snapshot(event)]
|
|
376
|
+
const bindings = eligible(event)
|
|
377
|
+
const full: Candidate[] = []
|
|
378
|
+
let waitFor = 0
|
|
379
|
+
for (const binding of bindings) {
|
|
380
|
+
const progress = sequenceProgress(presses, binding)
|
|
381
|
+
if (progress === "full") full.push({ binding, event })
|
|
382
|
+
else if (progress === "prefix") waitFor = Math.max(waitFor, binding.timeout ?? DEFAULT_TIMEOUT)
|
|
383
|
+
}
|
|
384
|
+
if (waitFor) {
|
|
385
|
+
clearTimeout(holdTimer)
|
|
386
|
+
pressed.push(snapshot(event))
|
|
387
|
+
held = full
|
|
388
|
+
pendingKeys.value = pressed.map(pressLabel)
|
|
389
|
+
holdTimer = setTimeout(flushHeld, waitFor)
|
|
390
|
+
event.preventDefault()
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
if (full.length) {
|
|
394
|
+
resetSequence()
|
|
395
|
+
if (run(full)) {
|
|
193
396
|
event.preventDefault()
|
|
194
397
|
return
|
|
195
398
|
}
|
|
196
399
|
}
|
|
400
|
+
if (pressed.length) {
|
|
401
|
+
flushHeld()
|
|
402
|
+
onKeydown(event)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function pressLabel(press: KeyLike): string {
|
|
407
|
+
return chordOf({
|
|
408
|
+
key: press.key,
|
|
409
|
+
ctrl: press.ctrlKey || press.metaKey,
|
|
410
|
+
alt: press.altKey,
|
|
411
|
+
shift: press.shiftKey && press.key.length !== 1,
|
|
412
|
+
})
|
|
197
413
|
}
|
|
198
414
|
|
|
199
415
|
export function useHotkeys(
|
|
200
416
|
bindings: MaybeRefOrGetter<HotkeyBinding[]>,
|
|
201
417
|
options: UseHotkeysOptions = {},
|
|
202
418
|
) {
|
|
419
|
+
const instance = getCurrentInstance()
|
|
203
420
|
const entry: MountedHotkeys = {
|
|
204
421
|
read: () => toValue(bindings),
|
|
205
422
|
scope: options.scope,
|
|
206
423
|
enabled: options.enabled,
|
|
207
424
|
priority: options.priority,
|
|
208
|
-
order: ++
|
|
425
|
+
order: ++state.order,
|
|
426
|
+
host: () => {
|
|
427
|
+
const el = instance?.vnode.el
|
|
428
|
+
return el instanceof Node ? el : null
|
|
429
|
+
},
|
|
209
430
|
}
|
|
210
431
|
onMounted(() => {
|
|
211
432
|
mounted.add(entry)
|
|
212
|
-
|
|
433
|
+
warnConflicts()
|
|
434
|
+
if (!state.listening) {
|
|
213
435
|
window.addEventListener("keydown", onKeydown)
|
|
214
|
-
|
|
436
|
+
window.addEventListener("blur", resetSequence)
|
|
437
|
+
state.listening = true
|
|
215
438
|
}
|
|
216
439
|
})
|
|
217
440
|
onBeforeUnmount(() => {
|
|
218
441
|
mounted.delete(entry)
|
|
219
|
-
if (!mounted.size && listening) {
|
|
442
|
+
if (!mounted.size && state.listening) {
|
|
220
443
|
window.removeEventListener("keydown", onKeydown)
|
|
221
|
-
|
|
444
|
+
window.removeEventListener("blur", resetSequence)
|
|
445
|
+
state.listening = false
|
|
446
|
+
resetSequence()
|
|
222
447
|
}
|
|
223
448
|
})
|
|
224
449
|
}
|
|
@@ -236,10 +461,110 @@ const KEY_NAMES: Record<string, string> = {
|
|
|
236
461
|
arrowright: "→",
|
|
237
462
|
}
|
|
238
463
|
|
|
464
|
+
function chordOf(step: HotkeyStep): string {
|
|
465
|
+
const key = step.key.toLowerCase()
|
|
466
|
+
const name = KEY_NAMES[key] ?? (key.length === 1 || /^f\d+$/.test(key) ? key.toUpperCase() : step.key)
|
|
467
|
+
return [step.ctrl && "Ctrl", step.alt && "Alt", step.shift && "Shift", name].filter(Boolean).join("+")
|
|
468
|
+
}
|
|
469
|
+
|
|
239
470
|
function comboOf(binding: HotkeyBinding): string {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
471
|
+
return hotkeySteps(binding).map(chordOf).join(" ")
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Identity of the keys a binding answers, for telling two of them apart. */
|
|
475
|
+
function signatureOf(binding: HotkeyBinding): string {
|
|
476
|
+
return hotkeySteps(binding)
|
|
477
|
+
.map((step) => {
|
|
478
|
+
const key = step.key.toLowerCase()
|
|
479
|
+
const symbol = key.length === 1 && key === key.toUpperCase()
|
|
480
|
+
const shift = step.shift === undefined && symbol ? "*" : String(Boolean(step.shift))
|
|
481
|
+
return `${Boolean(step.ctrl)}:${Boolean(step.alt)}:${shift}:${key}`
|
|
482
|
+
})
|
|
483
|
+
.join(" ")
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export interface HotkeyConflict {
|
|
487
|
+
/** The keys both answer, written as in the shortcut reference. */
|
|
488
|
+
combo: string
|
|
489
|
+
/** `help` labels of the clashing bindings; unlabelled ones are omitted. */
|
|
490
|
+
labels: string[]
|
|
491
|
+
priority: number
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Pure part of `hotkeyConflicts`: bindings from different `useHotkeys` calls
|
|
496
|
+
* answering the same keys at the same priority within the same scope. Within
|
|
497
|
+
* one call a repeat is deliberate (the first may decline), and a binding with
|
|
498
|
+
* `when` is conditional by design, so neither counts.
|
|
499
|
+
*/
|
|
500
|
+
export function findHotkeyConflicts(
|
|
501
|
+
groups: { bindings: HotkeyBinding[]; priority: number; scope: unknown }[],
|
|
502
|
+
): HotkeyConflict[] {
|
|
503
|
+
const scopeIds = new Map<unknown, number>()
|
|
504
|
+
const seen = new Map<string, { combo: string; labels: string[]; owners: Set<number>; priority: number }>()
|
|
505
|
+
groups.forEach((group, owner) => {
|
|
506
|
+
if (!scopeIds.has(group.scope)) scopeIds.set(group.scope, scopeIds.size)
|
|
507
|
+
for (const binding of group.bindings) {
|
|
508
|
+
if (binding.when || !hotkeySteps(binding).length) continue
|
|
509
|
+
const id = `${group.priority}|${scopeIds.get(group.scope)}|${signatureOf(binding)}`
|
|
510
|
+
const slot = seen.get(id) ?? {
|
|
511
|
+
combo: binding.help?.combo ?? comboOf(binding),
|
|
512
|
+
labels: [],
|
|
513
|
+
owners: new Set<number>(),
|
|
514
|
+
priority: group.priority,
|
|
515
|
+
}
|
|
516
|
+
slot.owners.add(owner)
|
|
517
|
+
if (binding.help && !slot.labels.includes(binding.help.label)) slot.labels.push(binding.help.label)
|
|
518
|
+
seen.set(id, slot)
|
|
519
|
+
}
|
|
520
|
+
})
|
|
521
|
+
return [...seen.values()]
|
|
522
|
+
.filter((slot) => slot.owners.size > 1)
|
|
523
|
+
.map(({ combo, labels, priority }) => ({ combo, labels, priority }))
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Bindings among the mounted ones that answer the same keys. */
|
|
527
|
+
export const hotkeyConflicts = computed<HotkeyConflict[]>(() =>
|
|
528
|
+
findHotkeyConflicts(
|
|
529
|
+
[...mounted]
|
|
530
|
+
.filter((entry) => toValue(entry.enabled) !== false)
|
|
531
|
+
.map((entry) => ({
|
|
532
|
+
bindings: entry.read(),
|
|
533
|
+
priority: Number(toValue(entry.priority) || 0),
|
|
534
|
+
scope: toValue(entry.scope) ?? null,
|
|
535
|
+
})),
|
|
536
|
+
),
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
const warned = new Set<string>()
|
|
540
|
+
|
|
541
|
+
declare const process: { env: { NODE_ENV?: string } }
|
|
542
|
+
|
|
543
|
+
/*
|
|
544
|
+
* Written as the literal `process.env.NODE_ENV` so the consumer's bundler
|
|
545
|
+
* replaces it (Vite and webpack both do for dependencies); where nothing
|
|
546
|
+
* replaces it and no `process` exists, the warning simply stays off.
|
|
547
|
+
*/
|
|
548
|
+
function isDev(): boolean {
|
|
549
|
+
try {
|
|
550
|
+
return process.env.NODE_ENV !== "production"
|
|
551
|
+
} catch {
|
|
552
|
+
return false
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function warnConflicts() {
|
|
557
|
+
if (!isDev()) return
|
|
558
|
+
for (const conflict of hotkeyConflicts.value) {
|
|
559
|
+
const id = `${conflict.priority}|${conflict.combo}`
|
|
560
|
+
if (warned.has(id)) continue
|
|
561
|
+
warned.add(id)
|
|
562
|
+
console.warn(
|
|
563
|
+
`[@leavepulse/ui] ${conflict.combo} is bound more than once at priority ${conflict.priority}`
|
|
564
|
+
+ (conflict.labels.length ? ` (${conflict.labels.join(", ")})` : "")
|
|
565
|
+
+ "; only the most recently mounted one runs unless it declines.",
|
|
566
|
+
)
|
|
567
|
+
}
|
|
243
568
|
}
|
|
244
569
|
|
|
245
570
|
/**
|
|
@@ -250,13 +575,15 @@ function comboOf(binding: HotkeyBinding): string {
|
|
|
250
575
|
*/
|
|
251
576
|
export const activeShortcuts = computed<ShortcutGroup[]>(() => {
|
|
252
577
|
const groups = new Map<string, ShortcutEntry[]>()
|
|
578
|
+
const clashing = new Set(hotkeyConflicts.value.map((c) => c.combo))
|
|
253
579
|
for (const entry of mounted) {
|
|
254
580
|
if (toValue(entry.enabled) === false) continue
|
|
255
581
|
for (const binding of entry.read()) {
|
|
256
582
|
if (!binding.help) continue
|
|
257
583
|
const combo = binding.help.combo ?? comboOf(binding)
|
|
258
584
|
const entries = groups.get(binding.help.group) ?? []
|
|
259
|
-
|
|
585
|
+
const conflict = clashing.has(combo) || undefined
|
|
586
|
+
if (!entries.some((e) => e.combo === combo)) entries.push({ combo, label: binding.help.label, conflict })
|
|
260
587
|
groups.set(binding.help.group, entries)
|
|
261
588
|
}
|
|
262
589
|
}
|
|
@@ -18,6 +18,12 @@ import { computed, type MaybeRefOrGetter, toValue } from "vue"
|
|
|
18
18
|
*
|
|
19
19
|
* A bare string pattern is treated as a full-string match (anchored), mirroring
|
|
20
20
|
* the native HTML `pattern` attribute.
|
|
21
|
+
*
|
|
22
|
+
* Composition cannot be vetoed (its `beforeinput` is not cancelable), and on
|
|
23
|
+
* Android most keyboards compose ordinary typing, so `beforeinput` alone left
|
|
24
|
+
* `restrict` doing nothing there. `onInput` and `onCompositionEnd` clean up
|
|
25
|
+
* whatever got through — composed text, a drop, an autocorrect replacement —
|
|
26
|
+
* keeping the caret where it was. Bind `onInput` BEFORE reading the value.
|
|
21
27
|
*/
|
|
22
28
|
|
|
23
29
|
export interface UseInputFilterOptions {
|
|
@@ -27,31 +33,57 @@ export interface UseInputFilterOptions {
|
|
|
27
33
|
pattern?: MaybeRefOrGetter<RegExp | string | undefined>
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Anchor a pattern to the whole string, like HTML's `pattern` attribute. The
|
|
38
|
+
* source is wrapped, never edited: anchors the caller wrote stay valid inside
|
|
39
|
+
* the group, while trimming a trailing `$` broke an escaped `\$` into an
|
|
40
|
+
* unterminated group.
|
|
41
|
+
*/
|
|
42
|
+
export function anchorPattern(p: RegExp | string): RegExp {
|
|
43
|
+
if (p instanceof RegExp) return new RegExp(`^(?:${p.source})$`, p.flags.replace(/[gy]/g, ""))
|
|
36
44
|
return new RegExp(`^(?:${p})$`)
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
// Strip g/y so lastIndex can't leak state across .test() calls.
|
|
48
|
+
function charTester(charRe: RegExp): (ch: string) => boolean {
|
|
49
|
+
const re = new RegExp(charRe.source, charRe.flags.replace(/[gy]/g, ""))
|
|
50
|
+
return (ch) => re.test(ch)
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
/** Does a string contain only restrict-allowed characters? */
|
|
40
54
|
function allAllowed(text: string, charRe: RegExp): boolean {
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
const re = new RegExp(charRe.source, charRe.flags.replace(/[gy]/g, ""))
|
|
55
|
+
const ok = charTester(charRe)
|
|
56
|
+
// By code point, so surrogate pairs hold.
|
|
44
57
|
for (const ch of text) {
|
|
45
|
-
if (!
|
|
58
|
+
if (!ok(ch)) return false
|
|
46
59
|
}
|
|
47
60
|
return true
|
|
48
61
|
}
|
|
49
62
|
|
|
63
|
+
/**
|
|
64
|
+
* `text` with every disallowed character removed, and where `caret` lands once
|
|
65
|
+
* the characters before it that were removed are gone.
|
|
66
|
+
*/
|
|
67
|
+
export function stripDisallowed(text: string, charRe: RegExp, caret = text.length): { text: string; caret: number } {
|
|
68
|
+
const ok = charTester(charRe)
|
|
69
|
+
let kept = ""
|
|
70
|
+
let at = 0
|
|
71
|
+
let offset = 0
|
|
72
|
+
for (const ch of text) {
|
|
73
|
+
if (ok(ch)) {
|
|
74
|
+
kept += ch
|
|
75
|
+
if (offset < caret) at += ch.length
|
|
76
|
+
}
|
|
77
|
+
offset += ch.length
|
|
78
|
+
}
|
|
79
|
+
return { text: kept, caret: at }
|
|
80
|
+
}
|
|
81
|
+
|
|
50
82
|
export function useInputFilter(options: UseInputFilterOptions = {}) {
|
|
51
83
|
const restrict = computed(() => toValue(options.restrict))
|
|
52
84
|
const patternRe = computed(() => {
|
|
53
85
|
const p = toValue(options.pattern)
|
|
54
|
-
return p === undefined ? undefined :
|
|
86
|
+
return p === undefined ? undefined : anchorPattern(p)
|
|
55
87
|
})
|
|
56
88
|
|
|
57
89
|
/** True when a non-empty value fails `pattern`. Empty is treated as valid. */
|
|
@@ -68,12 +100,43 @@ export function useInputFilter(options: UseInputFilterOptions = {}) {
|
|
|
68
100
|
function onBeforeInput(event: InputEvent): void {
|
|
69
101
|
const re = restrict.value
|
|
70
102
|
if (!re || event.isComposing) return
|
|
71
|
-
//
|
|
72
|
-
|
|
103
|
+
// A drop or a replacement carries its text in dataTransfer, not data.
|
|
104
|
+
// Deletions / formatting carry neither; let them through.
|
|
105
|
+
const data = event.data ?? event.dataTransfer?.getData("text") ?? null
|
|
73
106
|
if (data == null) return
|
|
74
107
|
if (!allAllowed(data, re)) event.preventDefault()
|
|
75
108
|
}
|
|
76
109
|
|
|
110
|
+
// Remove what slipped past `beforeinput`; true when the value changed.
|
|
111
|
+
function sanitize(target: EventTarget | null): boolean {
|
|
112
|
+
const re = restrict.value
|
|
113
|
+
if (!re) return false
|
|
114
|
+
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) return false
|
|
115
|
+
const { text, caret } = stripDisallowed(target.value, re, target.selectionStart ?? target.value.length)
|
|
116
|
+
if (text === target.value) return false
|
|
117
|
+
target.value = text
|
|
118
|
+
try {
|
|
119
|
+
target.setSelectionRange(caret, caret)
|
|
120
|
+
} catch {
|
|
121
|
+
// Input types without a selection (number, email) cannot place a caret.
|
|
122
|
+
}
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Attach to `input`, ahead of whatever reads the value. */
|
|
127
|
+
function onInput(event: Event): void {
|
|
128
|
+
if ((event as InputEvent).isComposing) return
|
|
129
|
+
sanitize(event.target)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Attach to `compositionend`. Some browsers fire the last `input` before
|
|
134
|
+
* this, so a cleaned value is announced with a fresh `input` event.
|
|
135
|
+
*/
|
|
136
|
+
function onCompositionEnd(event: CompositionEvent): void {
|
|
137
|
+
if (sanitize(event.target)) event.target?.dispatchEvent(new Event("input", { bubbles: true }))
|
|
138
|
+
}
|
|
139
|
+
|
|
77
140
|
/**
|
|
78
141
|
* Attach to `paste`. Veto the paste if any pasted char is disallowed, rather
|
|
79
142
|
* than silently dropping part of it — partial pastes confuse more than help.
|
|
@@ -85,5 +148,5 @@ export function useInputFilter(options: UseInputFilterOptions = {}) {
|
|
|
85
148
|
if (text && !allAllowed(text, re)) event.preventDefault()
|
|
86
149
|
}
|
|
87
150
|
|
|
88
|
-
return { isInvalid, onBeforeInput, onPaste, patternRe, restrict }
|
|
151
|
+
return { isInvalid, onBeforeInput, onInput, onCompositionEnd, onPaste, patternRe, restrict }
|
|
89
152
|
}
|