From bca4edff4dbdc565741ef797c901bb77c2ae2d93 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:03:54 +0900 Subject: [PATCH] feat(ui): add fit and zoom controls to trace viewer Co-Authored-By: Claude Opus 5.5 --- .../ui/client/components/trace/TraceView.vue | 100 ++++++++++++++++-- packages/ui/client/composables/trace-view.ts | 6 +- test/ui/test/trace.spec.ts | 48 +++++++++ 3 files changed, 144 insertions(+), 10 deletions(-) diff --git a/packages/ui/client/components/trace/TraceView.vue b/packages/ui/client/components/trace/TraceView.vue index 059ce7b27..3c40c8d67 100644 --- a/packages/ui/client/components/trace/TraceView.vue +++ b/packages/ui/client/components/trace/TraceView.vue @@ -5,15 +5,18 @@ import type { NormalizedBrowserTraceEntry, TraceSelection, } from '~/composables/trace-view' +import { useElementSize } from '@vueuse/core' import { createCache, createMirror, rebuild } from 'rrweb-snapshot' import { Pane, Splitpanes } from 'splitpanes' import { computed, ref, watch } from 'vue' +import IconButton from '~/components/IconButton.vue' import { openLocation } from '~/composables/location' import { traceViewSplitSizes } from '~/composables/navigation' import { getTraceEntryClass, selectActiveTraceStep, showTraceSelectorHighlight, + traceZoom, } from '~/composables/trace-view' const props = defineProps<{ @@ -31,6 +34,47 @@ const iframeSandbox = computed(() => { return props.trace.recordCanvas ? 'allow-same-origin allow-scripts' : 'allow-same-origin' }) +const ZOOM_LEVELS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2] + +const replayContainer = ref() +const replayContainerSize = useElementSize(replayContainer) +const viewport = computed(() => selectedStep.value?.snapshot.viewport) +const fitScale = computed(() => { + const { width, height } = replayContainerSize + if (!viewport.value || !width.value || !height.value) { + return 1 + } + return Math.min(width.value / viewport.value.width, height.value / viewport.value.height, 1) +}) +const scale = computed(() => traceZoom.value ?? fitScale.value) +const nextZoomIn = computed(() => ZOOM_LEVELS.find((level) => level > scale.value + 1e-3)) +const nextZoomOut = computed(() => ZOOM_LEVELS.findLast((level) => level < scale.value - 1e-3)) +const zoomLabel = computed(() => + viewport.value + ? `${viewport.value.width}x${viewport.value.height} · ${Math.round(scale.value * 100)}%` + : '', +) +const scaledViewportStyle = computed(() => + viewport.value + ? { + width: `${Math.floor(viewport.value.width * scale.value)}px`, + height: `${Math.floor(viewport.value.height * scale.value)}px`, + } + : undefined, +) + +function zoomIn() { + traceZoom.value = nextZoomIn.value ?? traceZoom.value +} + +function zoomOut() { + traceZoom.value = nextZoomOut.value ?? traceZoom.value +} + +function fitToPane() { + traceZoom.value = null +} + function onSelectStep(index: number) { selectActiveTraceStep(index) const step = entries.value[index] @@ -252,15 +296,53 @@ function onSplitpanesResized({ panes }: SplitpanesResizedPayload) { -
-