Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const router = useRouter()
// Initialize accent color before hydration to prevent flash
initAccentOnPrehydrate()
const isHomepage = computed(() => route.path === '/')
const isHomepage = computed(() => route.name === 'index')
useHead({
titleTemplate: titleChunk => {
Expand Down
121 changes: 81 additions & 40 deletions app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ withDefaults(
const { isConnected, npmUser } = useConnector()

const router = useRouter()
const route = useRoute()

const searchQuery = ref('')
const isSearchFocused = ref(false)

const showSearchBar = computed(() => {
return route.name !== 'search' && route.name !== 'index'
})

async function handleSearchInput() {
const query = searchQuery.value.trim()
await router.push({
name: 'search',
query: query ? { q: query } : undefined,
})
searchQuery.value = ''
}

onKeyStroke(',', e => {
// Don't trigger if user is typing in an input
const target = e.target as HTMLElement
Expand Down Expand Up @@ -45,40 +63,73 @@ onKeyStroke(',', e => {
<span v-else class="w-1" />
</div>

<!-- Center: Main nav items -->
<ul class="flex-1 flex items-center justify-center gap-4 sm:gap-6 list-none m-0 p-0">
<li class="flex items-center">
<NuxtLink
to="/search"
class="link-subtle font-mono text-sm inline-flex items-center gap-2"
aria-keyshortcuts="/"
<!-- Center: Search bar + nav items -->
<div class="flex-1 flex items-center justify-center gap-4 sm:gap-6">
<!-- Search bar (shown on all pages except home and search) -->
<search v-if="showSearchBar" class="hidden sm:block flex-1 max-w-md">
<form
role="search"
method="GET"
action="/search"
class="relative"
@submit.prevent="handleSearchInput"
>
{{ $t('nav.search') }}
<kbd
class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
aria-hidden="true"
>
/
</kbd>
</NuxtLink>
</li>

<!-- Packages dropdown (when connected) -->
<li v-if="isConnected && npmUser" class="flex items-center">
<HeaderPackagesDropdown :username="npmUser" />
</li>

<!-- Orgs dropdown (when connected) -->
<li v-if="isConnected && npmUser" class="flex items-center">
<HeaderOrgsDropdown :username="npmUser" />
</li>
</ul>
<label for="header-search" class="sr-only">
{{ $t('search.label') }}
</label>

<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
<div class="search-box relative flex items-center">
<span
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"
>
/
</span>

<input
id="header-search"
v-model="searchQuery"
type="search"
name="q"
:placeholder="$t('search.placeholder')"
v-bind="noCorrect"
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"
autocomplete="off"
@input="handleSearchInput"
@focus="isSearchFocused = true"
@blur="isSearchFocused = false"
/>
<button type="submit" class="sr-only">{{ $t('search.button') }}</button>
</div>
</div>
</form>
</search>

<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
<!-- Packages dropdown (when connected) -->
<li v-if="isConnected && npmUser" class="flex items-center">
<HeaderPackagesDropdown :username="npmUser" />
</li>

<!-- Orgs dropdown (when connected) -->
<li v-if="isConnected && npmUser" class="flex items-center">
<HeaderOrgsDropdown :username="npmUser" />
</li>
</ul>
</div>

<!-- Right: User status + GitHub -->
<div class="flex-shrink-0 flex items-center gap-6">
<div class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ml-auto sm:ml-0">
<NuxtLink
to="/about"
class="sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
>
{{ $t('footer.about') }}
</NuxtLink>

<NuxtLink
to="/settings"
class="link-subtle font-mono text-sm inline-flex items-center gap-2"
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"
aria-keyshortcuts=","
>
{{ $t('nav.settings') }}
Expand All @@ -90,19 +141,9 @@ onKeyStroke(',', e => {
</kbd>
</NuxtLink>

<div v-if="showConnector">
<div v-if="showConnector" class="hidden sm:block">
<ConnectorStatus />
</div>

<a
href="https://github.com/npmx-dev/npmx.dev"
target="_blank"
rel="noopener noreferrer"
class="link-subtle"
:aria-label="$t('header.github')"
>
<span class="i-carbon-logo-github w-5 h-5" aria-hidden="true" />
</a>
</div>
</nav>
</header>
Expand Down
72 changes: 48 additions & 24 deletions app/components/PackageList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { NpmSearchResult } from '#shared/types'
import type { WindowVirtualizerHandle } from '~/composables/useVirtualInfiniteScroll'
import { WindowVirtualizer } from 'virtua/vue'

/** Number of items to render statically during SSR */
const SSR_COUNT = 20

const props = defineProps<{
/** List of search results to display */
results: NpmSearchResult[]
Expand Down Expand Up @@ -96,31 +99,52 @@ defineExpose({

<template>
<div>
<WindowVirtualizer
ref="listRef"
:data="results"
:item-size="140"
as="ol"
item="li"
class="list-none m-0 p-0"
@scroll="handleScroll"
>
<template #default="{ item, index }">
<div class="pb-4">
<PackageCard
:result="item as NpmSearchResult"
:heading-level="headingLevel"
:show-publisher="showPublisher"
:selected="index === (selectedIndex ?? -1)"
:index="index"
:search-query="searchQuery"
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
@focus="emit('select', $event)"
/>
</div>
<!-- SSR: Render static list for first page, replaced by virtual list on client -->
<ClientOnly>
<WindowVirtualizer
ref="listRef"
:data="results"
:item-size="140"
as="ol"
item="li"
class="list-none m-0 p-0"
@scroll="handleScroll"
>
<template #default="{ item, index }">
<div class="pb-4">
<PackageCard
:result="item as NpmSearchResult"
:heading-level="headingLevel"
:show-publisher="showPublisher"
:selected="index === (selectedIndex ?? -1)"
:index="index"
:search-query="searchQuery"
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
@focus="emit('select', $event)"
/>
</div>
</template>
</WindowVirtualizer>

<!-- SSR fallback: static list of first page results -->
<template #fallback>
<ol class="list-none m-0 p-0">
<li v-for="(item, index) in results.slice(0, SSR_COUNT)" :key="item.package.name">
<div class="pb-4">
<PackageCard
:result="item"
:heading-level="headingLevel"
:show-publisher="showPublisher"
:selected="index === (selectedIndex ?? -1)"
:index="index"
:search-query="searchQuery"
/>
</div>
</li>
</ol>
</template>
</WindowVirtualizer>
</ClientOnly>

<!-- Loading indicator -->
<div v-if="isLoading" class="py-4 flex items-center justify-center">
Expand Down
8 changes: 7 additions & 1 deletion app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ defineOgImageComponent('Default')
class="w-full max-w-xl motion-safe:animate-slide-up motion-safe:animate-fill-both"
style="animation-delay: 0.2s"
>
<form role="search" class="relative" @submit.prevent="handleSearch">
<form
role="search"
method="GET"
action="/search"
class="relative"
@submit.prevent="handleSearch"
>
<label for="home-search" class="sr-only">
{{ $t('search.label') }}
</label>
Expand Down
2 changes: 1 addition & 1 deletion app/pages/search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ defineOgImageComponent('Default', {
<h1 class="font-mono text-xl sm:text-2xl font-medium mb-4">search</h1>

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

<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
Expand Down
15 changes: 15 additions & 0 deletions app/pages/settings.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
const router = useRouter()
const { settings } = useSettings()
const { locale, locales, setLocale } = useI18n()
const colorMode = useColorMode()
Expand All @@ -7,13 +8,27 @@ const availableLocales = computed(() =>
locales.value.map(l => (typeof l === 'string' ? { code: l, name: l } : l)),
)

function goBack() {
router.back()
}

useSeoMeta({
title: 'Settings - npmx',
})
</script>

<template>
<main class="container py-8 sm:py-12 w-full">
<!-- Back button -->
<button
type="button"
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"
@click="goBack"
>
<span class="i-carbon-arrow-left w-4 h-4" aria-hidden="true" />
{{ $t('nav.back') }}
</button>

<div class="space-y-1 p-4 rounded-lg bg-bg-muted border border-border">
<button
type="button"
Expand Down
3 changes: 2 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"nav": {
"popular_packages": "Popular packages",
"search": "search",
"settings": "settings"
"settings": "settings",
"back": "Back"
},
"settings": {
"relative_dates": "Relative dates",
Expand Down
3 changes: 2 additions & 1 deletion i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"nav": {
"popular_packages": "Paquets populaires",
"search": "recherche",
"settings": "paramètres"
"settings": "paramètres",
"back": "Retour"
},
"settings": {
"relative_dates": "Dates relatives",
Expand Down
3 changes: 2 additions & 1 deletion i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"nav": {
"popular_packages": "Pacchetti popolari",
"search": "cerca",
"settings": "impostazioni"
"settings": "impostazioni",
"back": "Indietro"
},
"settings": {
"relative_dates": "Date relative",
Expand Down
3 changes: 2 additions & 1 deletion i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"nav": {
"popular_packages": "热门软件包",
"search": "搜索",
"settings": "设置"
"settings": "设置",
"back": "返回"
},
"settings": {
"relative_dates": "相对时间",
Expand Down