Skip to content

Commit a9963b6

Browse files
committed
Merge branch 'main' into feat-production-setup
# Conflicts: # app/components/AppHeader.vue
2 parents 5a86e64 + cbfc01e commit a9963b6

76 files changed

Lines changed: 3823 additions & 920 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/autofix.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ jobs:
4242
- name: 🏃 Update component test snapshots
4343
run: pnpm test:nuxt -u
4444

45-
- name: 🖥️ Update browser test snapshots
46-
run: pnpm test:browser --update-snapshots
45+
# TODO: re-enable when we have snapshots in browser tests
46+
# - name: 🖥️ Update browser test snapshots
47+
# run: pnpm test:browser --update-snapshots
4748

4849
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization.
242242
### Approach
243243

244244
- All user-facing strings should use translation keys via `$t()` in templates and script
245-
- Translation files live in `i18n/locales/` (e.g., `en-US.json`)
245+
- Translation files live in [`i18n/locales/`](i18n/locales) (e.g., `en-US.json`)
246246
- We use the `no_prefix` strategy (no `/en-US/` or `/fr-FR/` in URLs)
247247
- Locale preference is stored in cookies and respected on subsequent visits
248248

app/app.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
11
<script setup lang="ts">
2+
import type { Directions } from '@nuxtjs/i18n'
23
import { useEventListener } from '@vueuse/core'
34
45
const route = useRoute()
56
const router = useRouter()
7+
const { locale, locales } = useI18n()
68
79
// Initialize accent color before hydration to prevent flash
810
initAccentOnPrehydrate()
911
1012
const isHomepage = computed(() => route.name === 'index')
1113
14+
const localeMap = locales.value.reduce(
15+
(acc, l) => {
16+
acc[l.code] = l.dir ?? 'ltr'
17+
return acc
18+
},
19+
{} as Record<string, Directions>,
20+
)
21+
1222
useHead({
23+
htmlAttrs: {
24+
lang: () => locale.value,
25+
dir: () => localeMap[locale.value] ?? 'ltr',
26+
},
1327
titleTemplate: titleChunk => {
1428
return titleChunk ? titleChunk : 'npmx - Better npm Package Browser'
1529
},
1630
})
1731
32+
if (import.meta.server) {
33+
setJsonLd(createWebSiteSchema())
34+
}
35+
1836
// Global keyboard shortcut: "/" focuses search or navigates to search page
1937
function handleGlobalKeydown(e: KeyboardEvent) {
2038
const target = e.target as HTMLElement

app/components/AccentColorPicker.vue

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,37 @@ const { accentColors, selectedAccentColor, setAccentColor } = useAccentColor()
55
</script>
66

77
<template>
8-
<div role="listbox" :aria-label="$t('settings.accent_colors')" class="flex items-center gap-4">
9-
<button
8+
<fieldset class="flex items-center gap-4">
9+
<legend class="sr-only">{{ $t('settings.accent_colors') }}</legend>
10+
<label
1011
v-for="color in accentColors"
1112
:key="color.id"
12-
type="button"
13-
role="option"
14-
:aria-selected="selectedAccentColor === color.id"
15-
:aria-label="color.name"
16-
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 focus-ring aria-selected:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle)"
13+
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 cursor-pointer has-[:checked]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) has-[:focus-visible]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle)"
1714
:style="{ backgroundColor: color.value }"
18-
@click="setAccentColor(color.id)"
19-
/>
20-
<button
21-
type="button"
22-
:aria-label="$t('settings.clear_accent')"
23-
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 focus-ring flex items-center justify-center bg-accent-fallback"
24-
@click="setAccentColor(null)"
2515
>
16+
<input
17+
type="radio"
18+
name="accent-color"
19+
class="sr-only"
20+
:value="color.id"
21+
:checked="selectedAccentColor === color.id"
22+
:aria-label="color.name"
23+
@change="setAccentColor(color.id)"
24+
/>
25+
</label>
26+
<label
27+
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 cursor-pointer has-[:checked]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) has-[:focus-visible]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) flex items-center justify-center bg-accent-fallback"
28+
>
29+
<input
30+
type="radio"
31+
name="accent-color"
32+
class="sr-only"
33+
value=""
34+
:checked="selectedAccentColor === null"
35+
:aria-label="$t('settings.clear_accent')"
36+
@change="setAccentColor(null)"
37+
/>
2638
<span class="i-carbon-error size-4 text-bg" aria-hidden="true" />
27-
</button>
28-
</div>
39+
</label>
40+
</fieldset>
2941
</template>

