Skip to content

Commit 5ab1dd3

Browse files
authored
Merge branch 'main' into byk/feat/md
2 parents edf5815 + 497ae72 commit 5ab1dd3

94 files changed

Lines changed: 9109 additions & 1399 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.vscode/extensions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"recommendations": ["oxc.oxc-vscode", "Vue.volar"]
2+
"recommendations": ["oxc.oxc-vscode", "Vue.volar", "lokalise.i18n-ally"]
33
}

.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"i18n-ally.localesPaths": ["./i18n/locales"],
3+
"i18n-ally.keystyle": "nested"
4+
}

CONTRIBUTING.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,90 @@ const props = defineProps<{
208208

209209
Ideally, extract utilities into separate files so they can be unit tested. 🙏
210210

211+
## Localization (i18n)
212+
213+
npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization. We aim to make the UI accessible to users in their preferred language.
214+
215+
### Approach
216+
217+
- All user-facing strings should use translation keys via `$t()` in templates and script
218+
- Translation files live in `i18n/locales/` (e.g., `en.json`)
219+
- We use the `no_prefix` strategy (no `/en/` or `/fr/` in URLs)
220+
- Locale preference is stored in cookies and respected on subsequent visits
221+
222+
### Adding translations
223+
224+
1. Add your translation key to `i18n/locales/en.json` first (English is the source of truth)
225+
2. Use the key in your component:
226+
227+
```vue
228+
<template>
229+
<p>{{ $t('my.translation.key') }}</p>
230+
</template>
231+
```
232+
233+
Or in script:
234+
235+
```typescript
236+
<script setup lang="ts">
237+
const message = computed(() => $t('my.translation.key'))
238+
</script>
239+
```
240+
241+
3. For dynamic values, use interpolation:
242+
243+
```json
244+
{ "greeting": "Hello, {name}!" }
245+
```
246+
247+
```vue
248+
<p>{{ $t('greeting', { name: userName }) }}</p>
249+
```
250+
251+
### Translation key conventions
252+
253+
- Use dot notation for hierarchy: `section.subsection.key`
254+
- Keep keys descriptive but concise
255+
- Group related keys together
256+
- Use `common.*` for shared strings (loading, retry, close, etc.)
257+
- Use component-specific prefixes: `package.card.*`, `settings.*`, `nav.*`
258+
259+
### Using i18n-ally (recommended)
260+
261+
We recommend the [i18n-ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) VSCode extension for a better development experience:
262+
263+
- Inline translation previews in your code
264+
- Auto-completion for translation keys
265+
- Missing translation detection
266+
- Easy navigation to translation files
267+
268+
The extension is included in our workspace recommendations, so VSCode should prompt you to install it.
269+
270+
### Adding a new locale
271+
272+
1. Create a new JSON file in `i18n/locales/` (e.g., `fr.json`)
273+
2. Add the locale to `nuxt.config.ts`:
274+
275+
```typescript
276+
i18n: {
277+
locales: [
278+
{ code: 'en', language: 'en-US', name: 'English', file: 'en.json' },
279+
{ code: 'fr', language: 'fr-FR', name: 'Francais', file: 'fr.json' },
280+
],
281+
}
282+
```
283+
284+
3. Translate all keys from `en.json`
285+
286+
### Formatting with locale
287+
288+
When formatting numbers or dates that should respect the user's locale, pass the locale:
289+
290+
```typescript
291+
const { locale } = useI18n()
292+
const formatted = formatNumber(12345, locale.value) // "12,345" in en-US
293+
```
294+
211295
## Testing
212296

213297
### Unit tests

app/app.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { useEventListener } from '@vueuse/core'
44
const route = useRoute()
55
const router = useRouter()
66
7+
// Initialize accent color before hydration to prevent flash
8+
initAccentOnPrehydrate()
9+
710
const isHomepage = computed(() => route.path === '/')
811
912
useHead({
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script setup lang="ts">
2+
import { useAccentColor } from '~/composables/useSettings'
3+
4+
const { accentColors, selectedAccentColor, setAccentColor } = useAccentColor()
5+
</script>
6+
7+
<template>
8+
<div role="listbox" aria-label="Accent colors" class="flex items-center justify-between">
9+
<button
10+
v-for="color in accentColors"
11+
:key="color.id"
12+
type="button"
13+
role="option"
14+
:aria-selected="selectedAccentColor === color.id"
15+
:aria-label="color.name"
16+
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 focus-ring aria-selected:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle)"
17+
:style="{ backgroundColor: color.value }"
18+
@click="setAccentColor(color.id)"
19+
/>
20+
<button
21+
type="button"
22+
aria-label="Clear accent color"
23+
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 focus-ring flex items-center justify-center bg-accent-fallback"
24+
@click="setAccentColor(null)"
25+
>
26+
<span class="i-carbon-error size-4 text-bg" aria-hidden="true" />
27+
</button>
28+
</div>
29+
</template>

app/components/AppFooter.vue

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,35 +85,35 @@ onMounted(() => {
8585
>
8686
<div class="container py-2 sm:py-6 flex flex-col gap-1 sm:gap-3 text-fg-subtle text-sm">
8787
<div class="flex flex-row items-center justify-between gap-2 sm:gap-4">
88-
<p class="font-mono m-0 hidden sm:block">a better browser for the npm registry</p>
88+
<p class="font-mono m-0 hidden sm:block">{{ $t('tagline') }}</p>
8989
<!-- On mobile, show disclaimer here instead of tagline -->
90-
<p class="text-xs text-fg-muted m-0 sm:hidden">not affiliated with npm, Inc.</p>
90+
<p class="text-xs text-fg-muted m-0 sm:hidden">{{ $t('non_affiliation_disclaimer') }}</p>
9191
<div class="flex items-center gap-4 sm:gap-6">
9292
<a
9393
href="https://repo.npmx.dev"
9494
rel="noopener noreferrer"
9595
class="link-subtle font-mono text-xs min-h-11 min-w- flex items-center"
9696
>
97-
source
97+
{{ $t('footer.source') }}
9898
</a>
9999
<a
100100
href="https://social.npmx.dev"
101101
rel="noopener noreferrer"
102102
class="link-subtle font-mono text-xs min-h-11 min-w-11 flex items-center"
103103
>
104-
social
104+
{{ $t('footer.social') }}
105105
</a>
106106
<a
107107
href="https://chat.npmx.dev"
108108
rel="noopener noreferrer"
109109
class="link-subtle font-mono text-xs min-h-11 min-w-11 flex items-center"
110110
>
111-
chat
111+
{{ $t('footer.chat') }}
112112
</a>
113113
</div>
114114
</div>
115115
<p class="text-xs text-fg-muted text-center sm:text-left m-0 hidden sm:block">
116-
npm is a registered trademark of npm, Inc. This site is not affiliated with npm, Inc.
116+
{{ $t('trademark_disclaimer') }}
117117
</p>
118118
</div>
119119
</footer>
@@ -137,7 +137,12 @@ onMounted(() => {
137137
z-index: 40;
138138
/* Hidden by default (translated off-screen) */
139139
transform: translateY(100%);
140-
transition: transform 0.3s ease-out;
140+
}
141+
142+
@media (prefers-reduced-motion: no-preference) {
143+
.footer-scroll-state {
144+
transition: transform 0.3s ease-out;
145+
}
141146
}
142147
143148
/* Show footer when user can scroll up (meaning they've scrolled down) */

app/components/AppHeader.vue

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,36 @@ withDefaults(
99
showConnector: true,
1010
},
1111
)
12+
13+
const { isConnected, npmUser } = useConnector()
1214
</script>
1315

1416
<template>
1517
<header class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border">
16-
<nav aria-label="Main navigation" class="container h-14 flex items-center justify-between">
17-
<NuxtLink
18-
v-if="showLogo"
19-
to="/"
20-
aria-label="npmx home"
21-
class="header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
22-
>
23-
<span class="text-fg-subtle"><span style="letter-spacing: -0.2em">.</span>/</span>npmx
24-
</NuxtLink>
25-
<!-- Spacer when logo is hidden -->
26-
<span v-else class="w-1" />
18+
<nav aria-label="Main navigation" class="container h-14 flex items-center">
19+
<!-- Left: Logo -->
20+
<div class="flex-shrink-0">
21+
<NuxtLink
22+
v-if="showLogo"
23+
to="/"
24+
:aria-label="$t('header.home')"
25+
class="header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
26+
>
27+
<span class="text-accent"><span class="-tracking-0.2em">.</span>/</span>npmx
28+
</NuxtLink>
29+
<!-- Spacer when logo is hidden -->
30+
<span v-else class="w-1" />
31+
</div>
2732

28-
<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
33+
<!-- Center: Main nav items -->
34+
<ul class="flex-1 flex items-center justify-center gap-4 sm:gap-6 list-none m-0 p-0">
2935
<li class="flex items-center">
3036
<NuxtLink
3137
to="/search"
3238
class="link-subtle font-mono text-sm inline-flex items-center gap-2"
3339
aria-keyshortcuts="/"
3440
>
35-
search
41+
{{ $t('nav.search') }}
3642
<kbd
3743
class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
3844
aria-hidden="true"
@@ -41,26 +47,38 @@ withDefaults(
4147
</kbd>
4248
</NuxtLink>
4349
</li>
44-
<li class="flex items-center">
45-
<ClientOnly>
46-
<SettingsMenu />
47-
</ClientOnly>
48-
</li>
49-
<li v-if="showConnector" class="flex items-center">
50-
<ConnectorStatus />
50+
51+
<!-- Packages dropdown (when connected) -->
52+
<li v-if="isConnected && npmUser" class="flex items-center">
53+
<HeaderPackagesDropdown :username="npmUser" />
5154
</li>
52-
<li v-else class="flex items-center">
53-
<a
54-
href="https://github.com/npmx-dev/npmx.dev"
55-
rel="noopener noreferrer"
56-
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
57-
aria-label="GitHub"
58-
>
59-
<span class="i-carbon-logo-github w-4 h-4" aria-hidden="true" />
60-
<span class="hidden sm:inline" aria-hidden="true">github</span>
61-
</a>
55+
56+
<!-- Orgs dropdown (when connected) -->
57+
<li v-if="isConnected && npmUser" class="flex items-center">
58+
<HeaderOrgsDropdown :username="npmUser" />
6259
</li>
6360
</ul>
61+
62+
<!-- Right: User status + GitHub -->
63+
<div class="flex-shrink-0 flex items-center gap-6">
64+
<ClientOnly>
65+
<SettingsMenu />
66+
</ClientOnly>
67+
68+
<div v-if="showConnector">
69+
<ConnectorStatus />
70+
</div>
71+
72+
<a
73+
href="https://github.com/npmx-dev/npmx.dev"
74+
target="_blank"
75+
rel="noopener noreferrer"
76+
class="link-subtle"
77+
:aria-label="$t('header.github')"
78+
>
79+
<span class="i-carbon-logo-github w-5 h-5" aria-hidden="true" />
80+
</a>
81+
</div>
6482
</nav>
6583
</header>
6684
</template>

app/components/ChartModal.vue

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<script setup lang="ts">
2+
const open = defineModel<boolean>('open', { default: false })
3+
4+
function handleKeydown(event: KeyboardEvent) {
5+
if (event.key === 'Escape') {
6+
open.value = false
7+
}
8+
}
9+
</script>
10+
11+
<template>
12+
<Teleport to="body">
13+
<Transition
14+
enter-active-class="transition-opacity duration-200"
15+
leave-active-class="transition-opacity duration-200"
16+
enter-from-class="opacity-0"
17+
leave-to-class="opacity-0"
18+
>
19+
<div
20+
v-if="open"
21+
class="fixed inset-0 z-50 flex items-center justify-center p-0 sm:p-4"
22+
@keydown="handleKeydown"
23+
>
24+
<!-- Backdrop -->
25+
<button
26+
type="button"
27+
class="absolute inset-0 bg-black/60 cursor-default"
28+
aria-label="Close modal"
29+
@click="open = false"
30+
/>
31+
32+
<div
33+
class="relative w-full h-full sm:h-auto bg-bg sm:border sm:border-border sm:rounded-lg shadow-xl sm:max-h-[90vh] overflow-y-auto overscroll-contain sm:max-w-3xl"
34+
role="dialog"
35+
aria-modal="true"
36+
aria-labelledby="chart-modal-title"
37+
>
38+
<div class="p-4 sm:p-6">
39+
<div class="flex items-center justify-between mb-4 sm:mb-6">
40+
<h2 id="chart-modal-title" class="font-mono text-lg font-medium">
41+
<slot name="title" />
42+
</h2>
43+
<button
44+
type="button"
45+
class="text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
46+
aria-label="Close"
47+
@click="open = false"
48+
>
49+
<span class="i-carbon-close block w-5 h-5" aria-hidden="true" />
50+
</button>
51+
</div>
52+
<div class="font-mono text-sm">
53+
<slot />
54+
</div>
55+
</div>
56+
</div>
57+
</div>
58+
</Transition>
59+
</Teleport>
60+
</template>

0 commit comments

Comments
 (0)