11<script setup lang="ts">
22import type { SlimPackumentVersion } from ' #shared/types'
3- import { onClickOutside , useEventListener , useMediaQuery } from ' @vueuse/core'
43
5- const props = withDefaults (
6- defineProps <{
7- packageName: string
8- version: SlimPackumentVersion
9- size? : ' small' | ' medium'
10- }>(),
11- {
12- size: ' medium' ,
13- },
14- )
4+ const props = defineProps <{
5+ packageName: string
6+ version: SlimPackumentVersion
7+ }>()
158
16- const triggerRef = useTemplateRef (' triggerRef' )
17- const listRef = useTemplateRef (' listRef' )
18- const isOpen = shallowRef (false )
19- const highlightedIndex = shallowRef (- 1 )
20- const dropdownPosition = shallowRef <{ top: number ; left: number } | null >(null )
21-
22- const menuId = ' download-menu'
23- const menuItems = computed (() => {
24- const items: { id: string ; icon: string ; disabled: boolean }[] = [
25- { id: ' package' , icon: ' i-lucide:package' , disabled: false },
26- ]
27- return items
28- })
29-
30- const prefersReducedMotion = useMediaQuery (' (prefers-reduced-motion: reduce)' )
31-
32- function getDropdownStyle(): Record <string , string > {
33- if (! dropdownPosition .value ) return {}
34- return {
35- top: ` ${dropdownPosition .value .top }px ` ,
36- left: ` ${dropdownPosition .value .left }px ` ,
37- }
38- }
39-
40- function toggle() {
41- if (isOpen .value ) {
42- close ()
43- } else {
44- const rect = triggerRef .value ?.$el ?.getBoundingClientRect ()
45- if (rect ) {
46- dropdownPosition .value = {
47- top: rect .bottom + 4 ,
48- left: rect .left ,
49- }
50- }
51- isOpen .value = true
52- highlightedIndex .value = 0
53- }
54- }
55-
56- function close() {
57- isOpen .value = false
58- highlightedIndex .value = - 1
59- }
60-
61- onClickOutside (listRef , close , { ignore: [triggerRef ] })
62-
63- function handleKeydown(event : KeyboardEvent ) {
64- if (! isOpen .value ) {
65- if (event .key === ' ArrowDown' || event .key === ' Enter' || event .key === ' ' ) {
66- event .preventDefault ()
67- toggle ()
68- }
69- return
70- }
71-
72- switch (event .key ) {
73- case ' ArrowDown' :
74- event .preventDefault ()
75- highlightedIndex .value = (highlightedIndex .value + 1 ) % menuItems .value .length
76- break
77- case ' ArrowUp' :
78- event .preventDefault ()
79- highlightedIndex .value =
80- highlightedIndex .value <= 0 ? menuItems .value .length - 1 : highlightedIndex .value - 1
81- break
82- case ' Enter' :
83- case ' ' :
84- event .preventDefault ()
85- handleAction (menuItems .value [highlightedIndex .value ])
86- break
87- case ' Escape' :
88- event .preventDefault ()
89- close ()
90- triggerRef .value ?.$el ?.focus ()
91- break
92- case ' Tab' :
93- close ()
94- break
95- }
96- }
97-
98- function handleAction(item : (typeof menuItems .value )[number ] | undefined ) {
99- if (! item || item .disabled ) return
100- if (item .id === ' package' ) {
101- downloadPackage ()
102- }
103- close ()
104- }
9+ const loading = shallowRef (false )
10510
10611async function downloadPackage() {
10712 const tarballUrl = props .version .dist .tarball
10813 if (! tarballUrl ) return
10914
15+ if (loading .value ) return
16+ loading .value = true
17+
11018 try {
11119 const response = await fetch (tarballUrl )
11220 if (! response .ok ) {
21+ loading .value = false
11322 throw new Error (` Failed to fetch tarball (${response .status }) ` )
11423 }
11524 const blob = await response .blob ()
@@ -130,80 +39,26 @@ async function downloadPackage() {
13039 link .click ()
13140 document .body .removeChild (link )
13241 }
133- }
13442
135- useEventListener (' scroll' , () => isOpen .value && close (), { passive: true })
136-
137- defineOptions ({
138- inheritAttrs: false ,
139- })
43+ loading .value = false
44+ }
14045 </script >
14146
14247<template >
143- <ButtonBase
144- ref =" triggerRef"
145- v-bind =" $attrs"
146- type =" button"
147- :variant =" size === 'small' ? 'subtle' : 'secondary'"
148- :size
149- classicon =" i-lucide:download"
150- :aria-expanded =" isOpen"
151- aria-haspopup =" menu"
152- :aria-controls =" menuId"
153- @click =" toggle"
154- @keydown =" handleKeydown"
155- >
156- {{ $t('package.download.button') }}
157- <span
158- class =" i-lucide:chevron-down ms-1"
159- :class =" [
160- size === 'small' ? 'w-3 h-3' : 'w-3.5 h-3.5',
161- { 'rotate-180': isOpen },
162- prefersReducedMotion ? '' : 'transition-transform duration-200',
163- ]"
164- aria-hidden =" true"
165- />
166- </ButtonBase >
167-
168- <Teleport to =" body" >
169- <Transition
170- :enter-active-class =" prefersReducedMotion ? '' : 'transition-opacity duration-150'"
171- :enter-from-class =" prefersReducedMotion ? '' : 'opacity-0'"
172- enter-to-class =" opacity-100"
173- :leave-active-class =" prefersReducedMotion ? '' : 'transition-opacity duration-100'"
174- leave-from-class =" opacity-100"
175- :leave-to-class =" prefersReducedMotion ? '' : 'opacity-0'"
48+ <TooltipApp :text =" $t('package.download.tarball')" >
49+ <ButtonBase
50+ ref =" triggerRef"
51+ v-bind =" $attrs"
52+ type =" button"
53+ @click =" downloadPackage"
54+ :disabled =" loading"
55+ class =" border-border-subtle bg-bg-subtle! text-fg-muted hover:enabled:(text-fg border-border-hover)"
17656 >
177- <ul
178- v-if =" isOpen"
179- :id =" menuId"
180- ref =" listRef"
181- role =" menu"
182- :aria-label =" $t('package.download.button')"
183- :style =" getDropdownStyle()"
184- class =" fixed bg-bg-subtle border border-border rounded-md shadow-lg z-50"
185- @keydown =" handleKeydown"
186- >
187- <li
188- v-for =" (item, index) in menuItems"
189- :key =" item.id"
190- role =" menuitem"
191- :aria-disabled =" item.disabled || undefined"
192- class =" flex items-center gap-2 px-3 py-1.5 text-sm transition-colors duration-150"
193- :class =" [
194- item.disabled
195- ? 'cursor-default text-fg-muted/50'
196- : highlightedIndex === index
197- ? 'cursor-pointer bg-bg-elevated text-fg'
198- : 'cursor-pointer text-fg-muted hover:bg-bg-elevated hover:text-fg',
199- ]"
200- @click =" handleAction(item)"
201- @mouseenter =" highlightedIndex = index"
202- >
203- <span :class =" item.icon" class =" w-4 h-4" aria-hidden =" true" />
204- {{ $t('package.download.package') }}
205- </li >
206- </ul >
207- </Transition >
208- </Teleport >
57+ <span
58+ class =" size-[1em]"
59+ aria-hidden =" true"
60+ :class =" loading ? 'i-lucide:loader-circle animate-spin' : 'i-lucide:download'"
61+ />
62+ </ButtonBase >
63+ </TooltipApp >
20964</template >
0 commit comments