Skip to content

Commit 4b5cbb8

Browse files
committed
Merge branch 'main' into feat-change-i18n-configuration
2 parents c52df4f + 42a17ba commit 4b5cbb8

35 files changed

Lines changed: 3345 additions & 900 deletions

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
</p>
1010

1111
- [👉 &nbsp;Check it out](https://npmx.dev/)
12+
- [📖 &nbsp;About npmx](https://npmx.dev/about)
1213

1314
## Vision
1415

15-
The aim of [npmx.dev](https://npmx.dev) is to provide a better browser for the npm registry &ndash; fast, modern, and accessible. We don't aim to replace the [npmjs.com](https://www.npmjs.com/) registry, just provide a better UI and DX.
16+
The aim of [npmx.dev](https://npmx.dev) is to provide a better browser for the npm registry &ndash; fast, modern, and accessible. We don't aim to replace the [npmjs.com](https://www.npmjs.com/) registry, just provide a better UI, DX, and admin experience.
1617

1718
- **Speed first** &ndash; Layout shift, flakiness, slowness is The Worst. Fast searching, filtering, and navigation.
1819
- **URL compatible** &ndash; Replace `npmjs.com` with `xnpmjs.com` or `npmx.dev` in any URL and it just works.
1920
- **Simplicity** &ndash; No noise, cluttered display, or confusing UI. If in doubt: choose simplicity.
21+
- **Admin UI** &ndash; Manage your packages, teams, and organizations from the browser, powered by your local npm CLI.
2022

2123
## Shortcuts
2224

app/app.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const { locale, locales } = useI18n()
99
// Initialize accent color before hydration to prevent flash
1010
initAccentOnPrehydrate()
1111
12-
const isHomepage = computed(() => route.path === '/')
12+
const isHomepage = computed(() => route.name === 'index')
1313
1414
useHead({
1515
titleTemplate: titleChunk => {

app/assets/main.css

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ html {
5858
-webkit-font-smoothing: antialiased;
5959
-moz-osx-font-smoothing: grayscale;
6060
text-rendering: optimizeLegibility;
61+
scroll-behavior: smooth;
62+
scroll-padding-top: 5rem; /* Offset for fixed header - otherwise anchor headers are cutted */
63+
}
64+
65+
/* Disable smooth scrolling if user prefers reduced motion */
66+
@media (prefers-reduced-motion: reduce) {
67+
html {
68+
scroll-behavior: auto;
69+
}
6170
}
6271

6372
/*
@@ -76,7 +85,6 @@ body {
7685
background-color: var(--bg);
7786
color: var(--fg);
7887
line-height: 1.6;
79-
padding-bottom: var(--footer-height, 0);
8088
}
8189

8290
/* Default link styling for accessibility on dark background */
@@ -247,7 +255,6 @@ html.light .shiki span {
247255
/* Code blocks - including Shiki output */
248256
.readme-content pre,
249257
.readme-content .shiki {
250-
background: oklch(0.145 0 0) !important;
251258
border: 1px solid var(--border);
252259
border-radius: 8px;
253260
padding: 1rem;

app/components/AppFooter.vue

Lines changed: 21 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,57 @@
1-
<script setup lang="ts">
2-
const isMounted = shallowRef(false)
3-
const isVisible = shallowRef(false)
4-
const isScrollable = shallowRef(true)
5-
const lastScrollY = shallowRef(0)
6-
const footerRef = useTemplateRef('footerRef')
7-
8-
// Check if CSS scroll-state container queries are supported
9-
// Once this becomes baseline, we can remove the JS scroll handling entirely
10-
const supportsScrollStateQueries = useSupported(() => {
11-
return isMounted.value && CSS.supports('container-type', 'scroll-state')
12-
})
13-
14-
function checkScrollable() {
15-
return document.documentElement.scrollHeight > window.innerHeight
16-
}
17-
18-
function onScroll() {
19-
// Skip JS-based visibility logic if CSS scroll-state queries handle it
20-
if (supportsScrollStateQueries.value) return
21-
22-
const currentY = window.scrollY
23-
const diff = lastScrollY.value - currentY
24-
const nearBottom = currentY + window.innerHeight >= document.documentElement.scrollHeight - 50
25-
26-
// Scrolling UP or near bottom -> show
27-
if (Math.abs(diff) > 10) {
28-
isVisible.value = diff > 0 || nearBottom
29-
lastScrollY.value = currentY
30-
}
31-
32-
// At top -> hide
33-
if (currentY < 100) {
34-
isVisible.value = false
35-
}
36-
37-
// Near bottom -> always show
38-
if (nearBottom) {
39-
isVisible.value = true
40-
}
41-
}
42-
43-
function updateFooterPadding() {
44-
const height = isScrollable.value && footerRef.value ? footerRef.value.offsetHeight : 0
45-
document.documentElement.style.setProperty('--footer-height', `${height}px`)
46-
}
47-
48-
function onResize() {
49-
isScrollable.value = checkScrollable()
50-
updateFooterPadding()
51-
}
52-
53-
useEventListener('scroll', onScroll, { passive: true })
54-
useEventListener('resize', onResize, { passive: true })
55-
56-
onMounted(() => {
57-
nextTick(() => {
58-
lastScrollY.value = window.scrollY
59-
isScrollable.value = checkScrollable()
60-
updateFooterPadding()
61-
// Only apply dynamic classes after mount to avoid hydration mismatch
62-
isMounted.value = true
63-
})
64-
})
65-
</script>
66-
671
<template>
68-
<footer
69-
ref="footerRef"
70-
aria-label="Site footer"
71-
class="border-t border-border bg-bg/90 backdrop-blur-md"
72-
:class="[
73-
// When CSS scroll-state queries are supported, use CSS-only approach
74-
supportsScrollStateQueries
75-
? 'footer-scroll-state'
76-
: // JS-controlled: fixed position, hidden by default, transition only after mount
77-
isScrollable
78-
? [
79-
'fixed bottom-0 left-0 right-0 z-40',
80-
isMounted && 'transition-transform duration-300 ease-out',
81-
isVisible ? 'translate-y-0' : 'translate-y-full',
82-
]
83-
: 'mt-auto',
84-
]"
85-
>
86-
<div class="container py-2 sm:py-6 flex flex-col gap-1 sm:gap-3 text-fg-subtle text-sm">
87-
<div class="flex flex-row items-center justify-between gap-2 sm:gap-4">
2+
<footer class="border-t border-border mt-auto" aria-label="Site footer">
3+
<div class="container py-3 sm:py-8 flex flex-col gap-2 sm:gap-4 text-fg-subtle text-sm">
4+
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 sm:gap-4">
885
<p class="font-mono m-0 hidden sm:block">{{ $t('tagline') }}</p>
89-
<!-- On mobile, show disclaimer here instead of tagline -->
90-
<p class="text-xs text-fg-muted m-0 sm:hidden">{{ $t('non_affiliation_disclaimer') }}</p>
91-
<div class="flex items-center gap-4 sm:gap-6">
6+
<div class="flex items-center gap-3 sm:gap-6">
7+
<NuxtLink
8+
to="/about"
9+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center"
10+
>
11+
{{ $t('footer.about') }}
12+
</NuxtLink>
9213
<a
9314
href="https://docs.npmx.dev"
9415
target="_blank"
9516
rel="noopener noreferrer"
96-
class="link-subtle font-mono text-xs min-h-11 min-w- flex items-center"
17+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
9718
>
9819
{{ $t('footer.docs') }}
20+
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
9921
</a>
10022
<a
10123
href="https://repo.npmx.dev"
10224
target="_blank"
10325
rel="noopener noreferrer"
104-
class="link-subtle font-mono text-xs min-h-11 min-w- flex items-center"
26+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
10527
>
10628
{{ $t('footer.source') }}
29+
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
10730
</a>
10831
<a
10932
href="https://social.npmx.dev"
11033
target="_blank"
11134
rel="noopener noreferrer"
112-
class="link-subtle font-mono text-xs min-h-11 min-w-11 flex items-center"
35+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
11336
>
11437
{{ $t('footer.social') }}
38+
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
11539
</a>
11640
<a
11741
href="https://chat.npmx.dev"
11842
target="_blank"
11943
rel="noopener noreferrer"
120-
class="link-subtle font-mono text-xs min-h-11 min-w-11 flex items-center"
44+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
12145
>
12246
{{ $t('footer.chat') }}
47+
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
12348
</a>
12449
</div>
12550
</div>
126-
<p class="text-xs text-fg-muted text-center sm:text-left m-0 hidden sm:block">
127-
{{ $t('trademark_disclaimer') }}
51+
<p class="text-xs text-fg-muted text-center sm:text-left m-0">
52+
<span class="sm:hidden">{{ $t('non_affiliation_disclaimer') }}</span>
53+
<span class="hidden sm:inline">{{ $t('trademark_disclaimer') }}</span>
12854
</p>
12955
</div>
13056
</footer>
13157
</template>
132-
133-
<style scoped>
134-
/*
135-
* CSS scroll-state container queries (Chrome 133+)
136-
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/@container#scroll-state_container_descriptors
137-
*
138-
* This provides a pure CSS solution for showing/hiding the footer based on scroll state.
139-
* The JS fallback handles browsers without support.
140-
* Once scroll-state queries become baseline, we can remove the JS scroll handling entirely.
141-
*/
142-
@supports (container-type: scroll-state) {
143-
.footer-scroll-state {
144-
position: fixed;
145-
bottom: 0;
146-
left: 0;
147-
right: 0;
148-
z-index: 40;
149-
/* Hidden by default (translated off-screen) */
150-
transform: translateY(100%);
151-
}
152-
153-
@media (prefers-reduced-motion: no-preference) {
154-
.footer-scroll-state {
155-
transition: transform 0.3s ease-out;
156-
}
157-
}
158-
159-
/* Show footer when user can scroll up (meaning they've scrolled down) */
160-
@container scroll-state(scrollable: top) {
161-
.footer-scroll-state {
162-
transform: translateY(0);
163-
}
164-
}
165-
}
166-
</style>

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>

0 commit comments

Comments
 (0)