@@ -12,15 +12,6 @@ const chartModal = useModal('chart-modal')
1212const hasChartModalTransitioned = shallowRef (false )
1313const isChartModalOpen = shallowRef (false )
1414
15- async function openChartModal() {
16- isChartModalOpen .value = true
17- hasChartModalTransitioned .value = false
18- // ensure the component renders before opening the dialog
19- await nextTick ()
20- await nextTick ()
21- chartModal .open ()
22- }
23-
2415function handleModalClose() {
2516 isChartModalOpen .value = false
2617 hasChartModalTransitioned .value = false
@@ -96,10 +87,24 @@ const pulseColor = computed(() => {
9687})
9788
9889const weeklyDownloads = shallowRef <WeeklyDownloadPoint []>([])
90+ const isLoadingWeeklyDownloads = shallowRef (true )
91+ const hasWeeklyDownloads = computed (() => weeklyDownloads .value .length > 0 )
92+
93+ async function openChartModal() {
94+ if (! hasWeeklyDownloads .value ) return
95+
96+ isChartModalOpen .value = true
97+ hasChartModalTransitioned .value = false
98+ // ensure the component renders before opening the dialog
99+ await nextTick ()
100+ await nextTick ()
101+ chartModal .open ()
102+ }
99103
100104async function loadWeeklyDownloads() {
101105 if (! import .meta .client ) return
102106
107+ isLoadingWeeklyDownloads .value = true
103108 try {
104109 const result = await fetchPackageDownloadEvolution (
105110 () => props .packageName ,
@@ -109,6 +114,8 @@ async function loadWeeklyDownloads() {
109114 weeklyDownloads .value = (result as WeeklyDownloadPoint []) ?? []
110115 } catch {
111116 weeklyDownloads .value = []
117+ } finally {
118+ isLoadingWeeklyDownloads .value = false
112119 }
113120}
114121
@@ -212,6 +219,7 @@ const config = computed(() => {
212219 <CollapsibleSection id =" downloads" :title =" $t('package.downloads.title')" >
213220 <template #actions >
214221 <ButtonBase
222+ v-if =" hasWeeklyDownloads"
215223 type =" button"
216224 @click =" openChartModal"
217225 class =" text-fg-subtle hover:text-fg transition-colors duration-200 inline-flex items-center justify-center min-w-6 min-h-6 -m-1 p-1 focus-visible:outline-accent/70 rounded"
@@ -223,44 +231,53 @@ const config = computed(() => {
223231 </template >
224232
225233 <div class =" w-full overflow-hidden" >
226- <ClientOnly >
227- <VueUiSparkline class =" w-full max-w-xs" :dataset :config >
228- <template #skeleton >
229- <!-- This empty div overrides the default built-in scanning animation on load -->
230- <div />
231- </template >
232- </VueUiSparkline >
233- <template #fallback >
234- <!-- Skeleton matching sparkline layout: title row + chart with data label -->
235- <div class =" min-h-[75.195px]" >
236- <!-- Title row: date range (24px height) -->
237- <div class =" h-6 flex items-center ps-3" >
238- <SkeletonInline class =" h-3 w-36" />
239- </div >
240- <!-- Chart area: data label left, sparkline right -->
241- <div class =" aspect-[500/80] flex items-center" >
242- <!-- Data label (covers ~42% width) -->
243- <div class =" w-[42%] flex items-center ps-0.5" >
244- <SkeletonInline class =" h-7 w-24" />
234+ <template v-if =" isLoadingWeeklyDownloads || hasWeeklyDownloads " >
235+ <ClientOnly >
236+ <VueUiSparkline class =" w-full max-w-xs" :dataset :config >
237+ <template #skeleton >
238+ <!-- This empty div overrides the default built-in scanning animation on load -->
239+ <div />
240+ </template >
241+ </VueUiSparkline >
242+ <template #fallback >
243+ <!-- Skeleton matching sparkline layout: title row + chart with data label -->
244+ <div class =" min-h-[75.195px]" >
245+ <!-- Title row: date range (24px height) -->
246+ <div class =" h-6 flex items-center ps-3" >
247+ <SkeletonInline class =" h-3 w-36" />
245248 </div >
246- <!-- Sparkline area (~58% width) -->
247- <div class =" flex-1 flex items-end gap-0.5 h-4/5 pe-3" >
248- <SkeletonInline
249- v-for =" i in 16"
250- :key =" i"
251- class =" flex-1 rounded-sm"
252- :style =" { height: `${25 + ((i * 7) % 50)}%` }"
253- />
249+ <!-- Chart area: data label left, sparkline right -->
250+ <div class =" aspect-[500/80] flex items-center" >
251+ <!-- Data label (covers ~42% width) -->
252+ <div class =" w-[42%] flex items-center ps-0.5" >
253+ <SkeletonInline class =" h-7 w-24" />
254+ </div >
255+ <!-- Sparkline area (~58% width) -->
256+ <div class =" flex-1 flex items-end gap-0.5 h-4/5 pe-3" >
257+ <SkeletonInline
258+ v-for =" i in 16"
259+ :key =" i"
260+ class =" flex-1 rounded-sm"
261+ :style =" { height: `${25 + ((i * 7) % 50)}%` }"
262+ />
263+ </div >
254264 </div >
255265 </div >
256- </div >
257- </template >
258- </ClientOnly >
266+ </template >
267+ </ClientOnly >
268+ </template >
269+ <p v-else class =" py-2 text-sm font-mono text-fg-subtle" >
270+ {{ $t('package.downloads.no_data') }}
271+ </p >
259272 </div >
260273 </CollapsibleSection >
261274 </div >
262275
263- <PackageChartModal @close =" handleModalClose" @transitioned =" handleModalTransitioned" >
276+ <PackageChartModal
277+ v-if =" isChartModalOpen && hasWeeklyDownloads"
278+ @close =" handleModalClose"
279+ @transitioned =" handleModalTransitioned"
280+ >
264281 <!-- The Chart is mounted after the dialog has transitioned -->
265282 <!-- This avoids flaky behavior that hides the chart's minimap half of the time -->
266283 <Transition name =" opacity" mode =" out-in" >
0 commit comments