From 99f2c28f28340467de93b5539344f8b064edd8ff Mon Sep 17 00:00:00 2001 From: arlo Date: Wed, 14 Oct 2026 18:41:04 +0800 Subject: [PATCH 1/2] refactor: timeline layout --- packages/client/src/pages/timeline.vue | 225 +++++++++++++++---------- 1 file changed, 137 insertions(+), 88 deletions(-) diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index 1acdb463d..469944e45 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -9,6 +9,11 @@ import { useDevtoolsClient } from '../composables/devtools-client' interface TimelineEventRow extends TimelineEntry { color: string + delta?: number + grouped: boolean + repeatsTitle: boolean + groupStart: boolean + groupEnd: boolean } interface InfoRow { @@ -27,8 +32,8 @@ interface RecycleScrollerHandle { scrollToItem: (index: number) => void } -const EVENT_COLORS = ['#3e5770', '#42b983', '#0098c4'] -const EVENT_ROW_HEIGHT = 52 +const LIMITED_COLOR = '#a17646' +const EVENT_ROW_HEIGHT = 28 const BUILTIN_LAYERS: TimelineLayerEntry[] = [ { id: 'mouse', label: 'Mouse' }, { id: 'keyboard', label: 'Keyboard' }, @@ -39,7 +44,6 @@ const BUILTIN_LAYERS: TimelineLayerEntry[] = [ const { apps, clearTimelineEvents, - formatTime, formatValue, isTimelineLayerEnabled, inspectTimelineEvent, @@ -57,6 +61,7 @@ const container = ref() const horizontal = ref(false) const activeTimelineLayer = selectedTimelineLayerId const selectedEventId = ref() +const collapsedSections = ref(new Set()) const eventListScroller = ref() let observer: ResizeObserver | undefined @@ -95,16 +100,23 @@ const selectedLayerEvents = computed(() => ) const eventRows = computed(() => { - let colorIndex = -1 - let currentGroupId = 0 - - return selectedLayerEvents.value.map((event) => { - if (event.type === 'timeline:eventsLimited') return { ...event, color: '#a17646' } - if (event.groupId !== currentGroupId || colorIndex === -1) - colorIndex = (colorIndex + 1) % EVENT_COLORS.length - - currentGroupId = event.groupId ?? currentGroupId - return { ...event, color: EVENT_COLORS[colorIndex] } + const events = selectedLayerEvents.value + const color = selectedLayer.value ? getLayerColor(selectedLayer.value) : '#42b983' + + return events.map((event, index) => { + const prev = events[index - 1] + const next = events[index + 1] + const inGroup = (other?: TimelineEntry) => + event.groupId != null && other?.groupId === event.groupId + return { + ...event, + color: event.type === 'timeline:eventsLimited' ? LIMITED_COLOR : color, + delta: prev ? event.time - prev.time : undefined, + grouped: inGroup(prev), + groupStart: !inGroup(prev) && inGroup(next), + repeatsTitle: inGroup(prev) && (prev!.title ?? prev!.label) === (event.title ?? event.label), + groupEnd: inGroup(prev) && !inGroup(next), + } }) }) const selectedEvent = computed( @@ -122,11 +134,16 @@ const infoSections = computed(() => { if (!event) return [] const sections: InfoSection[] = [] - const eventRows = createEventInfoRows(event) - if (eventRows.length) sections.push({ id: 'event', label: 'Event Info', rows: eventRows }) + sections.push({ id: 'event', label: 'Event', rows: createEventInfoRows(event) }) + + if (event.data) + sections.push({ id: 'data', label: 'Data', rows: createEncodedValueRows(event.data) }) + + if (event.meta) + sections.push({ id: 'meta', label: 'Meta', rows: createEncodedValueRows(event.meta) }) const groupRows = createGroupInfoRows(event) - if (groupRows.length) sections.push({ id: 'group', label: 'Group Info', rows: groupRows }) + if (groupRows.length) sections.push({ id: 'group', label: 'Group', rows: groupRows }) return sections }) @@ -151,6 +168,12 @@ watch( { immediate: true }, ) +function toggleSection(id: string) { + const next = new Set(collapsedSections.value) + if (!next.delete(id)) next.add(id) + collapsedSections.value = next +} + function selectLayer(layerId: string) { selectTimelineLayer(layerId) } @@ -174,20 +197,18 @@ function clearEvents() { selectedEventId.value = undefined } -function createEventInfoRows(event: TimelineEntry): InfoRow[] { +function createEventInfoRows(event: TimelineEventRow): InfoRow[] { const rows: InfoRow[] = [ { key: 'title', value: event.title ?? event.label }, - { key: 'time', value: formatTime(event.time) }, + { key: 'timestamp', value: formatClock(event.time) }, ] + if (event.delta != null) rows.push({ key: 'duration', value: formatDelta(event.delta) }) + if (event.subtitle) rows.push({ key: 'subtitle', value: event.subtitle }) if (event.layerId) rows.push({ key: 'layer', value: event.layerId }) if (event.logType) rows.push({ key: 'logType', value: event.logType }) - if (event.data) rows.push(...createEncodedValueRows(event.data)) - if (event.meta) - rows.push({ key: 'meta', value: formatValue(event.meta), children: previewRows(event.meta) }) - return rows } @@ -228,6 +249,19 @@ function getTimelineValuePreview(value: EncodedValue): ValueEntry[] { return 'preview' in value && Array.isArray(value.preview) ? value.preview.slice(0, 12) : [] } +function formatClock(time: number): string { + const date = new Date(time) + const pad = (value: number, length = 2) => String(value).padStart(length, '0') + return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}` +} + +function formatDelta(delta: number | undefined): string { + if (delta == null) return '' + if (delta < 1000) return `+${Math.max(0, Math.round(delta))}ms` + if (delta < 60_000) return `+${(delta / 1000).toFixed(1)}s` + return `+${Math.round(delta / 60_000)}m` +} + function getLayerColor(layer: TimelineLayerEntry): string { return layer.color == null ? '#42b983' : `#${layer.color.toString(16).padStart(6, '0')}` } @@ -266,11 +300,11 @@ onUnmounted(() => {