app/components/AppFooter.vue

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,51 @@
11
<template>
22
<footer class="border-t border-border mt-auto">
33
<div class="container py-3 sm:py-8 flex flex-col gap-2 sm:gap-4 text-fg-subtle text-sm">
4-
<div class="flex flex-col sm:flex-row items-center justify-start gap-2 sm:gap-4">
5-
<p class="font-mono m-0 hidden sm:block">{{ $t('tagline') }}</p>
6-
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
7-
<div class="flex items-center justify-start gap-3 sm:gap-6">
4+
<div
5+
class="flex flex-col sm:flex-row items-center sm:items-baseline justify-between gap-2 sm:gap-4"
6+
>
7+
<p class="font-mono text-balance m-0 hidden sm:block">{{ $t('tagline') }}</p>
8+
<div class="flex items-center gap-3 sm:gap-6">
89
<NuxtLink
910
to="/about"
1011
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center"
1112
>
12-
<span>{{ $t('footer.about') }}</span>
13+
{{ $t('footer.about') }}
1314
</NuxtLink>
1415
<a
1516
href="https://docs.npmx.dev"
1617
target="_blank"
1718
rel="noopener noreferrer"
18-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center justify-start gap-1"
19+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
1920
>
20-
<span>{{ $t('footer.docs') }}</span>
21+
{{ $t('footer.docs') }}
2122
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
2223
</a>
2324
<a
2425
href="https://repo.npmx.dev"
2526
target="_blank"
2627
rel="noopener noreferrer"
27-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex justify-start items-center gap-1"
28+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
2829
>
29-
<span>{{ $t('footer.source') }}</span>
30+
{{ $t('footer.source') }}
3031
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
3132
</a>
3233
<a
3334
href="https://social.npmx.dev"
3435
target="_blank"
3536
rel="noopener noreferrer"
36-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center justify-start gap-1"
37+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
3738
>
38-
<span>{{ $t('footer.social') }}</span>
39+
{{ $t('footer.social') }}
3940
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
4041
</a>
4142
<a
4243
href="https://chat.npmx.dev"
4344
target="_blank"
4445
rel="noopener noreferrer"
45-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center justify-start gap-1"
46+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
4647
>
47-
<span>{{ $t('footer.chat') }}</span>
48+
{{ $t('footer.chat') }}
4849
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
4950
</a>
5051
</div>

app/components/AppHeader.vue

