@@ -4,6 +4,8 @@ import type { VueUiXyDatasetItem } from 'vue-data-ui'
44import { VueUiXy } from ' vue-data-ui/vue-ui-xy'
55import { useDebounceFn } from ' @vueuse/core'
66
7+ const { t } = useI18n ()
8+
79const {
810 weeklyDownloads,
911 inModal = false ,
@@ -195,7 +197,7 @@ function initDateRangeFallbackClient() {
195197}
196198
197199watch (
198- () => weeklyDownloads ,
200+ () => weeklyDownloads ?. length ,
199201 () => {
200202 initDateRangeFromWeekly ()
201203 initDateRangeFallbackClient ()
@@ -323,6 +325,9 @@ async function load() {
323325
324326 evolution .value = (result as EvolutionData ) ?? []
325327 displayedGranularity .value = selectedGranularity .value
328+ } catch {
329+ if (currentToken !== requestToken ) return
330+ evolution .value = []
326331 } finally {
327332 if (currentToken === requestToken ) {
328333 pending .value = false
@@ -393,7 +398,7 @@ const config = computed(() => ({
393398 grid: {
394399 labels: {
395400 axis: {
396- yLabel: ` ${ selectedGranularity .value } downloads ` ,
401+ yLabel: t ( ' package.downloads.y_axis_label ' , { granularity: selectedGranularity .value }) ,
397402 xLabel: packageName ,
398403 yLabelOffsetX: 12 ,
399404 fontSize: 24 ,
@@ -453,153 +458,192 @@ const config = computed(() => ({
453458
454459<template >
455460 <div class =" w-full relative" >
456- <div class =" w-full mb-2 flex flex-col gap-2" >
457- <div class =" w-full grid grid-cols-1 sm:grid-cols-[1fr_auto] gap-2" >
458- <div class =" flex gap-2" >
459- <!-- Granularity -->
461+ <div class =" w-full mb-4 flex flex-col gap-3" >
462+ <!-- Mobile: stack vertically, Desktop: horizontal -->
463+ <div class =" flex flex-col sm:flex-row gap-3 sm:gap-2 sm:items-end" >
464+ <!-- Granularity -->
465+ <div class =" flex flex-col gap-1 sm:shrink-0" >
466+ <label
467+ for =" granularity"
468+ class =" text-[10px] font-mono text-fg-subtle tracking-wide uppercase"
469+ >
470+ {{ t('package.downloads.granularity') }}
471+ </label >
472+
473+ <div
474+ class =" flex items-center px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-fg/50)"
475+ >
476+ <select
477+ id =" granularity"
478+ v-model =" selectedGranularity"
479+ class =" w-full bg-transparent font-mono text-sm text-fg outline-none"
480+ >
481+ <option value =" daily" >{{ t('package.downloads.granularity_daily') }}</option >
482+ <option value =" weekly" >{{ t('package.downloads.granularity_weekly') }}</option >
483+ <option value =" monthly" >{{ t('package.downloads.granularity_monthly') }}</option >
484+ <option value =" yearly" >{{ t('package.downloads.granularity_yearly') }}</option >
485+ </select >
486+ </div >
487+ </div >
488+
489+ <!-- Date range inputs -->
490+ <div class =" grid grid-cols-2 gap-2 flex-1" >
460491 <div class =" flex flex-col gap-1" >
461492 <label
462- for =" granularity "
493+ for =" startDate "
463494 class =" text-[10px] font-mono text-fg-subtle tracking-wide uppercase"
464495 >
465- Granularity
496+ {{ t('package.downloads.start_date') }}
466497 </label >
467-
468498 <div
469- class =" flex items-center px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-fg/50)"
499+ class =" flex items-center gap-2 px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-fg/50)"
470500 >
471- <select
472- id =" granularity"
473- v-model =" selectedGranularity"
474- class =" w-full bg-transparent font-mono text-sm text-fg outline-none"
475- >
476- <option value =" daily" >Daily</option >
477- <option value =" weekly" >Weekly</option >
478- <option value =" monthly" >Monthly</option >
479- <option value =" yearly" >Yearly</option >
480- </select >
501+ <span class =" i-carbon-calendar w-4 h-4 text-fg-subtle shrink-0" aria-hidden =" true" />
502+ <input
503+ id =" startDate"
504+ v-model =" startDate"
505+ type =" date"
506+ class =" w-full min-w-0 bg-transparent font-mono text-sm text-fg outline-none [color-scheme:dark]"
507+ />
481508 </div >
482509 </div >
483510
484- <!-- Date range inputs -->
485- <div class =" grid grid-cols-2 gap-2" >
486- <div class =" flex flex-col gap-1" >
487- <label
488- for =" startDate"
489- class =" text-[10px] font-mono text-fg-subtle tracking-wide uppercase"
490- >
491- Start
492- </label >
493- <div
494- class =" flex items-center gap-2 px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-fg/50)"
495- >
496- <span class =" i-carbon-calendar w-4 h-4 text-fg-subtle" aria-hidden =" true" />
497- <input
498- id =" startDate"
499- v-model =" startDate"
500- type =" date"
501- class =" w-full bg-transparent font-mono text-sm text-fg outline-none [color-scheme:dark]"
502- />
503- </div >
504- </div >
505-
506- <div class =" flex flex-col gap-1" >
507- <label
508- for =" endDate"
509- class =" text-[10px] font-mono text-fg-subtle tracking-wide uppercase"
510- >
511- End
512- </label >
513- <div
514- class =" flex items-center gap-2 px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-fg/50)"
515- >
516- <span class =" i-carbon-calendar w-4 h-4 text-fg-subtle" aria-hidden =" true" />
517- <input
518- id =" endDate"
519- v-model =" endDate"
520- type =" date"
521- class =" w-full bg-transparent font-mono text-sm text-fg outline-none [color-scheme:dark]"
522- />
523- </div >
524- </div >
525- </div >
526- <!-- Reset -->
527511 <div class =" flex flex-col gap-1" >
528- <!-- spacer label to align with others -->
529- <div class =" font-mono tracking-wide uppercase invisible" >Reset</div >
530-
531- <button
532- v-if =" showResetButton"
533- type =" button"
534- title =" Reset date range"
535- class =" flex items-center justify-center px-2.5 py-1.75 border border-transparent rounded-md text-fg-subtle hover:text-fg transition-colors hover:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
536- @click ="
537- () => {
538- hasUserEditedDates = false
539- startDate = ''
540- endDate = ''
541- initDateRangeFromWeekly()
542- initDateRangeFallbackClient()
543- }
544- "
512+ <label
513+ for =" endDate"
514+ class =" text-[10px] font-mono text-fg-subtle tracking-wide uppercase"
515+ >
516+ {{ t('package.downloads.end_date') }}
517+ </label >
518+ <div
519+ class =" flex items-center gap-2 px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-fg/50)"
545520 >
546- <span class =" i-carbon-reset w-5 h-5 inline-block" />
547- </button >
521+ <span class =" i-carbon-calendar w-4 h-4 text-fg-subtle shrink-0" aria-hidden =" true" />
522+ <input
523+ id =" endDate"
524+ v-model =" endDate"
525+ type =" date"
526+ class =" w-full min-w-0 bg-transparent font-mono text-sm text-fg outline-none [color-scheme:dark]"
527+ />
528+ </div >
548529 </div >
549530 </div >
531+
532+ <!-- Reset button -->
533+ <button
534+ v-if =" showResetButton"
535+ type =" button"
536+ aria-label =" Reset date range"
537+ class =" self-end flex items-center justify-center px-2.5 py-1.75 border border-transparent rounded-md text-fg-subtle hover:text-fg transition-colors hover:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 sm:mb-0"
538+ @click ="
539+ () => {
540+ hasUserEditedDates = false
541+ startDate = ''
542+ endDate = ''
543+ initDateRangeFromWeekly()
544+ initDateRangeFallbackClient()
545+ }
546+ "
547+ >
548+ <span class =" i-carbon-reset w-5 h-5 inline-block" aria-hidden =" true" />
549+ </button >
550550 </div >
551551 </div >
552552
553- <ClientOnly v-if =" inModal" >
553+ <ClientOnly v-if =" inModal && chartData.dataset " >
554554 <VueUiXy :dataset =" chartData.dataset" :config =" config" >
555555 <template #menuIcon =" { isOpen } " >
556- <span class =" i-carbon-close w-6 h-6" v-if = " isOpen " />
557- <span class =" i-carbon-overflow-menu-vertical w-6 h-6" v-else />
556+ <span v-if = " isOpen " class =" i-carbon-close w-6 h-6" aria-hidden = " true " />
557+ <span v-else class =" i-carbon-overflow-menu-vertical w-6 h-6" aria-hidden = " true " />
558558 </template >
559559 <template #optionCsv >
560- <span class =" i-carbon-csv w-6 h-6 text-fg-subtle" style =" pointer-events : none " />
560+ <span
561+ class =" i-carbon-csv w-6 h-6 text-fg-subtle"
562+ style =" pointer-events : none "
563+ aria-hidden =" true"
564+ />
561565 </template >
562566 <template #optionImg >
563- <span class =" i-carbon-png w-6 h-6 text-fg-subtle" style =" pointer-events : none " />
567+ <span
568+ class =" i-carbon-png w-6 h-6 text-fg-subtle"
569+ style =" pointer-events : none "
570+ aria-hidden =" true"
571+ />
564572 </template >
565573 <template #optionSvg >
566- <span class =" i-carbon-svg w-6 h-6 text-fg-subtle" style =" pointer-events : none " />
574+ <span
575+ class =" i-carbon-svg w-6 h-6 text-fg-subtle"
576+ style =" pointer-events : none "
577+ aria-hidden =" true"
578+ />
567579 </template >
568580
569581 <template #annotator-action-close >
570- <span class =" i-carbon-close w-6 h-6 text-fg-subtle" style =" pointer-events : none " />
582+ <span
583+ class =" i-carbon-close w-6 h-6 text-fg-subtle"
584+ style =" pointer-events : none "
585+ aria-hidden =" true"
586+ />
571587 </template >
572588 <template #annotator-action-color =" { color } " >
573- <span class =" i-carbon-color-palette w-6 h-6" :style =" { color }" />
589+ <span class =" i-carbon-color-palette w-6 h-6" :style =" { color }" aria-hidden = " true " />
574590 </template >
575- <template #annotator-action-undo =" { disabled } " >
576- <span class =" i-carbon-undo w-6 h-6 text-fg-subtle" style =" pointer-events : none " />
591+ <template #annotator-action-undo >
592+ <span
593+ class =" i-carbon-undo w-6 h-6 text-fg-subtle"
594+ style =" pointer-events : none "
595+ aria-hidden =" true"
596+ />
577597 </template >
578- <template #annotator-action-redo =" { disabled } " >
579- <span class =" i-carbon-redo w-6 h-6 text-fg-subtle" style =" pointer-events : none " />
598+ <template #annotator-action-redo >
599+ <span
600+ class =" i-carbon-redo w-6 h-6 text-fg-subtle"
601+ style =" pointer-events : none "
602+ aria-hidden =" true"
603+ />
580604 </template >
581- <template #annotator-action-delete =" { disabled } " >
582- <span class =" i-carbon-trash-can w-6 h-6 text-fg-subtle" style =" pointer-events : none " />
605+ <template #annotator-action-delete >
606+ <span
607+ class =" i-carbon-trash-can w-6 h-6 text-fg-subtle"
608+ style =" pointer-events : none "
609+ aria-hidden =" true"
610+ />
583611 </template >
584612 <template #optionAnnotator =" { isAnnotator } " >
585613 <span
614+ v-if =" isAnnotator"
586615 class =" i-carbon-edit-off w-6 h-6 text-fg-subtle"
587616 style =" pointer-events : none "
588- v-if =" isAnnotator"
617+ aria-hidden =" true"
618+ />
619+ <span
620+ v-else
621+ class =" i-carbon-edit w-6 h-6 text-fg-subtle"
622+ style =" pointer-events : none "
623+ aria-hidden =" true"
589624 />
590- <span class =" i-carbon-edit w-6 h-6 text-fg-subtle" style =" pointer-events : none " v-else />
591625 </template >
592626 </VueUiXy >
593627 <template #fallback >
594628 <div class =" min-h-[260px]" />
595629 </template >
596630 </ClientOnly >
597631
632+ <!-- Empty state when no chart data -->
633+ <div
634+ v-if =" inModal && !chartData.dataset && !pending"
635+ class =" min-h-[260px] flex items-center justify-center text-fg-subtle font-mono text-sm"
636+ >
637+ {{ t('package.downloads.no_data') }}
638+ </div >
639+
598640 <div
599641 v-if =" pending"
642+ role =" status"
643+ aria-live =" polite"
600644 class =" absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-xs text-fg-subtle font-mono bg-bg/70 backdrop-blur px-3 py-2 rounded-md border border-border"
601645 >
602- Loading…
646+ {{ t('package.downloads.loading') }}
603647 </div >
604648 </div >
605649</template >
0 commit comments