From 3c39d86de72b352e03736969cf84cabdffa22942 Mon Sep 17 00:00:00 2001 From: Alec Lloyd Probert <55991794+graphieros@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:28:51 +0000 Subject: [PATCH 01/27] feat: add fullscreen mode for charts and new annotator actions (#3371) --- app/assets/main.css | 6 ++ app/components/Chart/AnnotatorIcon.vue | 21 +++++ app/components/Chart/FullscreenIcon.vue | 13 +++ app/components/Compare/FacetBarChart.vue | 6 +- app/components/Compare/FacetScatterChart.vue | 76 ++-------------- .../Package/DailyDownloadsHeatmap.vue | 25 ++---- app/components/Package/TimelineChart.vue | 35 +++----- app/components/Package/TrendsChart.vue | 78 +++------------- .../Package/VersionDistribution.vue | 89 +++---------------- app/utils/charts.ts | 16 +++- i18n/locales/en.json | 1 + i18n/locales/fr-FR.json | 1 + i18n/schema.json | 3 + package.json | 2 +- pnpm-lock.yaml | 10 +-- pnpm-workspace.yaml | 2 +- shared/utils/trends-chart.ts | 3 +- test/nuxt/a11y.spec.ts | 44 +++++++++ uno.config.ts | 2 + 19 files changed, 171 insertions(+), 262 deletions(-) create mode 100644 app/components/Chart/AnnotatorIcon.vue create mode 100644 app/components/Chart/FullscreenIcon.vue diff --git a/app/assets/main.css b/app/assets/main.css index bb6b830399..bce110f018 100644 --- a/app/assets/main.css +++ b/app/assets/main.css @@ -395,3 +395,9 @@ html:has(dialog:modal) { overflow: hidden; scrollbar-gutter: stable; } + +/* Match main annotator toolbar backdrop when group buttons are disabled + Override the background color set by vue-data-ui */ +.vue-ui-pen-and-paper-group-actions { + background-color: transparent !important; +} diff --git a/app/components/Chart/AnnotatorIcon.vue b/app/components/Chart/AnnotatorIcon.vue new file mode 100644 index 0000000000..84ce04d38a --- /dev/null +++ b/app/components/Chart/AnnotatorIcon.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/components/Chart/FullscreenIcon.vue b/app/components/Chart/FullscreenIcon.vue new file mode 100644 index 0000000000..49a1ce9287 --- /dev/null +++ b/app/components/Chart/FullscreenIcon.vue @@ -0,0 +1,13 @@ + + + diff --git a/app/components/Compare/FacetBarChart.vue b/app/components/Compare/FacetBarChart.vue index 276770dc46..6079e59706 100644 --- a/app/components/Compare/FacetBarChart.vue +++ b/app/components/Compare/FacetBarChart.vue @@ -117,7 +117,7 @@ const config = computed(() => { buttons: { tooltip: false, pdf: false, - fullscreen: false, + fullscreen: true, sort: false, annotator: false, table: false, @@ -125,6 +125,7 @@ const config = computed(() => { altCopy: true, }, buttonTitles: { + fullscreen: $t('package.trends.toggle_fullscreen'), img: $t('package.trends.download_file', { fileType: 'PNG' }), svg: $t('package.trends.download_file', { fileType: 'SVG' }), altCopy: $t('package.trends.copy_alt.button_label'), @@ -320,6 +321,9 @@ const config = computed(() => { + diff --git a/app/components/Compare/FacetScatterChart.vue b/app/components/Compare/FacetScatterChart.vue index 9c67a5b760..99c4863f3c 100644 --- a/app/components/Compare/FacetScatterChart.vue +++ b/app/components/Compare/FacetScatterChart.vue @@ -8,7 +8,7 @@ import { type VueUiScatterSeries, } from 'vue-data-ui/vue-ui-scatter' import { buildCompareScatterChartDataset } from '~/utils/compare-scatter-chart' -import { copyAltTextForCompareScatterChart } from '~/utils/charts' +import { CHART_ANNOTATOR_SLOTS, copyAltTextForCompareScatterChart } from '~/utils/charts' import { useColors } from '~/composables/useColors' import { downloadFileLink } from '~/utils/download' import { useCopyChartPng } from '~/composables/useCopyChartPng' @@ -125,12 +125,13 @@ const config = computed(() => { buttons: { tooltip: false, pdf: false, - fullscreen: false, + fullscreen: true, table: false, csv: false, altCopy: true, }, buttonTitles: { + fullscreen: $t('package.trends.toggle_fullscreen'), csv: $t('package.trends.download_file', { fileType: 'CSV' }), img: $t('package.trends.download_file', { fileType: 'PNG' }), svg: $t('package.trends.download_file', { fileType: 'SVG' }), @@ -465,78 +466,17 @@ onMounted(async () => { @click="copyChartPng" /> + - - - - - - - diff --git a/app/components/Package/TimelineChart.vue b/app/components/Package/TimelineChart.vue index 645b7687f8..3e67617865 100644 --- a/app/components/Package/TimelineChart.vue +++ b/app/components/Package/TimelineChart.vue @@ -23,8 +23,6 @@ import { type TimelineSizeCacheValue, CHART_ANNOTATOR_SLOTS, E18E_GRADIENT_COLORS, - getAnnotatorIcon, - getAnnotatorStyle, type TimelineChartMetric, type StackbarTooltipPoint, type TimelinePlotItem, @@ -430,6 +428,7 @@ const commonConfig = computed(() => ({ }, }, buttonTitles: { + fullscreen: $t('package.trends.toggle_fullscreen'), csv: $t('package.trends.download_file', { fileType: 'CSV' }), img: $t('package.trends.download_file', { fileType: 'PNG' }), svg: $t('package.trends.download_file', { fileType: 'SVG' }), @@ -535,7 +534,7 @@ const config = computed(() => { buttons: { pdf: false, labels: false, - fullscreen: false, + fullscreen: true, table: false, tooltip: false, altCopy: true, @@ -729,7 +728,7 @@ const stackbarConfig = computed(() => ({ buttons: { pdf: false, labels: false, - fullscreen: false, + fullscreen: true, table: false, tooltip: false, annotator: !isMobile.value, @@ -1018,6 +1017,9 @@ const timelineMetricTabs = computed(() => [ + @@ -1038,16 +1040,7 @@ const timelineMetricTabs = computed(() => [ diff --git a/app/components/Package/TrendsChart.vue b/app/components/Package/TrendsChart.vue index 3cffb9e4b0..09acdd2877 100644 --- a/app/components/Package/TrendsChart.vue +++ b/app/components/Package/TrendsChart.vue @@ -16,7 +16,12 @@ import type { import { DATE_INPUT_MAX } from '~/utils/input' import { endDateOnlyToUtcMs } from '~/utils/chart-data-prediction' import { applyBlocklistCorrection, getAnomaliesForPackages } from '~/utils/download-anomalies' -import { copyAltTextForTrendLineChart, sanitise, applyEllipsis } from '~/utils/charts' +import { + CHART_ANNOTATOR_SLOTS, + copyAltTextForTrendLineChart, + sanitise, + applyEllipsis, +} from '~/utils/charts' import { buildNormalisedTrendsDataset, buildTrendsChartConfig, @@ -2146,6 +2151,9 @@ const copyEmbedUrl = () => copyEmbed(embedUrl.value) @click="copyChartPng" /> + @@ -2165,72 +2173,8 @@ const copyEmbedUrl = () => copyEmbed(embedUrl.value) /> - - - - - - - - - - - - - - - - - - - - - diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts index 29bf40184e..a24132dd0b 100644 --- a/test/nuxt/a11y.spec.ts +++ b/test/nuxt/a11y.spec.ts @@ -4121,6 +4121,7 @@ describe('component accessibility audits', () => { it('should have no accessibility violations', async () => { const component = await mountSuspended(PackageSecurityDowngradeAlert, { props: { + packageName: 'vue', downgrade: { downgradedVersion: '2.0.0', downgradedTrustLevel: 'none', diff --git a/test/nuxt/components/Package/SecurityDowngradeAlert.spec.ts b/test/nuxt/components/Package/SecurityDowngradeAlert.spec.ts new file mode 100644 index 0000000000..32f76b5d35 --- /dev/null +++ b/test/nuxt/components/Package/SecurityDowngradeAlert.spec.ts @@ -0,0 +1,46 @@ +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { describe, expect, it } from 'vitest' +import PackageSecurityDowngradeAlert from '~/components/Package/SecurityDowngradeAlert.vue' + +describe('PackageSecurityDowngradeAlert', () => { + it.each(['vue', '@nuxt/kit'])( + 'links to the trusted-to-downgraded diff for %s', + async packageName => { + const component = await mountSuspended(PackageSecurityDowngradeAlert, { + props: { + packageName, + downgrade: { + downgradedVersion: '2.0.0', + downgradedTrustLevel: 'none', + downgradedTrustedPublisher: true, + trustedVersion: '1.9.0', + trustedTrustLevel: 'provenance', + }, + fallbackInstallText: 'Consider installing 1.9.0 instead.', + }, + }) + const link = component.get('a[href*="/diff/"]') + expect(link.text()).toBe('File Changes') + expect(link.attributes('href')).toBe(`/diff/${packageName}/v/1.9.0...2.0.0`) + }, + ) + + it('hides the diff link without a trusted version and hides the alert without a downgrade', async () => { + const component = await mountSuspended(PackageSecurityDowngradeAlert, { + props: { + packageName: 'vue', + downgrade: { + downgradedVersion: '2.0.0', + downgradedTrustLevel: 'none', + downgradedTrustedPublisher: false, + trustedTrustLevel: 'provenance', + }, + }, + }) + expect(component.find('[role="alert"]').exists()).toBe(true) + expect(component.find('a[href*="/diff/"]').exists()).toBe(false) + + await component.setProps({ downgrade: null }) + expect(component.find('[role="alert"]').exists()).toBe(false) + }) +}) From 5477c910b00d7463509d72972f733c932b82ff07 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 30 Oct 2026 18:56:25 +0000 Subject: [PATCH 03/27] feat: allow comparing specific versions of packages (#3350) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- app/components/Compare/FacetBarChart.vue | 49 ++++++++++-- app/components/Compare/FacetScatterChart.vue | 22 +++++- app/components/Compare/FacetSelector.vue | 26 ++++++- app/components/Compare/PackageSelector.vue | 53 +++++++++---- app/composables/useCompareReplacements.ts | 12 ++- app/composables/usePackageComparison.ts | 4 +- app/pages/compare.vue | 65 ++++++++++------ app/utils/compare-scatter-chart.ts | 15 ++-- i18n/locales/en.json | 3 +- i18n/schema.json | 3 + shared/utils/compare.ts | 41 ++++++++++ test/nuxt/a11y.spec.ts | 78 +++++++++++++++++++ .../app/utils/compare-scatter-chart.spec.ts | 76 +++++++++--------- 13 files changed, 346 insertions(+), 101 deletions(-) create mode 100644 shared/utils/compare.ts diff --git a/app/components/Compare/FacetBarChart.vue b/app/components/Compare/FacetBarChart.vue index 6079e59706..bc52fda4f2 100644 --- a/app/components/Compare/FacetBarChart.vue +++ b/app/components/Compare/FacetBarChart.vue @@ -26,8 +26,10 @@ import('vue-data-ui/style.css') const props = defineProps<{ values: (FacetValue | null | undefined)[] packages: string[] + packagesData: ReadonlyArray label: string description: string + facetId?: string facetLoading?: boolean }>() @@ -77,14 +79,47 @@ const isDarkMode = computed(() => resolvedMode.value === 'dark') const dataset = computed(() => { if (props.facetLoading) return [] - return props.packages.map((name, index) => { + + const omitVersion = props.facetId ? shouldHideFacetInVersionComparison(props.facetId) : false + + if (!omitVersion) { + // Version-specific facet: keep one bar per entry (with version in label) + return props.packages.flatMap((spec, index) => { + const rawValue = props.values[index]?.raw + const packageData = props.packagesData[index] + if (!packageData) return [] + const { name, displayName } = getPackageDisplayInfo(packageData, spec) + + return [ + { + name: insertLineBreaks(applyEllipsis(displayName)), + value: typeof rawValue === 'number' ? rawValue : 0, + color: isListedFramework(name) ? getFrameworkColor(name) : undefined, + formattedValue: props.values[index]?.display, + }, + ] + }) + } + + // Package-level facet: one bar per unique package name + const seen = new Set() + return props.packages.flatMap((spec, index) => { + const packageData = props.packagesData[index] + if (!packageData) return [] + + const { name } = getPackageDisplayInfo(packageData, spec) + if (seen.has(name)) return [] + seen.add(name) + const rawValue = props.values[index]?.raw - return { - name: insertLineBreaks(applyEllipsis(name)), - value: typeof rawValue === 'number' ? rawValue : 0, - color: isListedFramework(name) ? getFrameworkColor(name) : undefined, - formattedValue: props.values[index]?.display, - } + return [ + { + name: insertLineBreaks(applyEllipsis(name)), + value: typeof rawValue === 'number' ? rawValue : 0, + color: isListedFramework(name) ? getFrameworkColor(name) : undefined, + formattedValue: props.values[index]?.display, + }, + ] }) }) diff --git a/app/components/Compare/FacetScatterChart.vue b/app/components/Compare/FacetScatterChart.vue index 99c4863f3c..634a2cbaab 100644 --- a/app/components/Compare/FacetScatterChart.vue +++ b/app/components/Compare/FacetScatterChart.vue @@ -56,11 +56,19 @@ const isDarkMode = computed(() => resolvedMode.value === 'dark') const { facetLabels } = useFacetSelection() +const isVersionComparisonComputed = computed(() => isVersionComparison(props.packages)) + const chartableFacets = computed(() => ( Object.entries(facetLabels.value) as [ ComparisonFacet, - { label: string; description: string; chartable_scatter: boolean }, + { + label: string + description: string + chartable: boolean + chartable_scatter: boolean + formatter?: ((value: number) => string) | undefined + }, ][] ) .map(([name, facet]) => ({ @@ -69,10 +77,18 @@ const chartableFacets = computed(() => description: facet.description, chartable: facet.chartable_scatter, })) - .filter(facet => facet.chartable), + .filter(facet => { + if (!facet.chartable) return false + if (isVersionComparisonComputed.value && shouldHideFacetInVersionComparison(facet.name)) { + return false + } + return true + }), ) -const selectedFacetX = ref('downloads') +const selectedFacetX = ref( + isVersionComparisonComputed.value ? 'dependencies' : 'downloads', +) const selectedFacetY = ref('installSize') const dataset = computed(() => diff --git a/app/components/Compare/FacetSelector.vue b/app/components/Compare/FacetSelector.vue index 7ce289779c..f6cb8d13ef 100644 --- a/app/components/Compare/FacetSelector.vue +++ b/app/components/Compare/FacetSelector.vue @@ -1,5 +1,10 @@ - + {{ pkg }} { id="package-search" v-model="inputValue" type="text" + :aria-describedby="versionHintId" :placeholder=" packages.length === 0 ? $t('compare.selector.search_first') @@ -314,6 +324,23 @@ onClickOutside(containerRef, () => { + + + + + +

{{ $t('compare.selector.packages_selected', { diff --git a/app/composables/useCompareReplacements.ts b/app/composables/useCompareReplacements.ts index eda1d9f356..c578e06c5a 100644 --- a/app/composables/useCompareReplacements.ts +++ b/app/composables/useCompareReplacements.ts @@ -1,4 +1,6 @@ import type { ModuleReplacement, ModuleReplacementMapping } from 'module-replacements' +import { NO_DEPENDENCY_ID } from '#shared/utils/compare' +import { parsePackageSpec } from '#shared/utils/parse-package-param' export interface ReplacementSuggestion { forPackage: string @@ -31,6 +33,12 @@ export function useCompareReplacements(packageNames: MaybeRefOrGetter) const replacements = shallowRef(new Map()) const loading = shallowRef(false) + const basePackages = computed(() => + [...new Set(packages.value.map(p => parsePackageSpec(p).name))].filter( + name => name !== NO_DEPENDENCY_ID, + ), + ) + // Fetch replacements for all packages async function fetchReplacements(names: string[]) { if (names.length === 0) return @@ -70,7 +78,7 @@ export function useCompareReplacements(packageNames: MaybeRefOrGetter) // Watch for package changes and fetch replacements if (import.meta.client) { watch( - packages, + basePackages, newPackages => { fetchReplacements(newPackages) }, @@ -82,7 +90,7 @@ export function useCompareReplacements(packageNames: MaybeRefOrGetter) const allSuggestions = computed(() => { const result: ReplacementSuggestion[] = [] - for (const pkg of packages.value) { + for (const pkg of basePackages.value) { const replacement = replacements.value.get(pkg) if (!replacement) continue diff --git a/app/composables/usePackageComparison.ts b/app/composables/usePackageComparison.ts index a26e1de515..4ff4de7dbf 100644 --- a/app/composables/usePackageComparison.ts +++ b/app/composables/usePackageComparison.ts @@ -1,9 +1,7 @@ +import { NO_DEPENDENCY_ID } from '#shared/utils/compare' import { parsePackageSpec } from '#shared/utils/parse-package-param' import { getDependencyCount } from '~/utils/npm/dependency-count' -/** Special identifier for the "What Would James Do?" comparison column */ -export const NO_DEPENDENCY_ID = '__no_dependency__' - /** * Special display values for the "no dependency" column. * These are explicit markers that get special rendering treatment. diff --git a/app/pages/compare.vue b/app/pages/compare.vue index 1b8df11a90..6b7394adf0 100644 --- a/app/pages/compare.vue +++ b/app/pages/compare.vue @@ -1,5 +1,4 @@