Lines changed: 18 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
<script setup lang="ts">
2-
import { debounce } from 'perfect-debounce'
3-
42
withDefaults(
53
defineProps<{
64
showLogo?: boolean
@@ -21,23 +19,7 @@ const buildInfo = useAppConfig().buildInfo
2119
const searchQuery = ref('')
2220
const isSearchFocused = ref(false)
2321
24-
const showSearchBar = computed(() => {
25-
return route.name !== 'search' && route.name !== 'index'
26-
})
27-
28-
const debouncedNavigate = debounce(async () => {
29-
const query = searchQuery.value.trim()
30-
await router.push({
31-
name: 'search',
32-
query: query ? { q: query } : undefined,
33-
})
34-
// allow time for the navigation to occur before resetting searchQuery
35-
setTimeout(() => (searchQuery.value = ''), 1000)
36-
}, 100)
37-
38-
async function handleSearchInput() {
39-
debouncedNavigate()
40-
}
22+
const showFullSearch = ref(false)
4123
4224
onKeyStroke(',', e => {
4325
// Don't trigger if user is typing in an input
@@ -58,8 +40,8 @@ onKeyStroke(',', e => {
5840
class="container h-14 flex items-center justify-start"
5941
>
6042
<!-- Start: Logo -->
61-
<div class="flex-shrink-0">
62-
<div v-if="showLogo" class="">
43+
<div :class="{ 'hidden sm:block': showFullSearch }" class="flex-shrink-0">
44+
<div v-if="showLogo">
6345
<NuxtLink
6446
to="/"
6547
:aria-label="$t('header.home')"
@@ -76,41 +58,17 @@ onKeyStroke(',', e => {
7658
</div>
7759

7860
<!-- Center: Search bar + nav items -->
79-
<div class="flex-1 flex items-center justify-center gap-4 sm:gap-6">
80-
<!-- Search bar (shown on all pages except home and search) -->
81-
<search v-if="showSearchBar" class="hidden sm:block flex-1 max-w-md">
82-
<form method="GET" action="/search" class="relative" @submit.prevent="handleSearchInput">
83-
<label for="header-search" class="sr-only">
84-
{{ $t('search.label') }}
85-
</label>
86-
87-
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
88-
<div class="search-box relative flex items-center">
89-
<span
90-
class="absolute inset-is-3 text-fg-subtle font-mono text-sm pointer-events-none transition-colors duration-200 motion-reduce:transition-none group-focus-within:text-accent z-1"
91-
>
92-
/
93-
</span>
94-
95-
<input
96-
id="header-search"
97-
v-model="searchQuery"
98-
type="search"
99-
name="q"
100-
:placeholder="$t('search.placeholder')"
101-
v-bind="noCorrect"
102-
class="w-full bg-bg-subtle border border-border rounded-md ps-7 pe-3 py-1.5 font-mono text-sm text-fg placeholder:text-fg-subtle transition-border-color duration-300 motion-reduce:transition-none focus:border-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
103-
@input="handleSearchInput"
104-
@focus="isSearchFocused = true"
105-
@blur="isSearchFocused = false"
106-
/>
107-
<button type="submit" class="sr-only">{{ $t('search.button') }}</button>
108-
</div>
109-
</div>
110-
</form>
111-
</search>
112-
113-
<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
61+
<div class="flex-1 flex items-center justify-center md:gap-6 mx-2">
62+
<!-- Search bar (shown on all pages except home) -->
63+
<SearchBox
64+
:inputClass="showFullSearch ? '' : 'max-w[6rem]'"
65+
@focus="showFullSearch = true"
66+
@blur="showFullSearch = false"
67+
/>
68+
<ul
69+
:class="{ 'hidden sm:flex': showFullSearch }"
70+
class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0"
71+
>
11472
<!-- Packages dropdown (when connected) -->
11573
<li v-if="isConnected && npmUser" class="flex items-center">
11674
<HeaderPackagesDropdown :username="npmUser" />
@@ -124,7 +82,10 @@ onKeyStroke(',', e => {
12482
</div>
12583

12684
<!-- End: User status + GitHub -->
127-
<div class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ms-auto sm:ms-0">
85+
<div
86+
:class="{ 'hidden sm:flex': showFullSearch }"
87+
class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ms-auto sm:ms-0"
88+
>
12889
<NuxtLink
12990
to="/about"
13091
class="sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"

app/components/ColumnPicker.vue

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function handleReset() {
9393
v-if="isOpen"
9494
ref="menuRef"
9595
:id="menuId"
96-
class="absolute right-0 mt-2 w-56 bg-bg-subtle border border-border rounded-lg shadow-lg z-20"
96+
class="absolute right-0 mt-2 w-60 bg-bg-subtle border border-border rounded-lg shadow-lg z-20"
9797
role="group"
9898
:aria-label="$t('filters.columns.show')"
9999
>
@@ -109,7 +109,7 @@ function handleReset() {
109109
<label
110110
v-for="column in toggleableColumns"
111111
:key="column.id"
112-
class="flex items-center px-3 py-2 transition-colors duration-200"
112+
class="flex gap-2 items-center px-3 py-2 transition-colors duration-200"
113113
:class="
114114
column.disabled
115115
? 'opacity-50 cursor-not-allowed'
@@ -124,16 +124,20 @@ function handleReset() {
124124
class="w-4 h-4 accent-fg bg-bg-muted border-border rounded disabled:opacity-50"
125125
@change="!column.disabled && emit('toggle', column.id)"
126126
/>
127-
<span class="ml-2 text-sm text-fg-muted font-mono flex-1">
127+
<span class="text-sm text-fg-muted font-mono flex-1">
128128
{{ getColumnLabel(column.id) }}
129129
</span>
130-
<span
130+
<AppTooltip
131131
v-if="column.disabled"
132132
:id="`${column.id}-disabled-reason`"
133-
class="text-xs text-fg-subtle italic"
133+
class="text-fg-subtle"
134+
:text="$t('filters.columns.coming_soon')"
135+
position="left"
134136
>
135-
{{ $t('filters.columns.coming_soon') }}
136-
</span>
137+
<span class="size-4 flex justify-center items-center text-xs border rounded-full"
138+
>i</span
139+
>
140+
</AppTooltip>
137141
</label>
138142
</div>
139143

app/components/DateTime.vue

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,26 +51,17 @@ const titleValue = computed(() => {
5151
</script>
5252

5353
<template>
54-
<ClientOnly>
55-
<NuxtTime
56-
v-if="relativeDates"
57-
:datetime="datetime"
58-
:title="titleValue"
59-
relative
60-
:locale="locale"
61-
/>
62-
<NuxtTime
63-
v-else
64-
:datetime="datetime"
65-
:title="titleValue"
66-
:date-style="dateStyle"
67-
:year="year"
68-
:month="month"
69-
:day="day"
70-
:locale="locale"
71-
/>
72-
<template #fallback>
54+
<span>
55+
<ClientOnly>
7356
<NuxtTime
57+
v-if="relativeDates"
58+
:datetime="datetime"
59+
:title="titleValue"
60+
relative
61+
:locale="locale"
62+
/>
63+
<NuxtTime
64+
v-else
7465
:datetime="datetime"
7566
:title="titleValue"
7667
:date-style="dateStyle"
@@ -79,6 +70,17 @@ const titleValue = computed(() => {
7970
:day="day"
8071
:locale="locale"
8172
/>
82-
</template>
83-
</ClientOnly>
73+
<template #fallback>
74+
<NuxtTime
75+
:datetime="datetime"
76+
:title="titleValue"
77+
:date-style="dateStyle"
78+
:year="year"
79+
:month="month"
80+
:day="day"
81+
:locale="locale"
82+
/>
83+
</template>
84+
</ClientOnly>
85+
</span>
8486
</template>

0 commit comments

Comments
 (0)