From 34a0eb3d22f64f24cacdb4c348fc1a9340c27237 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Sat, 22 Aug 2026 08:58:24 +0000 Subject: [PATCH 1/8] feat(inspector): persist chart color mode in URL and add age/duplicated modes Persist the chart coloring mode in the query string so it can be shared and bookmarked, and add two new modes: "Published Age" (grays fresh packages and shifts through yellow/orange/red as they get older) and "Duplicated" (gives each multi-version package name its own color, leaving the rest gray). --- .../src/app/pages/chart/[...chart].vue | 127 +++++++++++++----- .../src/app/state/query.ts | 2 + .../src/app/state/settings.ts | 1 - .../src/shared/types.ts | 1 - 4 files changed, 99 insertions(+), 32 deletions(-) diff --git a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue index 3de05691..a4cd9d5b 100644 --- a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue +++ b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue @@ -17,7 +17,8 @@ import DisplayPackageSpec from '../../components/display/PackageSpec.vue' import OptionSelectGroup from '../../components/option/SelectGroup.vue' import { isDark } from '../../composables/dark' import { selectedNode } from '../../state/current' -import { payloads } from '../../state/payload' +import { getPublishTime, payloads } from '../../state/payload' +import { query } from '../../state/query' import { settings } from '../../state/settings' import { isSidepanelCollapsed } from '../../state/ui' import { bytesToHumanSize } from '../../utils/format' @@ -30,6 +31,56 @@ const nodeHover = shallowRef(undefined) const nodeSelected = shallowRef(undefined) const location = window.location +type ColoringMode = 'spectrum' | 'module' | 'age' | 'duplicated' +const COLORING_MODES = ['spectrum', 'module', 'age', 'duplicated'] as const + +// The coloring mode is persisted in the query string (URL hash) so that it can +// be shared/bookmarked. Default (`spectrum`) is stored as an empty string to +// keep the URL clean. +const coloringMode = computed({ + get() { + return (COLORING_MODES.includes(query.chartColoring as ColoringMode) + ? query.chartColoring + : 'spectrum') as ColoringMode + }, + set(value) { + query.chartColoring = value === 'spectrum' ? '' : value + }, +}) + +const YEAR = 365 * 24 * 60 * 60 * 1000 + +// "Published age" coloring: fresh packages stay gray, then shift towards +// yellow / orange / red the older their published date is. +function getAgeColor(pkg: PackageNode): string { + const time = getPublishTime(pkg) + if (!time) + return isDark.value ? '#3f3f46' : '#d4d4d8' + const age = Date.now() - +time + if (age < YEAR) + return isDark.value ? '#71717a' : '#a1a1aa' + if (age < 2 * YEAR) + return '#facc15' + if (age < 3 * YEAR) + return '#fb923c' + return '#ef4444' +} + +// "Duplicated" coloring: every package name that resolves to more than one +// version gets its own distinct color; all others stay gray. +const duplicatedColors = computed(() => { + const map = new Map() + const names = Array.from(payloads.filtered.versions.entries()) + .filter(([, pkgs]) => pkgs.length > 1) + .map(([name]) => name) + .sort() + names.forEach((name, i) => { + const hue = Math.round((i / Math.max(names.length, 1)) * 360) + map.set(name, `hsl(${hue}, 70%, ${isDark.value ? 62 : 45}%)`) + }) + return map +}) + const tree = computed(() => { const packages = payloads.filtered.packages const rootDepth = Math.min(...packages.map(i => i.depth)) @@ -149,6 +200,44 @@ const tree = computed(() => { let dispose: () => void | undefined const options = computed>(() => { + const mode = coloringMode.value + const spectrum = createColorGetterSpectrum( + tree.value.root, + isDark.value ? 0.8 : 0.9, + isDark.value ? 1 : 1.1, + ) + const gray = isDark.value ? '#3f3f46' : '#d4d4d8' + + const getColor: typeof spectrum = (node) => { + if (mode === 'spectrum') + return spectrum(node) + if (!node.meta) + return undefined + switch (mode) { + case 'module': { + const type = getModuleType(node.meta.resolved.module) + switch (type) { + case 'esm': + return '#4ade80' + case 'cjs': + return '#facc15' + case 'dual': + return '#2dd4bf' + case 'faux': + return '#a3e635' + case 'dts': + return '#888888' + } + return undefined + } + case 'age': + return getAgeColor(node.meta) + case 'duplicated': + return duplicatedColors.value.get(node.meta.name) ?? gray + } + return undefined + } + return { onClick(node) { if (node) @@ -173,34 +262,12 @@ const options = computed>(() => { fg: isDark.value ? '#fff' : '#000', bg: isDark.value ? '#111' : '#fff', }, - getColor: settings.value.chartColoringMode === 'module' - ? (node) => { - if (!node.meta) - return undefined - const type = getModuleType(node.meta?.resolved.module) - switch (type) { - case 'esm': - return '#4ade80' - case 'cjs': - return '#facc15' - case 'dual': - return '#2dd4bf' - case 'faux': - return '#a3e635' - case 'dts': - return '#888888' - } - } - : createColorGetterSpectrum( - tree.value.root, - isDark.value ? 0.8 : 0.9, - isDark.value ? 1 : 1.1, - ), + getColor, getSubtext: (node) => { if (!node.meta) return node.subtext - if (settings.value.chartColoringMode === 'module') { - const type = getModuleType(node.meta?.resolved.module) + if (coloringMode.value === 'module') { + const type = getModuleType(node.meta.resolved.module) return type.toUpperCase() } return node.subtext @@ -255,7 +322,7 @@ watch( ) watch( - () => settings.value.chartColoringMode, + () => coloringMode.value, () => { graph.value?.draw() }, @@ -317,10 +384,10 @@ onUnmounted(() => {
diff --git a/packages/node-modules-inspector/src/app/state/query.ts b/packages/node-modules-inspector/src/app/state/query.ts index af5c3df9..440288cc 100644 --- a/packages/node-modules-inspector/src/app/state/query.ts +++ b/packages/node-modules-inspector/src/app/state/query.ts @@ -9,6 +9,7 @@ export interface QueryOptions extends Partial<{ [x in keyof FilterOptions]?: str selected?: string install?: string mode?: string + chartColoring?: string selectedAction?: string selectedAuthors?: string actionAll?: string @@ -21,6 +22,7 @@ export const query = reactive({ selected: '', install: '', mode: '', + chartColoring: '', selectedAction: '', selectedAuthors: '', actionAll: '', diff --git a/packages/node-modules-inspector/src/app/state/settings.ts b/packages/node-modules-inspector/src/app/state/settings.ts index 59b02e3d..a65daa87 100644 --- a/packages/node-modules-inspector/src/app/state/settings.ts +++ b/packages/node-modules-inspector/src/app/state/settings.ts @@ -18,7 +18,6 @@ export const SETTINGS_DEFAULT: SettingsOptions = { showMaintainerActions: false, showThirdPartyServices: false, treatFauxAsESM: false, - chartColoringMode: 'spectrum', collapseSidepanel: false, chartAnimation: true, preferNpmx: true, diff --git a/packages/node-modules-inspector/src/shared/types.ts b/packages/node-modules-inspector/src/shared/types.ts index f83a5f4f..2d492190 100644 --- a/packages/node-modules-inspector/src/shared/types.ts +++ b/packages/node-modules-inspector/src/shared/types.ts @@ -87,7 +87,6 @@ export interface SettingsOptions { showPublintMessages: boolean showMaintainerActions: boolean showThirdPartyServices: boolean - chartColoringMode: 'spectrum' | 'module' collapseSidepanel: boolean chartAnimation: boolean preferNpmx: boolean From d487409b219da1a6993dad64a13f3ddc35b3fa1d Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Sat, 22 Aug 2026 09:04:00 +0000 Subject: [PATCH 2/8] feat(inspector): use subtle theme-aware shade for unhighlighted chart nodes --- .../src/app/pages/chart/[...chart].vue | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue index a4cd9d5b..fb81effc 100644 --- a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue +++ b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue @@ -50,15 +50,19 @@ const coloringMode = computed({ const YEAR = 365 * 24 * 60 * 60 * 1000 -// "Published age" coloring: fresh packages stay gray, then shift towards +// A subtle neutral shade for nodes that aren't highlighted by the current +// color mode, tuned per light/dark theme. +const baseShade = computed(() => isDark.value ? '#333' : '#eee') + +// "Published age" coloring: fresh packages stay neutral, then shift towards // yellow / orange / red the older their published date is. function getAgeColor(pkg: PackageNode): string { const time = getPublishTime(pkg) if (!time) - return isDark.value ? '#3f3f46' : '#d4d4d8' + return baseShade.value const age = Date.now() - +time if (age < YEAR) - return isDark.value ? '#71717a' : '#a1a1aa' + return baseShade.value if (age < 2 * YEAR) return '#facc15' if (age < 3 * YEAR) @@ -206,7 +210,7 @@ const options = computed>(() => { isDark.value ? 0.8 : 0.9, isDark.value ? 1 : 1.1, ) - const gray = isDark.value ? '#3f3f46' : '#d4d4d8' + const gray = baseShade.value const getColor: typeof spectrum = (node) => { if (mode === 'spectrum') From 7f143b100b82b1bef824ac17eee93fbfbb00324e Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Sat, 22 Aug 2026 09:10:45 +0000 Subject: [PATCH 3/8] feat(inspector): use mid gray for unhighlighted chart nodes --- .../src/app/pages/chart/[...chart].vue | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue index fb81effc..8a78d430 100644 --- a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue +++ b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue @@ -50,19 +50,21 @@ const coloringMode = computed({ const YEAR = 365 * 24 * 60 * 60 * 1000 -// A subtle neutral shade for nodes that aren't highlighted by the current -// color mode, tuned per light/dark theme. -const baseShade = computed(() => isDark.value ? '#333' : '#eee') +// A neutral shade for nodes that aren't highlighted by the current color mode. +// nanovis only supports a single global foreground/text color (`palette.fg`), +// so we can't lighten the text per-block on a dark base — a mid gray keeps the +// node readable against both the light and dark text color. +const baseShade = '#888' // "Published age" coloring: fresh packages stay neutral, then shift towards // yellow / orange / red the older their published date is. function getAgeColor(pkg: PackageNode): string { const time = getPublishTime(pkg) if (!time) - return baseShade.value + return baseShade const age = Date.now() - +time if (age < YEAR) - return baseShade.value + return baseShade if (age < 2 * YEAR) return '#facc15' if (age < 3 * YEAR) @@ -210,8 +212,6 @@ const options = computed>(() => { isDark.value ? 0.8 : 0.9, isDark.value ? 1 : 1.1, ) - const gray = baseShade.value - const getColor: typeof spectrum = (node) => { if (mode === 'spectrum') return spectrum(node) @@ -237,7 +237,7 @@ const options = computed>(() => { case 'age': return getAgeColor(node.meta) case 'duplicated': - return duplicatedColors.value.get(node.meta.name) ?? gray + return duplicatedColors.value.get(node.meta.name) ?? baseShade } return undefined } From f309fe28e4355bf89ebead7abd698ab8ac6beb9e Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Sat, 22 Aug 2026 09:34:19 +0000 Subject: [PATCH 4/8] feat(inspector): add color-mode legend and highlight version siblings on hover --- .../src/app/pages/chart/[...chart].vue | 72 +++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue index 8a78d430..c719f9fb 100644 --- a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue +++ b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue @@ -72,14 +72,19 @@ function getAgeColor(pkg: PackageNode): string { return '#ef4444' } +// Package names that resolve to more than one version. +const duplicatedNames = computed(() => + Array.from(payloads.filtered.versions.entries()) + .filter(([, pkgs]) => pkgs.length > 1) + .map(([name]) => name) + .sort(), +) + // "Duplicated" coloring: every package name that resolves to more than one // version gets its own distinct color; all others stay gray. const duplicatedColors = computed(() => { const map = new Map() - const names = Array.from(payloads.filtered.versions.entries()) - .filter(([, pkgs]) => pkgs.length > 1) - .map(([name]) => name) - .sort() + const names = duplicatedNames.value names.forEach((name, i) => { const hue = Math.round((i / Math.max(names.length, 1)) * 360) map.set(name, `hsl(${hue}, 70%, ${isDark.value ? 62 : 45}%)`) @@ -87,6 +92,42 @@ const duplicatedColors = computed(() => { return map }) +// Hovering a package that has multiple versions highlights every block that +// shares its name (i.e. all of its other versions), across all color modes. +const HIGHLIGHT_COLOR = '#ec4899' +const highlightName = computed(() => { + const name = nodeHover.value?.meta?.name + return name && duplicatedColors.value.has(name) ? name : undefined +}) + +// Legend entries for the current color mode (spectrum has none). +const legend = computed<{ background: string, label: string }[] | undefined>(() => { + switch (coloringMode.value) { + case 'module': + return [ + { background: '#4ade80', label: 'ESM' }, + { background: '#2dd4bf', label: 'Dual' }, + { background: '#facc15', label: 'CJS' }, + { background: '#a3e635', label: 'Faux' }, + { background: '#888888', label: 'DTS' }, + ] + case 'age': + return [ + { background: baseShade, label: '< 1 year' }, + { background: '#facc15', label: '> 1 year' }, + { background: '#fb923c', label: '> 2 years' }, + { background: '#ef4444', label: '> 3 years' }, + ] + case 'duplicated': + return [ + { background: 'linear-gradient(90deg, hsl(0,70%,55%), hsl(120,70%,55%), hsl(240,70%,55%))', label: 'Multiple versions' }, + { background: baseShade, label: 'Single version' }, + ] + default: + return undefined + } +}) + const tree = computed(() => { const packages = payloads.filtered.packages const rootDepth = Math.min(...packages.map(i => i.depth)) @@ -213,6 +254,10 @@ const options = computed>(() => { isDark.value ? 1 : 1.1, ) const getColor: typeof spectrum = (node) => { + // Read at draw time (not tracked by this computed) so hovering only + // triggers a redraw, never a full graph rebuild. + if (node.meta && node.meta.name === highlightName.value) + return HIGHLIGHT_COLOR if (mode === 'spectrum') return spectrum(node) if (!node.meta) @@ -332,6 +377,19 @@ watch( }, ) +// Re-color the chart when the hover highlight changes. The Treemap caches its +// base layer as a bitmap, so that cache has to be invalidated to re-run +// `getColor`; the other charts re-color on every `draw()`. +watch( + () => highlightName.value, + () => { + const graphAny = graph.value as unknown as { baseLayoutCache?: unknown } | undefined + if (graphAny && 'baseLayoutCache' in graphAny) + graphAny.baseLayoutCache = undefined + graph.value?.draw() + }, +) + watch( () => isSidepanelCollapsed.value, () => { @@ -394,6 +452,12 @@ onUnmounted(() => { :titles="['Spectrum', 'Module', 'Published Age', 'Duplicated']" />
+
+
+ + {{ item.label }} +
+
Date: Sat, 22 Aug 2026 22:52:32 +0000 Subject: [PATCH 5/8] feat(inspector): outline version siblings with a ring on treemap hover --- .../src/app/pages/chart/[...chart].vue | 63 ++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue index c719f9fb..09e1491b 100644 --- a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue +++ b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue @@ -92,8 +92,9 @@ const duplicatedColors = computed(() => { return map }) -// Hovering a package that has multiple versions highlights every block that -// shares its name (i.e. all of its other versions), across all color modes. +// Hovering a package that has multiple versions outlines every block that +// shares its name (i.e. all of its other versions) with a ring. Only the +// Treemap draws it — the other charts don't expose node geometry. const HIGHLIGHT_COLOR = '#ec4899' const highlightName = computed(() => { const name = nodeHover.value?.meta?.name @@ -254,10 +255,6 @@ const options = computed>(() => { isDark.value ? 1 : 1.1, ) const getColor: typeof spectrum = (node) => { - // Read at draw time (not tracked by this computed) so hovering only - // triggers a redraw, never a full graph rebuild. - if (node.meta && node.meta.name === highlightName.value) - return HIGHLIGHT_COLOR if (mode === 'spectrum') return spectrum(node) if (!node.meta) @@ -333,6 +330,45 @@ function selectNode(node: ChartNode | null, animate?: boolean) { graph.value?.select(node, animate) } +// nanovis has no per-node border, so we wrap the Treemap's `draw()` and, after +// it renders, stroke a ring around every block whose package shares the hovered +// name (its other versions). We reuse the Treemap's own layout boxes via the +// (private) `iterateNodeToDraw` generator, so the rings line up exactly. +interface TreemapLayout { + node: ChartNode + box: [number, number, number, number] + children: TreemapLayout[] +} +interface TreemapInternals { + draw: () => void + c: CanvasRenderingContext2D + layers: { base?: TreemapLayout | null, current?: TreemapLayout | null } + iterateNodeToDraw: (layout: TreemapLayout, culling: number, cullingLayouts: unknown[]) => Iterable +} + +function installTreemapHighlight(treemap: Treemap): void { + const tm = treemap as unknown as TreemapInternals + const original = tm.draw.bind(tm) + tm.draw = () => { + original() + const name = highlightName.value + const layout = tm.layers.current || tm.layers.base + if (!name || !layout) + return + const ctx = tm.c + ctx.save() + ctx.lineWidth = 2 + ctx.strokeStyle = HIGHLIGHT_COLOR + for (const item of tm.iterateNodeToDraw(layout, 0, [])) { + if (item.node.meta?.name !== name) + continue + const [x, y, w, h] = item.box + ctx.strokeRect(x + 1, y + 1, Math.max(w - 2, 1), Math.max(h - 2, 1)) + } + ctx.restore() + } +} + watch( () => [chart.value, tree.value, options.value], () => { @@ -346,11 +382,14 @@ watch( case 'flamegraph': graph.value = new Flamegraph(tree.value.root, options.value) break - default: - graph.value = new Treemap(tree.value.root, { + default: { + const treemap = new Treemap(tree.value.root, { ...options.value, selectedPaddingRatio: 0, }) + installTreemapHighlight(treemap) + graph.value = treemap + } } nextTick(() => { @@ -377,15 +416,11 @@ watch( }, ) -// Re-color the chart when the hover highlight changes. The Treemap caches its -// base layer as a bitmap, so that cache has to be invalidated to re-run -// `getColor`; the other charts re-color on every `draw()`. +// Redraw so the Treemap hover ring (see installTreemapHighlight) follows the +// currently highlighted package. A no-op for the other charts. watch( () => highlightName.value, () => { - const graphAny = graph.value as unknown as { baseLayoutCache?: unknown } | undefined - if (graphAny && 'baseLayoutCache' in graphAny) - graphAny.baseLayoutCache = undefined graph.value?.draw() }, ) From e09a14ed0fd17b92bc4f8fb24993028350664704 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 23 Aug 2026 08:00:50 +0900 Subject: [PATCH 6/8] chore: update --- .../src/app/pages/chart/[...chart].vue | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue index 09e1491b..6bbaf2b6 100644 --- a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue +++ b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue @@ -48,23 +48,19 @@ const coloringMode = computed({ }, }) -const YEAR = 365 * 24 * 60 * 60 * 1000 +const baseShade = computed(() => isDark.value ? '#999' : '#eee') -// A neutral shade for nodes that aren't highlighted by the current color mode. -// nanovis only supports a single global foreground/text color (`palette.fg`), -// so we can't lighten the text per-block on a dark base — a mid gray keeps the -// node readable against both the light and dark text color. -const baseShade = '#888' +const YEAR = 365 * 24 * 60 * 60 * 1000 // "Published age" coloring: fresh packages stay neutral, then shift towards // yellow / orange / red the older their published date is. function getAgeColor(pkg: PackageNode): string { const time = getPublishTime(pkg) if (!time) - return baseShade + return baseShade.value const age = Date.now() - +time if (age < YEAR) - return baseShade + return baseShade.value if (age < 2 * YEAR) return '#facc15' if (age < 3 * YEAR) @@ -110,11 +106,11 @@ const legend = computed<{ background: string, label: string }[] | undefined>(() { background: '#2dd4bf', label: 'Dual' }, { background: '#facc15', label: 'CJS' }, { background: '#a3e635', label: 'Faux' }, - { background: '#888888', label: 'DTS' }, + { background: baseShade.value, label: 'DTS' }, ] case 'age': return [ - { background: baseShade, label: '< 1 year' }, + { background: baseShade.value, label: '< 1 year' }, { background: '#facc15', label: '> 1 year' }, { background: '#fb923c', label: '> 2 years' }, { background: '#ef4444', label: '> 3 years' }, @@ -122,7 +118,7 @@ const legend = computed<{ background: string, label: string }[] | undefined>(() case 'duplicated': return [ { background: 'linear-gradient(90deg, hsl(0,70%,55%), hsl(120,70%,55%), hsl(240,70%,55%))', label: 'Multiple versions' }, - { background: baseShade, label: 'Single version' }, + { background: baseShade.value, label: 'Single version' }, ] default: return undefined @@ -272,16 +268,15 @@ const options = computed>(() => { case 'faux': return '#a3e635' case 'dts': - return '#888888' + return baseShade.value } return undefined } case 'age': return getAgeColor(node.meta) case 'duplicated': - return duplicatedColors.value.get(node.meta.name) ?? baseShade + return duplicatedColors.value.get(node.meta.name) ?? baseShade.value } - return undefined } return { @@ -304,7 +299,7 @@ const options = computed>(() => { }, animate: settings.value.chartAnimation, palette: { - stroke: isDark.value ? '#222' : '#555', + stroke: isDark.value ? '#444' : '#555', fg: isDark.value ? '#fff' : '#000', bg: isDark.value ? '#111' : '#fff', }, @@ -480,19 +475,27 @@ onUnmounted(() => {
- -
-
-
- - {{ item.label }} +
+
+
+ Colorization +
+ +
+
+
+ + {{ item.label }} +
+
+
Date: Sat, 22 Aug 2026 23:04:10 +0000 Subject: [PATCH 7/8] feat(inspector): show released time and duplicate versions in chart hover tooltip --- .../src/app/pages/chart/[...chart].vue | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue index 6bbaf2b6..c0866f63 100644 --- a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue +++ b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue @@ -11,6 +11,7 @@ import { NuxtLink } from '#components' import ChartFlamegraph from '../../components/chart/Flamegraph.vue' import ChartSunburst from '../../components/chart/Sunburst.vue' import ChartTreemap from '../../components/chart/Treemap.vue' +import DisplayDateBadge from '../../components/display/DateBadge.vue' import DisplayFileSizeBadge from '../../components/display/FileSizeBadge.vue' import DisplayModuleType from '../../components/display/ModuleType' import DisplayPackageSpec from '../../components/display/PackageSpec.vue' @@ -23,6 +24,7 @@ import { settings } from '../../state/settings' import { isSidepanelCollapsed } from '../../state/ui' import { bytesToHumanSize } from '../../utils/format' import { getModuleType } from '../../utils/module-type' +import { compareSemver } from '../../utils/semver' const mouse = reactive(useMouse()) const params = useRoute().params as Record @@ -97,6 +99,21 @@ const highlightName = computed(() => { return name && duplicatedColors.value.has(name) ? name : undefined }) +// The publish time of the currently hovered package, if known. +const hoverPublishTime = computed(() => + nodeHover.value?.meta ? getPublishTime(nodeHover.value.meta) : null, +) + +// All resolved versions of the hovered package (only meaningful when > 1), +// sorted by semver for the tooltip's duplicate list. +const hoverVersions = computed(() => { + const meta = nodeHover.value?.meta + if (!meta) + return [] + return [...(payloads.filtered.versions.get(meta.name) ?? [])] + .sort((a, b) => compareSemver(a.version, b.version)) +}) + // Legend entries for the current color mode (spectrum has none). const legend = computed<{ background: string, label: string }[] | undefined>(() => { switch (coloringMode.value) { @@ -536,5 +553,19 @@ onUnmounted(() => {
+
+ Released + +
+
+ {{ hoverVersions.length }} versions +
+ v{{ v.version }} +
+
From 95f73cb1614127a38f9423916a9272bbf215bfcb Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 23 Aug 2026 08:09:19 +0900 Subject: [PATCH 8/8] chore: update --- .../src/app/pages/chart/[...chart].vue | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue index c0866f63..2ffd8e94 100644 --- a/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue +++ b/packages/node-modules-inspector/src/app/pages/chart/[...chart].vue @@ -552,18 +552,15 @@ onUnmounted(() => { / -
-
- Released - +
{{ hoverVersions.length }} versions
v{{ v.version }}