@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
|
@@ -22,11 +22,29 @@ import {
|
|
|
22
22
|
DialogTitle,
|
|
23
23
|
} from "reka-ui"
|
|
24
24
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue"
|
|
25
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
25
26
|
import { useZoomPan } from "../composables/useZoomPan"
|
|
26
27
|
import { fileNameOf, type LightboxItem } from "./lightbox"
|
|
27
28
|
import LpIcon from "./LpIcon.vue"
|
|
28
29
|
import { useModalLayer } from "./modalDepth"
|
|
29
30
|
|
|
31
|
+
export type LightboxLabels = {
|
|
32
|
+
viewer: string
|
|
33
|
+
/** `{index}` and `{total}` are filled in. */
|
|
34
|
+
position: string
|
|
35
|
+
previous: string
|
|
36
|
+
next: string
|
|
37
|
+
zoomIn: string
|
|
38
|
+
zoomOut: string
|
|
39
|
+
rotate: string
|
|
40
|
+
copy: string
|
|
41
|
+
download: string
|
|
42
|
+
close: string
|
|
43
|
+
failed: string
|
|
44
|
+
spoiler: string
|
|
45
|
+
reveal: string
|
|
46
|
+
}
|
|
47
|
+
|
|
30
48
|
const props = withDefaults(
|
|
31
49
|
defineProps<{
|
|
32
50
|
open?: boolean
|
|
@@ -46,6 +64,7 @@ const props = withDefaults(
|
|
|
46
64
|
loop?: boolean
|
|
47
65
|
/** Largest zoom level. */
|
|
48
66
|
maxZoom?: number
|
|
67
|
+
labels?: Partial<LightboxLabels>
|
|
49
68
|
}>(),
|
|
50
69
|
{
|
|
51
70
|
open: false,
|
|
@@ -68,7 +87,31 @@ const emit = defineEmits<{
|
|
|
68
87
|
(e: "copy", item: LightboxItem, ok: boolean): void
|
|
69
88
|
}>()
|
|
70
89
|
|
|
71
|
-
const
|
|
90
|
+
const text = useLabels<LightboxLabels>(
|
|
91
|
+
"lightbox",
|
|
92
|
+
{
|
|
93
|
+
viewer: "Image viewer",
|
|
94
|
+
position: "Image {index} of {total}",
|
|
95
|
+
previous: "Previous image",
|
|
96
|
+
next: "Next image",
|
|
97
|
+
zoomIn: "Zoom in (+)",
|
|
98
|
+
zoomOut: "Zoom out (−)",
|
|
99
|
+
rotate: "Rotate (R)",
|
|
100
|
+
copy: "Copy image",
|
|
101
|
+
download: "Download",
|
|
102
|
+
close: "Close (Esc)",
|
|
103
|
+
failed: "This image couldn't be loaded.",
|
|
104
|
+
spoiler: "Spoiler",
|
|
105
|
+
reveal: "Show image",
|
|
106
|
+
},
|
|
107
|
+
() => props.labels,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const position = (i: number) =>
|
|
111
|
+
text.value.position.replace("{index}", String(i + 1)).replace("{total}", String(props.items.length))
|
|
112
|
+
|
|
113
|
+
const contentRef = ref<{ $el: HTMLElement } | null>(null)
|
|
114
|
+
const { claim, release, layer } = useModalLayer({ element: () => contentRef.value?.$el })
|
|
72
115
|
watch(() => props.open, (open) => (open ? claim() : release()), { immediate: true })
|
|
73
116
|
onBeforeUnmount(release)
|
|
74
117
|
|
|
@@ -79,7 +122,7 @@ defineSlots<{
|
|
|
79
122
|
actions(props: { item: LightboxItem; index: number }): unknown
|
|
80
123
|
}>()
|
|
81
124
|
|
|
82
|
-
const zoom = useZoomPan({ max: props.maxZoom })
|
|
125
|
+
const zoom = useZoomPan({ max: () => props.maxZoom })
|
|
83
126
|
|
|
84
127
|
// `zoom` is a plain object, so its refs don't unwrap in the template — surface
|
|
85
128
|
// the two the chrome reads as computeds rather than writing `.value` in markup.
|
|
@@ -87,7 +130,11 @@ const scale = computed(() => zoom.scale.value)
|
|
|
87
130
|
const isZoomed = computed(() => zoom.zoomed.value)
|
|
88
131
|
const isPanning = computed(() => zoom.panning.value)
|
|
89
132
|
|
|
90
|
-
|
|
133
|
+
// Works with and without v-model:index: the prop seeds and steers it, paging
|
|
134
|
+
// moves it locally and reports the move.
|
|
135
|
+
const shown = ref(props.index)
|
|
136
|
+
watch(() => props.index, (i) => (shown.value = i))
|
|
137
|
+
const current = computed(() => Math.min(Math.max(shown.value, 0), Math.max(props.items.length - 1, 0)))
|
|
91
138
|
const item = computed<LightboxItem | undefined>(() => props.items[current.value])
|
|
92
139
|
const total = computed(() => props.items.length)
|
|
93
140
|
const showStrip = computed(() => props.filmstrip ?? total.value > 1)
|
|
@@ -113,6 +160,7 @@ function go(next: number) {
|
|
|
113
160
|
// Compare against the raw target, not the wrapped one: stepping past the last
|
|
114
161
|
// image is still "forward" even though the index jumps back to 0.
|
|
115
162
|
direction.value = next > current.value ? 1 : -1
|
|
163
|
+
shown.value = wrapped
|
|
116
164
|
emit("update:index", wrapped)
|
|
117
165
|
emit("change", wrapped, props.items[wrapped])
|
|
118
166
|
}
|
|
@@ -193,6 +241,23 @@ function close() {
|
|
|
193
241
|
emit("update:open", false)
|
|
194
242
|
}
|
|
195
243
|
|
|
244
|
+
/*
|
|
245
|
+
* A spoiler stays covered until it is asked for, here as in the gallery: the
|
|
246
|
+
* viewer is often opened straight from a link or a filmstrip, past whatever
|
|
247
|
+
* cover the grid had.
|
|
248
|
+
*/
|
|
249
|
+
const revealed = ref<Set<number>>(new Set())
|
|
250
|
+
const covered = (i: number) => !!props.items[i]?.spoiler && !revealed.value.has(i)
|
|
251
|
+
function reveal(i: number) {
|
|
252
|
+
revealed.value = new Set(revealed.value).add(i)
|
|
253
|
+
}
|
|
254
|
+
watch(
|
|
255
|
+
() => props.open,
|
|
256
|
+
(open) => {
|
|
257
|
+
if (!open) revealed.value = new Set()
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
|
|
196
261
|
function onKeydown(e: KeyboardEvent) {
|
|
197
262
|
switch (e.key) {
|
|
198
263
|
case "ArrowRight":
|
|
@@ -270,21 +335,51 @@ function onViewportClick(e: MouseEvent) {
|
|
|
270
335
|
close()
|
|
271
336
|
}
|
|
272
337
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
338
|
+
function sameOrigin(src: string): boolean {
|
|
339
|
+
try {
|
|
340
|
+
const url = new URL(src, window.location.href)
|
|
341
|
+
return url.protocol === "blob:" || url.protocol === "data:" || url.origin === window.location.origin
|
|
342
|
+
} catch {
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function save(href: string, name: string, newTab = false) {
|
|
277
348
|
const a = document.createElement("a")
|
|
278
|
-
a.href =
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
349
|
+
a.href = href
|
|
350
|
+
if (newTab) {
|
|
351
|
+
a.target = "_blank"
|
|
352
|
+
a.rel = "noopener"
|
|
353
|
+
} else {
|
|
354
|
+
a.download = name
|
|
355
|
+
}
|
|
283
356
|
document.body.appendChild(a)
|
|
284
357
|
a.click()
|
|
285
358
|
a.remove()
|
|
286
359
|
}
|
|
287
360
|
|
|
361
|
+
/*
|
|
362
|
+
* A cross-origin `download` attribute is ignored and the link just navigates,
|
|
363
|
+
* which unloaded the whole app in favour of the image. Such a source is fetched
|
|
364
|
+
* into a blob first; one that refuses CORS opens in a new tab instead.
|
|
365
|
+
*/
|
|
366
|
+
async function download() {
|
|
367
|
+
const target = item.value
|
|
368
|
+
if (!target) return
|
|
369
|
+
emit("download", target)
|
|
370
|
+
const name = fileNameOf(target)
|
|
371
|
+
if (sameOrigin(target.src)) return save(target.src, name)
|
|
372
|
+
try {
|
|
373
|
+
const response = await fetch(target.src, { mode: "cors" })
|
|
374
|
+
if (!response.ok) throw new Error(String(response.status))
|
|
375
|
+
const url = URL.createObjectURL(await response.blob())
|
|
376
|
+
save(url, name)
|
|
377
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 0)
|
|
378
|
+
} catch {
|
|
379
|
+
save(target.src, name, true)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
288
383
|
async function copy() {
|
|
289
384
|
const target = item.value
|
|
290
385
|
if (!target) return
|
|
@@ -346,14 +441,15 @@ const ARROW =
|
|
|
346
441
|
:style="{ zIndex: layer.scrim }"
|
|
347
442
|
/>
|
|
348
443
|
<DialogContent
|
|
444
|
+
ref="contentRef"
|
|
349
445
|
class="fixed inset-0 flex flex-col outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none"
|
|
350
446
|
:style="{ zIndex: layer.panel }"
|
|
351
447
|
@keydown="onKeydown"
|
|
352
448
|
>
|
|
353
449
|
<!-- reka wants both for a labelled dialog; the caption is the title. -->
|
|
354
|
-
<DialogTitle class="sr-only">{{ item?.title ??
|
|
450
|
+
<DialogTitle class="sr-only">{{ item?.title ?? text.viewer }}</DialogTitle>
|
|
355
451
|
<DialogDescription class="sr-only">
|
|
356
|
-
{{ item?.description ??
|
|
452
|
+
{{ item?.description ?? position(current) }}
|
|
357
453
|
</DialogDescription>
|
|
358
454
|
|
|
359
455
|
<!-- Toolbar. Chrome drifts in from its own edge, slightly behind the
|
|
@@ -369,34 +465,34 @@ const ARROW =
|
|
|
369
465
|
<div class="ml-auto flex items-center gap-1">
|
|
370
466
|
<slot name="actions" :item="item!" :index="current" />
|
|
371
467
|
|
|
372
|
-
<button :class="BTN" type="button" title="
|
|
468
|
+
<button :class="BTN" type="button" :title="text.zoomOut" @click="zoom.zoomBy(1 / 1.3)">
|
|
373
469
|
<LpIcon name="lucide:zoom-out" :size="18" />
|
|
374
470
|
</button>
|
|
375
|
-
<button :class="BTN" type="button" title="
|
|
471
|
+
<button :class="BTN" type="button" :title="text.zoomIn" @click="zoom.zoomBy(1.3)">
|
|
376
472
|
<LpIcon name="lucide:zoom-in" :size="18" />
|
|
377
473
|
</button>
|
|
378
474
|
<button
|
|
379
475
|
v-if="rotatable"
|
|
380
476
|
:class="BTN"
|
|
381
477
|
type="button"
|
|
382
|
-
title="
|
|
478
|
+
:title="text.rotate"
|
|
383
479
|
@click="rotate"
|
|
384
480
|
>
|
|
385
481
|
<LpIcon name="lucide:rotate-cw" :size="18" />
|
|
386
482
|
</button>
|
|
387
|
-
<button v-if="copyable" :class="BTN" type="button" title="
|
|
483
|
+
<button v-if="copyable" :class="BTN" type="button" :title="text.copy" @click="copy">
|
|
388
484
|
<LpIcon name="lucide:copy" :size="18" />
|
|
389
485
|
</button>
|
|
390
486
|
<button
|
|
391
487
|
v-if="downloadable"
|
|
392
488
|
:class="BTN"
|
|
393
489
|
type="button"
|
|
394
|
-
title="
|
|
490
|
+
:title="text.download"
|
|
395
491
|
@click="download"
|
|
396
492
|
>
|
|
397
493
|
<LpIcon name="lucide:download" :size="18" />
|
|
398
494
|
</button>
|
|
399
|
-
<button :class="BTN" type="button" title="
|
|
495
|
+
<button :class="BTN" type="button" :title="text.close" @click="close">
|
|
400
496
|
<LpIcon name="lucide:x" :size="20" />
|
|
401
497
|
</button>
|
|
402
498
|
</div>
|
|
@@ -419,7 +515,7 @@ const ARROW =
|
|
|
419
515
|
:class="[ARROW, 'left-4']"
|
|
420
516
|
type="button"
|
|
421
517
|
:disabled="!canPrev"
|
|
422
|
-
aria-label="
|
|
518
|
+
:aria-label="text.previous"
|
|
423
519
|
@click.stop="go(current - 1)"
|
|
424
520
|
>
|
|
425
521
|
<LpIcon name="lucide:chevron-left" :size="24" />
|
|
@@ -430,14 +526,28 @@ const ARROW =
|
|
|
430
526
|
class="flex flex-col items-center gap-3 text-white/60"
|
|
431
527
|
>
|
|
432
528
|
<LpIcon name="lucide:image-off" :size="40" />
|
|
433
|
-
<p class="text-sm">
|
|
529
|
+
<p class="text-sm">{{ text.failed }}</p>
|
|
530
|
+
</div>
|
|
531
|
+
<div
|
|
532
|
+
v-else-if="item && covered(current)"
|
|
533
|
+
class="flex flex-col items-center gap-3 text-white/70"
|
|
534
|
+
>
|
|
535
|
+
<LpIcon name="lucide:eye-off" :size="36" />
|
|
536
|
+
<p class="text-sm font-medium">{{ text.spoiler }}</p>
|
|
537
|
+
<button
|
|
538
|
+
type="button"
|
|
539
|
+
class="rounded-control bg-white/15 px-3 py-1.5 text-sm text-white outline-none transition-colors hover:bg-white/25 focus-visible:ring-2 focus-visible:ring-ring"
|
|
540
|
+
@click.stop="reveal(current)"
|
|
541
|
+
>
|
|
542
|
+
{{ text.reveal }}
|
|
543
|
+
</button>
|
|
434
544
|
</div>
|
|
435
|
-
<!-- Keyed on src so paging remounts the img and replays
|
|
436
|
-
|
|
437
|
-
|
|
545
|
+
<!-- Keyed on position and src so paging remounts the img and replays
|
|
546
|
+
the slide, even between two items with the same source (whose
|
|
547
|
+
load event would otherwise never fire, leaving the spinner up). -->
|
|
438
548
|
<img
|
|
439
549
|
v-else-if="item"
|
|
440
|
-
:key="item.src"
|
|
550
|
+
:key="`${current}:${item.src}`"
|
|
441
551
|
data-lightbox-image
|
|
442
552
|
:src="item.src"
|
|
443
553
|
:alt="item.title ?? ''"
|
|
@@ -449,7 +559,7 @@ const ARROW =
|
|
|
449
559
|
/>
|
|
450
560
|
|
|
451
561
|
<LpIcon
|
|
452
|
-
v-if="loading && !failed"
|
|
562
|
+
v-if="loading && !failed && !covered(current)"
|
|
453
563
|
name="lucide:loader-circle"
|
|
454
564
|
:size="28"
|
|
455
565
|
class="absolute animate-spin text-white/50"
|
|
@@ -459,7 +569,7 @@ const ARROW =
|
|
|
459
569
|
:class="[ARROW, 'right-4']"
|
|
460
570
|
type="button"
|
|
461
571
|
:disabled="!canNext"
|
|
462
|
-
aria-label="
|
|
572
|
+
:aria-label="text.next"
|
|
463
573
|
@click.stop="go(current + 1)"
|
|
464
574
|
>
|
|
465
575
|
<LpIcon name="lucide:chevron-right" :size="24" />
|
|
@@ -494,13 +604,14 @@ const ARROW =
|
|
|
494
604
|
:style="{ '--thumb-delay': `${Math.min(i * 30, 240)}ms` }"
|
|
495
605
|
class="size-14 shrink-0 overflow-hidden rounded-control outline-none ring-2 transition-[opacity,box-shadow,scale] duration-[var(--duration-fast)] hover:scale-105 focus-visible:ring-ring animate-[lightbox-chrome-in_var(--duration-medium)_var(--ease-emphasized)_both] [animation-delay:var(--thumb-delay)] motion-reduce:animate-none"
|
|
496
606
|
:class="i === current ? 'opacity-100 ring-brand' : 'opacity-45 ring-transparent hover:opacity-80'"
|
|
497
|
-
:aria-label="entry.title ??
|
|
607
|
+
:aria-label="entry.title ?? position(i)"
|
|
498
608
|
@click="go(i)"
|
|
499
609
|
>
|
|
500
610
|
<img
|
|
501
611
|
:src="entry.thumb ?? entry.src"
|
|
502
612
|
:alt="entry.title ?? ''"
|
|
503
613
|
class="size-full object-cover"
|
|
614
|
+
:class="covered(i) ? 'scale-110 blur-md' : ''"
|
|
504
615
|
draggable="false"
|
|
505
616
|
/>
|
|
506
617
|
</button>
|
|
@@ -7,11 +7,15 @@ export default { inheritAttrs: false }
|
|
|
7
7
|
|
|
8
8
|
<script setup lang="ts">
|
|
9
9
|
import { computed, type Component } from "vue"
|
|
10
|
+
import { useLinkComponent } from "../composables/useLpConfig"
|
|
10
11
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
12
|
+
import { resolveLink, type LinkTarget } from "./link"
|
|
11
13
|
|
|
12
14
|
const props = withDefaults(
|
|
13
15
|
defineProps<{
|
|
14
16
|
href?: string
|
|
17
|
+
/** Router destination; rendered through the configured link component. */
|
|
18
|
+
to?: LinkTarget
|
|
15
19
|
external?: boolean
|
|
16
20
|
/** Shorthand for `tone="muted"`. */
|
|
17
21
|
muted?: boolean
|
|
@@ -23,11 +27,9 @@ const props = withDefaults(
|
|
|
23
27
|
*/
|
|
24
28
|
tone?: "brand" | "ink" | "muted" | "soft"
|
|
25
29
|
/**
|
|
26
|
-
* Root element/component.
|
|
27
|
-
* component
|
|
28
|
-
*
|
|
29
|
-
* Router links take their destination through their own prop (`to`), which
|
|
30
|
-
* falls through as a plain attribute.
|
|
30
|
+
* Root element/component. By default an internal `to`/`href` renders the
|
|
31
|
+
* link component set with provideLpConfig (NuxtLink under the Nuxt module),
|
|
32
|
+
* anything else a native <a>. Pass a component here to override per link.
|
|
31
33
|
*/
|
|
32
34
|
as?: string | Component
|
|
33
35
|
/**
|
|
@@ -50,13 +52,23 @@ const props = withDefaults(
|
|
|
50
52
|
*/
|
|
51
53
|
revealOn?: "self" | "group"
|
|
52
54
|
}>(),
|
|
53
|
-
{ variant: "default",
|
|
55
|
+
{ variant: "default", revealOn: "self" },
|
|
54
56
|
)
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
58
|
+
const linkComponent = useLinkComponent()
|
|
59
|
+
|
|
60
|
+
// An explicit `as` keeps its old contract (href on an anchor, `to` passed on to
|
|
61
|
+
// the component). Otherwise the target decides, and an external link is always
|
|
62
|
+
// a plain anchor.
|
|
63
|
+
const resolved = computed(() => {
|
|
64
|
+
if (props.as) {
|
|
65
|
+
return props.as === "a"
|
|
66
|
+
? { is: "a", props: { href: props.href } }
|
|
67
|
+
: { is: props.as, props: props.to != null ? { to: props.to } : { href: props.href } }
|
|
68
|
+
}
|
|
69
|
+
const component = props.external ? "a" : linkComponent.value
|
|
70
|
+
return resolveLink(component, { to: props.to, href: props.href }) ?? { is: "a", props: {} }
|
|
71
|
+
})
|
|
60
72
|
|
|
61
73
|
const rel = computed(() => (props.external ? "noopener noreferrer" : undefined))
|
|
62
74
|
const target = computed(() => (props.external ? "_blank" : undefined))
|
|
@@ -121,12 +133,11 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
121
133
|
|
|
122
134
|
<template>
|
|
123
135
|
<component
|
|
124
|
-
:is="
|
|
125
|
-
|
|
136
|
+
:is="resolved.is"
|
|
137
|
+
v-bind="{ ...resolved.props, ...rest }"
|
|
126
138
|
:target="target"
|
|
127
139
|
:rel="rel"
|
|
128
140
|
:class="rootClass"
|
|
129
|
-
v-bind="rest"
|
|
130
141
|
>
|
|
131
142
|
<slot />
|
|
132
143
|
</component>
|
|
@@ -31,6 +31,26 @@ import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
|
|
|
31
31
|
import LpIcon from "./LpIcon.vue"
|
|
32
32
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
33
33
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
34
|
+
import { useLabels } from "../composables/useLpConfig"
|
|
35
|
+
|
|
36
|
+
export interface LogViewerLabels {
|
|
37
|
+
[key: string]: string
|
|
38
|
+
empty: string
|
|
39
|
+
frozen: string
|
|
40
|
+
noMatch: string
|
|
41
|
+
loadingOlder: string
|
|
42
|
+
copyMessage: string
|
|
43
|
+
copyLine: string
|
|
44
|
+
copyTimestamp: string
|
|
45
|
+
copied: string
|
|
46
|
+
/** `{source}` is replaced by the line's source. */
|
|
47
|
+
filterSource: string
|
|
48
|
+
/** `{level}` is replaced by the line's level. */
|
|
49
|
+
filterLevel: string
|
|
50
|
+
/** `{n}` is replaced by the count. */
|
|
51
|
+
identical: string
|
|
52
|
+
jump: string
|
|
53
|
+
}
|
|
34
54
|
|
|
35
55
|
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal" | "success"
|
|
36
56
|
|
|
@@ -103,6 +123,7 @@ const props = withDefaults(
|
|
|
103
123
|
* so a duplicate level word / timestamp doesn't repeat in the message body.
|
|
104
124
|
*/
|
|
105
125
|
parse?: boolean
|
|
126
|
+
labels?: Partial<LogViewerLabels>
|
|
106
127
|
}>(),
|
|
107
128
|
{
|
|
108
129
|
showTime: true,
|
|
@@ -111,8 +132,6 @@ const props = withDefaults(
|
|
|
111
132
|
wrap: false,
|
|
112
133
|
tail: true,
|
|
113
134
|
height: "20rem",
|
|
114
|
-
emptyLabel: "No logs yet",
|
|
115
|
-
frozenLabel: "Held — release Ctrl to resume",
|
|
116
135
|
rowMenu: true,
|
|
117
136
|
parse: false,
|
|
118
137
|
},
|
|
@@ -129,6 +148,25 @@ const emit = defineEmits<{
|
|
|
129
148
|
(e: "reach-top"): void
|
|
130
149
|
}>()
|
|
131
150
|
|
|
151
|
+
const text = useLabels<LogViewerLabels>(
|
|
152
|
+
"logViewer",
|
|
153
|
+
{
|
|
154
|
+
empty: "No logs yet",
|
|
155
|
+
frozen: "Held — release Ctrl to resume",
|
|
156
|
+
noMatch: "No lines match",
|
|
157
|
+
loadingOlder: "loading older…",
|
|
158
|
+
copyMessage: "Copy message",
|
|
159
|
+
copyLine: "Copy line",
|
|
160
|
+
copyTimestamp: "Copy timestamp",
|
|
161
|
+
copied: "Copied to clipboard",
|
|
162
|
+
filterSource: "Filter by [{source}]",
|
|
163
|
+
filterLevel: "Filter by {level}",
|
|
164
|
+
identical: "{n} identical lines",
|
|
165
|
+
jump: "Jump to latest",
|
|
166
|
+
},
|
|
167
|
+
() => props.labels,
|
|
168
|
+
)
|
|
169
|
+
|
|
132
170
|
// Level → gutter rail + text colour. Reads only semantic tokens so re-skinning
|
|
133
171
|
// just works. trace/debug stay muted; success is green; warn amber; error/fatal
|
|
134
172
|
// red (fatal gets a filled chip to stand apart from plain error).
|
|
@@ -336,6 +374,24 @@ const visibleLines = computed<{ line: LogLine; n: number; count: number }[]>(()
|
|
|
336
374
|
return folded
|
|
337
375
|
})
|
|
338
376
|
|
|
377
|
+
/*
|
|
378
|
+
* Rows are keyed by the identity of the line object the consumer passed, not by
|
|
379
|
+
* position: prepending older history shifts every index, and index keys made
|
|
380
|
+
* the bottom rows play the enter animation instead of the new top ones.
|
|
381
|
+
*/
|
|
382
|
+
const lineIds = new WeakMap<LogLine, number>()
|
|
383
|
+
let nextLineId = 0
|
|
384
|
+
function lineKey(n: number): number {
|
|
385
|
+
const line = sourceLines.value[n]
|
|
386
|
+
if (!line) return -1 - n
|
|
387
|
+
let id = lineIds.get(line)
|
|
388
|
+
if (id === undefined) {
|
|
389
|
+
id = nextLineId++
|
|
390
|
+
lineIds.set(line, id)
|
|
391
|
+
}
|
|
392
|
+
return id
|
|
393
|
+
}
|
|
394
|
+
|
|
339
395
|
// True when filtering has emptied an otherwise non-empty stream.
|
|
340
396
|
const filteredEmpty = computed(
|
|
341
397
|
() => props.lines.length > 0 && visibleLines.value.length === 0,
|
|
@@ -356,7 +412,7 @@ function fmtTime(time?: number | string): string {
|
|
|
356
412
|
// A menu item closes the menu as it runs, leaving nothing on screen to say the
|
|
357
413
|
// copy happened — so unlike LpCodeBlock's button, this one announces itself.
|
|
358
414
|
const { copy: writeClipboard } = useClipboard()
|
|
359
|
-
const copy = (
|
|
415
|
+
const copy = (value: string) => writeClipboard(value, { toast: text.value.copied })
|
|
360
416
|
|
|
361
417
|
// The whole line as one copyable string: "time LEVEL [source] message".
|
|
362
418
|
function lineText(line: LogLine): string {
|
|
@@ -398,15 +454,15 @@ function onRowPointerDown(e: PointerEvent) {
|
|
|
398
454
|
function menuFor(line: LogLine, index: number): ContextMenuItemDef[] {
|
|
399
455
|
if (!props.rowMenu) return []
|
|
400
456
|
const items: ContextMenuItemDef[] = [
|
|
401
|
-
{ label:
|
|
402
|
-
{ label:
|
|
457
|
+
{ label: text.value.copyMessage, icon: "lucide:copy", onSelect: () => copy(line.message) },
|
|
458
|
+
{ label: text.value.copyLine, icon: "lucide:clipboard-list", onSelect: () => copy(lineText(line)) },
|
|
403
459
|
]
|
|
404
460
|
if (line.time != null) {
|
|
405
|
-
items.push({ label:
|
|
461
|
+
items.push({ label: text.value.copyTimestamp, icon: "lucide:clock", onSelect: () => copy(fmtTime(line.time)) })
|
|
406
462
|
}
|
|
407
463
|
if (line.source) {
|
|
408
464
|
items.push({
|
|
409
|
-
label:
|
|
465
|
+
label: text.value.filterSource.replace("{source}", line.source),
|
|
410
466
|
icon: "lucide:filter",
|
|
411
467
|
separatorBefore: true,
|
|
412
468
|
onSelect: () => emit("filter", { source: line.source }),
|
|
@@ -414,7 +470,7 @@ function menuFor(line: LogLine, index: number): ContextMenuItemDef[] {
|
|
|
414
470
|
}
|
|
415
471
|
if (line.level) {
|
|
416
472
|
items.push({
|
|
417
|
-
label:
|
|
473
|
+
label: text.value.filterLevel.replace("{level}", line.level),
|
|
418
474
|
icon: "lucide:filter",
|
|
419
475
|
separatorBefore: !line.source,
|
|
420
476
|
onSelect: () => emit("filter", { level: line.level }),
|
|
@@ -576,8 +632,15 @@ function scrollToBottom() {
|
|
|
576
632
|
// Snapshot the scroll anchor BEFORE the DOM updates for a line change (flush:pre)
|
|
577
633
|
// so a prepend can be compensated post-update. Only matters when NOT pinned to
|
|
578
634
|
// the bottom (i.e. the user is reading history up the stream).
|
|
635
|
+
/*
|
|
636
|
+
* A capped buffer (shift + push, or slice(-N)) keeps the length constant once
|
|
637
|
+
* full, so the length alone stops seeing new lines. The array, its length and
|
|
638
|
+
* its last line together change on every append, prepend or replacement.
|
|
639
|
+
*/
|
|
640
|
+
const linesSignal = () => [props.lines, props.lines.length, props.lines[props.lines.length - 1]]
|
|
641
|
+
|
|
579
642
|
watch(
|
|
580
|
-
|
|
643
|
+
linesSignal,
|
|
581
644
|
() => {
|
|
582
645
|
if (!pinned.value) captureScrollAnchor()
|
|
583
646
|
},
|
|
@@ -587,7 +650,7 @@ watch(
|
|
|
587
650
|
// After the DOM updates: tail to the bottom if pinned, else keep the view glued
|
|
588
651
|
// to where it was so a top prepend (older history) doesn't yank it.
|
|
589
652
|
watch(
|
|
590
|
-
|
|
653
|
+
linesSignal,
|
|
591
654
|
async () => {
|
|
592
655
|
await nextTick()
|
|
593
656
|
if (props.tail && pinned.value) {
|
|
@@ -673,7 +736,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
673
736
|
v-if="frozen"
|
|
674
737
|
class="pointer-events-none absolute top-2 left-1/2 z-10 -translate-x-1/2 rounded-pill bg-brand px-2.5 py-1 text-[11px] font-medium text-ink-inverse"
|
|
675
738
|
>
|
|
676
|
-
{{ frozenLabel }}
|
|
739
|
+
{{ frozenLabel ?? text.frozen }}
|
|
677
740
|
</div>
|
|
678
741
|
</Transition>
|
|
679
742
|
|
|
@@ -691,7 +754,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
691
754
|
v-if="lines.length === 0 || filteredEmpty"
|
|
692
755
|
class="flex h-full flex-col items-center justify-center gap-1 px-4 py-10 text-center"
|
|
693
756
|
>
|
|
694
|
-
<span class="text-muted">{{ filteredEmpty ?
|
|
757
|
+
<span class="text-muted">{{ filteredEmpty ? text.noMatch : (emptyLabel ?? text.empty) }}</span>
|
|
695
758
|
<span v-if="filteredEmpty" class="text-muted/70">“{{ highlight }}”</span>
|
|
696
759
|
</div>
|
|
697
760
|
|
|
@@ -704,7 +767,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
704
767
|
class="flex items-center justify-center gap-2 py-2 text-muted"
|
|
705
768
|
>
|
|
706
769
|
<LpIcon name="lucide:loader-circle" :size="14" class="animate-spin" />
|
|
707
|
-
<span class="text-[11px]">
|
|
770
|
+
<span class="text-[11px]">{{ text.loadingOlder }}</span>
|
|
708
771
|
</div>
|
|
709
772
|
|
|
710
773
|
<!-- ONE context menu wraps the whole list (not each row): @contextmenu
|
|
@@ -738,7 +801,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
738
801
|
/>
|
|
739
802
|
<li
|
|
740
803
|
v-for="{ line, n, count } in renderedLines"
|
|
741
|
-
:key="n"
|
|
804
|
+
:key="lineKey(n)"
|
|
742
805
|
:data-log-row="n"
|
|
743
806
|
class="group relative flex items-start gap-0 px-0 leading-5 transition-colors hover:bg-surface-soft/60"
|
|
744
807
|
:class="metaFor(line.level).sel"
|
|
@@ -785,7 +848,13 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
785
848
|
class="min-w-0 pr-3"
|
|
786
849
|
:class="[metaFor(line.level).text, wrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre']"
|
|
787
850
|
>
|
|
788
|
-
|
|
851
|
+
<!-- Virtualised rows must stay one row tall, so a multi-line
|
|
852
|
+
message (a stack trace) is shown on one line with ↵ marks;
|
|
853
|
+
copying still yields the original text. -->
|
|
854
|
+
<template
|
|
855
|
+
v-for="(part, p) in splitHighlight(virtualise ? line.message.replace(/\r?\n/g, ' ↵ ') : line.message)"
|
|
856
|
+
:key="p"
|
|
857
|
+
>
|
|
789
858
|
<mark
|
|
790
859
|
v-if="part.hit"
|
|
791
860
|
class="rounded-xs bg-accent/30 text-ink"
|
|
@@ -803,7 +872,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
803
872
|
v-if="count > 1"
|
|
804
873
|
:key="count"
|
|
805
874
|
class="ml-2 select-none rounded-pill bg-surface-soft px-1.5 align-[1px] text-[10px] font-semibold tabular-nums text-muted-strong"
|
|
806
|
-
:title="
|
|
875
|
+
:title="text.identical.replace('{n}', String(count))"
|
|
807
876
|
>×{{ count }}</span>
|
|
808
877
|
</Transition>
|
|
809
878
|
</span>
|
|
@@ -839,7 +908,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
839
908
|
:size="14"
|
|
840
909
|
class="transition-transform duration-200 ease-[var(--ease-emphasized)] group-hover:translate-y-0.5"
|
|
841
910
|
/>
|
|
842
|
-
|
|
911
|
+
{{ text.jump }}
|
|
843
912
|
</button>
|
|
844
913
|
</div>
|
|
845
914
|
</Transition>
|