Skip to content

Commit b1a4fec

Browse files
authored
Merge branch 'main' into userquin/feat-add-browser-locale-support
2 parents d1abd74 + 04bd490 commit b1a4fec

21 files changed

Lines changed: 526 additions & 71 deletions

app/components/CollapsibleSection.vue

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { LinkBase } from '#components'
44
55
interface Props {
66
title: string
7+
subtitle?: string
78
isLoading?: boolean
89
headingLevel?: `h${number}`
910
id: string
@@ -81,7 +82,8 @@ useHead({
8182
<div class="flex items-center justify-between mb-3 px-1">
8283
<component
8384
:is="headingLevel"
84-
class="group text-xs text-fg-subtle uppercase tracking-wider flex items-center gap-2"
85+
class="group text-xs text-fg-subtle uppercase tracking-wider flex gap-2"
86+
:class="subtitle ? 'items-start' : 'items-center'"
8587
>
8688
<button
8789
:id="buttonId"
@@ -101,9 +103,14 @@ useHead({
101103
/>
102104
</button>
103105

104-
<LinkBase :to="`#${id}`">
105-
{{ title }}
106-
</LinkBase>
106+
<span>
107+
<LinkBase :to="`#${id}`">
108+
{{ title }}
109+
</LinkBase>
110+
<span v-if="subtitle" class="block text-2xs normal-case tracking-normal">{{
111+
subtitle
112+
}}</span>
113+
</span>
107114
</component>
108115

109116
<!-- Actions slot for buttons or other elements -->

app/components/Modal.client.vue

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
const props = defineProps<{
33
modalTitle: string
4+
modalSubtitle?: string
45
}>()
56
67
const dialogRef = useTemplateRef('dialogRef')
@@ -14,6 +15,12 @@ const modalTitleId = computed(() => {
1415
return id ? `${id}-title` : undefined
1516
})
1617
18+
const modalSubtitleId = computed(() => {
19+
if (!props.modalSubtitle) return undefined
20+
const id = getCurrentInstance()?.attrs.id
21+
return id ? `${id}-subtitle` : undefined
22+
})
23+
1724
function handleModalClose() {
1825
dialogRef.value?.close()
1926
}
@@ -45,14 +52,20 @@ defineExpose({
4552
closedby="any"
4653
class="w-[calc(100%-2rem)] bg-bg border border-border rounded-lg shadow-xl max-h-[90vh] overflow-y-auto overscroll-contain m-0 m-auto p-6 text-fg focus-visible:outline focus-visible:outline-accent/70"
4754
:aria-labelledby="modalTitleId"
55+
:aria-describedby="modalSubtitleId"
4856
v-bind="$attrs"
4957
@transitionend="onDialogTransitionEnd"
5058
>
5159
<!-- Modal top header section -->
5260
<div class="flex items-center justify-between mb-6">
53-
<h2 :id="modalTitleId" class="font-mono text-lg font-medium">
54-
{{ modalTitle }}
55-
</h2>
61+
<div>
62+
<h2 :id="modalTitleId" class="font-mono text-lg font-medium">
63+
{{ modalTitle }}
64+
</h2>
65+
<p v-if="modalSubtitle" :id="modalSubtitleId" class="text-xs text-fg-subtle">
66+
{{ modalSubtitle }}
67+
</p>
68+
</div>
5669
<ButtonBase
5770
type="button"
5871
:aria-label="$t('common.close')"

app/components/Package/ChartModal.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
const props = defineProps<{
33
modalTitle?: string
4+
modalSubtitle?: string
45
}>()
56
67
const emit = defineEmits<{
@@ -11,6 +12,7 @@ const emit = defineEmits<{
1112
<template>
1213
<Modal
1314
:modalTitle="modalTitle ?? $t('package.trends.title')"
15+
:modalSubtitle="modalSubtitle"
1416
id="chart-modal"
1517
class="h-full sm:h-min sm:border sm:border-border sm:rounded-lg shadow-xl sm:max-h-[90vh] sm:max-w-3xl"
1618
@transitioned="emit('transitioned')"

app/components/Package/LikeCard.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ const likeAction = async () => {
7171

7272
<template>
7373
<NuxtLink :to="packageRoute(name)">
74-
<BaseCard class="font-mono flex justify-between">
75-
{{ name }}
76-
<div class="flex items-center gap-4 justify-between">
74+
<BaseCard class="font-mono flex justify-between min-w-0">
75+
<span class="truncate min-w-0" :title="name">{{ name }}</span>
76+
<div class="flex items-center gap-4 justify-between shrink-0">
7777
<ClientOnly>
7878
<TooltipApp
7979
:text="likesData?.userHasLiked ? $t('package.likes.unlike') : $t('package.likes.like')"

app/components/Package/TrendsChart.vue

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ function formatXyDataset(
219219
color: accent.value,
220220
temperatureColors,
221221
useArea: true,
222+
dashIndices: dataset
223+
.map((item, index) => (item.hasAnomaly ? index : -1))
224+
.filter(index => index !== -1),
222225
}
223226
224227
if (selectedGranularity === 'weekly' && isWeeklyDataset(dataset)) {
@@ -275,19 +278,26 @@ function formatXyDataset(
275278
function extractSeriesPoints(
276279
selectedGranularity: ChartTimeGranularity,
277280
dataset: EvolutionData,
278-
): Array<{ timestamp: number; value: number }> {
281+
): Array<{ timestamp: number; value: number; hasAnomaly: boolean }> {
279282
if (selectedGranularity === 'weekly' && isWeeklyDataset(dataset)) {
280-
return dataset.map(d => ({ timestamp: d.timestampEnd, value: d.value }))
283+
return dataset.map(d => ({
284+
timestamp: d.timestampEnd,
285+
value: d.value,
286+
hasAnomaly: !!d.hasAnomaly,
287+
}))
281288
}
282289
if (
283290
(selectedGranularity === 'daily' && isDailyDataset(dataset)) ||
284291
(selectedGranularity === 'monthly' && isMonthlyDataset(dataset)) ||
285292
(selectedGranularity === 'yearly' && isYearlyDataset(dataset))
286293
) {
287-
return (dataset as Array<{ timestamp: number; value: number }>).map(d => ({
288-
timestamp: d.timestamp,
289-
value: d.value,
290-
}))
294+
return (dataset as Array<{ timestamp: number; value: number; hasAnomaly?: boolean }>).map(
295+
d => ({
296+
timestamp: d.timestamp,
297+
value: d.value,
298+
hasAnomaly: !!d.hasAnomaly,
299+
}),
300+
)
291301
}
292302
return []
293303
}
@@ -380,6 +390,11 @@ const isEstimationGranularity = computed(
380390
const supportsEstimation = computed(
381391
() => isEstimationGranularity.value && selectedMetric.value !== 'contributors',
382392
)
393+
394+
const hasDownloadAnomalies = computed(() =>
395+
normalisedDataset.value?.some(datapoint => !!datapoint.dashIndices.length),
396+
)
397+
383398
const shouldRenderEstimationOverlay = computed(() => !pending.value && supportsEstimation.value)
384399
385400
const startDate = usePermalink<string>('start', '', {
@@ -955,11 +970,13 @@ const effectiveDataSingle = computed<EvolutionData>(() => {
955970
granularity: displayedGranularity.value,
956971
})
957972
}
973+
958974
return applyDataCorrection(
959975
data as Array<{ value: number }>,
960976
settings.value.chartFilter,
961977
) as EvolutionData
962978
}
979+
963980
return data
964981
})
965982
@@ -991,7 +1008,10 @@ const chartData = computed<{
9911008
const granularity = displayedGranularity.value
9921009
9931010
const timestampSet = new Set<number>()
994-
const pointsByPackage = new Map<string, Array<{ timestamp: number; value: number }>>()
1011+
const pointsByPackage = new Map<
1012+
string,
1013+
Array<{ timestamp: number; value: number; hasAnomaly?: boolean }>
1014+
>()
9951015
9961016
for (const pkg of names) {
9971017
let data = state.evolutionsByPackage[pkg] ?? []
@@ -1005,6 +1025,7 @@ const chartData = computed<{
10051025
) as EvolutionData
10061026
}
10071027
const points = extractSeriesPoints(granularity, data)
1028+
10081029
pointsByPackage.set(pkg, points)
10091030
for (const p of points) timestampSet.add(p.timestamp)
10101031
}
@@ -1014,15 +1035,23 @@ const chartData = computed<{
10141035
10151036
const dataset: VueUiXyDatasetItem[] = names.map(pkg => {
10161037
const points = pointsByPackage.get(pkg) ?? []
1017-
const map = new Map<number, number>()
1018-
for (const p of points) map.set(p.timestamp, p.value)
1038+
const valueByTimestamp = new Map<number, number>()
1039+
const anomalyTimestamps = new Set<number>()
1040+
for (const p of points) {
1041+
valueByTimestamp.set(p.timestamp, p.value)
1042+
if (p.hasAnomaly) anomalyTimestamps.add(p.timestamp)
1043+
}
10191044
1020-
const series = dates.map(t => map.get(t) ?? 0)
1045+
const series = dates.map(t => valueByTimestamp.get(t) ?? 0)
1046+
const dashIndices = dates
1047+
.map((t, index) => (anomalyTimestamps.has(t) ? index : -1))
1048+
.filter(index => index !== -1)
10211049
10221050
const item: VueUiXyDatasetItem = {
10231051
name: pkg,
10241052
type: 'line',
10251053
series,
1054+
dashIndices,
10261055
} as VueUiXyDatasetItem
10271056
10281057
if (isListedFramework(pkg)) {
@@ -1045,6 +1074,7 @@ const normalisedDataset = computed(() => {
10451074
return {
10461075
...d,
10471076
series: [...d.series.slice(0, -1), projectedLastValue],
1077+
dashIndices: d.dashIndices ?? [],
10481078
}
10491079
})
10501080
})
@@ -1408,7 +1438,10 @@ function drawSvgPrintLegend(svg: Record<string, any>) {
14081438
})
14091439
14101440
// Inject the estimation legend item when necessary
1411-
if (supportsEstimation.value && !isEndDateOnPeriodEnd.value && !isZoomed.value) {
1441+
if (
1442+
(supportsEstimation.value && !isEndDateOnPeriodEnd.value && !isZoomed.value) ||
1443+
hasDownloadAnomalies.value
1444+
) {
14121445
seriesNames.push(`
14131446
<line
14141447
x1="${svg.drawingArea.left + 12}"
@@ -1955,7 +1988,10 @@ watch(selectedMetric, value => {
19551988
</template>
19561989

19571990
<!-- Estimation extra legend item -->
1958-
<div class="flex gap-1 place-items-center" v-if="supportsEstimation">
1991+
<div
1992+
class="flex gap-1 place-items-center"
1993+
v-if="supportsEstimation || hasDownloadAnomalies"
1994+
>
19591995
<svg viewBox="0 0 20 2" width="20">
19601996
<line
19611997
x1="0"

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ const modalTitle = computed(() => {
2929
return $t('package.trends.items.downloads')
3030
})
3131
32+
const modalSubtitle = computed(() => {
33+
const facet = route.query.facet as string | undefined
34+
if (facet === 'likes' || facet === 'contributors') return undefined
35+
return $t('package.downloads.subtitle')
36+
})
37+
3238
const isChartModalOpen = shallowRef<boolean>(false)
3339
3440
function handleModalClose() {
@@ -285,7 +291,11 @@ const config = computed<VueUiSparklineConfig>(() => {
285291

286292
<template>
287293
<div class="space-y-8">
288-
<CollapsibleSection id="downloads" :title="$t('package.downloads.title')">
294+
<CollapsibleSection
295+
id="downloads"
296+
:title="$t('package.downloads.title')"
297+
:subtitle="$t('package.downloads.subtitle')"
298+
>
289299
<template #actions>
290300
<ButtonBase
291301
v-if="hasWeeklyDownloads"
@@ -355,6 +365,7 @@ const config = computed<VueUiSparklineConfig>(() => {
355365
<PackageChartModal
356366
v-if="isChartModalOpen && hasWeeklyDownloads"
357367
:modal-title="modalTitle"
368+
:modal-subtitle="modalSubtitle"
358369
@close="handleModalClose"
359370
@transitioned="handleModalTransitioned"
360371
>

app/components/diff/ViewerPanel.vue

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import type { FileDiffResponse, FileChange } from '#shared/types'
33
import { onClickOutside } from '@vueuse/core'
44
5+
const bytesFormatter = useBytesFormatter()
6+
57
const props = defineProps<{
68
packageName: string
79
fromVersion: string
@@ -20,9 +22,21 @@ onClickOutside(optionsDropdownRef, () => {
2022
showOptions.value = false
2123
})
2224
23-
const apiUrl = computed(
24-
() =>
25-
`/api/registry/compare-file/${props.packageName}/v/${props.fromVersion}...${props.toVersion}/${props.file.path}`,
25+
// Maximum file size we'll try to load (250KB) - must match server
26+
const MAX_FILE_SIZE = 250 * 1024
27+
const isFilesTooLarge = computed(() => {
28+
const newSize = props.file?.newSize
29+
const oldSize = props.file?.oldSize
30+
return (
31+
(newSize !== undefined && newSize > MAX_FILE_SIZE) ||
32+
(oldSize !== undefined && oldSize > MAX_FILE_SIZE)
33+
)
34+
})
35+
36+
const apiUrl = computed(() =>
37+
isFilesTooLarge.value
38+
? null
39+
: `/api/registry/compare-file/${props.packageName}/v/${props.fromVersion}...${props.toVersion}/${props.file.path}`,
2640
)
2741
2842
const apiQuery = computed(() => ({
@@ -36,7 +50,7 @@ const {
3650
data: diff,
3751
status,
3852
error: loadError,
39-
} = useFetch<FileDiffResponse>(apiUrl, {
53+
} = useFetch<FileDiffResponse>(() => apiUrl.value!, {
4054
query: apiQuery,
4155
timeout: 15000,
4256
})
@@ -69,13 +83,6 @@ const changeRatioPercent = computed(() => calcPercent(maxChangeRatio.value, 0, 1
6983
const diffDistancePercent = computed(() => calcPercent(maxDiffDistance.value, 1, 60))
7084
const charEditPercent = computed(() => calcPercent(inlineMaxCharEdits.value, 0, 10))
7185
72-
function formatBytes(bytes: number | undefined): string {
73-
if (bytes === undefined) return ''
74-
if (bytes < 1024) return `${bytes} B`
75-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
76-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
77-
}
78-
7986
// Build code browser URL
8087
function getCodeUrl(version: string): string {
8188
return `/package-code/${props.packageName}/v/${version}/${props.file.path}`
@@ -117,13 +124,14 @@ function getCodeUrl(version: string): string {
117124
<!-- File sizes -->
118125
<span v-if="file.oldSize || file.newSize" class="text-xs text-fg-subtle shrink-0">
119126
<template v-if="file.type === 'modified'">
120-
{{ formatBytes(file.oldSize) }} → {{ formatBytes(file.newSize) }}
127+
{{ bytesFormatter.format(file.oldSize ?? 0) }} →
128+
{{ bytesFormatter.format(file.newSize ?? 0) }}
121129
</template>
122130
<template v-else-if="file.type === 'added'">
123-
{{ formatBytes(file.newSize) }}
131+
{{ bytesFormatter.format(file.newSize ?? 0) }}
124132
</template>
125133
<template v-else>
126-
{{ formatBytes(file.oldSize) }}
134+
{{ bytesFormatter.format(file.oldSize ?? 0) }}
127135
</template>
128136
</span>
129137
</div>
@@ -295,8 +303,20 @@ function getCodeUrl(version: string): string {
295303

296304
<!-- Content -->
297305
<div class="flex-1 overflow-auto relative">
306+
<!-- File too large warning -->
307+
<div v-if="isFilesTooLarge" class="py-20 text-center">
308+
<div class="i-lucide:file-text w-12 h-12 mx-auto text-fg-subtle mb-4" />
309+
<p class="text-fg-muted mb-2">{{ $t('compare.file_too_large') }}</p>
310+
<p class="text-fg-subtle text-sm mb-4">
311+
{{
312+
$t('compare.file_size_warning', {
313+
size: bytesFormatter.format(Math.max(file.newSize ?? 0, file.oldSize ?? 0)),
314+
})
315+
}}
316+
</p>
317+
</div>
298318
<!-- Loading state -->
299-
<div v-if="status === 'pending'" class="py-12 text-center">
319+
<div v-else-if="status === 'pending'" class="py-12 text-center">
300320
<div class="i-svg-spinners-ring-resize w-6 h-6 mx-auto text-fg-muted" />
301321
<p class="mt-2 text-sm text-fg-muted">Loading diff...</p>
302322
</div>

0 commit comments

Comments
 (0)