Skip to content

Commit bfb7af2

Browse files
danielroefatfingers23
authored andcommitted
feat: header search bar + settings back button + hide connector (npmx-dev#251)
1 parent 0f70d11 commit bfb7af2

10 files changed

Lines changed: 141 additions & 64 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: 61 additions & 33 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
@@ -54,28 +72,28 @@ onKeyStroke('.', e => {
5472
<span v-else class="w-1" />
5573
</div>
5674

57-
<!-- Center: Main nav items -->
58-
<ul class="flex-1 flex items-center justify-center gap-4 sm:gap-6 list-none m-0 p-0">
59-
<li class="flex items-center">
60-
<NuxtLink
61-
to="/search"
62-
class="link-subtle font-mono text-sm inline-flex items-center gap-2"
63-
aria-keyshortcuts="/"
75+
<!-- Center: Search bar + nav items -->
76+
<div class="flex-1 flex items-center justify-center gap-4 sm:gap-6">
77+
<!-- Search bar (shown on all pages except home and search) -->
78+
<search v-if="showSearchBar" class="hidden sm:block flex-1 max-w-md">
79+
<form
80+
role="search"
81+
method="GET"
82+
action="/search"
83+
class="relative"
84+
@submit.prevent="handleSearchInput"
6485
>
65-
{{ $t('nav.search') }}
66-
<kbd
67-
class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
68-
aria-hidden="true"
69-
>
70-
/
71-
</kbd>
72-
</NuxtLink>
73-
</li>
86+
<label for="header-search" class="sr-only">
87+
{{ $t('search.label') }}
88+
</label>
7489

75-
<!-- Packages dropdown (when connected) -->
76-
<li v-if="isConnected && npmUser" class="flex items-center">
77-
<HeaderPackagesDropdown :username="npmUser" />
78-
</li>
90+
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
91+
<div class="search-box relative flex items-center">
92+
<span
93+
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"
94+
>
95+
/
96+
</span>
7997

8098
<!-- Orgs dropdown (when connected) -->
8199
<li v-if="isConnected && npmUser" class="flex items-center">
@@ -99,11 +117,31 @@ onKeyStroke('.', e => {
99117
</li>
100118
</ul>
101119

120+
<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
121+
<!-- Packages dropdown (when connected) -->
122+
<li v-if="isConnected && npmUser" class="flex items-center">
123+
<HeaderPackagesDropdown :username="npmUser" />
124+
</li>
125+
126+
<!-- Orgs dropdown (when connected) -->
127+
<li v-if="isConnected && npmUser" class="flex items-center">
128+
<HeaderOrgsDropdown :username="npmUser" />
129+
</li>
130+
</ul>
131+
</div>
132+
102133
<!-- Right: User status + GitHub -->
103-
<div class="flex-shrink-0 flex items-center gap-6">
134+
<div class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ml-auto sm:ml-0">
135+
<NuxtLink
136+
to="/about"
137+
class="sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
138+
>
139+
{{ $t('footer.about') }}
140+
</NuxtLink>
141+
104142
<NuxtLink
105143
to="/settings"
106-
class="link-subtle font-mono text-sm inline-flex items-center gap-2"
144+
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"
107145
aria-keyshortcuts=","
108146
>
109147
{{ $t('nav.settings') }}
@@ -115,19 +153,9 @@ onKeyStroke('.', e => {
115153
</kbd>
116154
</NuxtLink>
117155

118-
<div v-if="showConnector">
156+
<div v-if="showConnector" class="hidden sm:block">
119157
<ConnectorStatus />
120158
</div>
121-
122-
<a
123-
href="https://github.com/npmx-dev/npmx.dev"
124-
target="_blank"
125-
rel="noopener noreferrer"
126-
class="link-subtle"
127-
:aria-label="$t('header.github')"
128-
>
129-
<span class="i-carbon-logo-github w-5 h-5" aria-hidden="true" />
130-
</a>
131159
</div>
132160
</nav>
133161
</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
@@ -41,7 +41,8 @@
4141
"popular_packages": "Popular packages",
4242
"blog": "blog",
4343
"search": "search",
44-
"settings": "settings"
44+
"settings": "settings",
45+
"back": "Back"
4546
},
4647
"blog": {
4748
"title": "Blog",

i18n/locales/fr.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"popular_packages": "Paquets populaires",
4242
"blog": "blog",
4343
"search": "recherche",
44-
"settings": "paramètres"
44+
"settings": "paramètres",
45+
"back": "Retour"
4546
},
4647
"blog": {
4748
"title": "Blog",

i18n/locales/it.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"popular_packages": "Pacchetti popolari",
3535
"blog": "blog",
3636
"search": "cerca",
37-
"settings": "impostazioni"
37+
"settings": "impostazioni",
38+
"back": "Indietro"
3839
},
3940
"blog": {
4041
"title": "Blog",

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)