@leavepulse/ui 0.43.4 → 0.44.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 +75 -7
- package/dist/assets/{LpCommandPalette-B-0Est4Z.css → LpCommandPalette-DCP5g2ik.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/{LpShortcutsDialog-C6YoQ8_M.css → LpShortcutsDialog-ChUyU7oV.css} +7 -7
- package/dist/assets/{LpSkeleton-D7xU8IPV.css → LpSkeleton-H3Z5dEIk.css} +4 -1
- package/dist/assets/{LpTable-BgPLJOXa.css → LpTable-DDQNdf86.css} +9 -9
- package/dist/chunks/ContextMenuItems-CrfeNPWT.js +123 -0
- package/dist/chunks/{LayoutCanvas-Dd20boUG.js → LayoutCanvas-D-jPOAvg.js} +26 -9
- package/dist/chunks/{LayoutNode-DGpZA9LX.js → LayoutNode-D8Xy9N-o.js} +20 -8
- package/dist/chunks/{LpActionBar-BlvtGB2P.js → LpActionBar-BbwCF6l5.js} +24 -10
- package/dist/chunks/{LpAddressInput-B-Zvxsn7.js → LpAddressInput-BrOqkZqg.js} +53 -48
- package/dist/chunks/{LpAlert-BAYHDSS1.js → LpAlert-DkOIjsbq.js} +15 -8
- package/dist/chunks/{LpAppShell-BGQVIY-3.js → LpAppShell-BUKPcHco.js} +51 -24
- package/dist/chunks/{LpAutocomplete-DExlIZe_.js → LpAutocomplete-DxV3Y9h4.js} +50 -23
- package/dist/chunks/{LpAvatar-B3-xAOH3.js → LpAvatar-ClBbEYZN.js} +17 -8
- package/dist/chunks/LpBreadcrumbs-B8IJgFIc.js +95 -0
- package/dist/chunks/{LpButton-5VXXCjwF.js → LpButton-kqJBHIIb.js} +44 -14
- package/dist/chunks/{LpCard-BhI1N_vL.js → LpCard-CpRByR0q.js} +1 -1
- package/dist/chunks/{LpCheckbox-CVccKYaH.js → LpCheckbox-DpqchzH4.js} +7 -3
- package/dist/chunks/{LpCodeBlock-Bo0B4TFR.js → LpCodeBlock-DeHJPq7p.js} +48 -16
- package/dist/chunks/{LpColorPicker-BlXea7LM.js → LpColorPicker-Bhf1qxhB.js} +32 -9
- package/dist/chunks/{LpConfirmDialog-Q-bxff8z.js → LpConfirmDialog-CIUOSnWo.js} +58 -16
- package/dist/chunks/LpConfirmHost-CDm0Wtpv.js +49 -0
- package/dist/chunks/LpContextMenu-DQw5dGfG.js +68 -0
- package/dist/chunks/LpDatePicker-Drirx1AK.js +191 -0
- package/dist/chunks/{LpDrawer-BZ6k3hvo.js → LpDrawer-bScx-KMO.js} +63 -26
- package/dist/chunks/{LpFileTree-_5vFBgaK.js → LpFileTree-CSI_5SK2.js} +23 -8
- package/dist/chunks/{LpFileTreeNode-BdcIYKIE.js → LpFileTreeNode-B4MiSQX2.js} +2 -2
- package/dist/chunks/LpFormField-CIk5qosh.js +87 -0
- package/dist/chunks/{LpInput-9EXWUM9B.js → LpInput-C0e8y8_d.js} +26 -11
- package/dist/chunks/{LpLightbox-2sYVMY-S.js → LpLightbox-Mr0W_6HK.js} +147 -60
- package/dist/chunks/{LpLink-DCND-zyu.js → LpLink-Krkjhe_y.js} +15 -6
- package/dist/chunks/{LpLogViewer-CaRy5MeS.js → LpLogViewer-YBOaFnc_.js} +68 -34
- package/dist/chunks/{LpMapPicker-ul4oYno6.js → LpMapPicker-NUzS9e37.js} +38 -13
- package/dist/chunks/{LpMediaGallery-FF-NquIo.js → LpMediaGallery-DfJmMW0I.js} +6 -3
- package/dist/chunks/{LpModal-Bt0pQCI_.js → LpModal-DPwGKBPv.js} +49 -26
- package/dist/chunks/{LpModalNav-YaifHrXC.js → LpModalNav-DPg5H9f3.js} +9 -6
- package/dist/chunks/{LpNotificationBell-DyGGsMAM.js → LpNotificationBell-t5yn-siX.js} +58 -24
- package/dist/chunks/{LpNumberField-CWuX08iX.js → LpNumberField-DWnQKktS.js} +20 -7
- 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-DFngX7Xo.js} +30 -19
- package/dist/chunks/{LpPasswordInput--sjYCkLS.js → LpPasswordInput-CWVDuBJ5.js} +10 -7
- package/dist/chunks/{LpPhoneInput-BDSKVl_z.js → LpPhoneInput-CD-ZTeSd.js} +57 -33
- package/dist/chunks/{LpPie-D6OnzwlJ.js → LpPie-BLco31Nx.js} +78 -21
- package/dist/chunks/{LpPopover-8268UmlC.js → LpPopover-p_e_shN3.js} +4 -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/{LpSegmented-fV0Sk92p.js → LpSegmented-Dejd7cIx.js} +7 -3
- package/dist/chunks/{LpSelect-ANIVtrpM.js → LpSelect-DrRD9I7L.js} +40 -18
- package/dist/chunks/{LpSidebar-ClBUwoEz.js → LpSidebar-BgwUNWpJ.js} +30 -22
- package/dist/chunks/{LpSidebarNav-llOcs_Fn.js → LpSidebarNav-Bqxz8aWT.js} +14 -9
- 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-dXtyrdX9.js} +1 -1
- package/dist/chunks/{LpSwitch-Blizvqyt.js → LpSwitch-B1aLKulj.js} +40 -26
- package/dist/chunks/{LpTableOfContents-CjvxPMpU.js → LpTableOfContents-BUcAfGPQ.js} +36 -41
- package/dist/chunks/{LpTabs-Cefe_lys.js → LpTabs-_qMVaZJ6.js} +19 -5
- package/dist/chunks/LpTextarea-yP8hGedR.js +77 -0
- package/dist/chunks/LpThemeProvider-Wjm7Tuit.js +35 -0
- package/dist/chunks/LpThemeSwitcher-CKLc10KZ.js +169 -0
- package/dist/chunks/{LpTilt-DuyKbpkA.js → LpTilt-B7KHX1zH.js} +2 -2
- package/dist/chunks/{LpTimeField-K6nAGboX.js → LpTimeField-C5HRgS5H.js} +13 -7
- package/dist/chunks/{LpToaster-gDB1EaU3.js → LpToaster--oT6lfhL.js} +57 -28
- package/dist/chunks/{LpTooltip-hCLPAqMG.js → LpTooltip-CBdzeKNf.js} +13 -6
- package/dist/chunks/LpTooltipProvider-CftjV7Xc.js +25 -0
- package/dist/chunks/{LpTopologyCanvas-Cm1L39uL.js → LpTopologyCanvas-DocsLal3.js} +6 -1
- package/dist/chunks/{LpUptimeBar-DvAMno0S.js → LpUptimeBar-DLtxOMRy.js} +43 -14
- package/dist/chunks/{LpThemeSwitcher-Cue-myST.js → useTheme-C8JHUlPM.js} +173 -215
- 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/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.d.ts +31 -2
- package/dist/components/LpChart.vue.js +317 -95
- 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.js +1 -1
- package/dist/components/LpCommandPalette.vue.d.ts +8 -3
- package/dist/components/LpCommandPalette.vue.js +86 -72
- 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/LpDrawer.vue.d.ts +10 -0
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +22 -6
- 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 +9 -0
- package/dist/components/LpFormField.vue.js +1 -1
- package/dist/components/LpImageEditor.vue.js +92 -57
- package/dist/components/LpInput.vue.d.ts +3 -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 +20 -2
- 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.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.js +1 -1
- package/dist/components/LpRadioGroup.vue.js +1 -1
- package/dist/components/LpSection.vue.d.ts +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 +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +11 -8
- 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/LpSwitch.vue.js +1 -1
- package/dist/components/LpTable.vue.d.ts +23 -1
- package/dist/components/LpTable.vue.js +74 -36
- 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/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 +13 -0
- 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 +39 -4
- 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/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/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/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/sidebar.d.ts +7 -1
- package/dist/components/tableSort.d.ts +8 -0
- package/dist/components/tableSort.js +31 -0
- package/dist/composables/useField.d.ts +34 -0
- package/dist/composables/useField.js +45 -0
- package/dist/composables/useFormDraft.d.ts +10 -3
- package/dist/composables/useFormDraft.js +9 -14
- package/dist/composables/useHotkeys.d.ts +17 -1
- package/dist/composables/useHotkeys.js +55 -18
- package/dist/composables/useInputFilter.d.ts +17 -0
- package/dist/composables/useInputFilter.js +47 -10
- package/dist/composables/useLpConfig.d.ts +24 -0
- package/dist/composables/useLpConfig.js +44 -0
- package/dist/composables/useRovingFocus.js +7 -0
- 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/index.d.ts +32 -2
- package/dist/index.js +184 -137
- 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/useTheme.d.ts +32 -9
- package/package.json +8 -7
- package/src/component-names.ts +5 -7
- package/src/components/ContextMenuItems.vue +88 -0
- package/src/components/LayoutCanvas.vue +26 -6
- package/src/components/LayoutNode.vue +19 -2
- package/src/components/LpActionBar.vue +30 -7
- 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 +46 -10
- package/src/components/LpAvatar.vue +19 -5
- package/src/components/LpBreadcrumbs.vue +65 -41
- package/src/components/LpButton.vue +59 -13
- package/src/components/LpChart.vue +272 -28
- package/src/components/LpCheckbox.vue +7 -3
- package/src/components/LpCodeBlock.vue +50 -14
- package/src/components/LpColorPicker.vue +39 -9
- package/src/components/LpCommandPalette.vue +34 -13
- package/src/components/LpConfirmDialog.vue +68 -17
- package/src/components/LpConfirmHost.vue +59 -0
- package/src/components/LpContextMenu.vue +14 -115
- package/src/components/LpDatePicker.vue +99 -68
- package/src/components/LpDrawer.vue +56 -19
- package/src/components/LpDropzone.vue +21 -4
- package/src/components/LpFileTree.vue +33 -7
- package/src/components/LpFormField.vue +30 -7
- package/src/components/LpImageEditor.vue +96 -57
- package/src/components/LpInput.vue +18 -7
- package/src/components/LpLightbox.vue +140 -29
- package/src/components/LpLink.vue +24 -13
- package/src/components/LpLogViewer.vue +86 -17
- package/src/components/LpMapPicker.vue +36 -13
- package/src/components/LpMediaGallery.vue +8 -2
- package/src/components/LpModal.vue +53 -27
- package/src/components/LpModalNav.vue +5 -2
- package/src/components/LpNotificationBell.vue +63 -17
- 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 +53 -27
- package/src/components/LpPie.vue +94 -15
- package/src/components/LpPopover.vue +3 -0
- package/src/components/LpProgress.vue +5 -3
- package/src/components/LpRadioGroup.vue +9 -2
- package/src/components/LpSegmented.vue +12 -2
- package/src/components/LpSelect.vue +0 -0
- package/src/components/LpShortcutsDialog.vue +8 -3
- 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/LpSwitch.vue +30 -7
- package/src/components/LpTable.vue +91 -32
- package/src/components/LpTableOfContents.vue +50 -32
- package/src/components/LpTabs.vue +23 -6
- package/src/components/LpTextarea.vue +44 -6
- package/src/components/LpThemeProvider.vue +41 -0
- package/src/components/LpThemeSwitcher.vue +64 -17
- package/src/components/LpTilt.vue +2 -2
- package/src/components/LpTimeField.vue +14 -3
- package/src/components/LpToaster.vue +66 -26
- package/src/components/LpTooltip.vue +22 -5
- package/src/components/LpTooltipProvider.vue +24 -0
- package/src/components/LpTopologyCanvas.vue +12 -1
- package/src/components/LpUptimeBar.vue +46 -8
- package/src/components/chart.ts +77 -5
- package/src/components/codeHighlight.ts +111 -15
- package/src/components/color.ts +30 -9
- package/src/components/confirm.ts +58 -0
- package/src/components/countries.ts +12 -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/modalDepth.ts +48 -6
- package/src/components/otp.ts +16 -0
- package/src/components/sidebar.ts +7 -1
- package/src/components/tableSort.ts +37 -0
- package/src/composables/useField.ts +79 -0
- package/src/composables/useFormDraft.ts +25 -20
- package/src/composables/useHotkeys.ts +89 -18
- package/src/composables/useInputFilter.ts +77 -14
- package/src/composables/useLpConfig.ts +87 -0
- package/src/composables/useRovingFocus.ts +11 -0
- package/src/composables/useTilt.ts +5 -6
- package/src/composables/useToast.ts +56 -4
- package/src/composables/useZoomPan.ts +6 -6
- package/src/index.ts +37 -2
- package/src/layout/tree.ts +30 -0
- package/src/layout/useLayout.ts +68 -18
- package/src/nuxt.ts +59 -17
- package/src/runtime/config.plugin.ts +14 -0
- package/src/runtime/theme.plugin.ts +42 -22
- package/src/theme/useTheme.ts +206 -100
- 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/LpTextarea-t7F6gv8E.js +0 -47
|
@@ -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
|
|
@@ -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
|
|
@@ -39,6 +49,16 @@ export interface HotkeyBinding {
|
|
|
39
49
|
* something there — Escape, and the palette the user reaches from anywhere.
|
|
40
50
|
*/
|
|
41
51
|
allowInInput?: boolean
|
|
52
|
+
/**
|
|
53
|
+
* Runs even while an overlay owns the keyboard. By default a binding declared
|
|
54
|
+
* outside the topmost modal stays quiet while that modal is open, and an
|
|
55
|
+
* Escape binding declared outside the topmost dismissable layer (dialog,
|
|
56
|
+
* popover, menu) leaves Escape to that layer, so a page-level shortcut can
|
|
57
|
+
* neither act behind a dialog nor stop the dialog from closing.
|
|
58
|
+
*/
|
|
59
|
+
overLayers?: boolean
|
|
60
|
+
/** Set `false` to ignore auto-repeat while the key is held. */
|
|
61
|
+
repeat?: boolean
|
|
42
62
|
/**
|
|
43
63
|
* How the key reference lists this binding while it is mounted. Bindings
|
|
44
64
|
* without it still work, they just stay out of `activeShortcuts`.
|
|
@@ -87,12 +107,22 @@ export const isAppleKeyboard =
|
|
|
87
107
|
typeof navigator !== "undefined"
|
|
88
108
|
&& /Mac|iPhone|iPad|iPod/.test(navigator.platform || "")
|
|
89
109
|
|
|
110
|
+
/**
|
|
111
|
+
* `isAppleKeyboard` for render paths: false until mounted, so the server and
|
|
112
|
+
* the hydrating client print the same label and the client switches after.
|
|
113
|
+
*/
|
|
114
|
+
export function useAppleKeyboard() {
|
|
115
|
+
const apple = ref(false)
|
|
116
|
+
onMounted(() => (apple.value = isAppleKeyboard))
|
|
117
|
+
return apple
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
/**
|
|
91
121
|
* The symbol to print for a modifier on this platform. Bindings treat Ctrl and
|
|
92
122
|
* Meta as one modifier, so only the label may differ — never the binding.
|
|
93
123
|
*/
|
|
94
|
-
export function appleModifierLabel(key: string): string {
|
|
95
|
-
if (!
|
|
124
|
+
export function appleModifierLabel(key: string, apple = isAppleKeyboard): string {
|
|
125
|
+
if (!apple) return key
|
|
96
126
|
if (key === "Ctrl") return "⌘"
|
|
97
127
|
if (key === "Alt") return "⌥"
|
|
98
128
|
return key
|
|
@@ -123,13 +153,22 @@ function isTextEntry(target: EventTarget | null): boolean {
|
|
|
123
153
|
return false
|
|
124
154
|
}
|
|
125
155
|
|
|
126
|
-
|
|
156
|
+
const MODIFIER_KEYS = new Set(["control", "meta", "shift", "alt"])
|
|
157
|
+
|
|
158
|
+
export function matchesHotkey(event: KeyboardEvent, binding: HotkeyBinding): boolean {
|
|
159
|
+
const key = binding.key.toLowerCase()
|
|
127
160
|
const ctrlHeld = event.ctrlKey || event.metaKey
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
|
|
161
|
+
const wantsCtrl = Boolean(binding.ctrl) || key === "control" || key === "meta"
|
|
162
|
+
if (ctrlHeld !== wantsCtrl) return false
|
|
163
|
+
// Shift is part of typing a symbol on most layouts ("?", "+"), so a binding
|
|
164
|
+
// on a symbol that does not mention Shift accepts it either way.
|
|
165
|
+
const symbol = key.length === 1 && key.toLowerCase() === key.toUpperCase()
|
|
166
|
+
const shiftFree = key === "shift" || (binding.shift === undefined && symbol)
|
|
167
|
+
if (!shiftFree && event.shiftKey !== Boolean(binding.shift)) return false
|
|
168
|
+
if (key !== "alt" && event.altKey !== Boolean(binding.alt)) return false
|
|
169
|
+
if (MODIFIER_KEYS.has(key)) return event.key.toLowerCase() === key
|
|
131
170
|
return (
|
|
132
|
-
event.key.toLowerCase() ===
|
|
171
|
+
event.key.toLowerCase() === key
|
|
133
172
|
|| (binding.code !== undefined && event.code === binding.code)
|
|
134
173
|
)
|
|
135
174
|
}
|
|
@@ -165,11 +204,23 @@ interface MountedHotkeys {
|
|
|
165
204
|
enabled: MaybeRefOrGetter<boolean> | undefined
|
|
166
205
|
priority: MaybeRefOrGetter<number> | undefined
|
|
167
206
|
order: number
|
|
207
|
+
/** Where the declaring component sits in the DOM, for the overlay check. */
|
|
208
|
+
host: () => Node | null
|
|
168
209
|
}
|
|
169
210
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
211
|
+
// Pinned to a global symbol for the same reason as the toast queue: a barrel
|
|
212
|
+
// and a deep import must not end up with two registries and two listeners.
|
|
213
|
+
const STATE_KEY = Symbol.for("@leavepulse/ui:hotkeys")
|
|
214
|
+
type GlobalWithHotkeys = typeof globalThis & {
|
|
215
|
+
[STATE_KEY]?: { mounted: Set<MountedHotkeys>; order: number; listening: boolean }
|
|
216
|
+
}
|
|
217
|
+
const g = globalThis as GlobalWithHotkeys
|
|
218
|
+
const state = (g[STATE_KEY] ??= {
|
|
219
|
+
mounted: shallowReactive(new Set<MountedHotkeys>()),
|
|
220
|
+
order: 0,
|
|
221
|
+
listening: false,
|
|
222
|
+
})
|
|
223
|
+
const mounted = state.mounted
|
|
173
224
|
|
|
174
225
|
function inScope(target: EventTarget | null, scope: MountedHotkeys["scope"]): boolean {
|
|
175
226
|
const el = toValue(scope)
|
|
@@ -178,17 +229,32 @@ function inScope(target: EventTarget | null, scope: MountedHotkeys["scope"]): bo
|
|
|
178
229
|
return !!node && (el === node || el.contains(node))
|
|
179
230
|
}
|
|
180
231
|
|
|
232
|
+
function topLayer(): Element | null {
|
|
233
|
+
const layers = document.querySelectorAll("[data-dismissable-layer]")
|
|
234
|
+
return layers[layers.length - 1] ?? null
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function insideOf(entry: MountedHotkeys, layer: Element): boolean {
|
|
238
|
+
const scope = toValue(entry.scope)
|
|
239
|
+
const node = scope ?? entry.host()
|
|
240
|
+
return !!node && layer.contains(node)
|
|
241
|
+
}
|
|
242
|
+
|
|
181
243
|
function onKeydown(event: KeyboardEvent) {
|
|
182
|
-
if (event.isComposing) return
|
|
244
|
+
if (event.isComposing || event.defaultPrevented) return
|
|
183
245
|
const inTextEntry = isTextEntry(event.target)
|
|
246
|
+
const blocker = event.key === "Escape" ? topLayer() : topModalElement()
|
|
184
247
|
const entries = [...mounted].sort(
|
|
185
248
|
(a, b) => Number(toValue(b.priority) || 0) - Number(toValue(a.priority) || 0) || b.order - a.order,
|
|
186
249
|
)
|
|
187
250
|
for (const entry of entries) {
|
|
188
251
|
if (toValue(entry.enabled) === false || !inScope(event.target, entry.scope)) continue
|
|
252
|
+
const covered = !!blocker && !insideOf(entry, blocker)
|
|
189
253
|
for (const binding of entry.read()) {
|
|
190
254
|
if (inTextEntry && !binding.allowInInput) continue
|
|
191
|
-
if (
|
|
255
|
+
if (event.repeat && binding.repeat === false) continue
|
|
256
|
+
if (covered && !binding.overLayers) continue
|
|
257
|
+
if (!matchesHotkey(event, binding)) continue
|
|
192
258
|
if (binding.handler(event) === false) continue
|
|
193
259
|
event.preventDefault()
|
|
194
260
|
return
|
|
@@ -200,25 +266,30 @@ export function useHotkeys(
|
|
|
200
266
|
bindings: MaybeRefOrGetter<HotkeyBinding[]>,
|
|
201
267
|
options: UseHotkeysOptions = {},
|
|
202
268
|
) {
|
|
269
|
+
const instance = getCurrentInstance()
|
|
203
270
|
const entry: MountedHotkeys = {
|
|
204
271
|
read: () => toValue(bindings),
|
|
205
272
|
scope: options.scope,
|
|
206
273
|
enabled: options.enabled,
|
|
207
274
|
priority: options.priority,
|
|
208
|
-
order: ++
|
|
275
|
+
order: ++state.order,
|
|
276
|
+
host: () => {
|
|
277
|
+
const el = instance?.vnode.el
|
|
278
|
+
return el instanceof Node ? el : null
|
|
279
|
+
},
|
|
209
280
|
}
|
|
210
281
|
onMounted(() => {
|
|
211
282
|
mounted.add(entry)
|
|
212
|
-
if (!listening) {
|
|
283
|
+
if (!state.listening) {
|
|
213
284
|
window.addEventListener("keydown", onKeydown)
|
|
214
|
-
listening = true
|
|
285
|
+
state.listening = true
|
|
215
286
|
}
|
|
216
287
|
})
|
|
217
288
|
onBeforeUnmount(() => {
|
|
218
289
|
mounted.delete(entry)
|
|
219
|
-
if (!mounted.size && listening) {
|
|
290
|
+
if (!mounted.size && state.listening) {
|
|
220
291
|
window.removeEventListener("keydown", onKeydown)
|
|
221
|
-
listening = false
|
|
292
|
+
state.listening = false
|
|
222
293
|
}
|
|
223
294
|
})
|
|
224
295
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
inject,
|
|
4
|
+
provide,
|
|
5
|
+
toValue,
|
|
6
|
+
type App,
|
|
7
|
+
type Component,
|
|
8
|
+
type ComputedRef,
|
|
9
|
+
type InjectionKey,
|
|
10
|
+
type MaybeRefOrGetter,
|
|
11
|
+
} from "vue"
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* App-wide kit configuration, said once at the root instead of on every call
|
|
15
|
+
* site: the router link component that navigation pieces render, and the text
|
|
16
|
+
* the kit would otherwise hard-code in English.
|
|
17
|
+
*
|
|
18
|
+
* Labels are grouped by scope (one scope per component, e.g. "lightbox",
|
|
19
|
+
* "toast") so each component owns its own keys and defaults. The precedence is
|
|
20
|
+
* always: the component's own `labels` prop, then the nearest config, then the
|
|
21
|
+
* component's English defaults.
|
|
22
|
+
*/
|
|
23
|
+
export type LabelScope = Record<string, string>
|
|
24
|
+
|
|
25
|
+
export interface LpConfig {
|
|
26
|
+
/** Component rendered for internal links (RouterLink, NuxtLink). Plain `<a>` when unset. */
|
|
27
|
+
linkComponent?: Component | string
|
|
28
|
+
/** Text overrides by scope: `{ lightbox: { previous: "Back" } }`. */
|
|
29
|
+
labels?: Partial<Record<string, Partial<LabelScope>>>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const LP_CONFIG: InjectionKey<MaybeRefOrGetter<LpConfig>> = Symbol.for("lp-config")
|
|
33
|
+
|
|
34
|
+
/** Provide kit configuration to every component below the caller. */
|
|
35
|
+
export function provideLpConfig(config: MaybeRefOrGetter<LpConfig>): void {
|
|
36
|
+
provide(LP_CONFIG, layered(inject(LP_CONFIG, {}), config))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Install the configuration app-wide: `app.use(lpConfig({ ... }))`. */
|
|
40
|
+
export function lpConfig(config: MaybeRefOrGetter<LpConfig>) {
|
|
41
|
+
return {
|
|
42
|
+
install: (app: App) => {
|
|
43
|
+
const outer = app.runWithContext(() => inject(LP_CONFIG, {}))
|
|
44
|
+
app.provide(LP_CONFIG, layered(outer, config))
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* A nested config refines the one above it rather than replacing it: an app's
|
|
51
|
+
* own labels must not drop the link component a framework plugin provided.
|
|
52
|
+
*/
|
|
53
|
+
function layered(outer: MaybeRefOrGetter<LpConfig>, inner: MaybeRefOrGetter<LpConfig>): () => LpConfig {
|
|
54
|
+
return () => mergeConfig(toValue(outer), toValue(inner))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function mergeConfig(outer: LpConfig, inner: LpConfig): LpConfig {
|
|
58
|
+
const labels: Partial<Record<string, Partial<LabelScope>>> = { ...outer.labels }
|
|
59
|
+
for (const [scope, values] of Object.entries(inner.labels ?? {})) {
|
|
60
|
+
labels[scope] = { ...labels[scope], ...values }
|
|
61
|
+
}
|
|
62
|
+
return { ...outer, ...inner, linkComponent: inner.linkComponent ?? outer.linkComponent, labels }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useLpConfig(): ComputedRef<LpConfig> {
|
|
66
|
+
const config = inject(LP_CONFIG, {})
|
|
67
|
+
return computed(() => toValue(config))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The labels a component renders: its own `labels` prop over the configured
|
|
72
|
+
* scope over its defaults.
|
|
73
|
+
*/
|
|
74
|
+
export function useLabels<T extends LabelScope>(
|
|
75
|
+
scope: string,
|
|
76
|
+
defaults: T,
|
|
77
|
+
own?: () => Partial<T> | undefined,
|
|
78
|
+
): ComputedRef<T> {
|
|
79
|
+
const config = useLpConfig()
|
|
80
|
+
return computed(() => ({ ...defaults, ...config.value.labels?.[scope], ...own?.() }) as T)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The link component to render for an internal destination. */
|
|
84
|
+
export function useLinkComponent(): ComputedRef<Component | string> {
|
|
85
|
+
const config = useLpConfig()
|
|
86
|
+
return computed(() => config.value.linkComponent ?? "a")
|
|
87
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { computed, ref, watch, type MaybeRefOrGetter, toValue } from "vue"
|
|
2
2
|
|
|
3
|
+
const NON_TEXT_INPUTS = new Set(["checkbox", "radio", "button", "submit", "reset", "range", "color", "file", "image"])
|
|
4
|
+
|
|
3
5
|
/*
|
|
4
6
|
* Arrow-key navigation over a list, with a roving tabindex.
|
|
5
7
|
*
|
|
@@ -152,7 +154,16 @@ export function useRovingFocus(
|
|
|
152
154
|
}
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
function isTextEntry(target: EventTarget | null): boolean {
|
|
158
|
+
if (!(target instanceof HTMLElement)) return false
|
|
159
|
+
if (target.isContentEditable || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) return true
|
|
160
|
+
return target instanceof HTMLInputElement && !NON_TEXT_INPUTS.has(target.type)
|
|
161
|
+
}
|
|
162
|
+
|
|
155
163
|
function onKeydown(event: KeyboardEvent) {
|
|
164
|
+
// A field inside a row (an inline rename, a filter) owns its keys: the list
|
|
165
|
+
// taking letters for typeahead or Space for activation made it untypeable.
|
|
166
|
+
if (event.isComposing || isTextEntry(event.target)) return
|
|
156
167
|
const vertical = orientation === "vertical" || orientation === "both"
|
|
157
168
|
const horizontal = orientation === "horizontal" || orientation === "both"
|
|
158
169
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useReducedMotion } from "motion-v"
|
|
2
|
-
import { computed, ref, type ComputedRef, type CSSProperties, type Ref } from "vue"
|
|
2
|
+
import { computed, ref, toValue, type ComputedRef, type CSSProperties, type MaybeRefOrGetter, type Ref } from "vue"
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
* useTilt — pointer-reactive 3D tilt for a "tactile" surface (a card, a tile, a
|
|
@@ -35,10 +35,8 @@ export interface UseTilt {
|
|
|
35
35
|
active: Ref<boolean>
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function useTilt(
|
|
39
|
-
const
|
|
40
|
-
const lift = options.lift ?? 8
|
|
41
|
-
const scale = options.scale ?? 1.03
|
|
38
|
+
export function useTilt(source: MaybeRefOrGetter<UseTiltOptions> = {}): UseTilt {
|
|
39
|
+
const options = computed(() => toValue(source))
|
|
42
40
|
const reduceMotion = useReducedMotion()
|
|
43
41
|
|
|
44
42
|
const active = ref(false)
|
|
@@ -72,6 +70,7 @@ export function useTilt(options: UseTiltOptions = {}): UseTilt {
|
|
|
72
70
|
// rotateX tips away from the pointer's vertical position; rotateY toward its
|
|
73
71
|
// horizontal one. Only tilt while active so it rests flat when the pointer
|
|
74
72
|
// leaves (nx/ny are already zeroed on leave).
|
|
73
|
+
const { max = 10, lift = 8, scale = 1.03 } = options.value
|
|
75
74
|
const rx = active.value ? -ny.value * max : 0
|
|
76
75
|
const ry = active.value ? nx.value * max : 0
|
|
77
76
|
const z = active.value ? lift : 0
|
|
@@ -86,7 +85,7 @@ export function useTilt(options: UseTiltOptions = {}): UseTilt {
|
|
|
86
85
|
})
|
|
87
86
|
|
|
88
87
|
const glareStyle = computed<CSSProperties>(() => {
|
|
89
|
-
if (!options.glare || reduceMotion.value || !active.value) {
|
|
88
|
+
if (!options.value.glare || reduceMotion.value || !active.value) {
|
|
90
89
|
return { opacity: 0 }
|
|
91
90
|
}
|
|
92
91
|
// Bright spot tracks the pointer; map [-1,1] → [0%,100%].
|
|
@@ -11,6 +11,9 @@ export interface ToastAction {
|
|
|
11
11
|
|
|
12
12
|
export interface ToastItem {
|
|
13
13
|
id: number
|
|
14
|
+
/** Bumped when a toast with the same `key` replaces this one in place. */
|
|
15
|
+
rev: number
|
|
16
|
+
key?: string
|
|
14
17
|
title?: string
|
|
15
18
|
message: string
|
|
16
19
|
variant: ToastVariant
|
|
@@ -23,6 +26,12 @@ export interface ToastItem {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export interface ToastOptions {
|
|
29
|
+
/**
|
|
30
|
+
* A toast with the same key replaces the one on screen instead of stacking
|
|
31
|
+
* beside it: "Saved" ten times is one toast, and a progress toast can turn
|
|
32
|
+
* into its result.
|
|
33
|
+
*/
|
|
34
|
+
key?: string
|
|
26
35
|
title?: string
|
|
27
36
|
duration?: number
|
|
28
37
|
actions?: ToastAction[]
|
|
@@ -48,10 +57,12 @@ function push(
|
|
|
48
57
|
message: string,
|
|
49
58
|
options: ToastOptions = {},
|
|
50
59
|
): number {
|
|
51
|
-
|
|
60
|
+
// A server render has no one to show it to, and the queue would outlive the
|
|
61
|
+
// request and surface in the next visitor's page.
|
|
62
|
+
if (typeof window === "undefined") return -1
|
|
52
63
|
const duration = options.duration ?? DEFAULT_DURATION
|
|
53
|
-
const
|
|
54
|
-
|
|
64
|
+
const fields = {
|
|
65
|
+
key: options.key,
|
|
55
66
|
title: options.title,
|
|
56
67
|
message,
|
|
57
68
|
variant,
|
|
@@ -61,14 +72,54 @@ function push(
|
|
|
61
72
|
onClick: options.onClick,
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
const same = options.key === undefined ? -1 : queue.findIndex((t) => t.key === options.key)
|
|
76
|
+
const existing = queue[same]
|
|
77
|
+
if (existing) {
|
|
78
|
+
queue.splice(same, 1, { ...fields, id: existing.id, rev: existing.rev + 1 })
|
|
79
|
+
return existing.id
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const id = state.nextId++
|
|
64
83
|
// Cap the active count: drop the oldest beyond the limit.
|
|
65
84
|
const overflow = queue.length - (MAX_ACTIVE - 1)
|
|
66
85
|
if (overflow > 0) queue.splice(0, overflow)
|
|
67
|
-
queue.push(
|
|
86
|
+
queue.push({ ...fields, id, rev: 0 })
|
|
68
87
|
|
|
69
88
|
return id
|
|
70
89
|
}
|
|
71
90
|
|
|
91
|
+
export interface ToastPromiseMessages<T> {
|
|
92
|
+
loading: string
|
|
93
|
+
success: string | ((value: T) => string)
|
|
94
|
+
error: string | ((error: unknown) => string)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let promiseKey = 0
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* One toast that follows a promise: a persistent "loading" toast that turns
|
|
101
|
+
* into the success or error toast when it settles. Returns the promise itself.
|
|
102
|
+
*/
|
|
103
|
+
function promise<T>(
|
|
104
|
+
task: Promise<T>,
|
|
105
|
+
messages: ToastPromiseMessages<T>,
|
|
106
|
+
options: ToastOptions = {},
|
|
107
|
+
): Promise<T> {
|
|
108
|
+
const key = options.key ?? `@leavepulse/ui:promise-${promiseKey++}`
|
|
109
|
+
push("info", messages.loading, { ...options, key, duration: 0, actions: undefined })
|
|
110
|
+
task.then(
|
|
111
|
+
(value) => {
|
|
112
|
+
const text = typeof messages.success === "function" ? messages.success(value) : messages.success
|
|
113
|
+
push("success", text, { ...options, key })
|
|
114
|
+
},
|
|
115
|
+
(error: unknown) => {
|
|
116
|
+
const text = typeof messages.error === "function" ? messages.error(error) : messages.error
|
|
117
|
+
push("error", text, { ...options, key })
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
return task
|
|
121
|
+
}
|
|
122
|
+
|
|
72
123
|
function dismiss(id: number): void {
|
|
73
124
|
const i = queue.findIndex((t) => t.id === id)
|
|
74
125
|
if (i !== -1) queue.splice(i, 1)
|
|
@@ -86,6 +137,7 @@ export function useToast() {
|
|
|
86
137
|
success: (message: string, o?: ToastOptions) => push("success", message, o),
|
|
87
138
|
warning: (message: string, o?: ToastOptions) => push("warning", message, o),
|
|
88
139
|
error: (message: string, o?: ToastOptions) => push("error", message, o),
|
|
140
|
+
promise,
|
|
89
141
|
/** Low-level push for bridges (e.g. @leavepulse/ui-sdk apiError). */
|
|
90
142
|
push,
|
|
91
143
|
dismiss,
|