Skip to content

Commit 8f810a0

Browse files
committed
Merge branch 'main' into refactor/generalize-i18n-downloads
2 parents c18de6b + 188e887 commit 8f810a0

File tree

7 files changed

+239
-139
lines changed

7 files changed

+239
-139
lines changed
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
<template>
2-
<div class="flex items-center justify-end gap-2 px-2 py-1.5 min-w-24">
3-
<span class="font-mono text-sm text-fg-muted">{{ $t('account_menu.connect') }}</span>
4-
<span class="i-carbon-chevron-down w-3 h-3 text-fg-muted" aria-hidden="true" />
2+
<div class="relative flex min-w-24 justify-end">
3+
<div
4+
class="inline-flex gap-x-1 items-center justify-center font-mono border border-border rounded-md text-sm px-4 py-2 bg-transparent text-fg border-none"
5+
>
6+
<span class="font-mono text-sm text-fg-muted">{{ $t('account_menu.connect') }}</span>
7+
<span class="i-carbon-chevron-down w-3 h-3 text-fg-muted" aria-hidden="true" />
8+
</div>
59
</div>
610
</template>

app/components/Header/MobileMenu.client.vue

Lines changed: 99 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -99,28 +99,84 @@ onUnmounted(deactivate)
9999
</button>
100100
</div>
101101

102-
<!-- Navigation links -->
103-
<div class="flex-1 overflow-y-auto overscroll-contain py-2">
104-
<!-- Main navigation -->
105-
<div class="px-2 py-2">
106-
<NuxtLink
107-
:to="{ name: 'about' }"
108-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
109-
@click="closeMenu"
102+
<!-- Account section -->
103+
<div class="px-2 py-2">
104+
<span
105+
class="px-3 py-2 block font-mono text-xs text-fg-subtle uppercase tracking-wider"
106+
>
107+
{{ $t('account_menu.account') }}
108+
</span>
109+
110+
<!-- npm CLI connection status (only show if connected) -->
111+
<button
112+
v-if="isConnected && npmUser"
113+
type="button"
114+
class="w-full flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200 text-start"
115+
@click="handleShowConnector"
116+
>
117+
<img
118+
v-if="npmAvatar"
119+
:src="npmAvatar"
120+
:alt="npmUser"
121+
width="20"
122+
height="20"
123+
class="w-5 h-5 rounded-full object-cover"
124+
/>
125+
<span
126+
v-else
127+
class="w-5 h-5 rounded-full bg-bg-muted flex items-center justify-center"
110128
>
111-
<span class="i-carbon:information w-5 h-5 text-fg-muted" aria-hidden="true" />
112-
{{ $t('footer.about') }}
113-
</NuxtLink>
129+
<span class="i-carbon-terminal w-3 h-3 text-fg-muted" aria-hidden="true" />
130+
</span>
131+
<span class="flex-1">~{{ npmUser }}</span>
132+
<span class="w-2 h-2 rounded-full bg-green-500" aria-hidden="true" />
133+
</button>
114134

115-
<NuxtLink
116-
:to="{ name: 'privacy' }"
117-
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
118-
@click="closeMenu"
135+
<!-- Atmosphere connection status -->
136+
<button
137+
v-if="atprotoUser"
138+
type="button"
139+
class="w-full flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200 text-start"
140+
@click="handleShowAuth"
141+
>
142+
<img
143+
v-if="atprotoUser.avatar"
144+
:src="atprotoUser.avatar"
145+
:alt="atprotoUser.handle"
146+
width="20"
147+
height="20"
148+
class="w-5 h-5 rounded-full object-cover"
149+
/>
150+
<span
151+
v-else
152+
class="w-5 h-5 rounded-full bg-bg-muted flex items-center justify-center"
119153
>
120-
<span class="i-carbon:security w-5 h-5 text-fg-muted" aria-hidden="true" />
121-
{{ $t('privacy_policy.title') }}
122-
</NuxtLink>
154+
<span class="i-carbon-cloud w-3 h-3 text-fg-muted" aria-hidden="true" />
155+
</span>
156+
<span class="flex-1 truncate">@{{ atprotoUser.handle }}</span>
157+
</button>
123158

159+
<!-- Connect Atmosphere button (show if not connected) -->
160+
<button
161+
v-else
162+
type="button"
163+
class="w-full flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200 text-start"
164+
@click="handleShowAuth"
165+
>
166+
<span class="w-5 h-5 rounded-full bg-bg-muted flex items-center justify-center">
167+
<span class="i-carbon-cloud w-3 h-3 text-fg-muted" aria-hidden="true" />
168+
</span>
169+
<span class="flex-1">{{ $t('account_menu.connect_atmosphere') }}</span>
170+
</button>
171+
</div>
172+
173+
<!-- Divider -->
174+
<div class="mx-4 my-2 border-t border-border" />
175+
176+
<!-- Navigation links -->
177+
<div class="flex-1 overflow-y-auto overscroll-contain py-2">
178+
<!-- App navigation -->
179+
<div class="px-2 py-2">
124180
<NuxtLink
125181
:to="{ name: 'compare' }"
126182
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
@@ -164,7 +220,31 @@ onUnmounted(deactivate)
164220
<!-- Divider -->
165221
<div class="mx-4 my-2 border-t border-border" />
166222

