Skip to content

Commit 2a06e5a

Browse files
committed
Merge branch 'main' of github.com:Kai-ros/npmx.dev
2 parents e952583 + 2fd341d commit 2a06e5a

File tree

104 files changed

+7435
-1345
lines changed

Some content is hidden

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

104 files changed

+7435
-1345
lines changed

.lighthouserc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
},
1717
"assert": {
1818
"assertions": {
19-
"categories:accessibility": ["warn", { "minScore": 0.9 }]
19+
"categories:accessibility": ["warn", { "minScore": 1 }]
2020
}
2121
},
2222
"upload": {

.oxlintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"no-console": "warn",
1111
"no-await-in-loop": "off",
1212
"unicorn/no-array-sort": "off",
13-
"no-restricted-globals": "error"
13+
"no-restricted-globals": "error",
14+
"typescript/consistent-type-imports": "error"
1415
},
1516
"ignorePatterns": [
1617
".output/**",

.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: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,89 @@ 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 or `t()` in 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+
const { t } = useI18n()
237+
const message = t('my.translation.key')
238+
```
239+
240+
3. For dynamic values, use interpolation:
241+
242+
```json
243+
{ "greeting": "Hello, {name}!" }
244+
```
245+
246+
```vue
247+
<p>{{ $t('greeting', { name: userName }) }}</p>
248+
```
249+
250+
### Translation key conventions
251+
252+
- Use dot notation for hierarchy: `section.subsection.key`
253+
- Keep keys descriptive but concise
254+
- Group related keys together
255+
- Use `common.*` for shared strings (loading, retry, close, etc.)
256+
- Use component-specific prefixes: `package.card.*`, `settings.*`, `nav.*`
257+
258+
### Using i18n-ally (recommended)
259+
260+
We recommend the [i18n-ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) VSCode extension for a better development experience:
261+
262+
- Inline translation previews in your code
263+
- Auto-completion for translation keys
264+
- Missing translation detection
265+
- Easy navigation to translation files
266+
267+
The extension is included in our workspace recommendations, so VSCode should prompt you to install it.
268+
269+
### Adding a new locale
270+
271+
1. Create a new JSON file in `i18n/locales/` (e.g., `fr.json`)
272+
2. Add the locale to `nuxt.config.ts`:
273+
274+
```typescript
275+
i18n: {
276+
locales: [
277+
{ code: 'en', language: 'en-US', name: 'English', file: 'en.json' },
278+
{ code: 'fr', language: 'fr-FR', name: 'Francais', file: 'fr.json' },
279+
],
280+
}
281+
```
282+
283+
3. Translate all keys from `en.json`
284+
285+
### Formatting with locale
286+
287+
When formatting numbers or dates that should respect the user's locale, pass the locale:
288+
289+
```typescript
290+
const { locale } = useI18n()
291+
const formatted = formatNumber(12345, locale.value) // "12,345" in en-US
292+
```
293+
211294
## Testing
212295

213296
### Unit tests

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ The aim of [npmx.dev](https://npmx.dev) is to provide a better browser for the n
1818
- **URL compatible** &ndash; Replace `npmjs.com` with `xnpmjs.com` or `npmx.dev` in any URL and it just works.
1919
- **Simplicity** &ndash; No noise, cluttered display, or confusing UI. If in doubt: choose simplicity.
2020

21+
## Shortcuts
22+
23+
> [!IMPORTANT]
24+
> We're keeping the website, repository, and our discord community low-profile until the browser is polished enough. We'll do a formal announcement at that point. Please avoid sharing the website or the invite link to discord on social media directly. The repo is public, so people who care about the project can easily find it and join us. Anyone who wants to help is more than welcome to [join the community](https://chat.npm.dev). If you know others who would be interested, please invite them too!
25+
26+
- [chat.npmx.dev](https://chat.npmx.dev) - Discord Server
27+
- [social.npmx.dev](https://social.npmx.dev) - Bluesky Profile
28+
- [repo.npmx.dev](https://repo.npmx.dev) - GitHub Repository
29+
- [issues.npmx.dev](https://issues.npmx.dev) - GitHub Issues
30+
- [coc.npmx.dev](https://coc.npmx.dev) - Code of Conduct
31+
- [contributing.npmx.dev](https://contributing.npmx.dev) - Contributing Guide
32+
2133
## Features
2234

2335
### Package browsing

app/app.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ button {
195195
margin-top: 2rem;
196196
margin-bottom: 1rem;
197197
line-height: 1.3;
198+
199+
a {
200+
text-decoration: none;
201+
}
198202
}
199203
200204
/* Visual styling based on original README heading level */

app/components/AppFooter.vue

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
<script setup lang="ts">
2-
const isMounted = ref(false)
3-
const isVisible = ref(false)
4-
const isScrollable = ref(true)
5-
const lastScrollY = ref(0)
6-
const footerRef = ref<HTMLElement>()
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')
77
88
// Check if CSS scroll-state container queries are supported
99
// Once this becomes baseline, we can remove the JS scroll handling entirely
10-
const supportsScrollStateQueries = ref(false)
10+
const supportsScrollStateQueries = useSupported(() => {
11+
return isMounted.value && CSS.supports('container-type', 'scroll-state')
12+
})
1113
1214
function checkScrollable() {
1315
return document.documentElement.scrollHeight > window.innerHeight
@@ -48,26 +50,17 @@ function onResize() {
4850
updateFooterPadding()
4951
}
5052
51-
onMounted(() => {
52-
// Feature detect CSS scroll-state container queries (Chrome 133+)
53-
// @see https://developer.mozilla.org/en-US/docs/Web/CSS/@container#scroll-state_container_descriptors
54-
supportsScrollStateQueries.value = CSS.supports('container-type', 'scroll-state')
53+
useEventListener('scroll', onScroll, { passive: true })
54+
useEventListener('resize', onResize, { passive: true })
5555
56+
onMounted(() => {
5657
nextTick(() => {
5758
lastScrollY.value = window.scrollY
5859
isScrollable.value = checkScrollable()
5960
updateFooterPadding()
6061
// Only apply dynamic classes after mount to avoid hydration mismatch
6162
isMounted.value = true
6263
})
63-
64-
window.addEventListener('scroll', onScroll, { passive: true })
65-
window.addEventListener('resize', onResize, { passive: true })
66-
})
67-
68-
onUnmounted(() => {
69-
window.removeEventListener('scroll', onScroll)
70-
window.removeEventListener('resize', onResize)
7164
})
7265
</script>
7366

@@ -92,29 +85,35 @@ onUnmounted(() => {
9285
>
9386
<div class="container py-2 sm:py-6 flex flex-col gap-1 sm:gap-3 text-fg-subtle text-sm">
9487
<div class="flex flex-row items-center justify-between gap-2 sm:gap-4">
95-
<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>
9689
<!-- On mobile, show disclaimer here instead of tagline -->
97-
<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>
9891
<div class="flex items-center gap-4 sm:gap-6">
9992
<a
100-
href="https://github.com/npmx-dev/npmx.dev"
93+
href="https://repo.npmx.dev"
94+
rel="noopener noreferrer"
95+
class="link-subtle font-mono text-xs min-h-11 min-w- flex items-center"
96+
>
97+
{{ $t('footer.source') }}
98+
</a>
99+
<a
100+
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-
source
104+
{{ $t('footer.social') }}
105105
</a>
106-
<span class="text-border">|</span>
107106
<a
108-
href="https://roe.dev"
107+
href="https://chat.npmx.dev"
109108
rel="noopener noreferrer"
110109
class="link-subtle font-mono text-xs min-h-11 min-w-11 flex items-center"
111110
>
112-
@danielroe
111+
{{ $t('footer.chat') }}
113112
</a>
114113
</div>
115114
</div>
116115
<p class="text-xs text-fg-muted text-center sm:text-left m-0 hidden sm:block">
117-
npm is a registered trademark of npm, Inc. This site is not affiliated with npm, Inc.
116+
{{ $t('trademark_disclaimer') }}
118117
</p>
119118
</div>
120119
</footer>

app/components/AppHeader.vue

Lines changed: 50 additions & 26 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" />
27-
28-
<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
29-
<li class="flex">
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-fg-subtle"><span style="letter-spacing: -0.2em">.</span>/</span>npmx
28+
</NuxtLink>
29+
<!-- Spacer when logo is hidden -->
30+
<span v-else class="w-1" />
31+
</div>
32+
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">
35+
<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,20 +47,38 @@ withDefaults(
4147
</kbd>
4248
</NuxtLink>
4349
</li>
44-
<li v-if="showConnector" class="flex">
45-
<ConnectorStatus />
50+
51+
<!-- Packages dropdown (when connected) -->
52+
<li v-if="isConnected && npmUser" class="flex items-center">
53+
<HeaderPackagesDropdown :username="npmUser" />
4654
</li>
47-
<li v-else class="flex">
48-
<a
49-
href="https://github.com/npmx-dev/npmx.dev"
50-
rel="noopener noreferrer"
51-
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
52-
>
53-
<span class="i-carbon-logo-github w-4 h-4" />
54-
<span class="hidden sm:inline">github</span>
55-
</a>
55+
56+
<!-- Orgs dropdown (when connected) -->
57+
<li v-if="isConnected && npmUser" class="flex items-center">
58+
<HeaderOrgsDropdown :username="npmUser" />
5659
</li>
5760
</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>
5882
</nav>
5983
</header>
6084
</template>

app/components/AppTooltip.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const props = defineProps<{
66
position?: 'top' | 'bottom' | 'left' | 'right'
77
}>()
88
9-
const isVisible = ref(false)
9+
const isVisible = shallowRef(false)
1010
const tooltipId = useId()
1111
1212
const positionClasses: Record<string, string> = {

0 commit comments

Comments
 (0)