Skip to content

Commit 8a11728

Browse files
authored
feat: header search bar + settings back button + hide connector (#251)
1 parent 1f74353 commit 8a11728

10 files changed

Lines changed: 161 additions & 71 deletions

File tree

app/app.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const router = useRouter()
77
// Initialize accent color before hydration to prevent flash
88
initAccentOnPrehydrate()
99
10-
const isHomepage = computed(() => route.path === '/')
10+
const isHomepage = computed(() => route.name === 'index')
1111
1212
useHead({
1313
titleTemplate: titleChunk => {

app/components/AppHeader.vue

Lines changed: 81 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@ withDefaults(
1313
const { isConnected, npmUser } = useConnector()
1414
1515
const router = useRouter()
16+
const route = useRoute()
17+
18+
const searchQuery = ref('')
19+
const isSearchFocused = ref(false)
20+
21+
const showSearchBar = computed(() => {
22+
return route.name !== 'search' && route.name !== 'index'
23+
})
24+
25+
async function handleSearchInput() {
26+
const query = searchQuery.value.trim()
27+
await router.push({
28+
name: 'search',
29+
query: query ? { q: query } : undefined,
30+
})
31+
searchQuery.value = ''
32+
}
33+
1634
onKeyStroke(',', e => {
1735
// Don't trigger if user is typing in an input
1836
const target = e.target as HTMLElement
@@ -45,40 +63,73 @@ onKeyStroke(',', e => {
4563
<span v-else class="w-1" />
4664
</div>
4765

48-
<!-- Center: Main nav items -->
49-
<ul class="flex-1 flex items-center justify-center gap-4 sm:gap-6 list-none m-0 p-0">
50-
<li class="flex items-center">
51-
<NuxtLink
52-
to="/search"
53-
class="link-subtle font-mono text-sm inline-flex items-center gap-2"
54-
aria-keyshortcuts="/"
66+
<!-- Center: Search bar + nav items -->
67+
<div class="flex-1 flex items-center justify-center gap-4 sm:gap-6">
68+
<!-- Search bar (shown on all pages except home and search) -->
69+
<search v-if="showSearchBar" class="hidden sm:block flex-1 max-w-md">
70+
<form
71+
role="search"
72+
method="GET"
73+
action="/search"
74+
class="relative"
75+
@submit.prevent="handleSearchInput"
5576
>
56-
{{ $t('nav.search') }}
57-
<kbd
58-
class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
59-
aria-hidden="true"
60-
>
61-
/
62-
</kbd>
63-
</NuxtLink>
64-
</li>
65-
66-
<!-- Packages dropdown (when connected) -->
67-
<li v-if="isConnected && npmUser" class="flex items-center">
68-
<HeaderPackagesDropdown :username="npmUser" />
69-
</li>
70-
71-
<!-- Orgs dropdown (when connected) -->
72-
<li v-if="isConnected && npmUser" class="flex items-center">
73-
<HeaderOrgsDropdown :username="npmUser" />
74-
</li>
75-
</ul>
77+
<label for="header-search" class="sr-only">
78+
{{ $t('search.label') }}
79+
</label>
80+
81+
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
82+
<div class="search-box relative flex items-center">
83+
<span
84+
class="absolute left-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"
85+
>
86+
/
87+
</span>
88+
89+
<input
90+
id="header-search"
91+
v-model="searchQuery"
92+
type="search"
93+
name="q"
94+
:placeholder="$t('search.placeholder')"
95+
v-bind="noCorrect"
96+
class="w-full bg-bg-subtle border border-border rounded-md pl-7 pr-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"
97+
autocomplete="off"
98+
@input="handleSearchInput"
99+
@focus="isSearchFocused = true"
100+
@blur="isSearchFocused = false"
101+
/>
102+
<button type="submit" class="sr-only">{{ $t('search.button') }}</button>
103+
</div>
104+
</div>
105+
</form>
106+
</search>
107+
108+
<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
109+
<!-- Packages dropdown (when connected) -->
110+
<li v-if="isConnected && npmUser" class="flex items-center">
111+
<HeaderPackagesDropdown :username="npmUser" />
112+
</li>
113+
114+
<!-- Orgs dropdown (when connected) -->
115+
<li v-if="isConnected && npmUser" class="flex items-center">
116+
<HeaderOrgsDropdown :username="npmUser" />
117+
</li>
118+
</ul>
119+
</div>
76120

77121
<!-- Right: User status + GitHub -->
78-
<div class="flex-shrink-0 flex items-center gap-6">
122+
<div class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ml-auto sm:ml-0">
123+
<NuxtLink
124+
to="/about"
125+
class="sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
126+
>
127+
{{ $t('footer.about') }}
128+
</NuxtLink>
129+
79130
<NuxtLink
80131
to="/settings"
81-
class="link-subtle font-mono text-sm inline-flex items-center gap-2"
132+
class="link-subtle font-mono text-sm inline-flex items-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
82133
aria-keyshortcuts=","
83134
>
84135
{{ $t('nav.settings') }}
@@ -90,19 +141,9 @@ onKeyStroke(',', e => {
90141
</kbd>
91142
</NuxtLink>
92143

93-
<div v-if="showConnector">
144+
<div v-if="showConnector" class="hidden sm:block">
94145
<ConnectorStatus />
95146
</div>
96-
97-
<a
98-
href="https://github.com/npmx-dev/npmx.dev"
99-
target="_blank"
100-
rel="noopener noreferrer"
101-
class="link-subtle"
102-
:aria-label="$t('header.github')"
103-
>
104-
<span class="i-carbon-logo-github w-5 h-5" aria-hidden="true" />
105-
</a>
106147
</div>
107148
</nav>
108149
</header>

app/components/PackageList.vue

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import type { NpmSearchResult } from '#shared/types'
33
import type { WindowVirtualizerHandle } from '~/composables/useVirtualInfiniteScroll'
44
import { WindowVirtualizer } from 'virtua/vue'
55
6+
/** Number of items to render statically during SSR */
7+
const SSR_COUNT = 20
8+
69
const props = defineProps<{
710
/** List of search results to display */
811
results: NpmSearchResult[]
@@ -96,31 +99,52 @@ defineExpose({
9699

97100
<template>
98101
<div>
99-
<WindowVirtualizer
100-
ref="listRef"
101-
:data="results"
102-
:item-size="140"
103-
as="ol"
104-
item="li"
105-
class="list-none m-0 p-0"
106-
@scroll="handleScroll"
107-
>
108-
<template #default="{ item, index }">
109-
<div class="pb-4">
110-
<PackageCard
111-
:result="item as NpmSearchResult"
112-
:heading-level="headingLevel"
113-
:show-publisher="showPublisher"
114-
:selected="index === (selectedIndex ?? -1)"
115-
:index="index"
116-
:search-query="searchQuery"
117-
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
118-
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
119-
@focus="emit('select', $event)"
120-
/>
121-
</div>
102+
<!-- SSR: Render static list for first page, replaced by virtual list on client -->
103+
<ClientOnly>
104+
<WindowVirtualizer
105+
ref="listRef"
106+
:data="results"
107+
:item-size="140"
108+
as="ol"
109+
item="li"
110+
class="list-none m-0 p-0"
111+
@scroll="handleScroll"
112+
>
113+
<template #default="{ item, index }">
114+
<div class="pb-4">
115+
<PackageCard
116+
:result="item as NpmSearchResult"
117+
:heading-level="headingLevel"
118+
:show-publisher="showPublisher"
119+
:selected="index === (selectedIndex ?? -1)"
120+
:index="index"
121+
:search-query="searchQuery"
122+
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
123+
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
124+
@focus="emit('select', $event)"
125+
/>
126+
</div>
127+
</template>
128+
</WindowVirtualizer>
129+
130+
<!-- SSR fallback: static list of first page results -->
131+
<template #fallback>
132+
<ol class="list-none m-0 p-0">
133+
<li v-for="(item, index) in results.slice(0, SSR_COUNT)" :key="item.package.name">
134+
<div class="pb-4">
135+
<PackageCard
136+
:result="item"
137+
:heading-level="headingLevel"
138+
:show-publisher="showPublisher"
139+
:selected="index === (selectedIndex ?? -1)"
140+
:index="index"
141+
:search-query="searchQuery"
142+
/>
143+
</div>
144+
</li>
145+
</ol>
122146
</template>
123-
</WindowVirtualizer>
147+
</ClientOnly>
124148

125149
<!-- Loading indicator -->
126150
<div v-if="isLoading" class="py-4 flex items-center justify-center">

app/pages/index.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ defineOgImageComponent('Default')
4141
class="w-full max-w-xl motion-safe:animate-slide-up motion-safe:animate-fill-both"
4242
style="animation-delay: 0.2s"
4343
>
44-
<form role="search" class="relative" @submit.prevent="handleSearch">
44+
<form
45+
role="search"
46+
method="GET"
47+
action="/search"
48+
class="relative"
49+
@submit.prevent="handleSearch"
50+
>
4551
<label for="home-search" class="sr-only">
4652
{{ $t('search.label') }}
4753
</label>

app/pages/search.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ defineOgImageComponent('Default', {
706706
<h1 class="font-mono text-xl sm:text-2xl font-medium mb-4">search</h1>
707707

708708
<search>
709-
<form role="search" class="relative" @submit.prevent>
709+
<form role="search" method="GET" action="/search" class="relative" @submit.prevent>
710710
<label for="search-input" class="sr-only">{{ $t('search.label') }}</label>
711711

712712
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">

app/pages/settings.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
const router = useRouter()
23
const { settings } = useSettings()
34
const { locale, locales, setLocale } = useI18n()
45
const colorMode = useColorMode()
@@ -7,13 +8,27 @@ const availableLocales = computed(() =>
78
locales.value.map(l => (typeof l === 'string' ? { code: l, name: l } : l)),
89
)
910
11+
function goBack() {
12+
router.back()
13+
}
14+
1015
useSeoMeta({
1116
title: 'Settings - npmx',
1217
})
1318
</script>
1419

1520
<template>
1621
<main class="container py-8 sm:py-12 w-full">
22+
<!-- Back button -->
23+
<button
24+
type="button"
25+
class="inline-flex items-center gap-2 mb-6 text-sm text-fg-muted hover:text-fg transition-colors duration-150 motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
26+
@click="goBack"
27+
>
28+
<span class="i-carbon-arrow-left w-4 h-4" aria-hidden="true" />
29+
{{ $t('nav.back') }}
30+
</button>
31+
1732
<div class="space-y-1 p-4 rounded-lg bg-bg-muted border border-border">
1833
<button
1934
type="button"

i18n/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"nav": {
4141
"popular_packages": "Popular packages",
4242
"search": "search",
43-
"settings": "settings"
43+
"settings": "settings",
44+
"back": "Back"
4445
},
4546
"settings": {
4647
"relative_dates": "Relative dates",

i18n/locales/fr.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"nav": {
4141
"popular_packages": "Paquets populaires",
4242
"search": "recherche",
43-
"settings": "paramètres"
43+
"settings": "paramètres",
44+
"back": "Retour"
4445
},
4546
"settings": {
4647
"relative_dates": "Dates relatives",

i18n/locales/it.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"nav": {
3434
"popular_packages": "Pacchetti popolari",
3535
"search": "cerca",
36-
"settings": "impostazioni"
36+
"settings": "impostazioni",
37+
"back": "Indietro"
3738
},
3839
"settings": {
3940
"relative_dates": "Date relative",

i18n/locales/zh-CN.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"nav": {
4141
"popular_packages": "热门软件包",
4242
"search": "搜索",
43-
"settings": "设置"
43+
"settings": "设置",
44+
"back": "返回"
4445
},
4546
"settings": {
4647
"relative_dates": "相对时间",

0 commit comments

Comments
 (0)