@leavepulse/ui 0.12.0 → 0.14.6
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/dist/assets/LpCommandPalette-D315B47-.css +16 -0
- package/dist/chunks/LayoutCanvas-Dd20boUG.js +50 -0
- package/dist/chunks/LayoutNode-DGpZA9LX.js +225 -0
- package/dist/chunks/LpAlert-BAYHDSS1.js +69 -0
- package/dist/chunks/LpAppShell-DBTsNQaM.js +170 -0
- package/dist/chunks/LpAutocomplete-B9uoCs4t.js +196 -0
- package/dist/chunks/LpAvatar-B1Ez0ZAx.js +88 -0
- package/dist/chunks/LpBadge-CXzBPnwO.js +42 -0
- package/dist/chunks/LpBreadcrumbs-B7Aqatzw.js +42 -0
- package/dist/chunks/LpButton-5VXXCjwF.js +101 -0
- package/dist/chunks/LpCalendar-B7ZL0ty3.js +150 -0
- package/dist/chunks/LpCard-1yvWT9fx.js +66 -0
- package/dist/chunks/LpCheckbox-BO0zuuPh.js +55 -0
- package/dist/chunks/LpCodeBlock-CSiOSPs8.js +253 -0
- package/dist/chunks/LpConfirmDialog-KLtTtCiT.js +62 -0
- package/dist/chunks/LpContextMenu-D0mzXqA7.js +151 -0
- package/dist/chunks/LpDatePicker-dY2q4dr4.js +122 -0
- package/dist/chunks/LpDisclosure-DP6yfgfM.js +76 -0
- package/dist/chunks/LpDivider-CUX46KR0.js +43 -0
- package/dist/chunks/LpDrawer-Ce6g9ZMr.js +310 -0
- package/dist/chunks/LpDropdownMenu-DJpoqRCb.js +65 -0
- package/dist/chunks/LpEmptyState-CrfyRJUG.js +47 -0
- package/dist/chunks/LpFormField-Bn-ZwC6z.js +47 -0
- package/dist/chunks/LpIcon-CCnX5_2j.js +41 -0
- package/dist/chunks/LpInfraNode-C_PHcthC.js +99 -0
- package/dist/chunks/LpInput-BauZRyzm.js +136 -0
- package/dist/chunks/LpLaneNode-HtnQf2FF.js +33 -0
- package/dist/chunks/LpLink-DHwOEp4e.js +36 -0
- package/dist/chunks/LpLogViewer-hC0OKkeE.js +482 -0
- package/dist/chunks/LpModal-CWoOExzT.js +128 -0
- package/dist/chunks/LpNotificationBell-BDy0tQwr.js +232 -0
- package/dist/chunks/LpNumberField-C5ar-OwE.js +55 -0
- package/dist/chunks/LpOtpInput-DqaT0Z75.js +65 -0
- package/dist/chunks/LpPagination-CeGuNe_q.js +132 -0
- package/dist/chunks/LpPasswordInput-DIHFw4u0.js +64 -0
- package/dist/chunks/LpPhoneInput-LQvVFGMw.js +199 -0
- package/dist/chunks/LpPopover-8268UmlC.js +54 -0
- package/dist/chunks/LpProgress-BhI0IbjC.js +37 -0
- package/dist/chunks/LpRadio-CL-pI0_O.js +39 -0
- package/dist/chunks/LpRadioGroup-B4yDTmi6.js +52 -0
- package/dist/chunks/LpScrollArea-K1x-azgY.js +67 -0
- package/dist/chunks/LpSegmented-BmpanyAo.js +74 -0
- package/dist/chunks/LpSelect-DznhPEWX.js +222 -0
- package/dist/chunks/LpServiceNode-Dc-Ut5RS.js +51 -0
- package/dist/chunks/LpSidebar-BPQabjae.js +152 -0
- package/dist/chunks/LpSidebarNav-CpF7rZxx.js +126 -0
- package/dist/chunks/LpSkeleton-eSFIo4kD.js +22 -0
- package/dist/chunks/LpSlider-B1SF3pRV.js +53 -0
- package/dist/chunks/LpStat-CdbEFOvn.js +88 -0
- package/dist/chunks/LpStepper-5dFTrW21.js +53 -0
- package/dist/chunks/LpSwitch-v6fnccSM.js +28 -0
- package/dist/chunks/LpTable-CJ4Zt1UH.js +229 -0
- package/dist/chunks/LpTableOfContents-CjvxPMpU.js +155 -0
- package/dist/chunks/LpTabs-D7aTVPOb.js +115 -0
- package/dist/chunks/LpTextarea-CRAkhemZ.js +44 -0
- package/dist/chunks/LpThemeSwitcher-BgWwfkxs.js +601 -0
- package/dist/chunks/LpTilt-DuyKbpkA.js +49 -0
- package/dist/chunks/LpToaster-gDB1EaU3.js +196 -0
- package/dist/chunks/LpTooltip-CIwoRCag.js +49 -0
- package/dist/chunks/LpTopologyCanvas-DVwyQfKa.js +237 -0
- package/dist/chunks/LpUptimeBar-CB366k0O.js +127 -0
- package/dist/component-names.d.ts +1 -1
- package/dist/components/LayoutCanvas.vue.js +4 -0
- package/dist/components/LayoutNode.vue.js +4 -0
- package/dist/components/LpAlert.vue.js +4 -0
- package/dist/components/LpAppShell.vue.js +4 -0
- package/dist/components/LpAutocomplete.vue.js +4 -0
- package/dist/components/LpAvatar.vue.js +4 -0
- package/dist/components/LpBadge.vue.js +4 -0
- package/dist/components/LpBreadcrumbs.vue.js +4 -0
- package/dist/components/LpButton.vue.js +4 -0
- package/dist/components/LpCalendar.vue.js +4 -0
- package/dist/components/LpCard.vue.js +4 -0
- package/dist/components/LpCheckbox.vue.js +4 -0
- package/dist/components/LpCodeBlock.vue.js +4 -0
- package/dist/components/LpCommandPalette.vue.js +310 -0
- package/dist/components/LpConfirmDialog.vue.js +4 -0
- package/dist/components/LpContextMenu.vue.d.ts +7 -0
- package/dist/components/LpContextMenu.vue.js +4 -0
- package/dist/components/LpDatePicker.vue.js +4 -0
- package/dist/components/LpDisclosure.vue.js +4 -0
- package/dist/components/LpDivider.vue.js +4 -0
- package/dist/components/LpDrawer.vue.d.ts +8 -0
- package/dist/components/LpDrawer.vue.js +4 -0
- package/dist/components/LpDropdownMenu.vue.js +4 -0
- package/dist/components/LpEmptyState.vue.js +4 -0
- package/dist/components/LpFormField.vue.js +4 -0
- package/dist/components/LpIcon.vue.js +4 -0
- package/dist/components/LpInfraNode.vue.js +4 -0
- package/dist/components/LpInput.vue.js +4 -0
- package/dist/components/LpLaneNode.vue.js +4 -0
- package/dist/components/LpLink.vue.js +4 -0
- package/dist/components/LpLogViewer.vue.js +4 -0
- package/dist/components/LpModal.vue.js +4 -0
- package/dist/components/LpNotificationBell.vue.js +4 -0
- package/dist/components/LpNumberField.vue.js +4 -0
- package/dist/components/LpOtpInput.vue.js +4 -0
- package/dist/components/LpPagination.vue.js +4 -0
- package/dist/components/LpPasswordInput.vue.js +4 -0
- package/dist/components/LpPhoneInput.vue.js +4 -0
- package/dist/components/LpPopover.vue.js +4 -0
- package/dist/components/LpProgress.vue.js +4 -0
- package/dist/components/LpRadio.vue.js +4 -0
- package/dist/components/LpRadioGroup.vue.js +4 -0
- package/dist/components/LpScrollArea.vue.d.ts +2 -2
- package/dist/components/LpScrollArea.vue.js +4 -0
- package/dist/components/LpSegmented.vue.js +4 -0
- package/dist/components/LpSelect.vue.d.ts +2 -0
- package/dist/components/LpSelect.vue.js +4 -0
- package/dist/components/LpServiceNode.vue.js +4 -0
- package/dist/components/LpSidebar.vue.js +4 -0
- package/dist/components/LpSidebarNav.vue.js +4 -0
- package/dist/components/LpSkeleton.vue.js +4 -0
- package/dist/components/LpSlider.vue.js +4 -0
- package/dist/components/LpStat.vue.js +4 -0
- package/dist/components/LpStepper.vue.js +4 -0
- package/dist/components/LpSwitch.vue.js +4 -0
- package/dist/components/LpTable.vue.js +4 -0
- package/dist/components/LpTableOfContents.vue.js +4 -0
- package/dist/components/LpTabs.vue.d.ts +4 -2
- package/dist/components/LpTabs.vue.js +4 -0
- package/dist/components/LpTextarea.vue.js +4 -0
- package/dist/components/LpThemeSwitcher.vue.js +4 -0
- package/dist/components/LpTilt.vue.d.ts +31 -0
- package/dist/components/LpTilt.vue.js +4 -0
- package/dist/components/LpToaster.vue.js +4 -0
- package/dist/components/LpTooltip.vue.js +4 -0
- package/dist/components/LpTopologyCanvas.vue.js +4 -0
- package/dist/components/LpUptimeBar.vue.js +4 -0
- package/dist/components/codeHighlight.js +230 -0
- package/dist/{countries-data-9rgqJWHo.js → components/countries-data.js} +2 -2
- package/dist/components/countries.js +38 -0
- package/dist/components/dropdown.js +14 -0
- package/dist/components/sidebar.js +1 -0
- package/dist/composables/useInputFilter.js +44 -0
- package/dist/composables/usePillTransition.js +11 -0
- package/dist/composables/useTilt.d.ts +23 -0
- package/dist/composables/useTilt.js +57 -0
- package/dist/composables/useToast.d.ts +2 -0
- package/dist/composables/useToast.js +49 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +197 -17596
- package/dist/layout/registry.js +10 -0
- package/dist/layout/tree.js +192 -0
- package/dist/layout/useLayout.js +61 -0
- package/package.json +5 -3
- package/src/component-names.ts +1 -0
- package/src/components/LpContextMenu.vue +14 -4
- package/src/components/LpDrawer.vue +29 -1
- package/src/components/LpLogViewer.vue +180 -31
- package/src/components/LpScrollArea.vue +20 -15
- package/src/components/LpSelect.vue +0 -0
- package/src/components/LpSidebar.vue +5 -2
- package/src/components/LpTabs.vue +19 -6
- package/src/components/LpThemeSwitcher.vue +13 -2
- package/src/components/LpTilt.vue +61 -0
- package/src/components/LpToaster.vue +11 -4
- package/src/composables/useTilt.ts +103 -0
- package/src/composables/useToast.ts +2 -0
- package/src/index.ts +3 -0
- package/src/nuxt.ts +10 -1
- package/dist/index.css +0 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* viewport element and re-emits the native scroll event — exactly what tailing
|
|
8
8
|
* needs.
|
|
9
9
|
*/
|
|
10
|
-
import { computed, nextTick, ref, watch } from "vue"
|
|
10
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, TransitionGroup, watch } from "vue"
|
|
11
11
|
import LpContextMenu, { type ContextMenuItemDef } from "./LpContextMenu.vue"
|
|
12
12
|
import LpIcon from "./LpIcon.vue"
|
|
13
13
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
@@ -108,14 +108,18 @@ const emit = defineEmits<{
|
|
|
108
108
|
// Level → gutter rail + text colour. Reads only semantic tokens so re-skinning
|
|
109
109
|
// just works. trace/debug stay muted; success is green; warn amber; error/fatal
|
|
110
110
|
// red (fatal gets a filled chip to stand apart from plain error).
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
// `sel` tints ::selection in the row's semantic colour (info→brand, warn→accent,
|
|
112
|
+
// error/fatal→danger) via Tailwind's `selection:` variant — the highlight then
|
|
113
|
+
// matches the level instead of the browser default. Static classes (not built
|
|
114
|
+
// from a var) so Tailwind emits them from the @source scan.
|
|
115
|
+
const levelMeta: Record<LogLevel, { rail: string; text: string; chip: string; sel: string }> = {
|
|
116
|
+
trace: { rail: "bg-line-strong", text: "text-muted", chip: "text-muted", sel: "selection:bg-muted/40" },
|
|
117
|
+
debug: { rail: "bg-line-strong", text: "text-muted-strong", chip: "text-muted-strong", sel: "selection:bg-muted-strong/40" },
|
|
118
|
+
info: { rail: "bg-brand", text: "text-ink", chip: "text-brand", sel: "selection:bg-brand/40" },
|
|
119
|
+
success: { rail: "bg-action", text: "text-ink", chip: "text-action", sel: "selection:bg-action/40" },
|
|
120
|
+
warn: { rail: "bg-accent", text: "text-ink", chip: "text-accent", sel: "selection:bg-accent/40" },
|
|
121
|
+
error: { rail: "bg-danger", text: "text-ink", chip: "text-danger", sel: "selection:bg-danger/40" },
|
|
122
|
+
fatal: { rail: "bg-danger", text: "text-ink", chip: "rounded-xs bg-danger px-1 text-ink-inverse", sel: "selection:bg-danger/40" },
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
function metaFor(level?: LogLevel) {
|
|
@@ -264,6 +268,34 @@ function lineText(line: LogLine): string {
|
|
|
264
268
|
.join(" ")
|
|
265
269
|
}
|
|
266
270
|
|
|
271
|
+
// One LpContextMenu wraps the whole list (not each row) so a mouse drag can
|
|
272
|
+
// select text across rows — a per-row reka trigger would bind pointerdown and
|
|
273
|
+
// pin the selection to one row. On right-click we resolve which row is under the
|
|
274
|
+
// cursor (data-log-row) and rebuild the menu items for it before reka opens.
|
|
275
|
+
const menuRow = ref<{ line: LogLine; n: number } | null>(null)
|
|
276
|
+
const menuItems = computed<ContextMenuItemDef[]>(() =>
|
|
277
|
+
menuRow.value ? menuFor(menuRow.value.line, menuRow.value.n) : [],
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
// Resolve the right-clicked row so menuItems reflects it before reka's trigger
|
|
281
|
+
// opens the menu (runs on the same contextmenu event, ahead of the open).
|
|
282
|
+
function onRowContext(e: MouseEvent) {
|
|
283
|
+
if (!props.rowMenu) return
|
|
284
|
+
const el = (e.target as HTMLElement | null)?.closest<HTMLElement>("[data-log-row]")
|
|
285
|
+
const n = el ? Number(el.dataset.logRow) : NaN
|
|
286
|
+
const row = Number.isNaN(n) ? undefined : visibleLines.value.find((r) => r.n === n)
|
|
287
|
+
menuRow.value = row ? { line: row.line, n: row.n } : null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// vaul (the drawer lib) treats a left-button pointerdown on a side drawer as the
|
|
291
|
+
// start of a swipe-to-dismiss (`shouldDrag` returns true unconditionally for
|
|
292
|
+
// left/right drawers) and its preventDefault kills text selection. Stop that
|
|
293
|
+
// pointerdown from bubbling to vaul — left button only, so scroll and the
|
|
294
|
+
// right-click context menu still reach it.
|
|
295
|
+
function onRowPointerDown(e: PointerEvent) {
|
|
296
|
+
if (e.button === 0) e.stopPropagation()
|
|
297
|
+
}
|
|
298
|
+
|
|
267
299
|
// Built-in copy actions + optional "filter by" + consumer extras. Rebuilt per
|
|
268
300
|
// right-click so it reflects the line under the cursor.
|
|
269
301
|
function menuFor(line: LogLine, index: number): ContextMenuItemDef[] {
|
|
@@ -322,6 +354,52 @@ function splitHighlight(message: string): Part[] {
|
|
|
322
354
|
return parts
|
|
323
355
|
}
|
|
324
356
|
|
|
357
|
+
// ── virtualisation ───────────────────────────────────────────────────
|
|
358
|
+
// A live log stream can reach tens of thousands of rows; rendering them all
|
|
359
|
+
// (a v-for over the whole list, each row a flex box with a highlight-split and,
|
|
360
|
+
// worse, a TransitionGroup computing FLIP offsets for every node on each push)
|
|
361
|
+
// pins the main thread and the viewer stops responding. So past a threshold we
|
|
362
|
+
// render only the slice inside the viewport plus a small overscan, padding the
|
|
363
|
+
// rest with two spacer rows. This needs a uniform row height, which only holds
|
|
364
|
+
// when lines don't wrap — with `wrap` (or short lists) we fall back to the
|
|
365
|
+
// plain full render that keeps the fold/enter transitions.
|
|
366
|
+
const ROW_HEIGHT = 20 // px; matches leading-5 (1.25rem) monospace rows
|
|
367
|
+
const VIRTUALISE_ABOVE = 200 // rows; below this the full render is cheap enough
|
|
368
|
+
const OVERSCAN = 12 // rows rendered beyond each edge to cover fast scrolls
|
|
369
|
+
|
|
370
|
+
const virtualise = computed(() => !props.wrap && visibleLines.value.length > VIRTUALISE_ABOVE)
|
|
371
|
+
|
|
372
|
+
const viewportHeight = ref(0)
|
|
373
|
+
const scrollTop = ref(0)
|
|
374
|
+
|
|
375
|
+
// Window of rows to actually render when virtualising: [start, end) by index
|
|
376
|
+
// into visibleLines, widened by OVERSCAN on both sides and clamped.
|
|
377
|
+
const virtualRange = computed(() => {
|
|
378
|
+
const total = visibleLines.value.length
|
|
379
|
+
if (!virtualise.value) return { start: 0, end: total }
|
|
380
|
+
const visibleCount = Math.ceil(viewportHeight.value / ROW_HEIGHT) + 1
|
|
381
|
+
const start = Math.max(0, Math.floor(scrollTop.value / ROW_HEIGHT) - OVERSCAN)
|
|
382
|
+
const end = Math.min(total, start + visibleCount + OVERSCAN * 2)
|
|
383
|
+
return { start, end }
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
const renderedLines = computed(() => {
|
|
387
|
+
if (!virtualise.value) return visibleLines.value
|
|
388
|
+
const { start, end } = virtualRange.value
|
|
389
|
+
return visibleLines.value.slice(start, end)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// Spacer heights that stand in for the un-rendered rows above/below the window,
|
|
393
|
+
// so the scrollbar and scroll position stay honest.
|
|
394
|
+
const padTop = computed(() =>
|
|
395
|
+
virtualise.value ? virtualRange.value.start * ROW_HEIGHT : 0,
|
|
396
|
+
)
|
|
397
|
+
const padBottom = computed(() =>
|
|
398
|
+
virtualise.value
|
|
399
|
+
? (visibleLines.value.length - virtualRange.value.end) * ROW_HEIGHT
|
|
400
|
+
: 0,
|
|
401
|
+
)
|
|
402
|
+
|
|
325
403
|
// ── tail / auto-scroll ───────────────────────────────────────────────
|
|
326
404
|
const scrollRef = ref<{ viewportEl: HTMLElement | null } | null>(null)
|
|
327
405
|
const pinned = ref(true) // currently glued to the bottom
|
|
@@ -349,6 +427,10 @@ let topArmed = true
|
|
|
349
427
|
function onScroll(e: Event) {
|
|
350
428
|
const el = e.target as HTMLElement
|
|
351
429
|
pinned.value = atBottom(el)
|
|
430
|
+
// Feed the virtual window. Reading clientHeight here keeps it in sync with
|
|
431
|
+
// runtime resizes (drawer drag, window resize) without a ResizeObserver.
|
|
432
|
+
scrollTop.value = el.scrollTop
|
|
433
|
+
if (el.clientHeight !== viewportHeight.value) viewportHeight.value = el.clientHeight
|
|
352
434
|
if (!props.loadOlder) return
|
|
353
435
|
if (el.scrollTop <= TOP_NEAR) {
|
|
354
436
|
if (topArmed && !props.loadingOlder) {
|
|
@@ -422,6 +504,47 @@ watch(
|
|
|
422
504
|
)
|
|
423
505
|
|
|
424
506
|
const showJump = computed(() => props.tail && !pinned.value && props.lines.length > 0)
|
|
507
|
+
|
|
508
|
+
// Track the viewport height for the whole lifetime, not just at mount. Reading
|
|
509
|
+
// it once was a deadlock when the viewer mounted inside a container that had no
|
|
510
|
+
// height yet (a drawer mid open-animation, a hidden tab): the virtual window
|
|
511
|
+
// collapsed to a single row, so there was nothing to scroll, and `onScroll` —
|
|
512
|
+
// the only other place that refreshed the height — could never fire to correct
|
|
513
|
+
// it. The list looked stuck and wouldn't scroll at all.
|
|
514
|
+
let viewportResize: ResizeObserver | null = null
|
|
515
|
+
|
|
516
|
+
function measureViewport() {
|
|
517
|
+
const el = viewport()
|
|
518
|
+
if (el) viewportHeight.value = el.clientHeight
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
onMounted(() => {
|
|
522
|
+
measureViewport()
|
|
523
|
+
const el = viewport()
|
|
524
|
+
if (el && typeof ResizeObserver !== "undefined") {
|
|
525
|
+
viewportResize = new ResizeObserver(measureViewport)
|
|
526
|
+
viewportResize.observe(el)
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
onBeforeUnmount(() => {
|
|
531
|
+
viewportResize?.disconnect()
|
|
532
|
+
viewportResize = null
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
// FLIP / enter / leave classes for the full-render TransitionGroup, bound via
|
|
536
|
+
// v-bind so the same list element can fall back to a plain <ol> when virtualised
|
|
537
|
+
// (a component :is can't carry TransitionGroup-only props inertly otherwise).
|
|
538
|
+
const transitionProps = {
|
|
539
|
+
tag: "ol",
|
|
540
|
+
"enter-active-class": "transition duration-200 ease-[var(--ease-emphasized)]",
|
|
541
|
+
"enter-from-class": "-translate-x-1 opacity-0",
|
|
542
|
+
"leave-active-class":
|
|
543
|
+
"overflow-hidden transition-all duration-200 ease-[var(--ease-emphasized)]",
|
|
544
|
+
"leave-from-class": "max-h-8 opacity-100",
|
|
545
|
+
"leave-to-class": "max-h-0 -translate-y-1 opacity-0",
|
|
546
|
+
"move-class": "transition-transform duration-200 ease-[var(--ease-emphasized)]",
|
|
547
|
+
}
|
|
425
548
|
</script>
|
|
426
549
|
|
|
427
550
|
<template>
|
|
@@ -455,33 +578,53 @@ const showJump = computed(() => props.tail && !pinned.value && props.lines.lengt
|
|
|
455
578
|
<span class="text-[11px]">loading older…</span>
|
|
456
579
|
</div>
|
|
457
580
|
|
|
458
|
-
<!--
|
|
581
|
+
<!-- ONE context menu wraps the whole list (not each row): @contextmenu
|
|
582
|
+
resolves which row was right-clicked so menuItems reflects it before
|
|
583
|
+
reka opens. @pointerdown stops a left-button drag from reaching vaul
|
|
584
|
+
(which would start a swipe-dismiss and cancel text selection); the
|
|
585
|
+
right button still bubbles so the menu opens. reka's own trigger
|
|
586
|
+
pointerdown is touch/pen-only, so it doesn't interfere with the mouse.
|
|
587
|
+
Leave collapses a folded-away duplicate (max-height + opacity + a small
|
|
459
588
|
lift) while the survivors FLIP-slide up via move-class — so toggling
|
|
460
|
-
`compact` reads as the dupes melting into the kept line
|
|
461
|
-
<
|
|
462
|
-
|
|
589
|
+
`compact` reads as the dupes melting into the kept line. -->
|
|
590
|
+
<!-- Component switches on `virtualise`: a plain <ol> when we render only a
|
|
591
|
+
windowed slice (FLIP/enter/leave transitions can't run over a moving
|
|
592
|
+
virtual window, and computing them for thousands of nodes is exactly
|
|
593
|
+
the cost we're avoiding), or a TransitionGroup for the full render on
|
|
594
|
+
small lists, where the fold/enter animations are affordable and nice.
|
|
595
|
+
Spacer rows stand in for the un-rendered slices above/below. -->
|
|
596
|
+
<LpContextMenu :items="menuItems" :always="rowMenu">
|
|
597
|
+
<component
|
|
598
|
+
:is="virtualise ? 'ol' : TransitionGroup"
|
|
599
|
+
v-bind="virtualise ? {} : transitionProps"
|
|
463
600
|
class="py-1"
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
leave-active-class="overflow-hidden transition-all duration-200 ease-[var(--ease-emphasized)]"
|
|
467
|
-
leave-from-class="max-h-8 opacity-100"
|
|
468
|
-
leave-to-class="max-h-0 -translate-y-1 opacity-0"
|
|
469
|
-
move-class="transition-transform duration-200 ease-[var(--ease-emphasized)]"
|
|
601
|
+
@contextmenu="onRowContext"
|
|
602
|
+
@pointerdown="onRowPointerDown"
|
|
470
603
|
>
|
|
471
|
-
<LpContextMenu
|
|
472
|
-
v-for="{ line, n, count } in visibleLines"
|
|
473
|
-
:key="n"
|
|
474
|
-
:items="menuFor(line, n)"
|
|
475
|
-
>
|
|
476
604
|
<li
|
|
477
|
-
|
|
605
|
+
v-if="virtualise && padTop > 0"
|
|
606
|
+
key="pad-top"
|
|
607
|
+
:style="{ height: padTop + 'px' }"
|
|
608
|
+
aria-hidden="true"
|
|
609
|
+
/>
|
|
610
|
+
<li
|
|
611
|
+
v-for="{ line, n, count } in renderedLines"
|
|
612
|
+
:key="n"
|
|
613
|
+
:data-log-row="n"
|
|
614
|
+
class="group relative flex items-start gap-0 px-0 leading-5 transition-colors hover:bg-surface-soft/60"
|
|
615
|
+
:class="metaFor(line.level).sel"
|
|
478
616
|
>
|
|
479
617
|
<!-- level rail -->
|
|
618
|
+
<!-- Level rail: absolutely positioned so it spans the FULL row height
|
|
619
|
+
regardless of content — self-stretch only reached the flex line box
|
|
620
|
+
(shorter than the relaxed line-height), which left visible gaps
|
|
621
|
+
between rows. A spacer keeps the text indented past it. -->
|
|
480
622
|
<span
|
|
481
|
-
class="
|
|
623
|
+
class="absolute inset-y-0 left-0 w-0.5"
|
|
482
624
|
:class="metaFor(line.level).rail"
|
|
483
625
|
aria-hidden="true"
|
|
484
626
|
/>
|
|
627
|
+
<span class="mr-2 w-0.5 shrink-0" aria-hidden="true" />
|
|
485
628
|
|
|
486
629
|
<!-- line number -->
|
|
487
630
|
<span
|
|
@@ -492,20 +635,20 @@ const showJump = computed(() => props.tail && !pinned.value && props.lines.lengt
|
|
|
492
635
|
<!-- timestamp -->
|
|
493
636
|
<span
|
|
494
637
|
v-if="showTime"
|
|
495
|
-
class="mr-3 shrink-0
|
|
638
|
+
class="mr-3 shrink-0 tabular-nums text-muted"
|
|
496
639
|
>{{ fmtTime(line.time) }}</span>
|
|
497
640
|
|
|
498
641
|
<!-- level chip -->
|
|
499
642
|
<span
|
|
500
643
|
v-if="showLevel"
|
|
501
|
-
class="mr-3 w-12 shrink-0
|
|
644
|
+
class="mr-3 w-12 shrink-0 text-[10px] font-semibold uppercase tracking-[0.06em]"
|
|
502
645
|
:class="metaFor(line.level).chip"
|
|
503
646
|
>{{ line.level ?? "info" }}</span>
|
|
504
647
|
|
|
505
648
|
<!-- source tag -->
|
|
506
649
|
<span
|
|
507
650
|
v-if="line.source"
|
|
508
|
-
class="mr-2 shrink-0
|
|
651
|
+
class="mr-2 shrink-0 text-muted-strong"
|
|
509
652
|
>[{{ line.source }}]</span>
|
|
510
653
|
|
|
511
654
|
<!-- message -->
|
|
@@ -536,8 +679,14 @@ const showJump = computed(() => props.tail && !pinned.value && props.lines.lengt
|
|
|
536
679
|
</Transition>
|
|
537
680
|
</span>
|
|
538
681
|
</li>
|
|
539
|
-
|
|
540
|
-
|
|
682
|
+
<li
|
|
683
|
+
v-if="virtualise && padBottom > 0"
|
|
684
|
+
key="pad-bottom"
|
|
685
|
+
:style="{ height: padBottom + 'px' }"
|
|
686
|
+
aria-hidden="true"
|
|
687
|
+
/>
|
|
688
|
+
</component>
|
|
689
|
+
</LpContextMenu>
|
|
541
690
|
</template>
|
|
542
691
|
</LpScrollArea>
|
|
543
692
|
|
|
@@ -47,36 +47,41 @@ const barFade =
|
|
|
47
47
|
|
|
48
48
|
<template>
|
|
49
49
|
<ScrollAreaRoot class="relative overflow-hidden" type="hover" :scroll-hide-delay="500">
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
>
|
|
56
|
-
<div :class="contentClass">
|
|
57
|
-
<slot />
|
|
58
|
-
</div>
|
|
59
|
-
</ScrollAreaViewport>
|
|
60
|
-
|
|
50
|
+
<!-- The bar itself is a pointer target far wider than it looks: the visible
|
|
51
|
+
rail is the thumb, sized below, while the track pads it out to a
|
|
52
|
+
grabbable strip. A 4px-wide hit area is near-impossible to catch with a
|
|
53
|
+
mouse. Padding (not width) carries the extra room so the thumb stays
|
|
54
|
+
flush against the wall. -->
|
|
61
55
|
<ScrollAreaScrollbar
|
|
62
56
|
orientation="vertical"
|
|
63
|
-
class="group flex w-
|
|
57
|
+
class="group flex w-3.5 touch-none select-none justify-end py-px pl-2.5 pr-px"
|
|
64
58
|
:class="barFade"
|
|
65
59
|
:style="barInsetTop ? { marginTop: barInsetTop } : undefined"
|
|
66
60
|
>
|
|
67
61
|
<ScrollAreaThumb
|
|
68
|
-
class="
|
|
62
|
+
class="w-1 rounded-l-pill bg-brand/40 transition-[width,background-color] duration-[var(--duration-fast)] group-hover:w-1.5 group-hover:bg-brand"
|
|
69
63
|
/>
|
|
70
64
|
</ScrollAreaScrollbar>
|
|
71
65
|
|
|
72
66
|
<ScrollAreaScrollbar
|
|
73
67
|
orientation="horizontal"
|
|
74
|
-
class="group flex h-
|
|
68
|
+
class="group flex h-3.5 flex-col touch-none select-none justify-end px-px pb-px pt-2.5"
|
|
75
69
|
:class="barFade"
|
|
76
70
|
>
|
|
77
71
|
<ScrollAreaThumb
|
|
78
|
-
class="
|
|
72
|
+
class="h-1 rounded-t-pill bg-brand/40 transition-[height,background-color] duration-[var(--duration-fast)] group-hover:h-1.5 group-hover:bg-brand"
|
|
79
73
|
/>
|
|
80
74
|
</ScrollAreaScrollbar>
|
|
75
|
+
|
|
76
|
+
<ScrollAreaViewport
|
|
77
|
+
ref="viewportRef"
|
|
78
|
+
class="size-full min-w-0 [&>div]:!block [&>div]:!min-w-0"
|
|
79
|
+
:class="fade ? '[mask-image:linear-gradient(to_bottom,transparent_0,black_14px,black_calc(100%-14px),transparent_100%)]' : ''"
|
|
80
|
+
@scroll.passive="$emit('scroll', $event)"
|
|
81
|
+
>
|
|
82
|
+
<div :class="contentClass">
|
|
83
|
+
<slot />
|
|
84
|
+
</div>
|
|
85
|
+
</ScrollAreaViewport>
|
|
81
86
|
</ScrollAreaRoot>
|
|
82
87
|
</template>
|
|
Binary file
|
|
@@ -122,10 +122,12 @@ defineOptions({ inheritAttrs: false })
|
|
|
122
122
|
aria-label="Sidebar"
|
|
123
123
|
v-bind="$attrs"
|
|
124
124
|
>
|
|
125
|
+
<!-- Edge-to-edge (negative margins cancel the rail p-3) and h-16 to match the
|
|
126
|
+
app header, so its bottom hairline continues the header's border-b. -->
|
|
125
127
|
<div
|
|
126
128
|
v-if="$slots.header"
|
|
127
|
-
class="shrink-0"
|
|
128
|
-
:class="divider ? '
|
|
129
|
+
class="-mx-3 -mt-3 mb-3 flex h-16 shrink-0 items-center px-3"
|
|
130
|
+
:class="divider ? 'border-b border-line' : ''"
|
|
129
131
|
>
|
|
130
132
|
<slot name="header" />
|
|
131
133
|
</div>
|
|
@@ -163,6 +165,7 @@ defineOptions({ inheritAttrs: false })
|
|
|
163
165
|
direction="left"
|
|
164
166
|
size="sm"
|
|
165
167
|
:edge-open="edgeOpen"
|
|
168
|
+
:edge-breakpoint="mobileBreakpoint"
|
|
166
169
|
@update:open="(v) => emit('update:open', v)"
|
|
167
170
|
>
|
|
168
171
|
<div class="flex h-full flex-col gap-1">
|
|
@@ -20,8 +20,10 @@ withDefaults(
|
|
|
20
20
|
* "contained" (default) wraps the triggers in a bordered, filled bar.
|
|
21
21
|
* "plain" drops the container chrome — a flat row of tabs with only the
|
|
22
22
|
* sliding pill marking the active one (for nav bars over a custom surface).
|
|
23
|
+
* "underline" is a flat row over a hairline baseline, marking the active
|
|
24
|
+
* tab with a sliding bottom bar instead of a pill (page-section nav style).
|
|
23
25
|
*/
|
|
24
|
-
variant?: "contained" | "plain"
|
|
26
|
+
variant?: "contained" | "plain" | "underline"
|
|
25
27
|
/** Tint the active pill + label with the brand colour (nav-bar style). */
|
|
26
28
|
accent?: boolean
|
|
27
29
|
/** Stretch the bar to full width with equal-share triggers. */
|
|
@@ -67,6 +69,7 @@ function pillUnder(value: string, active?: string): boolean {
|
|
|
67
69
|
:class="[
|
|
68
70
|
block ? 'flex w-full' : 'inline-flex',
|
|
69
71
|
variant === 'contained' ? 'rounded-control border border-line bg-surface-soft p-1' : '',
|
|
72
|
+
variant === 'underline' ? 'border-b border-line' : '',
|
|
70
73
|
]"
|
|
71
74
|
@pointerleave="hovered = null"
|
|
72
75
|
>
|
|
@@ -74,22 +77,32 @@ function pillUnder(value: string, active?: string): boolean {
|
|
|
74
77
|
v-for="item in items"
|
|
75
78
|
:key="item.value"
|
|
76
79
|
:value="item.value"
|
|
77
|
-
class="relative inline-flex items-center justify-center gap-1.5
|
|
80
|
+
class="relative inline-flex items-center justify-center gap-1.5 text-sm outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring"
|
|
78
81
|
:class="[
|
|
79
82
|
block ? 'flex-1' : '',
|
|
83
|
+
variant === 'underline' ? 'rounded-t-md px-3 pt-1.5 pb-2' : 'rounded-md px-3 py-1.5',
|
|
80
84
|
accent ? 'data-[state=active]:text-brand' : 'data-[state=active]:text-ink',
|
|
81
85
|
hovered === item.value ? 'text-ink' : 'text-muted',
|
|
82
86
|
]"
|
|
83
87
|
@pointerenter="hovered = item.value"
|
|
84
88
|
>
|
|
89
|
+
<!-- Active marker: a bottom bar in "underline", a filled pill otherwise.
|
|
90
|
+
Both slide between tabs via the shared motion layoutId. -->
|
|
85
91
|
<Motion
|
|
86
92
|
v-if="pillUnder(item.value, modelValue)"
|
|
87
93
|
:layout-id="pillId"
|
|
88
94
|
:transition="pillTransition"
|
|
89
|
-
class="
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
:class="variant === 'underline'
|
|
96
|
+
? [
|
|
97
|
+
'absolute inset-x-0 -bottom-px z-0 h-0.5 rounded-full',
|
|
98
|
+
accent ? 'bg-brand' : 'bg-ink',
|
|
99
|
+
]
|
|
100
|
+
: [
|
|
101
|
+
'absolute inset-0 z-0 rounded-md shadow-sm',
|
|
102
|
+
accent
|
|
103
|
+
? 'border border-brand/35 bg-brand/12'
|
|
104
|
+
: 'border border-line bg-surface-raised',
|
|
105
|
+
]"
|
|
93
106
|
/>
|
|
94
107
|
<LpIcon v-if="item.icon" :name="item.icon" :size="14" class="relative z-10" />
|
|
95
108
|
<span class="relative z-10">{{ item.label }}</span>
|
|
@@ -90,6 +90,10 @@ const ICON_SIZE = { sm: 15, md: 16, lg: 18 } as const
|
|
|
90
90
|
|
|
91
91
|
const wantsLabel = computed(() => props.showLabel || props.variant === "pill")
|
|
92
92
|
|
|
93
|
+
// Labelless icon variant renders as a plain square icon button (no border/fill),
|
|
94
|
+
// so it sits inline with other toolbar icon buttons. Other variants stay framed.
|
|
95
|
+
const bareIcon = computed(() => props.variant === "icon" && !wantsLabel.value)
|
|
96
|
+
|
|
93
97
|
// Apply a theme through the kit's self-managed setter (it persists + updates
|
|
94
98
|
// the shared `current`), then notify. The reveal animates from the click point.
|
|
95
99
|
function applyTheme(t: TokenSet, ev?: MouseEvent) {
|
|
@@ -138,8 +142,15 @@ const triggerLabel = computed(
|
|
|
138
142
|
type="button"
|
|
139
143
|
:aria-label="triggerLabel"
|
|
140
144
|
:title="triggerLabel"
|
|
141
|
-
class="inline-flex items-center gap-2 rounded-control
|
|
142
|
-
:class="
|
|
145
|
+
class="inline-flex items-center justify-center gap-2 rounded-control outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
|
146
|
+
:class="
|
|
147
|
+
bareIcon
|
|
148
|
+
? [SIZES[size], 'text-muted hover:bg-surface-soft hover:text-ink']
|
|
149
|
+
: [
|
|
150
|
+
'border border-line bg-surface-raised px-1.5 text-ink hover:border-line-strong',
|
|
151
|
+
wantsLabel ? 'py-1 pr-2.5' : '',
|
|
152
|
+
]
|
|
153
|
+
"
|
|
143
154
|
@click="cycle"
|
|
144
155
|
>
|
|
145
156
|
<!-- swatch: brand over surface, hinting the theme at a glance -->
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* LpTilt — a tactile 3D-tilt wrapper. Its slot content rotates toward the
|
|
4
|
+
* pointer and lifts on hover (see useTilt), so any element — a card, a stat
|
|
5
|
+
* tile, a floating island illustration — gains hand-held depth by wrapping it.
|
|
6
|
+
*
|
|
7
|
+
* <LpTilt :max="12" :lift="14" glare>
|
|
8
|
+
* <LpCard>…</LpCard>
|
|
9
|
+
* </LpTilt>
|
|
10
|
+
*
|
|
11
|
+
* The wrapper owns the `perspective` stage; the inner element is what actually
|
|
12
|
+
* tilts, so sibling content outside the wrapper is unaffected. Honors reduced
|
|
13
|
+
* motion (falls back to a flat, still element).
|
|
14
|
+
*/
|
|
15
|
+
import { computed } from "vue"
|
|
16
|
+
import { useTilt } from "../composables/useTilt"
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(
|
|
19
|
+
defineProps<{
|
|
20
|
+
/** Max rotation toward the pointer, in degrees. */
|
|
21
|
+
max?: number
|
|
22
|
+
/** Upward lift on hover, in px. */
|
|
23
|
+
lift?: number
|
|
24
|
+
/** Scale on hover. */
|
|
25
|
+
scale?: number
|
|
26
|
+
/** Render a pointer-tracking glare highlight over the content. */
|
|
27
|
+
glare?: boolean
|
|
28
|
+
/** Perspective depth for the 3D stage, in px. Lower = stronger. */
|
|
29
|
+
perspective?: number
|
|
30
|
+
}>(),
|
|
31
|
+
{ max: 10, lift: 8, scale: 1.03, glare: false, perspective: 900 },
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const tilt = useTilt({
|
|
35
|
+
max: props.max,
|
|
36
|
+
lift: props.lift,
|
|
37
|
+
scale: props.scale,
|
|
38
|
+
glare: props.glare,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const stageStyle = computed(() => ({ perspective: `${props.perspective}px` }))
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div :style="stageStyle" class="lp-tilt-stage">
|
|
46
|
+
<div
|
|
47
|
+
class="lp-tilt-inner relative"
|
|
48
|
+
:style="tilt.style.value"
|
|
49
|
+
@pointermove="tilt.onPointerMove"
|
|
50
|
+
@pointerenter="tilt.onPointerEnter"
|
|
51
|
+
@pointerleave="tilt.onPointerLeave"
|
|
52
|
+
>
|
|
53
|
+
<slot />
|
|
54
|
+
<div
|
|
55
|
+
v-if="glare"
|
|
56
|
+
class="pointer-events-none absolute inset-0 rounded-[inherit] mix-blend-overlay"
|
|
57
|
+
:style="tilt.glareStyle.value"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
@@ -178,9 +178,10 @@ async function runAction(t: ToastItem, action: NonNullable<ToastItem["actions"]>
|
|
|
178
178
|
v-for="(action, i) in t.actions"
|
|
179
179
|
:key="i"
|
|
180
180
|
type="button"
|
|
181
|
-
class="rounded-control border border-line bg-surface-soft px-2.5 py-1 text-xs font-medium text-ink outline-none transition-[border-color,background-color,scale] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] hover:border-line-strong hover:bg-surface-soft/70 active:scale-95 motion-reduce:active:scale-100 focus-visible:ring-2 focus-visible:ring-ring"
|
|
181
|
+
class="inline-flex items-center gap-1.5 rounded-control border border-line bg-surface-soft px-2.5 py-1 text-xs font-medium text-ink outline-none transition-[border-color,background-color,scale] duration-[var(--duration-fast)] ease-[var(--ease-emphasized)] hover:border-line-strong hover:bg-surface-soft/70 active:scale-95 motion-reduce:active:scale-100 focus-visible:ring-2 focus-visible:ring-ring"
|
|
182
182
|
@click="runAction(t, action)"
|
|
183
183
|
>
|
|
184
|
+
<LpIcon v-if="action.icon" :name="action.icon" :size="14" />
|
|
184
185
|
{{ action.label }}
|
|
185
186
|
</button>
|
|
186
187
|
</div>
|
|
@@ -209,8 +210,14 @@ async function runAction(t: ToastItem, action: NonNullable<ToastItem["actions"]>
|
|
|
209
210
|
/>
|
|
210
211
|
</ToastRoot>
|
|
211
212
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
<!-- Teleported to body so the viewport escapes any stacking context the
|
|
214
|
+
consumer's app root creates (e.g. an opaque `#app { z-index }` wrapper).
|
|
215
|
+
Modals portal to body too, so both compare at the same root level and
|
|
216
|
+
--z-toast (200) > --z-modal (110) wins as intended. -->
|
|
217
|
+
<Teleport to="body">
|
|
218
|
+
<ToastViewport
|
|
219
|
+
class="fixed bottom-0 right-0 z-(--z-toast) flex w-[min(92vw,22rem)] flex-col gap-2 p-4 outline-none"
|
|
220
|
+
/>
|
|
221
|
+
</Teleport>
|
|
215
222
|
</ToastProvider>
|
|
216
223
|
</template>
|