Skip to content

Commit d1d8ead

Browse files
authored
Merge branch 'main' into lino/fix-light-mode-non-shiki-code-blocks
2 parents 6bafc9d + 9fd2778 commit d1d8ead

11 files changed

Lines changed: 212 additions & 143 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/[...package].vue

Lines changed: 43 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -820,34 +820,22 @@ defineOgImageComponent('Package', {
820820
role="tablist"
821821
aria-label="Package manager"
822822
>
823-
<ClientOnly>
824-
<button
825-
v-for="pm in packageManagers"
826-
:key="pm.id"
827-
role="tab"
828-
:aria-selected="selectedPM === pm.id"
829-
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-solid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
830-
:class="
831-
selectedPM === pm.id
832-
? 'bg-bg shadow text-fg border-border'
833-
: 'text-fg-subtle hover:text-fg border-transparent'
834-
"
835-
@click="selectedPM = pm.id"
836-
>
837-
<span class="inline-block h-3 w-3" :class="pm.icon" aria-hidden="true" />
838-
{{ pm.label }}
839-
</button>
840-
<template #fallback>
841-
<span
842-
v-for="pm in packageManagers"
843-
:key="pm.id"
844-
class="px-2 py-1 font-mono text-xs rounded"
845-
:class="pm.id === 'npm' ? 'bg-bg-elevated text-fg' : 'text-fg-subtle'"
846-
>
847-
{{ pm.label }}
848-
</span>
849-
</template>
850-
</ClientOnly>
823+
<button
824+
v-for="pm in packageManagers"
825+
:key="pm.id"
826+
role="tab"
827+
:aria-selected="selectedPM === pm.id"
828+
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-solid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
829+
:class="
830+
selectedPM === pm.id
831+
? 'bg-bg shadow text-fg border-border'
832+
: 'text-fg-subtle hover:text-fg border-transparent'
833+
"
834+
@click="selectedPM = pm.id"
835+
>
836+
<span class="inline-block h-3 w-3" :class="pm.icon" aria-hidden="true" />
837+
{{ pm.label }}
838+
</button>
851839
</div>
852840
</div>
853841
<div class="relative group">
@@ -891,17 +879,20 @@ defineOgImageComponent('Package', {
891879
<!-- Regular packages: Install command with optional run command -->
892880
<section
893881
v-else
894-
id="install"
895-
aria-labelledby="install-heading"
882+
id="get-started"
883+
aria-labelledby="get-started-heading"
896884
class="area-install scroll-mt-20"
897885
>
898886
<div class="flex flex-wrap items-center justify-between mb-3">
899-
<h2 id="install-heading" class="group text-xs text-fg-subtle uppercase tracking-wider">
887+
<h2
888+
id="get-started-heading"
889+
class="group text-xs text-fg-subtle uppercase tracking-wider"
890+
>
900891
<a
901-
href="#install"
892+
href="#get-started"
902893
class="inline-flex items-center gap-1.5 py-1 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
903894
>
904-
{{ $t('package.install.title') }}
895+
{{ $t('package.get_started.title') }}
905896
<span
906897
class="i-carbon-link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
907898
aria-hidden="true"
@@ -912,36 +903,24 @@ defineOgImageComponent('Package', {
912903
<div
913904
class="flex items-center gap-1 p-0.5 bg-bg-subtle border border-border-subtle rounded-md overflow-x-auto"
914905
role="tablist"
915-
:aria-label="$t('package.install.pm_label')"
906+
:aria-label="$t('package.get_started.pm_label')"
916907
>
917-
<ClientOnly>
918-
<button
919-
v-for="pm in packageManagers"
920-
:key="pm.id"
921-
role="tab"
922-
:aria-selected="selectedPM === pm.id"
923-
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-solid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
924-
:class="
925-
selectedPM === pm.id
926-
? 'bg-bg shadow text-fg border-border'
927-
: 'text-fg-subtle hover:text-fg border-transparent'
928-
"
929-
@click="selectedPM = pm.id"
930-
>
931-
<span class="inline-block h-3 w-3" :class="pm.icon" aria-hidden="true" />
932-
{{ pm.label }}
933-
</button>
934-
<template #fallback>
935-
<span
936-
v-for="pm in packageManagers"
937-
:key="pm.id"
938-
class="px-2 py-1 font-mono text-xs rounded"
939-
:class="pm.id === 'npm' ? 'bg-bg-elevated text-fg' : 'text-fg-subtle'"
940-
>
941-
{{ pm.label }}
942-
</span>
943-
</template>
944-
</ClientOnly>
908+
<button
909+
v-for="pm in packageManagers"
910+
:key="pm.id"
911+
role="tab"
912+
:aria-selected="selectedPM === pm.id"
913+
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-solid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
914+
:class="
915+
selectedPM === pm.id
916+
? 'bg-bg shadow text-fg border-border'
917+
: 'text-fg-subtle hover:text-fg border-transparent'
918+
"
919+
@click="selectedPM = pm.id"
920+
>
921+
<span class="inline-block h-3 w-3" :class="pm.icon" aria-hidden="true" />
922+
{{ pm.label }}
923+
</button>
945924
</div>
946925
</div>
947926
<div class="relative group">
@@ -972,7 +951,7 @@ defineOgImageComponent('Package', {
972951
<button
973952
type="button"
974953
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/installcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
975-
:aria-label="$t('package.install.copy_command')"
954+
:aria-label="$t('package.get_started.copy_command')"
976955
@click.stop="copyInstallCommand"
977956
>
978957
<span aria-live="polite">{{
@@ -996,7 +975,7 @@ defineOgImageComponent('Package', {
996975
v-if="typesPackageName"
997976
:to="`/${typesPackageName}`"
998977
class="text-fg-subtle hover:text-fg-muted text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
999-
:title="$t('package.install.view_types', { package: typesPackageName })"
978+
:title="$t('package.get_started.view_types', { package: typesPackageName })"
1000979
>
1001980
<span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" />
1002981
<span class="sr-only">View {{ typesPackageName }}</span>

0 commit comments

Comments
 (0)