167-
<!-- External links (from footer) -->
223+
<!-- Informational links -->
224+
<div class="px-2 py-2">
225+
<NuxtLink
226+
:to="{ name: 'about' }"
227+
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
228+
@click="closeMenu"
229+
>
230+
<span class="i-carbon:information w-5 h-5 text-fg-muted" aria-hidden="true" />
231+
{{ $t('footer.about') }}
232+
</NuxtLink>
233+
234+
<NuxtLink
235+
:to="{ name: 'privacy' }"
236+
class="flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200"
237+
@click="closeMenu"
238+
>
239+
<span class="i-carbon:security w-5 h-5 text-fg-muted" aria-hidden="true" />
240+
{{ $t('privacy_policy.title') }}
241+
</NuxtLink>
242+
</div>
243+
244+
<!-- Divider -->
245+
<div class="mx-4 my-2 border-t border-border" />
246+
247+
<!-- External links -->
168248
<div class="px-2 py-2">
169249
<span class="px-3 py-2 font-mono text-xs text-fg-subtle uppercase tracking-wider">
170250
{{ $t('nav.links') }}
@@ -227,80 +307,6 @@ onUnmounted(deactivate)
227307
</a>
228308
</div>
229309
</div>
230-
231-
<!-- Divider -->
232-
<div class="mx-4 my-2 border-t border-border" />
233-
234-
<!-- Account section -->
235-
<div class="px-2 py-2">
236-
<span
237-
class="px-3 py-2 block font-mono text-xs text-fg-subtle uppercase tracking-wider"
238-
>
239-
{{ $t('account_menu.account') }}
240-
</span>
241-
242-
<!-- npm CLI connection status (only show if connected) -->
243-
<button
244-
v-if="isConnected && npmUser"
245-
type="button"
246-
class="w-full flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200 text-start"
247-
@click="handleShowConnector"
248-
>
249-
<img
250-
v-if="npmAvatar"
251-
:src="npmAvatar"
252-
:alt="npmUser"
253-
width="20"
254-
height="20"
255-
class="w-5 h-5 rounded-full object-cover"
256-
/>
257-
<span
258-
v-else
259-
class="w-5 h-5 rounded-full bg-bg-muted flex items-center justify-center"
260-
>
261-
<span class="i-carbon-terminal w-3 h-3 text-fg-muted" aria-hidden="true" />
262-
</span>
263-
<span class="flex-1">~{{ npmUser }}</span>
264-
<span class="w-2 h-2 rounded-full bg-green-500" aria-hidden="true" />
265-
</button>
266-
267-
<!-- Atmosphere connection status -->
268-
<button
269-
v-if="atprotoUser"
270-
type="button"
271-
class="w-full flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200 text-start"
272-
@click="handleShowAuth"
273-
>
274-
<img
275-
v-if="atprotoUser.avatar"
276-
:src="atprotoUser.avatar"
277-
:alt="atprotoUser.handle"
278-
width="20"
279-
height="20"
280-
class="w-5 h-5 rounded-full object-cover"
281-
/>
282-
<span
283-
v-else
284-
class="w-5 h-5 rounded-full bg-bg-muted flex items-center justify-center"
285-
>
286-
<span class="i-carbon-cloud w-3 h-3 text-fg-muted" aria-hidden="true" />
287-
</span>
288-
<span class="flex-1 truncate">@{{ atprotoUser.handle }}</span>
289-
</button>
290-
291-
<!-- Connect Atmosphere button (show if not connected) -->
292-
<button
293-
v-else
294-
type="button"
295-
class="w-full flex items-center gap-3 px-3 py-3 rounded-md font-mono text-sm text-fg hover:bg-bg-subtle transition-colors duration-200 text-start"
296-
@click="handleShowAuth"
297-
>
298-
<span class="w-5 h-5 rounded-full bg-bg-muted flex items-center justify-center">
299-
<span class="i-carbon-cloud w-3 h-3 text-fg-muted" aria-hidden="true" />
300-
</span>
301-
<span class="flex-1">{{ $t('account_menu.connect_atmosphere') }}</span>
302-
</button>
303-
</div>
304310
</nav>
305311
</Transition>
306312
</div>

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,6 @@ const chartModal = useModal('chart-modal')
1212
const hasChartModalTransitioned = shallowRef(false)
1313
const 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-
2415
function handleModalClose() {
2516
isChartModalOpen.value = false
2617
hasChartModalTransitioned.value = false
@@ -96,10 +87,24 @@ const pulseColor = computed(() => {
9687
})
9788
9889
const 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
100104
async 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">

app/components/Settings/AccentColorPicker.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ onPrehydrate(el => {
1010
const input = el.querySelector<HTMLInputElement>(`input[value="${id}"]`)
1111
if (input) {
1212
input.checked = true
13+
input.setAttribute('checked', '')
14+
}
15+
// Remove checked from the server-default (clear button, value="")
16+
const clearInput = el.querySelector<HTMLInputElement>('input[value=""]')
17+
if (clearInput) {
18+
clearInput.checked = false
19+
clearInput.removeAttribute('checked')
1320
}
1421
}
1522
})

app/components/Settings/BgThemePicker.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ onPrehydrate(el => {
55
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
66
const id = settings.preferredBackgroundTheme
77
if (id) {
8-
const input = el.querySelector<HTMLInputElement>(`input[value="${id || 'neutral'}"]`)
8+
const input = el.querySelector<HTMLInputElement>(`input[value="${id}"]`)
99
if (input) {
1010
input.checked = true
11+
input.setAttribute('checked', '')
1112
}
1213
}
1314
})

0 commit comments

Comments
 (0)