Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8b6f4d8
feat: add useMobileNav composable for mobile nav state
Adebesin-Cell Apr 20, 2026
4deb10d
chore(i18n): add keys for mobile bottom-bar menu
Adebesin-Cell Apr 20, 2026
84ce80f
feat: add MobileMenuRootView component
Adebesin-Cell Apr 20, 2026
11eb957
feat: add MobileMenuDocsView with minimal docs drill-down
Adebesin-Cell Apr 20, 2026
43c1e30
feat: add MobileMenuSheet overlay with drill-down slide
Adebesin-Cell Apr 20, 2026
4e97065
feat: add MobileBottomBar fixed to viewport bottom
Adebesin-Cell Apr 20, 2026
669b90c
feat: mount mobile bottom bar globally; hide AppHeader on mobile
Adebesin-Cell Apr 20, 2026
255030a
fix(mobile-nav): make context label a visible back button in the bar
Adebesin-Cell Apr 20, 2026
c9de3e1
chore: remove legacy MobileMenu component and tests
Adebesin-Cell Apr 20, 2026
e1ce416
fix(mobile-nav): partial bottom sheet with backdrop, fix scroll overflow
Adebesin-Cell Apr 20, 2026
2f6ebb0
fix(mobile-nav): keep bar above backdrop; tighten row spacing
Adebesin-Cell Apr 20, 2026
bd0fe09
feat(mobile-nav): hide bar on scroll down, reveal on scroll up
Adebesin-Cell Apr 20, 2026
0a57021
chore: drop stale comment
Adebesin-Cell Apr 20, 2026
59d2cd5
fix(package): remove top gap above sticky subheader on mobile
Adebesin-Cell Apr 20, 2026
582e713
fix(package): readme sticky header offset on mobile
Adebesin-Cell Apr 20, 2026
ec62a0e
fix: skip a11y coverage for new mobile nav components; drop unused na…
Adebesin-Cell Apr 20, 2026
f7bc83d
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 20, 2026
cf53a63
fix(mobile-nav): address PR feedback (dedupe Docs, i18n labels, link …
Adebesin-Cell Apr 20, 2026
0dfb06e
fix(i18n): use static $t call so scanner can detect nav.docs_home usage
Adebesin-Cell Apr 20, 2026
f959a57
Merge branch 'main' into feat/mobile-bottom-bar-nav
Adebesin-Cell Apr 21, 2026
67a268e
chore(i18n): regenerate schema after nav key changes
Adebesin-Cell Apr 21, 2026
69cab0b
revert: drop non-English locale edits (handled by CI sync)
Adebesin-Cell Apr 21, 2026
7003e0c
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 21, 2026
1c357d9
fix(mobile-nav): defer modal open after close; respect safe-area inset
Adebesin-Cell Apr 21, 2026
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
4 changes: 4 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { isEditableElement } from '~/utils/input'

const route = useRoute()
const router = useRouter()
const { mobileLinks } = useGlobalNavLinks()
const { locale, locales } = useI18n()

// Initialize user preferences (accent color, package manager) before hydration to prevent flash/CLS
Expand Down Expand Up @@ -155,6 +156,9 @@ defineOgImage('Page.takumi', {}, { alt: 'npmx — a fast, modern browser for the
<AppFooter />

<ScrollToTop />

<HeaderMobileBottomBar />
<HeaderMobileMenuSheet :links="mobileLinks" />
</div>
</template>

Expand Down
204 changes: 7 additions & 197 deletions app/components/AppHeader.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
<script setup lang="ts">
import { LinkBase } from '#components'
import type { NavigationConfig, NavigationConfigWithGroups } from '~/types'
import { NPMX_DOCS_SITE } from '#shared/utils/constants'

const discord = useDiscordLink()
const { open: openCommandPalette } = useCommandPalette()
const { commandPaletteShortcutLabel } = usePlatformModifierKey()

Expand All @@ -18,159 +15,16 @@ withDefaults(

const { isConnected, npmUser } = useConnector()

const desktopLinks = computed<NavigationConfig>(() => [
{
name: 'Compare',
label: $t('nav.compare'),
to: { name: 'compare' },
keyshortcut: 'c',
type: 'link',
external: false,
iconClass: 'i-lucide:git-compare',
},
{
name: 'Settings',
label: $t('nav.settings'),
to: { name: 'settings' },
keyshortcut: ',',
type: 'link',
external: false,
iconClass: 'i-lucide:settings',
},
])

const mobileLinks = computed<NavigationConfigWithGroups>(() => [
{
name: 'Desktop Links',
type: 'group',
items: [...desktopLinks.value],
},
{
type: 'separator',
},
{
name: 'About & Policies',
type: 'group',
items: [
{
name: 'About',
label: $t('footer.about'),
to: { name: 'about' },
type: 'link',
external: false,
iconClass: 'i-lucide:info',
},
{
name: 'Blog',
label: $t('footer.blog'),
to: { name: 'blog' },
type: 'link',
external: false,
iconClass: 'i-lucide:notebook-pen',
},
{
name: 'Privacy Policy',
label: $t('privacy_policy.title'),
to: { name: 'privacy' },
type: 'link',
external: false,
iconClass: 'i-lucide:shield-check',
},
{
name: 'Accessibility',
label: $t('a11y.title'),
to: { name: 'accessibility' },
type: 'link',
external: false,
iconClass: 'i-custom:a11y',
},
{
name: 'Translation Status',
label: $t('translation_status.title'),
to: { name: 'translation-status' },
type: 'link',
external: false,
iconClass: 'i-lucide:languages',
},
{
name: 'Brand',
label: $t('footer.brand'),
to: { name: 'brand' },
type: 'link',
external: false,
iconClass: 'i-lucide:palette',
},
],
},
{
type: 'separator',
},
{
name: 'External Links',
type: 'group',
label: $t('nav.links'),
items: [
{
name: 'Docs',
label: $t('footer.docs'),
href: NPMX_DOCS_SITE,
target: '_blank',
type: 'link',
external: true,
iconClass: 'i-lucide:file-text',
},
{
name: 'Source',
label: $t('footer.source'),
href: 'https://repo.npmx.dev',
target: '_blank',
type: 'link',
external: true,
iconClass: 'i-simple-icons:github',
},
{
name: 'Social',
label: $t('footer.social'),
href: 'https://social.npmx.dev',
target: '_blank',
type: 'link',
external: true,
iconClass: 'i-simple-icons:bluesky',
},
{
name: 'Chat',
label: discord.value.label,
href: discord.value.url,
target: '_blank',
type: 'link',
external: true,
iconClass: 'i-lucide:message-circle',
},
],
},
])
const { desktopLinks } = useGlobalNavLinks()

const showFullSearch = shallowRef(false)
const showMobileMenu = shallowRef(false)
const { env, prNumber } = useAppConfig().buildInfo

// On mobile, clicking logo+search button expands search
const route = useRoute()
const isMobile = useIsMobile()
const isSearchExpandedManually = shallowRef(false)
const searchBoxRef = useTemplateRef('searchBoxRef')

// On search page, always show search expanded on mobile
const isOnHomePage = computed(() => route.name === 'index')
const isOnSearchPage = computed(() => route.name === 'search')
const isSearchExpanded = computed(() => isOnSearchPage.value || isSearchExpandedManually.value)

function expandMobileSearch() {
isSearchExpandedManually.value = true
nextTick(() => {
searchBoxRef.value?.focus()
})
}

watch(
isOnSearchPage,
Expand All @@ -187,13 +41,6 @@ watch(

function handleSearchBlur() {
showFullSearch.value = false
// Collapse expanded search on mobile after blur (with delay for click handling)
// But don't collapse if we're on the search page
if (isMobile.value && !isOnSearchPage.value) {
setTimeout(() => {
isSearchExpandedManually.value = false
}, 150)
}
}

function handleSearchFocus() {
Expand All @@ -207,23 +54,12 @@ useShortcuts({
</script>

<template>
<header class="sticky top-0 z-50 border-b border-border">
<header class="hidden sm:block sticky top-0 z-50 border-b border-border">
<div class="absolute inset-0 bg-bg/80 backdrop-blur-md" />
<nav
:aria-label="$t('nav.main_navigation')"
class="relative container min-h-14 flex items-center gap-2 z-1 justify-end"
>
<!-- Mobile: Logo (navigates home) -->
<LogoContextMenu v-if="!isSearchExpanded && !isOnHomePage" class="sm:hidden flex-shrink-0">
<NuxtLink
to="/"
:aria-label="$t('header.home')"
class="font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring me-4"
>
<AppMark class="w-6 h-auto" />
</NuxtLink>
</LogoContextMenu>

<!-- Desktop: Logo (navigates home) -->
<LogoContextMenu v-if="showLogo" class="hidden sm:flex flex-shrink-0 items-center">
<NuxtLink
Expand All @@ -243,7 +79,7 @@ useShortcuts({
</LogoContextMenu>

<NuxtLink
v-if="showLogo && !isSearchExpanded && prNumber"
v-if="showLogo && prNumber"
:to="`https://github.com/npmx-dev/npmx.dev/pull/${prNumber}`"
:aria-label="$t('header.pr', { prNumber })"
>
Expand Down Expand Up @@ -277,21 +113,19 @@ useShortcuts({
<div
class="flex-1 flex items-center md:gap-6"
:class="{
'hidden sm:flex': !isSearchExpanded,
'justify-end': isOnHomePage,
'justify-center': !isOnHomePage,
}"
>
<!-- Search bar (hidden on mobile unless expanded) -->
<!-- Search bar -->
<HeaderSearchBox
ref="searchBoxRef"
:inputClass="isSearchExpanded ? 'w-full' : ''"
:class="{ 'max-w-md': !isSearchExpanded }"
:class="{ 'max-w-md': !showFullSearch }"
@focus="handleSearchFocus"
@blur="handleSearchBlur"
/>
<ul
v-if="!isSearchExpanded && isConnected && npmUser"
v-if="isConnected && npmUser"
:class="{ hidden: showFullSearch }"
class="hidden sm:flex items-center gap-4 sm:gap-6 list-none m-0 p-0"
>
Expand All @@ -307,7 +141,7 @@ useShortcuts({
</ul>
</div>

<!-- End: Desktop nav items + Mobile menu button -->
<!-- End: Desktop nav items -->
<div class="hidden sm:flex flex-shrink-0 items-center gap-2">
<!-- Desktop: Explore link -->
<LinkBase
Expand All @@ -323,30 +157,6 @@ useShortcuts({

<HeaderAccountMenu />
</div>

<!-- Mobile: Search button (expands search) -->
<ButtonBase
type="button"
class="sm:hidden ms-auto"
:aria-label="$t('nav.tap_to_search')"
:aria-expanded="showMobileMenu"
@click="expandMobileSearch"
v-if="!isSearchExpanded && !isOnHomePage"
classicon="i-lucide:search"
/>

<!-- Mobile: Menu button (always visible, click to open menu) -->
<ButtonBase
type="button"
class="sm:hidden"
:aria-label="$t('nav.open_menu')"
:aria-expanded="showMobileMenu"
@click="showMobileMenu = !showMobileMenu"
classicon="i-lucide:menu"
/>
</nav>

<!-- Mobile menu -->
<HeaderMobileMenu :links="mobileLinks" v-model:open="showMobileMenu" />
</header>
</template>
Loading
Loading