From 79ef3655f9aa52fdcc8af76cfd9afa1c6d5d2886 Mon Sep 17 00:00:00 2001 From: Vincent Taverna Date: Sun, 8 Feb 2026 18:25:55 -0500 Subject: [PATCH 01/32] feat(frontend): community version distribution --- app/components/Package/ChartModal.vue | 18 +- .../Package/VersionDistribution.vue | 326 ++++++++++++++++++ app/components/Package/Versions.vue | 75 +++- app/components/Settings/Toggle.client.vue | 18 +- app/composables/useVersionDistribution.ts | 178 ++++++++++ i18n/locales/en.json | 8 +- lunaria/files/en-GB.json | 8 +- lunaria/files/en-US.json | 8 +- .../api/registry/downloads/[...slug].get.ts | 143 ++++++++ server/utils/version-downloads.ts | 169 +++++++++ shared/types/index.ts | 1 + shared/types/version-downloads.ts | 60 ++++ test/unit/a11y-component-coverage.spec.ts | 2 + 13 files changed, 1002 insertions(+), 12 deletions(-) create mode 100644 app/components/Package/VersionDistribution.vue create mode 100644 app/composables/useVersionDistribution.ts create mode 100644 server/api/registry/downloads/[...slug].get.ts create mode 100644 server/utils/version-downloads.ts create mode 100644 shared/types/version-downloads.ts diff --git a/app/components/Package/ChartModal.vue b/app/components/Package/ChartModal.vue index bc7e97148..cd6bb4dce 100644 --- a/app/components/Package/ChartModal.vue +++ b/app/components/Package/ChartModal.vue @@ -1,10 +1,24 @@ - + + + diff --git a/app/components/Settings/Toggle.client.vue b/app/components/Settings/Toggle.client.vue index d1619eaad..a3cc5c360 100644 --- a/app/components/Settings/Toggle.client.vue +++ b/app/components/Settings/Toggle.client.vue @@ -1,8 +1,12 @@ From d264b7d0bd29f7e19db4eac86c97e187a92009f7 Mon Sep 17 00:00:00 2001 From: Vincent Taverna Date: Sun, 8 Feb 2026 23:35:29 -0500 Subject: [PATCH 18/32] fix: add timeout cleanup --- app/components/Package/Versions.vue | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/components/Package/Versions.vue b/app/components/Package/Versions.vue index 60d0798b8..135a34d6b 100644 --- a/app/components/Package/Versions.vue +++ b/app/components/Package/Versions.vue @@ -22,6 +22,14 @@ const props = defineProps<{ const chartModal = useModal('chart-modal') const hasDistributionModalTransitioned = shallowRef(false) const isDistributionModalOpen = shallowRef(false) +let distributionModalFallbackTimer: ReturnType | null = null + +function clearDistributionModalFallbackTimer() { + if (distributionModalFallbackTimer) { + clearTimeout(distributionModalFallbackTimer) + distributionModalFallbackTimer = null + } +} async function openDistributionModal() { isDistributionModalOpen.value = true @@ -31,7 +39,8 @@ async function openDistributionModal() { chartModal.open() // Fallback: Force mount if transition event doesn't fire - setTimeout(() => { + clearDistributionModalFallbackTimer() + distributionModalFallbackTimer = setTimeout(() => { if (!hasDistributionModalTransitioned.value) { hasDistributionModalTransitioned.value = true } @@ -41,10 +50,12 @@ async function openDistributionModal() { function closeDistributionModal() { isDistributionModalOpen.value = false hasDistributionModalTransitioned.value = false + clearDistributionModalFallbackTimer() } function handleDistributionModalTransitioned() { hasDistributionModalTransitioned.value = true + clearDistributionModalFallbackTimer() } /** Maximum number of dist-tag rows to show before collapsing into "Other versions" */ From e65bf622bab8664fbdb0710321fd9fcf0e4d588b Mon Sep 17 00:00:00 2001 From: Vincent Taverna Date: Mon, 9 Feb 2026 02:20:13 -0500 Subject: [PATCH 19/32] fix: caching best practices --- server/api/registry/downloads/[...slug].get.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/server/api/registry/downloads/[...slug].get.ts b/server/api/registry/downloads/[...slug].get.ts index e12d21904..20bca9625 100644 --- a/server/api/registry/downloads/[...slug].get.ts +++ b/server/api/registry/downloads/[...slug].get.ts @@ -1,5 +1,6 @@ -import { getQuery } from 'h3' +import { getQuery, setHeader } from 'h3' import * as v from 'valibot' +import { hash } from 'ohash' import type { VersionDistributionResponse } from '#shared/types' import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants' import { groupVersionDownloads } from '#server/utils/version-downloads' @@ -96,6 +97,14 @@ export default defineCachedEventHandler( timestamp: new Date().toISOString(), } + // Set cache headers for both client and CDN/edge caching + // s-maxage controls shared cache (Vercel edge) behavior + setHeader( + event, + 'Cache-Control', + `public, max-age=${CACHE_MAX_AGE_ONE_HOUR}, s-maxage=${CACHE_MAX_AGE_ONE_HOUR}`, + ) + if (filterOldVersionsBool) { try { const oneYearAgo = new Date() @@ -134,10 +143,9 @@ export default defineCachedEventHandler( getKey: event => { const slug = getRouterParam(event, 'slug') ?? '' const query = getQuery(event) - const mode = query.mode || 'major' - const filterThreshold = query.filterThreshold || 1 - const filterOldVersions = query.filterOldVersions === 'true' - return `version-downloads:v3:${slug}:${mode}:${filterThreshold}:${filterOldVersions ? 'filtered' : 'all'}` + // Use ohash to create deterministic cache key from query params + // This ensures different param combinations = different cache entries + return `version-downloads:v4:${slug}:${hash(query)}` }, }, ) From 205c634f0071000b3acf30b39b177f400bb9cfcf Mon Sep 17 00:00:00 2001 From: Vincent Taverna Date: Mon, 9 Feb 2026 02:39:15 -0500 Subject: [PATCH 20/32] fix: last week version nitpicks --- .../api/registry/downloads/[...slug].get.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/server/api/registry/downloads/[...slug].get.ts b/server/api/registry/downloads/[...slug].get.ts index 20bca9625..e11829a1a 100644 --- a/server/api/registry/downloads/[...slug].get.ts +++ b/server/api/registry/downloads/[...slug].get.ts @@ -3,6 +3,7 @@ import * as v from 'valibot' import { hash } from 'ohash' import type { VersionDistributionResponse } from '#shared/types' import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants' +import { encodePackageName } from '#shared/utils/npm' import { groupVersionDownloads } from '#server/utils/version-downloads' /** @@ -19,7 +20,13 @@ interface NpmVersionDownloadsResponse { */ const QuerySchema = v.object({ mode: v.optional(v.picklist(['major', 'minor'] as const), 'major'), - filterThreshold: v.optional(v.pipe(v.string(), v.transform(Number)), '1'), + filterThreshold: v.optional( + v.pipe( + v.string(), + v.toNumber(), // Fails validation on invalid conversion (e.g., "abc") instead of producing NaN + v.minValue(0), // Ensure non-negative values + ), + ), filterOldVersions: v.optional(v.picklist(['true', 'false'] as const), 'false'), }) @@ -40,7 +47,8 @@ export default defineCachedEventHandler( const slugParam = getRouterParam(event, 'slug') const pkgParamSegments = slugParam?.split('/') ?? [] - if (pkgParamSegments[pkgParamSegments.length - 1] !== 'versions') { + const lastSegment = pkgParamSegments.at(-1) + if (!lastSegment || lastSegment !== 'versions') { throw createError({ statusCode: 404, message: 'Invalid endpoint. Expected /versions', @@ -59,11 +67,15 @@ export default defineCachedEventHandler( } const query = getQuery(event) - const { mode, filterThreshold, filterOldVersions } = v.parse(QuerySchema, query) - const filterOldVersionsBool = filterOldVersions === 'true' + const parsed = v.parse(QuerySchema, query) + const mode = parsed.mode + const filterThreshold = parsed.filterThreshold ?? 1 + const filterOldVersionsBool = parsed.filterOldVersions === 'true' try { - const url = `https://api.npmjs.org/versions/${rawPackageName}/last-week` + // URL-encode package name for scoped packages (e.g., @types/node -> @types%2Fnode) + const encodedPackageName = encodePackageName(rawPackageName) + const url = `https://api.npmjs.org/versions/${encodedPackageName}/last-week` const npmResponse = await fetch(url) if (!npmResponse.ok) { From 75c778f8baadcef65a4aef04c7dfd665ab6c46dd Mon Sep 17 00:00:00 2001 From: Vincent Taverna Date: Mon, 9 Feb 2026 02:44:09 -0500 Subject: [PATCH 21/32] fix: revert ai recommendation Double encoding broke the requests here --- server/api/registry/downloads/[...slug].get.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/api/registry/downloads/[...slug].get.ts b/server/api/registry/downloads/[...slug].get.ts index e11829a1a..f10f32e40 100644 --- a/server/api/registry/downloads/[...slug].get.ts +++ b/server/api/registry/downloads/[...slug].get.ts @@ -3,7 +3,6 @@ import * as v from 'valibot' import { hash } from 'ohash' import type { VersionDistributionResponse } from '#shared/types' import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants' -import { encodePackageName } from '#shared/utils/npm' import { groupVersionDownloads } from '#server/utils/version-downloads' /** @@ -73,9 +72,7 @@ export default defineCachedEventHandler( const filterOldVersionsBool = parsed.filterOldVersions === 'true' try { - // URL-encode package name for scoped packages (e.g., @types/node -> @types%2Fnode) - const encodedPackageName = encodePackageName(rawPackageName) - const url = `https://api.npmjs.org/versions/${encodedPackageName}/last-week` + const url = `https://api.npmjs.org/versions/${rawPackageName}/last-week` const npmResponse = await fetch(url) if (!npmResponse.ok) { From bcf6f430ff267c3d3ea559dd41bee10f4e4e1f93 Mon Sep 17 00:00:00 2001 From: Vincent Taverna Date: Mon, 9 Feb 2026 10:13:58 -0500 Subject: [PATCH 22/32] fix: move validations into try/catch --- server/api/registry/downloads/[...slug].get.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/api/registry/downloads/[...slug].get.ts b/server/api/registry/downloads/[...slug].get.ts index f10f32e40..faf54edf5 100644 --- a/server/api/registry/downloads/[...slug].get.ts +++ b/server/api/registry/downloads/[...slug].get.ts @@ -65,13 +65,13 @@ export default defineCachedEventHandler( }) } - const query = getQuery(event) - const parsed = v.parse(QuerySchema, query) - const mode = parsed.mode - const filterThreshold = parsed.filterThreshold ?? 1 - const filterOldVersionsBool = parsed.filterOldVersions === 'true' - try { + const query = getQuery(event) + const parsed = v.parse(QuerySchema, query) + const mode = parsed.mode + const filterThreshold = parsed.filterThreshold ?? 1 + const filterOldVersionsBool = parsed.filterOldVersions === 'true' + const url = `https://api.npmjs.org/versions/${rawPackageName}/last-week` const npmResponse = await fetch(url) From 277b6a0bd33f770eb81bdba8bf6c2bbb8a9b29f8 Mon Sep 17 00:00:00 2001 From: Vincent Taverna Date: Mon, 9 Feb 2026 10:25:26 -0500 Subject: [PATCH 23/32] chore: i18n schema with version distribution strings --- i18n/schema.json | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/i18n/schema.json b/i18n/schema.json index 337375aa7..b3a40584e 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -867,6 +867,33 @@ }, "view_all": { "type": "string" + }, + "distribution_title": { + "type": "string" + }, + "distribution_modal_title": { + "type": "string" + }, + "grouping_major": { + "type": "string" + }, + "grouping_minor": { + "type": "string" + }, + "show_old_versions": { + "type": "string" + }, + "show_old_versions_tooltip": { + "type": "string" + }, + "show_low_usage": { + "type": "string" + }, + "show_low_usage_tooltip": { + "type": "string" + }, + "date_range_tooltip": { + "type": "string" } }, "additionalProperties": false From 20e8826bea1c362b8d55a91b415268ead9e30a1a Mon Sep 17 00:00:00 2001 From: Vincent Taverna Date: Mon, 9 Feb 2026 11:25:38 -0500 Subject: [PATCH 24/32] fix: isr cache key got me dizzy --- app/composables/useVersionDistribution.ts | 1 + nuxt.config.ts | 7 +++++++ server/api/registry/downloads/[...slug].get.ts | 12 ++---------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/composables/useVersionDistribution.ts b/app/composables/useVersionDistribution.ts index 1c15614d6..62f29d090 100644 --- a/app/composables/useVersionDistribution.ts +++ b/app/composables/useVersionDistribution.ts @@ -51,6 +51,7 @@ export function useVersionDistribution(packageName: MaybeRefOrGetter) { filterOldVersions: showOldVersions.value ? 'false' : 'true', filterThreshold: showLowUsageVersions.value ? '0' : '1', }, + cache: 'default', // Don't force-cache since query params change frequently }, ) diff --git a/nuxt.config.ts b/nuxt.config.ts index b460d8a8e..1116ec657 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -95,6 +95,13 @@ export default defineNuxtConfig({ allowQuery: ['color', 'labelColor', 'label', 'name'], }, }, + '/api/registry/downloads/**': { + isr: { + expiration: 60 * 60 /* one hour */, + passQuery: true, + allowQuery: ['mode', 'filterOldVersions', 'filterThreshold'], + }, + }, '/api/registry/docs/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, '/api/registry/file/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, '/api/registry/provenance/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, diff --git a/server/api/registry/downloads/[...slug].get.ts b/server/api/registry/downloads/[...slug].get.ts index faf54edf5..8388d6c25 100644 --- a/server/api/registry/downloads/[...slug].get.ts +++ b/server/api/registry/downloads/[...slug].get.ts @@ -1,4 +1,4 @@ -import { getQuery, setHeader } from 'h3' +import { getQuery } from 'h3' import * as v from 'valibot' import { hash } from 'ohash' import type { VersionDistributionResponse } from '#shared/types' @@ -106,14 +106,6 @@ export default defineCachedEventHandler( timestamp: new Date().toISOString(), } - // Set cache headers for both client and CDN/edge caching - // s-maxage controls shared cache (Vercel edge) behavior - setHeader( - event, - 'Cache-Control', - `public, max-age=${CACHE_MAX_AGE_ONE_HOUR}, s-maxage=${CACHE_MAX_AGE_ONE_HOUR}`, - ) - if (filterOldVersionsBool) { try { const oneYearAgo = new Date() @@ -154,7 +146,7 @@ export default defineCachedEventHandler( const query = getQuery(event) // Use ohash to create deterministic cache key from query params // This ensures different param combinations = different cache entries - return `version-downloads:v4:${slug}:${hash(query)}` + return `version-downloads:v5:${slug}:${hash(query)}` }, }, ) From f0a386d768072bdd33fce67b624877e72f47a47b Mon Sep 17 00:00:00 2001 From: Vincent Taverna Date: Mon, 9 Feb 2026 18:58:50 -0500 Subject: [PATCH 25/32] fix: chart enhancements from feedback --- app/components/Package/DownloadAnalytics.vue | 34 +-- .../Package/VersionDistribution.vue | 214 ++++++++++++++---- app/components/Package/Versions.vue | 2 +- app/composables/useChartWatermark.ts | 90 ++++++++ 4 files changed, 272 insertions(+), 68 deletions(-) create mode 100644 app/composables/useChartWatermark.ts diff --git a/app/components/Package/DownloadAnalytics.vue b/app/components/Package/DownloadAnalytics.vue index eee26a2b6..25d8860ae 100644 --- a/app/components/Package/DownloadAnalytics.vue +++ b/app/components/Package/DownloadAnalytics.vue @@ -5,6 +5,7 @@ import { useDebounceFn, useElementSize } from '@vueuse/core' import { useCssVariables } from '~/composables/useColors' import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch } from '~/utils/colors' import { getFrameworkColor, isListedFramework } from '~/utils/frameworks' +import { drawNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark' const props = defineProps<{ // For single package downloads history @@ -97,6 +98,12 @@ const accent = computed(() => { : (colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK) }) +const watermarkColors = computed(() => ({ + fg: colors.value.fg ?? OKLCH_NEUTRAL_FALLBACK, + bg: colors.value.bg ?? OKLCH_NEUTRAL_FALLBACK, + fgSubtle: colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK, +})) + const mobileBreakpointWidth = 640 const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth) @@ -1389,31 +1396,6 @@ function drawSvgPrintLegend(svg: Record) { return seriesNames.join('\n') } -/** - * Build and return npmx svg logo and tagline, to be injected during PNG & SVG exports - */ -function drawNpmxLogoAndTaglineWatermark(svg: Record) { - if (!svg?.drawingArea) return '' - const npmxLogoWidthToHeight = 2.64 - const npmxLogoWidth = 100 - const npmxLogoHeight = npmxLogoWidth / npmxLogoWidthToHeight - - return ` - - - - - ${$t('tagline')} - - ` -} - // VueUiXy chart component configuration const chartConfig = computed(() => { return { @@ -1715,7 +1697,7 @@ const chartConfig = computed(() => { diff --git a/app/components/Package/VersionDistribution.vue b/app/components/Package/VersionDistribution.vue index 1cd95a69e..5c53a97ea 100644 --- a/app/components/Package/VersionDistribution.vue +++ b/app/components/Package/VersionDistribution.vue @@ -9,6 +9,10 @@ import type { import { useElementSize } from '@vueuse/core' import { useCssVariables } from '~/composables/useColors' import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch } from '~/utils/colors' +import { + drawSvgPrintLegend, + drawNpmxLogoAndTaglineWatermark, +} from '~/composables/useChartWatermark' import TooltipApp from '~/components/Tooltip/App.vue' type TooltipParams = MinimalCustomFormatParams & { @@ -64,6 +68,12 @@ const accent = computed(() => { : (colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK) }) +const watermarkColors = computed(() => ({ + fg: colors.value.fg ?? OKLCH_NEUTRAL_FALLBACK, + bg: colors.value.bg ?? OKLCH_NEUTRAL_FALLBACK, + fgSubtle: colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK, +})) + const { width } = useElementSize(rootEl) const mobileBreakpointWidth = 640 const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth) @@ -80,6 +90,9 @@ const { const compactNumberFormatter = useCompactNumberFormatter() +// Show loading indicator immediately to maintain stable layout +const showLoadingIndicator = computed(() => pending.value) + const chartConfig = computed(() => { return { theme: isDarkMode.value ? 'dark' : 'default', @@ -89,11 +102,17 @@ const chartConfig = computed(() => { padding: { top: 24, right: 24, - bottom: xAxisLabels.value.length > 10 ? 100 : 72, // More space for rotated labels + bottom: xAxisLabels.value.length > 10 ? 100 : 88, // Space for rotated labels + watermark left: isMobile.value ? 60 : 80, }, userOptions: { - buttons: { pdf: false, labels: false, fullscreen: false, table: false, tooltip: false }, + buttons: { + pdf: false, + labels: false, + fullscreen: false, + table: false, + tooltip: false, + }, }, grid: { stroke: colors.value.border, @@ -102,7 +121,6 @@ const chartConfig = computed(() => { color: pending.value ? colors.value.border : colors.value.fgSubtle, axis: { yLabel: 'Downloads', - xLabel: props.packageName, yLabelOffsetX: 12, fontSize: isMobile.value ? 32 : 24, }, @@ -115,17 +133,14 @@ const chartConfig = computed(() => { xAxisLabels: { show: xAxisLabels.value.length <= 25, values: xAxisLabels.value, - fontSize: isMobile.value ? 14 : 12, + fontSize: isMobile.value ? 16 : 14, color: colors.value.fgSubtle, rotation: xAxisLabels.value.length > 10 ? 45 : 0, }, }, }, - timeTag: { - show: false, - }, highlighter: { useLine: false }, - legend: { show: false }, + legend: { show: false, position: 'top' }, bar: { periodGap: 16, innerGap: 8, @@ -184,9 +199,6 @@ const chartConfig = computed(() => { }, }, }, - userOptions: { - show: false, - }, table: { show: false, }, @@ -199,7 +211,7 @@ const xyDataset = computed(() => { return [ { - name: 'Downloads', + name: props.packageName, series: chartDataset.value.map(item => item.downloads), type: 'bar' as const, color: accent.value, @@ -247,8 +259,7 @@ const endDate = computed(() => { diff --git a/app/components/Package/Versions.vue b/app/components/Package/Versions.vue index c17a22478..929908447 100644 --- a/app/components/Package/Versions.vue +++ b/app/components/Package/Versions.vue @@ -821,7 +821,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
diff --git a/app/composables/useChartWatermark.ts b/app/composables/useChartWatermark.ts new file mode 100644 index 000000000..36c267ce1 --- /dev/null +++ b/app/composables/useChartWatermark.ts @@ -0,0 +1,90 @@ +/** + * Shared utilities for chart watermarks and legends in SVG/PNG exports + */ + +interface WatermarkColors { + fg: string + bg: string + fgSubtle: string +} + +/** + * Build and return legend as SVG for export + * Legend items are displayed in a column, on the top left of the chart. + */ +export function drawSvgPrintLegend(svg: Record, colors: WatermarkColors) { + const data = Array.isArray(svg?.data) ? svg.data : [] + if (!data.length) return '' + + const seriesNames: string[] = [] + + data.forEach((serie, index) => { + seriesNames.push(` + + + ${serie.name} + + `) + }) + + return seriesNames.join('') +} + +/** + * Build and return npmx svg logo and tagline, to be injected during PNG & SVG exports + */ +export function drawNpmxLogoAndTaglineWatermark( + svg: Record, + colors: WatermarkColors, + translateFn: (key: string) => string, + positioning: 'bottom' | 'belowDrawingArea' = 'bottom', +) { + if (!svg?.drawingArea) return '' + const npmxLogoWidthToHeight = 2.64 + const npmxLogoWidth = 100 + const npmxLogoHeight = npmxLogoWidth / npmxLogoWidthToHeight + + // Position watermark based on the positioning strategy + const watermarkY = + positioning === 'belowDrawingArea' + ? svg.drawingArea.top + svg.drawingArea.height + 48 + : svg.height - npmxLogoHeight + + const taglineY = + positioning === 'belowDrawingArea' ? watermarkY - 6 : svg.height - npmxLogoHeight - 6 + + // Center the watermark horizontally relative to the full SVG width + const watermarkX = svg.width / 2 - npmxLogoWidth / 2 + + return ` + + + + + ${translateFn('tagline')} + + ` +} From 43394a0dd995069069165be4237f08c5ee7e09bc Mon Sep 17 00:00:00 2001 From: Vincent Taverna Date: Mon, 9 Feb 2026 19:32:24 -0500 Subject: [PATCH 26/32] fix: tooltips in version distributions modal --- app/components/Package/VersionDistribution.vue | 8 ++++---- app/components/Settings/Toggle.client.vue | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/components/Package/VersionDistribution.vue b/app/components/Package/VersionDistribution.vue index 5c53a97ea..965c55e55 100644 --- a/app/components/Package/VersionDistribution.vue +++ b/app/components/Package/VersionDistribution.vue @@ -313,7 +313,7 @@ const endDate = computed(() => { @@ -344,7 +344,7 @@ const endDate = computed(() => { @@ -380,7 +380,7 @@ const endDate = computed(() => { :label="$t('package.versions.show_old_versions')" :tooltip="$t('package.versions.show_old_versions_tooltip')" tooltip-position="bottom" - :tooltip-teleport-to="inModal ? '#chart-modal' : undefined" + :tooltip-to="inModal ? '#chart-modal' : undefined" :tooltip-offset="8" justify="between" :class="pending ? 'opacity-50 pointer-events-none' : ''" @@ -391,7 +391,7 @@ const endDate = computed(() => { :label="$t('package.versions.show_low_usage')" :tooltip="$t('package.versions.show_low_usage_tooltip')" tooltip-position="bottom" - :tooltip-teleport-to="inModal ? '#chart-modal' : undefined" + :tooltip-to="inModal ? '#chart-modal' : undefined" :tooltip-offset="8" justify="between" :class="pending ? 'opacity-50 pointer-events-none' : ''" diff --git a/app/components/Settings/Toggle.client.vue b/app/components/Settings/Toggle.client.vue index ad9dbcdbb..d1be8bbeb 100644 --- a/app/components/Settings/Toggle.client.vue +++ b/app/components/Settings/Toggle.client.vue @@ -9,7 +9,7 @@ withDefaults( justify?: 'between' | 'start' tooltip?: string tooltipPosition?: 'top' | 'bottom' | 'left' | 'right' - tooltipTeleportTo?: string + tooltipTo?: string tooltipOffset?: number }>(), { @@ -35,7 +35,7 @@ const checked = defineModel({ v-if="tooltip && label" :text="tooltip" :position="tooltipPosition ?? 'top'" - :teleportTo="tooltipTeleportTo" + :to="tooltipTo" :offset="tooltipOffset" > From 99073d941b78e3e210f6798d28918ff696e283f8 Mon Sep 17 00:00:00 2001 From: Vincent Taverna Date: Tue, 10 Feb 2026 13:47:12 -0500 Subject: [PATCH 27/32] feat: reverse order for toggles --- .../Package/VersionDistribution.vue | 2 + app/components/Settings/Toggle.client.vue | 104 +++++++++++++----- 2 files changed, 78 insertions(+), 28 deletions(-) diff --git a/app/components/Package/VersionDistribution.vue b/app/components/Package/VersionDistribution.vue index 965c55e55..b071d4a71 100644 --- a/app/components/Package/VersionDistribution.vue +++ b/app/components/Package/VersionDistribution.vue @@ -383,6 +383,7 @@ const endDate = computed(() => { :tooltip-to="inModal ? '#chart-modal' : undefined" :tooltip-offset="8" justify="between" + reverse-order :class="pending ? 'opacity-50 pointer-events-none' : ''" /> @@ -394,6 +395,7 @@ const endDate = computed(() => { :tooltip-to="inModal ? '#chart-modal' : undefined" :tooltip-offset="8" justify="between" + reverse-order :class="pending ? 'opacity-50 pointer-events-none' : ''" />
diff --git a/app/components/Settings/Toggle.client.vue b/app/components/Settings/Toggle.client.vue index d1be8bbeb..8d56de72c 100644 --- a/app/components/Settings/Toggle.client.vue +++ b/app/components/Settings/Toggle.client.vue @@ -1,7 +1,7 @@