@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
|
@@ -79,19 +79,67 @@ const BOOLEANS = new Set([
|
|
|
79
79
|
"Some", "Ok", "Err", // rust common variants read as constants
|
|
80
80
|
])
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"
|
|
94
|
-
|
|
82
|
+
/*
|
|
83
|
+
* Comment syntax is the one thing the shared tokenizer cannot guess: `#` opens
|
|
84
|
+
* a comment in shell, Python, YAML and TOML but is a private field in
|
|
85
|
+
* TypeScript, and `//` is a comment in C-family code but part of every URL in a
|
|
86
|
+
* YAML file. So each language names its own comment forms.
|
|
87
|
+
*/
|
|
88
|
+
type CommentStyle = "c" | "hash" | "html" | "none"
|
|
89
|
+
|
|
90
|
+
const COMMENTS: Record<CodeLang, CommentStyle> = {
|
|
91
|
+
ts: "c",
|
|
92
|
+
js: "c",
|
|
93
|
+
json: "none",
|
|
94
|
+
bash: "hash",
|
|
95
|
+
html: "html",
|
|
96
|
+
python: "hash",
|
|
97
|
+
rust: "c",
|
|
98
|
+
yaml: "hash",
|
|
99
|
+
toml: "hash",
|
|
100
|
+
kotlin: "c",
|
|
101
|
+
java: "c",
|
|
102
|
+
groovy: "c",
|
|
103
|
+
plain: "none",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const COMMENT_PATTERN: Record<CommentStyle, string> = {
|
|
107
|
+
c: "\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\/",
|
|
108
|
+
// A `#` only starts a comment at the start of a word, so `a#b` in a URL or
|
|
109
|
+
// `${#arr[@]}` in shell stay code.
|
|
110
|
+
hash: "(?<![\\w$])#[^\\n]*",
|
|
111
|
+
html: "<!--[\\s\\S]*?-->",
|
|
112
|
+
none: "(?!)",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Where an unterminated block comment opens on a line, and what closes it. */
|
|
116
|
+
const BLOCK: Partial<Record<CommentStyle, { open: string; close: string }>> = {
|
|
117
|
+
c: { open: "/*", close: "*/" },
|
|
118
|
+
html: { open: "<!--", close: "-->" },
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const masters = new Map<CommentStyle, RegExp>()
|
|
122
|
+
|
|
123
|
+
// One master regex per comment style, alternation ordered so longer/greedier
|
|
124
|
+
// rules win first. Order: comments → strings → numbers → identifiers → punctuation.
|
|
125
|
+
function masterFor(style: CommentStyle): RegExp {
|
|
126
|
+
let re = masters.get(style)
|
|
127
|
+
if (!re) {
|
|
128
|
+
re = new RegExp(
|
|
129
|
+
[
|
|
130
|
+
`(${COMMENT_PATTERN[style]})`, // 1 comment
|
|
131
|
+
"(\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|`(?:\\\\.|[^`\\\\])*`)", // 2 string
|
|
132
|
+
"(@[A-Za-z_$][\\w$]*)", // 3 annotation (@Override, @ConfigSerializable — JVM)
|
|
133
|
+
"(\\b\\d[\\d_]*(?:\\.\\d+)?(?:e[+-]?\\d+)?[fFlLdD]?\\b)", // 4 number (JVM suffixes)
|
|
134
|
+
"(#?[A-Za-z_$][\\w$]*)", // 5 identifier (keyword/bool/func/prop/plain), incl. #private
|
|
135
|
+
"([{}()\\[\\];:,.<>=+\\-*/%!&|?]+)", // 6 punctuation
|
|
136
|
+
].join("|"),
|
|
137
|
+
"g",
|
|
138
|
+
)
|
|
139
|
+
masters.set(style, re)
|
|
140
|
+
}
|
|
141
|
+
return re
|
|
142
|
+
}
|
|
95
143
|
|
|
96
144
|
function classifyIdent(name: string, before: string, after: string): string {
|
|
97
145
|
if (KEYWORDS.has(name)) return C.keyword
|
|
@@ -103,11 +151,13 @@ function classifyIdent(name: string, before: string, after: string): string {
|
|
|
103
151
|
|
|
104
152
|
/** Tokenize one line into coloured spans. JSON keys get the property colour. */
|
|
105
153
|
export function tokenizeLine(line: string, lang: CodeLang): Token[] {
|
|
154
|
+
if (lang === "plain") return line ? [{ text: line, cls: "" }] : []
|
|
106
155
|
const out: Token[] = []
|
|
107
156
|
let last = 0
|
|
108
157
|
let m: RegExpExecArray | null
|
|
109
|
-
|
|
110
|
-
|
|
158
|
+
const master = masterFor(COMMENTS[lang])
|
|
159
|
+
master.lastIndex = 0
|
|
160
|
+
while ((m = master.exec(line)) !== null) {
|
|
111
161
|
if (m.index > last) out.push({ text: line.slice(last, m.index), cls: "" })
|
|
112
162
|
const [full, comment, str, annotation, num, ident, punct] = m
|
|
113
163
|
if (comment != null) out.push({ text: full, cls: C.comment })
|
|
@@ -135,3 +185,49 @@ export function tokenizeLine(line: string, lang: CodeLang): Token[] {
|
|
|
135
185
|
if (last < line.length) out.push({ text: line.slice(last), cls: "" })
|
|
136
186
|
return out
|
|
137
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Tokenize a whole snippet, carrying a block comment (`/* … *\/`, `<!-- … -->`)
|
|
191
|
+
* across the lines it spans. `tokenizeLine` alone sees each line in isolation
|
|
192
|
+
* and coloured the middle of a doc comment as code.
|
|
193
|
+
*/
|
|
194
|
+
export function tokenizeLines(lines: string[], lang: CodeLang): Token[][] {
|
|
195
|
+
const block = BLOCK[COMMENTS[lang]]
|
|
196
|
+
if (!block) return lines.map((line) => tokenizeLine(line, lang))
|
|
197
|
+
const out: Token[][] = []
|
|
198
|
+
let open = false
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
let rest = line
|
|
201
|
+
const tokens: Token[] = []
|
|
202
|
+
if (open) {
|
|
203
|
+
const end = rest.indexOf(block.close)
|
|
204
|
+
if (end === -1) {
|
|
205
|
+
out.push(rest ? [{ text: rest, cls: C.comment }] : [])
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
tokens.push({ text: rest.slice(0, end + block.close.length), cls: C.comment })
|
|
209
|
+
rest = rest.slice(end + block.close.length)
|
|
210
|
+
open = false
|
|
211
|
+
}
|
|
212
|
+
const lineTokens = tokenizeLine(rest, lang)
|
|
213
|
+
// A closed comment is already a comment token, so an opener left in plain
|
|
214
|
+
// code is one the line never closes: from there on, the line is comment.
|
|
215
|
+
const at = lineTokens.findIndex(
|
|
216
|
+
(t) => t.cls !== C.comment && t.cls !== C.string && t.text.includes(block.open),
|
|
217
|
+
)
|
|
218
|
+
if (at >= 0) {
|
|
219
|
+
const cut = lineTokens[at].text.indexOf(block.open)
|
|
220
|
+
const tail = lineTokens
|
|
221
|
+
.slice(at)
|
|
222
|
+
.map((t) => t.text)
|
|
223
|
+
.join("")
|
|
224
|
+
.slice(cut)
|
|
225
|
+
tokens.push(...lineTokens.slice(0, at))
|
|
226
|
+
if (cut > 0) tokens.push({ text: lineTokens[at].text.slice(0, cut), cls: lineTokens[at].cls })
|
|
227
|
+
tokens.push({ text: tail, cls: C.comment })
|
|
228
|
+
open = true
|
|
229
|
+
} else tokens.push(...lineTokens)
|
|
230
|
+
out.push(tokens)
|
|
231
|
+
}
|
|
232
|
+
return out
|
|
233
|
+
}
|
package/src/components/color.ts
CHANGED
|
@@ -50,7 +50,15 @@ export function clampHsv(c: Hsv): Hsv {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
// NaN and Infinity read as 0: one bad channel must not throw out of a public
|
|
54
|
+
// conversion or poison the others.
|
|
55
|
+
const finite = (n: number) => (Number.isFinite(n) ? n : 0)
|
|
56
|
+
|
|
57
|
+
export function hsvToRgb(color: Hsv): Rgb {
|
|
58
|
+
const h = finite(color.h)
|
|
59
|
+
const s = clamp(finite(color.s), 0, 1)
|
|
60
|
+
const v = clamp(finite(color.v), 0, 1)
|
|
61
|
+
const a = Number.isFinite(color.a) ? color.a : 1
|
|
54
62
|
const hue = normalizeHue(h) / 60
|
|
55
63
|
const c = v * s
|
|
56
64
|
const x = c * (1 - Math.abs((hue % 2) - 1))
|
|
@@ -99,10 +107,20 @@ export function rgbToHsv({ r, g, b, a }: Rgb): Hsv {
|
|
|
99
107
|
|
|
100
108
|
const hex2 = (n: number) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0")
|
|
101
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Alpha quantised to the one precision every format can carry, a byte, so hex,
|
|
112
|
+
* rgb and hsl agree on whether a colour is opaque: 0.996 used to print as an
|
|
113
|
+
* opaque rgb() and as "#…fe" in hex.
|
|
114
|
+
*/
|
|
115
|
+
function alphaByte(a: number): number {
|
|
116
|
+
return Math.round(clamp(a, 0, 1) * 255)
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
/** "#rrggbb", or "#rrggbbaa" when the colour is not fully opaque. */
|
|
103
120
|
export function rgbToHex({ r, g, b, a }: Rgb): string {
|
|
104
121
|
const base = `#${hex2(r)}${hex2(g)}${hex2(b)}`
|
|
105
|
-
|
|
122
|
+
const byte = alphaByte(a)
|
|
123
|
+
return byte >= 255 ? base : base + hex2(byte)
|
|
106
124
|
}
|
|
107
125
|
|
|
108
126
|
/**
|
|
@@ -199,20 +217,23 @@ export function hsvToHsl({ h, s, v }: Hsv): { h: number; s: number; l: number }
|
|
|
199
217
|
/** The colour as a CSS string in the requested format. */
|
|
200
218
|
export function formatColor(c: Hsv, format: ColorFormat): string {
|
|
201
219
|
const rgb = hsvToRgb(c)
|
|
202
|
-
const
|
|
220
|
+
const byte = alphaByte(rgb.a)
|
|
221
|
+
const a = Math.round((byte / 255) * 1000) / 1000
|
|
203
222
|
|
|
204
223
|
if (format === "hex") return rgbToHex(rgb)
|
|
205
224
|
if (format === "rgb") {
|
|
206
|
-
return
|
|
225
|
+
return byte >= 255
|
|
207
226
|
? `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`
|
|
208
227
|
: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`
|
|
209
228
|
}
|
|
210
229
|
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
230
|
+
// One decimal: whole percentages drift up to 4 per channel on the way back.
|
|
231
|
+
const { h, s, l } = hsvToHsl({ ...c, h: finite(c.h), s: clamp(finite(c.s), 0, 1), v: clamp(finite(c.v), 0, 1) })
|
|
232
|
+
const dec = (n: number) => Math.round(n * 10) / 10
|
|
233
|
+
const hh = dec(h) % 360
|
|
234
|
+
const ss = dec(s * 100)
|
|
235
|
+
const ll = dec(l * 100)
|
|
236
|
+
return byte >= 255 ? `hsl(${hh}, ${ss}%, ${ll}%)` : `hsla(${hh}, ${ss}%, ${ll}%, ${a})`
|
|
216
237
|
}
|
|
217
238
|
|
|
218
239
|
/** Always opaque — for the swatch behind an alpha checkerboard, and for rails. */
|
|
@@ -63,6 +63,12 @@ function score(command: SearchableCommand, terms: CommandSearchTerms): number {
|
|
|
63
63
|
if (!hit) return 0
|
|
64
64
|
total += hit
|
|
65
65
|
}
|
|
66
|
+
// A label that IS what was typed, or starts with it, is what the person is
|
|
67
|
+
// naming: "theme" must put the Theme command above ThemeSwitcher, which only
|
|
68
|
+
// contains the word but also matches its keywords and description.
|
|
69
|
+
const typed = terms.include.join(" ")
|
|
70
|
+
if (typed && label === typed) total += 6
|
|
71
|
+
else if (typed && label.startsWith(typed)) total += 2
|
|
66
72
|
return total
|
|
67
73
|
}
|
|
68
74
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { reactive } from "vue"
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Imperative confirmation: `if (await confirm({ title: "Delete?" })) ...`.
|
|
5
|
+
* Requests queue here and the one mounted LpConfirmHost shows them one at a
|
|
6
|
+
* time. Pinned to a global symbol like the toast queue, so a barrel and a deep
|
|
7
|
+
* import share one host.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ConfirmOptions {
|
|
11
|
+
title: string
|
|
12
|
+
description?: string
|
|
13
|
+
danger?: boolean
|
|
14
|
+
icon?: string
|
|
15
|
+
confirmLabel?: string
|
|
16
|
+
cancelLabel?: string
|
|
17
|
+
loadingLabel?: string
|
|
18
|
+
/**
|
|
19
|
+
* Work to run on confirm. The dialog stays open with a loading state while it
|
|
20
|
+
* runs and shows the error's message if it rejects; the promise from
|
|
21
|
+
* `confirm()` resolves true only once it succeeded.
|
|
22
|
+
*/
|
|
23
|
+
action?: () => unknown
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ConfirmRequest extends ConfirmOptions {
|
|
27
|
+
id: number
|
|
28
|
+
resolve: (confirmed: boolean) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const STATE_KEY = Symbol.for("@leavepulse/ui:confirm-queue")
|
|
32
|
+
type GlobalWithConfirm = typeof globalThis & {
|
|
33
|
+
[STATE_KEY]?: { queue: ConfirmRequest[]; nextId: number }
|
|
34
|
+
}
|
|
35
|
+
const g = globalThis as GlobalWithConfirm
|
|
36
|
+
const state = (g[STATE_KEY] ??= { queue: reactive<ConfirmRequest[]>([]), nextId: 0 })
|
|
37
|
+
|
|
38
|
+
export const confirmQueue = state.queue
|
|
39
|
+
|
|
40
|
+
/** Ask for confirmation. Resolves false when the user cancels or dismisses. */
|
|
41
|
+
export function confirm(options: ConfirmOptions): Promise<boolean> {
|
|
42
|
+
if (typeof window === "undefined") return Promise.resolve(false)
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
state.queue.push({ ...options, id: state.nextId++, resolve })
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Settle a request and drop it from the queue. Used by the host. */
|
|
49
|
+
export function settleConfirm(id: number, confirmed: boolean): void {
|
|
50
|
+
const i = state.queue.findIndex((r) => r.id === id)
|
|
51
|
+
if (i === -1) return
|
|
52
|
+
const [request] = state.queue.splice(i, 1)
|
|
53
|
+
request?.resolve(confirmed)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function useConfirm(): typeof confirm {
|
|
57
|
+
return confirm
|
|
58
|
+
}
|
|
@@ -65,3 +65,15 @@ export function matchCountryByValue(
|
|
|
65
65
|
}
|
|
66
66
|
return best
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Countries matching a search query: by name, or by dial code when the query
|
|
71
|
+
* carries digits ("+44", "44"). A query without digits must not match on the
|
|
72
|
+
* dial code at all, or every country would match the empty string.
|
|
73
|
+
*/
|
|
74
|
+
export function filterCountries(list: Country[], query: string): Country[] {
|
|
75
|
+
const q = query.trim().toLowerCase()
|
|
76
|
+
if (!q) return list
|
|
77
|
+
const digits = q.replace(/\D/g, "")
|
|
78
|
+
return list.filter((c) => c.name.toLowerCase().includes(q) || (digits !== "" && c.dial.includes(digits)))
|
|
79
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CalendarDate,
|
|
3
|
+
endOfMonth,
|
|
4
|
+
getLocalTimeZone,
|
|
5
|
+
parseDate,
|
|
6
|
+
startOfMonth,
|
|
7
|
+
today,
|
|
8
|
+
} from "@internationalized/date"
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Date ranges as the kit models them: two ISO "YYYY-MM-DD" strings, inclusive
|
|
12
|
+
* on both ends, the same shape LpDatePicker uses for a single day. Presets are
|
|
13
|
+
* plain functions so a consumer can add "This quarter" or "Year to date"
|
|
14
|
+
* without the kit knowing about it.
|
|
15
|
+
*/
|
|
16
|
+
export interface DateRange {
|
|
17
|
+
start: string
|
|
18
|
+
end: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DateRangePreset {
|
|
22
|
+
label: string
|
|
23
|
+
range: () => DateRange
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DateRangePresetLabels {
|
|
27
|
+
today: string
|
|
28
|
+
last7: string
|
|
29
|
+
last30: string
|
|
30
|
+
thisMonth: string
|
|
31
|
+
lastMonth: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function span(start: CalendarDate, end: CalendarDate): DateRange {
|
|
35
|
+
return { start: start.toString(), end: end.toString() }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The default preset list. `now` is injectable so the set can be tested. */
|
|
39
|
+
export function defaultRangePresets(
|
|
40
|
+
labels: DateRangePresetLabels,
|
|
41
|
+
now: () => CalendarDate = () => today(getLocalTimeZone()),
|
|
42
|
+
): DateRangePreset[] {
|
|
43
|
+
return [
|
|
44
|
+
{ label: labels.today, range: () => span(now(), now()) },
|
|
45
|
+
{ label: labels.last7, range: () => span(now().subtract({ days: 6 }), now()) },
|
|
46
|
+
{ label: labels.last30, range: () => span(now().subtract({ days: 29 }), now()) },
|
|
47
|
+
{ label: labels.thisMonth, range: () => span(startOfMonth(now()), endOfMonth(now())) },
|
|
48
|
+
{
|
|
49
|
+
label: labels.lastMonth,
|
|
50
|
+
range: () => {
|
|
51
|
+
const previous = now().subtract({ months: 1 })
|
|
52
|
+
return span(startOfMonth(previous), endOfMonth(previous))
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseIsoDay(iso: string | undefined): CalendarDate | undefined {
|
|
59
|
+
if (!iso) return undefined
|
|
60
|
+
try {
|
|
61
|
+
return parseDate(iso.slice(0, 10))
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A range with both ends parsed and in order, or undefined. */
|
|
68
|
+
export function normalizeRange(range: Partial<DateRange> | undefined): DateRange | undefined {
|
|
69
|
+
const start = parseIsoDay(range?.start)
|
|
70
|
+
const end = parseIsoDay(range?.end)
|
|
71
|
+
if (!start || !end) return undefined
|
|
72
|
+
return start.compare(end) <= 0 ? span(start, end) : span(end, start)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function sameRange(a: Partial<DateRange> | undefined, b: Partial<DateRange> | undefined): boolean {
|
|
76
|
+
const x = normalizeRange(a)
|
|
77
|
+
const y = normalizeRange(b)
|
|
78
|
+
return !!x && !!y && x.start === y.start && x.end === y.end
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Clip a preset to the allowed bounds; undefined when nothing of it is left. */
|
|
82
|
+
export function clampRange(range: DateRange, min?: string, max?: string): DateRange | undefined {
|
|
83
|
+
const r = normalizeRange(range)
|
|
84
|
+
if (!r) return undefined
|
|
85
|
+
const start = min && r.start < min ? min : r.start
|
|
86
|
+
const end = max && r.end > max ? max : r.end
|
|
87
|
+
return start <= end ? { start, end } : undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* "Jun 3 – 18, 2026" style text: Intl's own range formatting collapses the
|
|
92
|
+
* shared month and year, which is how a range reads naturally in every locale.
|
|
93
|
+
*/
|
|
94
|
+
export function formatRange(
|
|
95
|
+
range: DateRange,
|
|
96
|
+
locale?: string,
|
|
97
|
+
format: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric" },
|
|
98
|
+
): string {
|
|
99
|
+
const start = parseIsoDay(range.start)
|
|
100
|
+
const end = parseIsoDay(range.end)
|
|
101
|
+
if (!start || !end) return ""
|
|
102
|
+
const zone = "UTC"
|
|
103
|
+
const fmt = new Intl.DateTimeFormat(locale, { ...format, timeZone: zone })
|
|
104
|
+
return fmt.formatRange(start.toDate(zone), end.toDate(zone))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ReshapeRules {
|
|
108
|
+
/** Whether the moving end may land on this day (min, max, disabled). */
|
|
109
|
+
allowed?: (iso: string) => boolean
|
|
110
|
+
/** Days a range may not include. */
|
|
111
|
+
unavailable?: (iso: string) => boolean
|
|
112
|
+
/** Longest range, in days, counting both ends. */
|
|
113
|
+
maximumDays?: number
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The range one end makes when it is dragged to `moving` while the other stays
|
|
118
|
+
* at `anchor`; passing the anchor flips which end is which. Null when the result
|
|
119
|
+
* would break a rule, so a drag simply does not follow onto that day.
|
|
120
|
+
*/
|
|
121
|
+
export function reshapeRange(anchor: string, moving: string, rules: ReshapeRules = {}): DateRange | null {
|
|
122
|
+
const a = parseIsoDay(anchor)
|
|
123
|
+
const m = parseIsoDay(moving)
|
|
124
|
+
if (!a || !m) return null
|
|
125
|
+
if (rules.allowed && !rules.allowed(m.toString())) return null
|
|
126
|
+
const [start, end] = a.compare(m) <= 0 ? [a, m] : [m, a]
|
|
127
|
+
if (rules.maximumDays && end.compare(start) + 1 > rules.maximumDays) return null
|
|
128
|
+
if (rules.unavailable) {
|
|
129
|
+
for (let d = start; d.compare(end) <= 0; d = d.add({ days: 1 })) {
|
|
130
|
+
if (rules.unavailable(d.toString())) return null
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { start: start.toString(), end: end.toString() }
|
|
134
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Data export shared by the table and the chart: CSV text, a file download,
|
|
3
|
+
* and an SVG rasterised with its theme resolved.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CsvColumn<Row> {
|
|
7
|
+
key: string
|
|
8
|
+
/** Header cell. Defaults to the key. */
|
|
9
|
+
label?: string
|
|
10
|
+
/** The value written for a row; the row's `key` field when omitted. */
|
|
11
|
+
value?: (row: Row) => unknown
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CsvOptions {
|
|
15
|
+
/** Field separator. Default ",". Some locales' spreadsheets expect ";". */
|
|
16
|
+
separator?: string
|
|
17
|
+
/** Prefix a UTF-8 byte order mark so Excel reads non-ASCII text correctly. */
|
|
18
|
+
bom?: boolean
|
|
19
|
+
/**
|
|
20
|
+
* Neutralise cells a spreadsheet would run as a formula (leading = + - @,
|
|
21
|
+
* tab or CR) by prefixing a quote. On by default: exported data is often
|
|
22
|
+
* user-supplied, and opening it is enough to trigger a formula.
|
|
23
|
+
*/
|
|
24
|
+
guardFormulas?: boolean
|
|
25
|
+
/** Line ending. Default "\r\n", as RFC 4180 specifies. */
|
|
26
|
+
newline?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const FORMULA_START = /^[=+\-@\t\r]/
|
|
30
|
+
|
|
31
|
+
function cellText(value: unknown): string {
|
|
32
|
+
if (value == null) return ""
|
|
33
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toISOString()
|
|
34
|
+
if (typeof value === "object") return JSON.stringify(value)
|
|
35
|
+
return String(value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One CSV field, quoted when it holds a separator, a quote or a line break. */
|
|
39
|
+
export function csvField(value: unknown, options: CsvOptions = {}): string {
|
|
40
|
+
const separator = options.separator ?? ","
|
|
41
|
+
let text = cellText(value)
|
|
42
|
+
if ((options.guardFormulas ?? true) && typeof value === "string" && FORMULA_START.test(text)) {
|
|
43
|
+
text = `'${text}`
|
|
44
|
+
}
|
|
45
|
+
const needsQuotes = text.includes(separator) || /["\r\n]/.test(text) || /^\s|\s$/.test(text)
|
|
46
|
+
return needsQuotes ? `"${text.replace(/"/g, '""')}"` : text
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Rows as CSV text, a header line first. */
|
|
50
|
+
export function toCsv<Row>(rows: readonly Row[], columns: readonly CsvColumn<Row>[], options: CsvOptions = {}): string {
|
|
51
|
+
const separator = options.separator ?? ","
|
|
52
|
+
const newline = options.newline ?? "\r\n"
|
|
53
|
+
const line = (cells: unknown[]) => cells.map((c) => csvField(c, options)).join(separator)
|
|
54
|
+
const out = [line(columns.map((c) => c.label ?? c.key))]
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
out.push(
|
|
57
|
+
line(columns.map((c) => (c.value ? c.value(row) : (row as Record<string, unknown>)[c.key]))),
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
return (options.bom ? "" : "") + out.join(newline) + newline
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Hand the viewer a file to save. Does nothing outside a browser, so a call
|
|
65
|
+
* that runs during SSR is harmless.
|
|
66
|
+
*/
|
|
67
|
+
export function downloadFile(name: string, data: Blob | string, type = "text/plain;charset=utf-8"): void {
|
|
68
|
+
if (typeof document === "undefined" || typeof URL === "undefined") return
|
|
69
|
+
const blob = typeof data === "string" ? new Blob([data], { type }) : data
|
|
70
|
+
const url = URL.createObjectURL(blob)
|
|
71
|
+
const a = document.createElement("a")
|
|
72
|
+
a.href = url
|
|
73
|
+
a.download = name
|
|
74
|
+
a.rel = "noopener"
|
|
75
|
+
a.style.display = "none"
|
|
76
|
+
document.body.append(a)
|
|
77
|
+
a.click()
|
|
78
|
+
a.remove()
|
|
79
|
+
setTimeout(() => URL.revokeObjectURL(url), 0)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const PAINT_PROPS = [
|
|
83
|
+
"fill",
|
|
84
|
+
"fill-opacity",
|
|
85
|
+
"fill-rule",
|
|
86
|
+
"stroke",
|
|
87
|
+
"stroke-opacity",
|
|
88
|
+
"stroke-width",
|
|
89
|
+
"stroke-dasharray",
|
|
90
|
+
"stroke-dashoffset",
|
|
91
|
+
"stroke-linecap",
|
|
92
|
+
"stroke-linejoin",
|
|
93
|
+
"opacity",
|
|
94
|
+
"stop-color",
|
|
95
|
+
"stop-opacity",
|
|
96
|
+
"color",
|
|
97
|
+
"font-family",
|
|
98
|
+
"font-size",
|
|
99
|
+
"font-weight",
|
|
100
|
+
"letter-spacing",
|
|
101
|
+
"text-anchor",
|
|
102
|
+
"dominant-baseline",
|
|
103
|
+
"visibility",
|
|
104
|
+
"display",
|
|
105
|
+
] as const
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A standalone copy of a live SVG: every element carries its computed paint as
|
|
109
|
+
* an inline style, so CSS custom properties and classes from the page's
|
|
110
|
+
* stylesheet survive being rendered as an image, and running SMIL animations
|
|
111
|
+
* are frozen at their end state.
|
|
112
|
+
*/
|
|
113
|
+
export function inlineSvg(svg: SVGSVGElement): SVGSVGElement {
|
|
114
|
+
const clone = svg.cloneNode(true) as SVGSVGElement
|
|
115
|
+
const source = [svg, ...svg.querySelectorAll("*")]
|
|
116
|
+
const target = [clone, ...clone.querySelectorAll("*")]
|
|
117
|
+
source.forEach((el, i) => {
|
|
118
|
+
const copy = target[i]
|
|
119
|
+
if (!(copy instanceof Element)) return
|
|
120
|
+
const computed = getComputedStyle(el)
|
|
121
|
+
const style = PAINT_PROPS.map((p) => `${p}:${computed.getPropertyValue(p)}`)
|
|
122
|
+
.filter((d) => !d.endsWith(":"))
|
|
123
|
+
.join(";")
|
|
124
|
+
copy.setAttribute("style", style)
|
|
125
|
+
copy.removeAttribute("class")
|
|
126
|
+
})
|
|
127
|
+
// Comment nodes (Vue leaves them in development builds) may hold "--", which
|
|
128
|
+
// is invalid inside an XML comment and makes the whole image fail to load.
|
|
129
|
+
const comments = document.createTreeWalker(clone, NodeFilter.SHOW_COMMENT)
|
|
130
|
+
const dead: Node[] = []
|
|
131
|
+
while (comments.nextNode()) dead.push(comments.currentNode)
|
|
132
|
+
for (const node of dead) node.parentNode?.removeChild(node)
|
|
133
|
+
for (const anim of clone.querySelectorAll("animate")) {
|
|
134
|
+
const attr = anim.getAttribute("attributeName")
|
|
135
|
+
const to = anim.getAttribute("to")
|
|
136
|
+
if (attr && to != null) anim.parentElement?.setAttribute(attr, to)
|
|
137
|
+
anim.remove()
|
|
138
|
+
}
|
|
139
|
+
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg")
|
|
140
|
+
const { width, height } = svg.getBoundingClientRect()
|
|
141
|
+
clone.setAttribute("width", String(width))
|
|
142
|
+
clone.setAttribute("height", String(height))
|
|
143
|
+
clone.style.overflow = "visible"
|
|
144
|
+
return clone
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface RasterOptions {
|
|
148
|
+
/** Device pixels per CSS pixel. Default: the screen's ratio, at least 2. */
|
|
149
|
+
scale?: number
|
|
150
|
+
/** Fill behind the drawing; transparent when omitted. */
|
|
151
|
+
background?: string
|
|
152
|
+
/** Extra space around the drawing, in CSS pixels. */
|
|
153
|
+
padding?: number
|
|
154
|
+
/** Drawn above the SVG, in the page's own font. */
|
|
155
|
+
header?: (ctx: CanvasRenderingContext2D, width: number) => number
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Rasterise an SVG element to a PNG blob with its theme resolved. */
|
|
159
|
+
export async function svgToPng(svg: SVGSVGElement, options: RasterOptions = {}): Promise<Blob> {
|
|
160
|
+
const copy = inlineSvg(svg)
|
|
161
|
+
const width = Number(copy.getAttribute("width"))
|
|
162
|
+
const height = Number(copy.getAttribute("height"))
|
|
163
|
+
const markup = new XMLSerializer().serializeToString(copy)
|
|
164
|
+
const url = URL.createObjectURL(new Blob([markup], { type: "image/svg+xml;charset=utf-8" }))
|
|
165
|
+
try {
|
|
166
|
+
const img = new Image()
|
|
167
|
+
img.decoding = "async"
|
|
168
|
+
const loaded = new Promise<void>((resolve, reject) => {
|
|
169
|
+
img.onload = () => resolve()
|
|
170
|
+
img.onerror = () => reject(new Error("SVG could not be rasterised"))
|
|
171
|
+
})
|
|
172
|
+
img.src = url
|
|
173
|
+
await loaded
|
|
174
|
+
|
|
175
|
+
const scale = options.scale ?? Math.max(2, globalThis.devicePixelRatio || 1)
|
|
176
|
+
const pad = options.padding ?? 0
|
|
177
|
+
const measure = document.createElement("canvas").getContext("2d")
|
|
178
|
+
const headerHeight = measure && options.header ? options.header(measure, width) : 0
|
|
179
|
+
const canvas = document.createElement("canvas")
|
|
180
|
+
canvas.width = Math.ceil((width + pad * 2) * scale)
|
|
181
|
+
canvas.height = Math.ceil((height + headerHeight + pad * 2) * scale)
|
|
182
|
+
const ctx = canvas.getContext("2d")
|
|
183
|
+
if (!ctx) throw new Error("Canvas 2D is unavailable")
|
|
184
|
+
ctx.scale(scale, scale)
|
|
185
|
+
if (options.background) {
|
|
186
|
+
ctx.fillStyle = options.background
|
|
187
|
+
ctx.fillRect(0, 0, width + pad * 2, height + headerHeight + pad * 2)
|
|
188
|
+
}
|
|
189
|
+
ctx.translate(pad, pad)
|
|
190
|
+
if (options.header) options.header(ctx, width)
|
|
191
|
+
ctx.drawImage(img, 0, headerHeight, width, height)
|
|
192
|
+
return await new Promise<Blob>((resolve, reject) =>
|
|
193
|
+
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error("PNG encoding failed"))), "image/png"),
|
|
194
|
+
)
|
|
195
|
+
} finally {
|
|
196
|
+
URL.revokeObjectURL(url)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The first non-transparent background at or above an element. */
|
|
201
|
+
export function backgroundOf(el: Element | null): string | undefined {
|
|
202
|
+
for (let node = el; node; node = node.parentElement) {
|
|
203
|
+
const bg = getComputedStyle(node).backgroundColor
|
|
204
|
+
if (bg && bg !== "transparent" && !/rgba\([^)]*,\s*0\)$/.test(bg)) return bg
|
|
205
|
+
}
|
|
206
|
+
return undefined
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A colour as the browser resolves it where `within` sits, for APIs that cannot
|
|
211
|
+
* read custom properties (canvas, an SVG drawn as an image).
|
|
212
|
+
*/
|
|
213
|
+
export function resolveCssColor(value: string, within: Element): string {
|
|
214
|
+
if (!value.includes("var(") && !value.toLowerCase().includes("currentcolor")) return value
|
|
215
|
+
const probe = document.createElement("span")
|
|
216
|
+
probe.style.color = value
|
|
217
|
+
probe.style.display = "none"
|
|
218
|
+
within.append(probe)
|
|
219
|
+
const resolved = getComputedStyle(probe).color
|
|
220
|
+
probe.remove()
|
|
221
|
+
return resolved || value
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** A filesystem-safe name from a title. */
|
|
225
|
+
export function fileStem(title: string | undefined, fallback: string): string {
|
|
226
|
+
const stem = (title ?? "").trim().replace(/[\\/:*?"<>|\u0000-\u001f]+/g, "").replace(/\s+/g, "-")
|
|
227
|
+
return stem || fallback
|
|
228
|
+
}
|