@leavepulse/ui 0.44.0 → 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 +49 -3
- package/dist/assets/{LpCommandPalette-DCP5g2ik.css → LpCommandPalette-nGNnzXqQ.css} +21 -21
- package/dist/assets/{LpRollingText-CR1mb0Vt.css → LpRollingText-BGnE6q9j.css} +3 -0
- package/dist/assets/{LpShortcutsDialog-ChUyU7oV.css → LpShortcutsDialog-CqYX3yy6.css} +28 -7
- package/dist/assets/{LpTable-DDQNdf86.css → LpTable-CPwWxTJS.css} +49 -9
- package/dist/chunks/{ContextMenuItems-CrfeNPWT.js → ContextMenuItems-BtKU7074.js} +2 -1
- package/dist/chunks/{LayoutCanvas-D-jPOAvg.js → LayoutCanvas-BkaGXTjx.js} +1 -1
- package/dist/chunks/{LayoutNode-D8Xy9N-o.js → LayoutNode-B3_9E1Xa.js} +9 -5
- package/dist/chunks/{LpActionBar-BbwCF6l5.js → LpActionBar-CnfFVIwC.js} +3 -3
- package/dist/chunks/{LpAddressInput-BrOqkZqg.js → LpAddressInput-BwX8uo4_.js} +7 -7
- package/dist/chunks/{LpAlert-DkOIjsbq.js → LpAlert-B6ilrM7T.js} +1 -1
- package/dist/chunks/{LpAppShell-BUKPcHco.js → LpAppShell-C53_RaGF.js} +4 -4
- package/dist/chunks/{LpAutocomplete-DxV3Y9h4.js → LpAutocomplete-C8IgSMsi.js} +2 -1
- package/dist/chunks/{LpAvatar-ClBbEYZN.js → LpAvatar-C1XStdOo.js} +3 -3
- package/dist/chunks/{LpBreadcrumbs-B8IJgFIc.js → LpBreadcrumbs-CQJd73Gm.js} +1 -1
- package/dist/chunks/{LpButton-kqJBHIIb.js → LpButton-CKSwC6Mu.js} +1 -1
- package/dist/chunks/{LpCalendar-B7Ko5RdR.js → LpCalendar-DrA2McTU.js} +1 -1
- package/dist/chunks/{LpCard-CpRByR0q.js → LpCard-C2IvucmX.js} +1 -1
- package/dist/chunks/{LpCheckbox-DpqchzH4.js → LpCheckbox-Bkj13CbW.js} +1 -1
- package/dist/chunks/{LpCodeBlock-DeHJPq7p.js → LpCodeBlock-ca7ANFyW.js} +1 -1
- package/dist/chunks/{LpColorPicker-Bhf1qxhB.js → LpColorPicker-zz4VGkG4.js} +46 -20
- package/dist/chunks/{LpConfirmDialog-CIUOSnWo.js → LpConfirmDialog-DCw5jR1t.js} +3 -3
- package/dist/chunks/{LpConfirmHost-CDm0Wtpv.js → LpConfirmHost-CbywMpMP.js} +1 -1
- package/dist/chunks/{LpContextMenu-DQw5dGfG.js → LpContextMenu-BqkAYa9X.js} +2 -1
- package/dist/chunks/{LpDatePicker-Drirx1AK.js → LpDatePicker-DuusgT69.js} +13 -9
- package/dist/chunks/LpDateRangePicker-DHeuznfC.js +208 -0
- package/dist/chunks/{LpDisclosure-ChQYmjlR.js → LpDisclosure-Cc23-JOT.js} +1 -1
- package/dist/chunks/{LpDrawer-bScx-KMO.js → LpDrawer-Dt4aB4Fr.js} +2 -2
- 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-CSI_5SK2.js → LpFileTree-CNurOvkV.js} +3 -3
- package/dist/chunks/{LpFileTreeNode-B4MiSQX2.js → LpFileTreeNode-D_qRRdsH.js} +3 -3
- package/dist/chunks/{LpFormField-CIk5qosh.js → LpFormField-CtVYj6tO.js} +29 -15
- package/dist/chunks/{LpIcon-CCnX5_2j.js → LpIcon-DYtaid0K.js} +20 -6
- package/dist/chunks/{LpInput-C0e8y8_d.js → LpInput-D-YGQbEv.js} +39 -16
- package/dist/chunks/{LpLightbox-Mr0W_6HK.js → LpLightbox-DSDahF23.js} +1 -1
- package/dist/chunks/{LpLogViewer-YBOaFnc_.js → LpLogViewer-KypbgOyo.js} +252 -58
- package/dist/chunks/{LpMapPicker-NUzS9e37.js → LpMapPicker-Cc84LaM3.js} +1 -1
- package/dist/chunks/{LpMediaGallery-DfJmMW0I.js → LpMediaGallery-YKr-cQPN.js} +2 -2
- package/dist/chunks/{LpModal-DPwGKBPv.js → LpModal-DMMDQHAu.js} +3 -3
- package/dist/chunks/{LpModalNav-DPg5H9f3.js → LpModalNav-BVPDyIup.js} +4 -4
- package/dist/chunks/{LpNotificationBell-t5yn-siX.js → LpNotificationBell-CCwyJP5C.js} +26 -14
- package/dist/chunks/{LpNumberField-DWnQKktS.js → LpNumberField-B77O-z28.js} +1 -1
- package/dist/chunks/{LpPagination-DFngX7Xo.js → LpPagination-BcDlo3r_.js} +1 -1
- package/dist/chunks/{LpPasswordInput-CWVDuBJ5.js → LpPasswordInput-eKzQhbQP.js} +2 -2
- package/dist/chunks/{LpPhoneInput-CD-ZTeSd.js → LpPhoneInput-BCbrrhgE.js} +126 -23
- package/dist/chunks/{LpPie-BLco31Nx.js → LpPie-BEJ2SfI6.js} +9 -10
- package/dist/chunks/{LpPopover-p_e_shN3.js → LpPopover-DCKeqiVn.js} +1 -0
- 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-Dejd7cIx.js → LpSegmented-C09EEam2.js} +1 -1
- package/dist/chunks/{LpSelect-DrRD9I7L.js → LpSelect-DNIL6MB9.js} +20 -23
- package/dist/chunks/{LpSidebar-BgwUNWpJ.js → LpSidebar-CQNB0-yU.js} +3 -3
- package/dist/chunks/{LpSidebarNav-Bqxz8aWT.js → LpSidebarNav-kx-LdqNU.js} +3 -3
- package/dist/chunks/{LpStat-dXtyrdX9.js → LpStat-B-7udBRP.js} +1 -1
- package/dist/chunks/{LpStepper-Bk-XaQYU.js → LpStepper-C7XiVKZj.js} +34 -19
- package/dist/chunks/{LpSwitch-B1aLKulj.js → LpSwitch-OrcOJX9m.js} +1 -1
- package/dist/chunks/LpTableFilter-BV8PpLM9.js +158 -0
- package/dist/chunks/{LpTabs-_qMVaZJ6.js → LpTabs-29iW1XOx.js} +1 -1
- package/dist/chunks/LpTagsInput-D1SuqbhB.js +300 -0
- package/dist/chunks/{LpTextarea-yP8hGedR.js → LpTextarea-uuScWo6-.js} +1 -1
- package/dist/chunks/{LpThemeProvider-Wjm7Tuit.js → LpThemeProvider-Be4r2TQX.js} +1 -1
- package/dist/chunks/{LpThemeSwitcher-CKLc10KZ.js → LpThemeSwitcher-B4aVPbNC.js} +19 -22
- package/dist/chunks/{LpTimeField-C5HRgS5H.js → LpTimeField-Cqe7fXnh.js} +1 -1
- package/dist/chunks/{LpToaster--oT6lfhL.js → LpToaster-DpJvoCOh.js} +37 -3
- package/dist/chunks/{LpTooltip-CBdzeKNf.js → LpTooltip-CXVKcOXT.js} +3 -2
- package/dist/chunks/{LpTopologyCanvas-DocsLal3.js → LpTopologyCanvas-6ozNrQKh.js} +2 -1
- package/dist/chunks/{LpUptimeBar-DLtxOMRy.js → LpUptimeBar-ugcv25xj.js} +2 -2
- package/dist/chunks/icons-CkA0njzm.js +43 -0
- package/dist/chunks/{useTheme-C8JHUlPM.js → useTheme-DJm1JSsY.js} +47 -50
- package/dist/component-names.d.ts +1 -1
- package/dist/components/ContextMenuItems.vue.js +1 -1
- package/dist/components/LayoutCanvas.vue.js +1 -1
- package/dist/components/LayoutNode.vue.js +1 -1
- package/dist/components/LpActionBar.vue.js +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAlert.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAutocomplete.vue.js +1 -1
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpButton.vue.d.ts +2 -2
- 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 +16 -1
- package/dist/components/LpChart.vue.js +63 -5
- package/dist/components/LpCheckbox.vue.js +1 -1
- 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 +6 -0
- package/dist/components/LpCommandPalette.vue.js +61 -10
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.js +1 -1
- 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.js +1 -1
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.js +1 -1
- 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.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpFormField.vue.d.ts +4 -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 +2 -2
- package/dist/components/LpInput.vue.d.ts +19 -0
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.d.ts +52 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpNumberField.vue.js +1 -1
- 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.js +1 -1
- package/dist/components/LpPopover.vue.js +1 -1
- package/dist/components/LpProgress.vue.d.ts +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.js +1 -1
- package/dist/components/LpSegmented.vue.js +1 -1
- package/dist/components/LpSelect.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.d.ts +6 -0
- package/dist/components/LpShortcutsDialog.vue.js +56 -14
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.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 +84 -2
- package/dist/components/LpTable.vue.js +481 -107
- package/dist/components/LpTableFilter.vue.d.ts +31 -0
- package/dist/components/LpTableFilter.vue.js +4 -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.js +1 -1
- package/dist/components/LpThemeProvider.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.d.ts +2 -2
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/LpTimeField.vue.js +1 -1
- package/dist/components/LpToaster.vue.js +1 -1
- package/dist/components/LpTooltip.vue.js +1 -1
- package/dist/components/LpTopologyCanvas.vue.js +1 -1
- package/dist/components/LpUptimeBar.vue.js +1 -1
- package/dist/components/chart.js +3 -2
- package/dist/components/commandSearch.js +3 -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/logSearch.d.ts +18 -0
- package/dist/components/logSearch.js +36 -0
- package/dist/components/phone.d.ts +41 -0
- package/dist/components/phone.js +80 -0
- package/dist/components/tableFilter.d.ts +36 -0
- package/dist/components/tableFilter.js +84 -0
- package/dist/components/tags.d.ts +20 -0
- package/dist/components/tags.js +37 -0
- package/dist/composables/useField.d.ts +4 -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/useHotkeys.d.ts +65 -5
- package/dist/composables/useHotkeys.js +170 -9
- package/dist/composables/useInputMask.d.ts +59 -0
- package/dist/composables/useInputMask.js +170 -0
- package/dist/composables/usePaintSelect.d.ts +22 -0
- package/dist/composables/usePaintSelect.js +169 -0
- package/dist/composables/useShift.js +7 -2
- 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 +29 -3
- package/dist/index.js +195 -138
- package/dist/kit-icons.d.ts +2 -0
- package/dist/theme/backdrop.d.ts +12 -0
- package/dist/theme/presets.d.ts +0 -2
- package/package.json +13 -4
- package/src/component-names.ts +4 -0
- package/src/components/ContextMenuItems.vue +1 -1
- package/src/components/LayoutNode.vue +11 -2
- package/src/components/LpActionBar.vue +1 -1
- package/src/components/LpAutocomplete.vue +1 -0
- package/src/components/LpAvatar.vue +2 -2
- package/src/components/LpButton.vue +1 -1
- package/src/components/LpChart.vue +80 -1
- package/src/components/LpColorPicker.vue +34 -1
- package/src/components/LpCommandPalette.vue +81 -7
- package/src/components/LpContextMenu.vue +1 -1
- package/src/components/LpDatePicker.vue +8 -3
- package/src/components/LpDateRangePicker.vue +224 -0
- package/src/components/LpDropdownMenu.vue +1 -0
- package/src/components/LpFieldNotice.vue +65 -0
- package/src/components/LpFormField.vue +33 -4
- package/src/components/LpIcon.vue +22 -4
- package/src/components/LpInput.vue +46 -6
- package/src/components/LpLogViewer.vue +225 -22
- package/src/components/LpModal.vue +1 -1
- package/src/components/LpNotificationBell.vue +15 -7
- package/src/components/LpPhoneInput.vue +179 -20
- package/src/components/LpPie.vue +8 -5
- package/src/components/LpPopover.vue +1 -0
- package/src/components/LpRangeCalendar.vue +365 -0
- package/src/components/LpRollingText.vue +3 -0
- package/src/components/LpScrollArea.vue +22 -2
- package/src/components/LpSelect.vue +0 -0
- package/src/components/LpShortcutsDialog.vue +77 -8
- package/src/components/LpStepper.vue +23 -10
- package/src/components/LpSwitch.vue +1 -1
- package/src/components/LpTable.vue +584 -65
- package/src/components/LpTableFilter.vue +175 -0
- package/src/components/LpTagsInput.vue +364 -0
- package/src/components/LpThemeSwitcher.vue +19 -23
- package/src/components/LpToaster.vue +49 -1
- package/src/components/LpTooltip.vue +9 -2
- package/src/components/LpTopologyCanvas.vue +7 -1
- package/src/components/chart.ts +5 -3
- package/src/components/commandSearch.ts +6 -0
- package/src/components/dateRange.ts +134 -0
- package/src/components/exportData.ts +228 -0
- package/src/components/logSearch.ts +46 -0
- package/src/components/phone.ts +140 -0
- package/src/components/tableFilter.ts +122 -0
- package/src/components/tags.ts +62 -0
- package/src/composables/useField.ts +4 -0
- package/src/composables/useFieldNotice.ts +59 -0
- package/src/composables/useForm.ts +314 -0
- package/src/composables/useHotkeys.ts +269 -13
- package/src/composables/useInputMask.ts +254 -0
- package/src/composables/usePaintSelect.ts +243 -0
- package/src/composables/useShift.ts +13 -2
- 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 +37 -3
- package/src/kit-icons.ts +5 -0
- package/src/nuxt.ts +30 -2
- package/src/runtime/theme.plugin.ts +2 -2
- package/src/theme/backdrop.ts +69 -0
- package/src/theme/presets.ts +6 -64
- package/src/tokens/tokens.css +46 -0
|
@@ -32,6 +32,7 @@ import LpIcon from "./LpIcon.vue"
|
|
|
32
32
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
33
33
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
34
34
|
import { useLabels } from "../composables/useLpConfig"
|
|
35
|
+
import { countLevels, matchPositions, ringWindow, stepMatch } from "./logSearch"
|
|
35
36
|
|
|
36
37
|
export interface LogViewerLabels {
|
|
37
38
|
[key: string]: string
|
|
@@ -50,6 +51,14 @@ export interface LogViewerLabels {
|
|
|
50
51
|
/** `{n}` is replaced by the count. */
|
|
51
52
|
identical: string
|
|
52
53
|
jump: string
|
|
54
|
+
search: string
|
|
55
|
+
clearSearch: string
|
|
56
|
+
previousMatch: string
|
|
57
|
+
nextMatch: string
|
|
58
|
+
/** `{current}` and `{total}` are replaced. */
|
|
59
|
+
matchCount: string
|
|
60
|
+
noMatches: string
|
|
61
|
+
levels: string
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal" | "success"
|
|
@@ -123,6 +132,18 @@ const props = withDefaults(
|
|
|
123
132
|
* so a duplicate level word / timestamp doesn't repeat in the message body.
|
|
124
133
|
*/
|
|
125
134
|
parse?: boolean
|
|
135
|
+
/**
|
|
136
|
+
* Keep only the newest N lines on screen. The consumer can keep pushing
|
|
137
|
+
* into the same array (or hand over a longer one) and the viewer shows the
|
|
138
|
+
* tail; trimming the source itself is still the cheaper way to cap memory.
|
|
139
|
+
*/
|
|
140
|
+
maxLines?: number
|
|
141
|
+
/**
|
|
142
|
+
* A bar above the stream with a search box (Enter / Shift+Enter or F3 step
|
|
143
|
+
* through matches) and level chips. The term binds via `v-model:search`,
|
|
144
|
+
* the chosen levels via `v-model:levels`.
|
|
145
|
+
*/
|
|
146
|
+
toolbar?: boolean
|
|
126
147
|
labels?: Partial<LogViewerLabels>
|
|
127
148
|
}>(),
|
|
128
149
|
{
|
|
@@ -148,6 +169,12 @@ const emit = defineEmits<{
|
|
|
148
169
|
(e: "reach-top"): void
|
|
149
170
|
}>()
|
|
150
171
|
|
|
172
|
+
/** The toolbar's search term. Without the toolbar, `highlight` is the term. */
|
|
173
|
+
const query = defineModel<string>("search", { default: "" })
|
|
174
|
+
/** Levels to show. Empty shows every level. */
|
|
175
|
+
const levels = defineModel<LogLevel[]>("levels", { default: () => [] })
|
|
176
|
+
const term = computed(() => (props.toolbar && query.value ? query.value : (props.highlight ?? "")).trim())
|
|
177
|
+
|
|
151
178
|
const text = useLabels<LogViewerLabels>(
|
|
152
179
|
"logViewer",
|
|
153
180
|
{
|
|
@@ -163,6 +190,13 @@ const text = useLabels<LogViewerLabels>(
|
|
|
163
190
|
filterLevel: "Filter by {level}",
|
|
164
191
|
identical: "{n} identical lines",
|
|
165
192
|
jump: "Jump to latest",
|
|
193
|
+
search: "Search logs",
|
|
194
|
+
clearSearch: "Clear search",
|
|
195
|
+
previousMatch: "Previous match",
|
|
196
|
+
nextMatch: "Next match",
|
|
197
|
+
matchCount: "{current} / {total}",
|
|
198
|
+
noMatches: "No matches",
|
|
199
|
+
levels: "Levels",
|
|
166
200
|
},
|
|
167
201
|
() => props.labels,
|
|
168
202
|
)
|
|
@@ -280,7 +314,10 @@ const root = ref<HTMLElement | null>(null)
|
|
|
280
314
|
const frozen = ref(false)
|
|
281
315
|
const frozenLines = shallowRef<LogLine[] | null>(null)
|
|
282
316
|
|
|
283
|
-
const
|
|
317
|
+
const windowed = computed(() => ringWindow(frozenLines.value ?? props.lines, props.maxLines))
|
|
318
|
+
const sourceLines = computed<LogLine[]>(() => windowed.value.lines as LogLine[])
|
|
319
|
+
// Line numbers keep counting from the start of the stream after the cap trims it.
|
|
320
|
+
const lineOffset = computed(() => windowed.value.offset)
|
|
284
321
|
|
|
285
322
|
function freeze(): boolean {
|
|
286
323
|
// Declining unless the stream is being looked at: `false` hands Ctrl back
|
|
@@ -291,7 +328,9 @@ function freeze(): boolean {
|
|
|
291
328
|
// Snapshotted by reference, not copied: the array the consumer hands over is
|
|
292
329
|
// replaced wholesale on every update, so holding the old one is enough — and
|
|
293
330
|
// a copy of ten thousand lines on every keypress is not.
|
|
294
|
-
|
|
331
|
+
// A capped stream is usually one array pushed into in place, which a
|
|
332
|
+
// reference would not hold still; its window is small enough to copy.
|
|
333
|
+
frozenLines.value = props.maxLines ? props.lines.slice(-props.maxLines) : props.lines
|
|
295
334
|
frozen.value = true
|
|
296
335
|
return true
|
|
297
336
|
}
|
|
@@ -317,9 +356,13 @@ function focusWithin() {
|
|
|
317
356
|
return !!el && el.contains(document.activeElement)
|
|
318
357
|
}
|
|
319
358
|
|
|
359
|
+
// `when` rather than a bare decline: several viewers on one page all bind these
|
|
360
|
+
// keys, and only the one being looked at should claim them.
|
|
361
|
+
const lookedAt = () => hovered.value || focusWithin()
|
|
362
|
+
|
|
320
363
|
useHotkeys(() => [
|
|
321
|
-
{ key: "control", allowInInput: true, handler: freeze },
|
|
322
|
-
{ key: "meta", allowInInput: true, handler: freeze },
|
|
364
|
+
{ key: "control", allowInInput: true, when: lookedAt, handler: freeze },
|
|
365
|
+
{ key: "meta", allowInInput: true, when: lookedAt, handler: freeze },
|
|
323
366
|
])
|
|
324
367
|
|
|
325
368
|
// Released on keyup anywhere, and on blur: a window that loses focus mid-hold
|
|
@@ -347,10 +390,12 @@ const parsedLines = computed<LogLine[]>(() =>
|
|
|
347
390
|
// With filterMatches + a search term, keep only lines whose message contains it
|
|
348
391
|
// (case-insensitive); otherwise pass everything.
|
|
349
392
|
const visibleLines = computed<{ line: LogLine; n: number; count: number }[]>(() => {
|
|
350
|
-
const
|
|
393
|
+
const needle = props.filterMatches ? term.value.toLowerCase() : ""
|
|
394
|
+
const shown = levels.value.length ? new Set<LogLevel>(levels.value) : null
|
|
351
395
|
const filtered = parsedLines.value
|
|
352
396
|
.map((line, n) => ({ line, n }))
|
|
353
|
-
.filter(({ line }) => !
|
|
397
|
+
.filter(({ line }) => !shown || shown.has(line.level ?? "info"))
|
|
398
|
+
.filter(({ line }) => !needle || matchPositions([line], needle).length > 0)
|
|
354
399
|
|
|
355
400
|
if (!props.compact) return filtered.map((row) => ({ ...row, count: 1 }))
|
|
356
401
|
|
|
@@ -392,6 +437,88 @@ function lineKey(n: number): number {
|
|
|
392
437
|
return id
|
|
393
438
|
}
|
|
394
439
|
|
|
440
|
+
// ── toolbar: level chips and match navigation ───────────────────────
|
|
441
|
+
const LEVEL_ORDER: LogLevel[] = ["trace", "debug", "info", "success", "warn", "error", "fatal"]
|
|
442
|
+
const levelCounts = computed(() => countLevels<LogLevel>(parsedLines.value, "info"))
|
|
443
|
+
const presentLevels = computed(() => LEVEL_ORDER.filter((l) => levelCounts.value.has(l)))
|
|
444
|
+
|
|
445
|
+
function toggleLevel(level: LogLevel) {
|
|
446
|
+
levels.value = levels.value.includes(level)
|
|
447
|
+
? levels.value.filter((l) => l !== level)
|
|
448
|
+
: [...levels.value, level]
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const matches = computed(() => matchPositions(visibleLines.value.map((r) => r.line), term.value))
|
|
452
|
+
const current = ref(-1)
|
|
453
|
+
const currentRow = computed(() => {
|
|
454
|
+
const pos = matches.value[current.value]
|
|
455
|
+
return pos === undefined ? -1 : (visibleLines.value[pos]?.n ?? -1)
|
|
456
|
+
})
|
|
457
|
+
watch(term, () => {
|
|
458
|
+
current.value = -1
|
|
459
|
+
})
|
|
460
|
+
watch(matches, (list) => {
|
|
461
|
+
if (current.value >= list.length) current.value = list.length - 1
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
const matchLabel = computed(() => {
|
|
465
|
+
if (!term.value) return ""
|
|
466
|
+
const total = matches.value.length
|
|
467
|
+
if (!total) return text.value.noMatches
|
|
468
|
+
return text.value.matchCount
|
|
469
|
+
.replace("{current}", current.value >= 0 ? String(current.value + 1) : "–")
|
|
470
|
+
.replace("{total}", String(total))
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
function step(direction: 1 | -1) {
|
|
474
|
+
const next = stepMatch(current.value, matches.value.length, direction)
|
|
475
|
+
if (next < 0) return
|
|
476
|
+
current.value = next
|
|
477
|
+
pinned.value = false
|
|
478
|
+
nextTick(() => revealMatch(matches.value[next]))
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Brings the match to the middle of the viewport. A virtualised row may not be
|
|
482
|
+
// in the DOM yet, so the offset is computed from the fixed row height instead.
|
|
483
|
+
function revealMatch(position: number | undefined) {
|
|
484
|
+
const el = viewport()
|
|
485
|
+
if (!el || position === undefined) return
|
|
486
|
+
const behavior: ScrollBehavior = prefersReducedMotion() ? "auto" : "smooth"
|
|
487
|
+
if (virtualise.value) {
|
|
488
|
+
el.scrollTo({ top: Math.max(0, position * ROW_HEIGHT - el.clientHeight / 2), behavior })
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
const n = visibleLines.value[position]?.n
|
|
492
|
+
el.querySelector<HTMLElement>(`[data-log-row="${n}"]`)?.scrollIntoView({ block: "center", behavior })
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const searchEl = ref<HTMLInputElement | null>(null)
|
|
496
|
+
|
|
497
|
+
function onSearchKeydown(event: KeyboardEvent) {
|
|
498
|
+
if (event.isComposing) return
|
|
499
|
+
if (event.key === "Enter") {
|
|
500
|
+
event.preventDefault()
|
|
501
|
+
step(event.shiftKey ? -1 : 1)
|
|
502
|
+
} else if (event.key === "Escape" && query.value) {
|
|
503
|
+
event.preventDefault()
|
|
504
|
+
event.stopPropagation()
|
|
505
|
+
query.value = ""
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const searching = () => !!term.value && lookedAt()
|
|
510
|
+
|
|
511
|
+
useHotkeys(() => [
|
|
512
|
+
{ key: "f3", allowInInput: true, when: searching, handler: () => step(1) },
|
|
513
|
+
{ key: "f3", shift: true, allowInInput: true, when: searching, handler: () => step(-1) },
|
|
514
|
+
])
|
|
515
|
+
|
|
516
|
+
defineExpose({
|
|
517
|
+
next: () => step(1),
|
|
518
|
+
previous: () => step(-1),
|
|
519
|
+
focusSearch: () => searchEl.value?.focus(),
|
|
520
|
+
})
|
|
521
|
+
|
|
395
522
|
// True when filtering has emptied an otherwise non-empty stream.
|
|
396
523
|
const filteredEmpty = computed(
|
|
397
524
|
() => props.lines.length > 0 && visibleLines.value.length === 0,
|
|
@@ -473,7 +600,10 @@ function menuFor(line: LogLine, index: number): ContextMenuItemDef[] {
|
|
|
473
600
|
label: text.value.filterLevel.replace("{level}", line.level),
|
|
474
601
|
icon: "lucide:filter",
|
|
475
602
|
separatorBefore: !line.source,
|
|
476
|
-
onSelect: () =>
|
|
603
|
+
onSelect: () => {
|
|
604
|
+
if (props.toolbar && line.level) levels.value = [line.level]
|
|
605
|
+
emit("filter", { level: line.level })
|
|
606
|
+
},
|
|
477
607
|
})
|
|
478
608
|
}
|
|
479
609
|
const extra = props.extraRowItems?.(line, index) ?? []
|
|
@@ -488,11 +618,10 @@ interface Part {
|
|
|
488
618
|
hit: boolean
|
|
489
619
|
}
|
|
490
620
|
function splitHighlight(message: string): Part[] {
|
|
491
|
-
|
|
492
|
-
if (!term) return [{ text: message, hit: false }]
|
|
621
|
+
if (!term.value) return [{ text: message, hit: false }]
|
|
493
622
|
const parts: Part[] = []
|
|
494
623
|
const lower = message.toLowerCase()
|
|
495
|
-
const needle = term.toLowerCase()
|
|
624
|
+
const needle = term.value.toLowerCase()
|
|
496
625
|
let i = 0
|
|
497
626
|
while (i < message.length) {
|
|
498
627
|
const at = lower.indexOf(needle, i)
|
|
@@ -625,6 +754,7 @@ function rideToBottom(smooth = true, force = false) {
|
|
|
625
754
|
}
|
|
626
755
|
|
|
627
756
|
function scrollToBottom() {
|
|
757
|
+
current.value = -1
|
|
628
758
|
rideToBottom(true, true)
|
|
629
759
|
pinned.value = true
|
|
630
760
|
}
|
|
@@ -653,7 +783,7 @@ watch(
|
|
|
653
783
|
linesSignal,
|
|
654
784
|
async () => {
|
|
655
785
|
await nextTick()
|
|
656
|
-
if (props.tail && pinned.value) {
|
|
786
|
+
if (props.tail && pinned.value && current.value < 0) {
|
|
657
787
|
requestAnimationFrame(() => rideToBottom(true))
|
|
658
788
|
} else {
|
|
659
789
|
restoreScrollAnchor()
|
|
@@ -695,13 +825,13 @@ onBeforeUnmount(() => {
|
|
|
695
825
|
// (a component :is can't carry TransitionGroup-only props inertly otherwise).
|
|
696
826
|
const transitionProps = {
|
|
697
827
|
tag: "ol",
|
|
698
|
-
"enter-active-class": "transition duration-
|
|
828
|
+
"enter-active-class": "transition duration-[var(--duration-medium)] ease-[var(--ease-emphasized)]",
|
|
699
829
|
"enter-from-class": "-translate-x-1 opacity-0",
|
|
700
830
|
"leave-active-class":
|
|
701
|
-
"overflow-hidden transition-all duration-
|
|
831
|
+
"overflow-hidden transition-all duration-[var(--duration-medium)] ease-[var(--ease-emphasized)]",
|
|
702
832
|
"leave-from-class": "max-h-8 opacity-100",
|
|
703
833
|
"leave-to-class": "max-h-0 -translate-y-1 opacity-0",
|
|
704
|
-
"move-class": "transition-transform duration-
|
|
834
|
+
"move-class": "transition-transform duration-[var(--duration-medium)] ease-[var(--ease-emphasized)]",
|
|
705
835
|
}
|
|
706
836
|
|
|
707
837
|
// The viewer draws its own card frame and hides its overflow; a consumer
|
|
@@ -740,6 +870,78 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
740
870
|
</div>
|
|
741
871
|
</Transition>
|
|
742
872
|
|
|
873
|
+
<div
|
|
874
|
+
v-if="toolbar"
|
|
875
|
+
class="flex flex-wrap items-center gap-2 border-b border-line px-2 py-1.5 font-sans"
|
|
876
|
+
>
|
|
877
|
+
<div class="flex min-w-48 flex-1 items-center gap-1.5 rounded-control bg-surface-soft px-2 focus-within:ring-2 focus-within:ring-ring">
|
|
878
|
+
<LpIcon name="lucide:search" :size="14" class="shrink-0 text-muted" />
|
|
879
|
+
<input
|
|
880
|
+
ref="searchEl"
|
|
881
|
+
v-model="query"
|
|
882
|
+
type="search"
|
|
883
|
+
:aria-label="text.search"
|
|
884
|
+
:placeholder="text.search"
|
|
885
|
+
class="h-7 min-w-0 flex-1 bg-transparent text-xs text-ink outline-none placeholder:text-muted [&::-webkit-search-cancel-button]:hidden"
|
|
886
|
+
@keydown="onSearchKeydown"
|
|
887
|
+
/>
|
|
888
|
+
<span
|
|
889
|
+
v-if="term"
|
|
890
|
+
class="shrink-0 text-[11px] tabular-nums text-muted"
|
|
891
|
+
aria-live="polite"
|
|
892
|
+
>{{ matchLabel }}</span>
|
|
893
|
+
<button
|
|
894
|
+
v-if="query"
|
|
895
|
+
type="button"
|
|
896
|
+
class="rounded-xs p-0.5 text-muted outline-none hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
897
|
+
:aria-label="text.clearSearch"
|
|
898
|
+
@click="query = ''; searchEl?.focus()"
|
|
899
|
+
>
|
|
900
|
+
<LpIcon name="lucide:x" :size="12" />
|
|
901
|
+
</button>
|
|
902
|
+
</div>
|
|
903
|
+
<div class="flex items-center">
|
|
904
|
+
<button
|
|
905
|
+
type="button"
|
|
906
|
+
class="rounded-xs p-1 text-muted outline-none hover:text-ink focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40"
|
|
907
|
+
:aria-label="text.previousMatch"
|
|
908
|
+
:disabled="!matches.length"
|
|
909
|
+
@click="step(-1)"
|
|
910
|
+
>
|
|
911
|
+
<LpIcon name="lucide:chevron-up" :size="14" />
|
|
912
|
+
</button>
|
|
913
|
+
<button
|
|
914
|
+
type="button"
|
|
915
|
+
class="rounded-xs p-1 text-muted outline-none hover:text-ink focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40"
|
|
916
|
+
:aria-label="text.nextMatch"
|
|
917
|
+
:disabled="!matches.length"
|
|
918
|
+
@click="step(1)"
|
|
919
|
+
>
|
|
920
|
+
<LpIcon name="lucide:chevron-down" :size="14" />
|
|
921
|
+
</button>
|
|
922
|
+
</div>
|
|
923
|
+
<div
|
|
924
|
+
v-if="presentLevels.length > 1"
|
|
925
|
+
role="group"
|
|
926
|
+
:aria-label="text.levels"
|
|
927
|
+
class="flex flex-wrap items-center gap-1"
|
|
928
|
+
>
|
|
929
|
+
<button
|
|
930
|
+
v-for="level in presentLevels"
|
|
931
|
+
:key="level"
|
|
932
|
+
type="button"
|
|
933
|
+
class="flex items-center gap-1 rounded-pill border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.06em] outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
|
934
|
+
:class="levels.includes(level) ? 'border-line-strong bg-surface-soft text-ink' : 'border-line text-muted hover:text-ink'"
|
|
935
|
+
:aria-pressed="levels.includes(level)"
|
|
936
|
+
@click="toggleLevel(level)"
|
|
937
|
+
>
|
|
938
|
+
<span class="size-1.5 rounded-full" :class="metaFor(level).rail" aria-hidden="true" />
|
|
939
|
+
{{ level }}
|
|
940
|
+
<span class="font-normal tabular-nums text-muted">{{ levelCounts.get(level) }}</span>
|
|
941
|
+
</button>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
|
|
743
945
|
<!-- `instant`: this viewer decides for itself when a jump animates (see
|
|
744
946
|
rideToBottom) and restores scrollTop directly after prepending older
|
|
745
947
|
lines. A CSS smooth scroll would override both. -->
|
|
@@ -755,7 +957,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
755
957
|
class="flex h-full flex-col items-center justify-center gap-1 px-4 py-10 text-center"
|
|
756
958
|
>
|
|
757
959
|
<span class="text-muted">{{ filteredEmpty ? text.noMatch : (emptyLabel ?? text.empty) }}</span>
|
|
758
|
-
<span v-if="filteredEmpty" class="text-muted/70">“{{
|
|
960
|
+
<span v-if="filteredEmpty && term" class="text-muted/70">“{{ term }}”</span>
|
|
759
961
|
</div>
|
|
760
962
|
|
|
761
963
|
<template v-else>
|
|
@@ -804,7 +1006,8 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
804
1006
|
:key="lineKey(n)"
|
|
805
1007
|
:data-log-row="n"
|
|
806
1008
|
class="group relative flex items-start gap-0 px-0 leading-5 transition-colors hover:bg-surface-soft/60"
|
|
807
|
-
:class="metaFor(line.level).sel"
|
|
1009
|
+
:class="[metaFor(line.level).sel, n === currentRow ? 'bg-accent/15' : '']"
|
|
1010
|
+
:aria-current="n === currentRow ? 'true' : undefined"
|
|
808
1011
|
>
|
|
809
1012
|
<!-- level rail -->
|
|
810
1013
|
<!-- Level rail: absolutely positioned so it spans the FULL row height
|
|
@@ -822,7 +1025,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
822
1025
|
<span
|
|
823
1026
|
v-if="lineNumbers"
|
|
824
1027
|
class="mr-3 w-10 shrink-0 select-none text-right tabular-nums text-muted/70"
|
|
825
|
-
>{{ n + 1 }}</span>
|
|
1028
|
+
>{{ n + lineOffset + 1 }}</span>
|
|
826
1029
|
|
|
827
1030
|
<!-- timestamp -->
|
|
828
1031
|
<span
|
|
@@ -865,13 +1068,13 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
865
1068
|
Keyed on count so each new fold re-mounts the chip and pops it,
|
|
866
1069
|
cueing where the duplicate just merged. -->
|
|
867
1070
|
<Transition
|
|
868
|
-
enter-active-class="
|
|
1071
|
+
enter-active-class="transition duration-[var(--duration-medium)] ease-[var(--ease-emphasized)]"
|
|
869
1072
|
enter-from-class="scale-50 opacity-0"
|
|
870
1073
|
>
|
|
871
1074
|
<span
|
|
872
1075
|
v-if="count > 1"
|
|
873
1076
|
:key="count"
|
|
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"
|
|
1077
|
+
class="ml-2 inline-block select-none rounded-pill bg-surface-soft px-1.5 align-[1px] text-[10px] font-semibold tabular-nums text-muted-strong"
|
|
875
1078
|
:title="text.identical.replace('{n}', String(count))"
|
|
876
1079
|
>×{{ count }}</span>
|
|
877
1080
|
</Transition>
|
|
@@ -892,9 +1095,9 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
892
1095
|
Transition rides an outer wrapper so its transform/opacity don't fight
|
|
893
1096
|
the button's own -translate-x-1/2 centring and hover colour transition. -->
|
|
894
1097
|
<Transition
|
|
895
|
-
enter-active-class="transition duration-
|
|
1098
|
+
enter-active-class="transition duration-[var(--duration-medium)] ease-[var(--ease-emphasized)]"
|
|
896
1099
|
enter-from-class="translate-y-3 scale-90 opacity-0"
|
|
897
|
-
leave-active-class="transition duration-
|
|
1100
|
+
leave-active-class="transition duration-[var(--duration-fast)] ease-in"
|
|
898
1101
|
leave-to-class="translate-y-3 scale-90 opacity-0"
|
|
899
1102
|
>
|
|
900
1103
|
<div v-if="showJump" class="absolute inset-x-0 bottom-3 flex justify-center">
|
|
@@ -906,7 +1109,7 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(() =>
|
|
|
906
1109
|
<LpIcon
|
|
907
1110
|
name="lucide:arrow-down"
|
|
908
1111
|
:size="14"
|
|
909
|
-
class="transition-transform duration-
|
|
1112
|
+
class="transition-transform duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] group-hover:translate-y-0.5"
|
|
910
1113
|
/>
|
|
911
1114
|
{{ text.jump }}
|
|
912
1115
|
</button>
|
|
@@ -324,7 +324,7 @@ watch(
|
|
|
324
324
|
</aside>
|
|
325
325
|
<div
|
|
326
326
|
ref="panelRef"
|
|
327
|
-
class="flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line shadow-panel"
|
|
327
|
+
class="relative flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line shadow-panel"
|
|
328
328
|
:class="[frosted ? (layer.nested ? 'lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
|
|
329
329
|
:style="{ ...(width ? { width } : {}), ...(hasAside ? { alignSelf: 'stretch' } : {}) }"
|
|
330
330
|
>
|
|
@@ -16,6 +16,7 @@ import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
|
|
|
16
16
|
import LpEmptyState from "./LpEmptyState.vue"
|
|
17
17
|
import LpIcon from "./LpIcon.vue"
|
|
18
18
|
import LpPopover from "./LpPopover.vue"
|
|
19
|
+
import LpRollingText from "./LpRollingText.vue"
|
|
19
20
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
20
21
|
import LpShift from "./LpShift.vue"
|
|
21
22
|
|
|
@@ -291,14 +292,21 @@ function timeAgo(iso?: string): string {
|
|
|
291
292
|
@click="open = !open"
|
|
292
293
|
>
|
|
293
294
|
<LpIcon name="lucide:bell" :size="18" />
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
class="
|
|
295
|
+
<!-- The badge pops in and out, and its count rolls rather than
|
|
296
|
+
swapping, so new mail reads as arriving. -->
|
|
297
|
+
<Transition
|
|
298
|
+
enter-active-class="animate-[chip-in_var(--duration-fast)_var(--ease-emphasized)] motion-reduce:animate-none"
|
|
299
|
+
leave-active-class="animate-[chip-out_var(--duration-fast)_ease] motion-reduce:animate-none"
|
|
299
300
|
>
|
|
300
|
-
|
|
301
|
-
|
|
301
|
+
<LpBadge
|
|
302
|
+
v-if="hasUnread"
|
|
303
|
+
aria-hidden="true"
|
|
304
|
+
tone="danger"
|
|
305
|
+
class="absolute -right-1 -top-1 min-w-4 justify-center px-1 py-0 text-[10px] leading-4"
|
|
306
|
+
>
|
|
307
|
+
<LpRollingText :value="badgeLabel" :duration="220" />
|
|
308
|
+
</LpBadge>
|
|
309
|
+
</Transition>
|
|
302
310
|
</LpButton>
|
|
303
311
|
</LpContextMenu>
|
|
304
312
|
</template>
|