Skip to content

Commit e8a9a1c

Browse files
authored
Merge branch 'main' into perf/ofetch-timeout
2 parents decdba6 + 2c83358 commit e8a9a1c

61 files changed

Lines changed: 707 additions & 417 deletions

Some content is hidden

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

CONTRIBUTING.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,14 @@ For example to check if all Japanese translation keys are up-to-date, run:
301301
pnpm i18n:check ja-JP
302302
```
303303

304+
To automatically add missing keys with English placeholders, use `--fix`:
305+
306+
```bash
307+
pnpm i18n:check:fix fr-FR
308+
```
309+
310+
This will add missing keys with `"EN TEXT TO REPLACE: {english text}"` as placeholder values, making it easier to see what needs translation.
311+
304312
#### Country variants (advanced)
305313

306314
Most languages only need a single locale file. Country variants are only needed when you want to support regional differences (e.g., `es-ES` for Spain vs `es-419` for Latin America).
@@ -361,13 +369,17 @@ We recommend the [i18n-ally](https://marketplace.visualstudio.com/items?itemName
361369

362370
The extension is included in our workspace recommendations, so VSCode should prompt you to install it.
363371

364-
### Formatting with locale
372+
### Formatting numbers and dates
365373

366-
When formatting numbers or dates that should respect the user's locale, pass the locale:
374+
Use vue-i18n's built-in formatters for locale-aware formatting:
367375

368-
```typescript
369-
const { locale } = useI18n()
370-
const formatted = formatNumber(12345, locale.value) // "12,345" in en-US
376+
```vue
377+
<template>
378+
<p>{{ $n(12345) }}</p>
379+
<!-- "12,345" in en-US, "12 345" in fr-FR -->
380+
<p>{{ $d(new Date()) }}</p>
381+
<!-- locale-aware date -->
382+
</template>
371383
```
372384

373385
## Testing

app/assets/main.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ html.light .shiki span {
463463
border-collapse: collapse;
464464
margin: 1.5rem 0;
465465
font-size: 0.875rem;
466+
word-break: keep-all;
466467
}
467468

468469
.readme-content th,

app/components/AppFooter.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const isHome = computed(() => route.name === 'index')
1313
<p class="font-mono text-balance m-0 hidden sm:block">{{ $t('tagline') }}</p>
1414
<BuildEnvironment v-if="!isHome" footer />
1515
</div>
16-
<div class="flex items-center gap-3 sm:gap-6">
16+
<div class="flex flex-wrap items-center gap-x-3 sm:gap-6">
1717
<NuxtLink
1818
to="/about"
1919
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center"

app/components/AppHeader.vue

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,29 @@ withDefaults(
1010
1111
const { isConnected, npmUser } = useConnector()
1212
13-
const router = useRouter()
14-
1513
const showFullSearch = shallowRef(false)
1614
17-
onKeyStroke(',', e => {
18-
// Don't trigger if user is typing in an input
19-
const target = e.target as HTMLElement
20-
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
21-
return
22-
}
15+
onKeyStroke(
16+
',',
17+
e => {
18+
// Don't trigger if user is typing in an input
19+
const target = e.target as HTMLElement
20+
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
21+
return
22+
}
2323
24-
e.preventDefault()
25-
router.push('/settings')
26-
})
24+
e.preventDefault()
25+
navigateTo('/settings')
26+
},
27+
{ dedupe: true },
28+
)
2729
</script>
2830

2931
<template>
3032
<header class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border">
3133
<nav
3234
:aria-label="$t('nav.main_navigation')"
33-
class="container h-14 flex items-center justify-start"
35+
class="container min-h-14 flex items-center justify-start"
3436
>
3537
<!-- Start: Logo -->
3638
<div :class="{ 'hidden sm:block': showFullSearch }" class="flex-shrink-0">
@@ -83,18 +85,18 @@ onKeyStroke(',', e => {
8385
<!-- End: User status + GitHub -->
8486
<div
8587
:class="{ 'hidden sm:flex': showFullSearch }"
86-
class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ms-auto sm:ms-0"
88+
class="flex-1 flex flex-wrap items-center justify-end sm:gap-3 ms-auto sm:ms-0"
8789
>
8890
<NuxtLink
8991
to="/about"
90-
class="sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
92+
class="px-2 py-1.5 sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
9193
>
9294
{{ $t('footer.about') }}
9395
</NuxtLink>
9496

9597
<NuxtLink
9698
to="/settings"
97-
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"
99+
class="link-subtle font-mono text-sm inline-flex items-center gap-2 px-2 py-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
98100
aria-keyshortcuts=","
99101
>
100102
{{ $t('nav.settings') }}

app/components/BuildEnvironment.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defineProps<{
33
footer?: boolean
44
}>()
55
6+
const { locale } = useI18n()
67
const buildInfo = useAppConfig().buildInfo
78
</script>
89

@@ -13,7 +14,7 @@ const buildInfo = useAppConfig().buildInfo
1314
style="animation-delay: 0.05s"
1415
>
1516
<i18n-t keypath="built_at">
16-
<NuxtTime :datetime="buildInfo.time" relative />
17+
<NuxtTime :datetime="buildInfo.time" :locale="locale" relative />
1718
</i18n-t>
1819
<span>&middot;</span>
1920
<NuxtLink
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<script setup lang="ts">
2+
import { ref, computed } from 'vue'
3+
4+
interface Props {
5+
title: string
6+
isLoading?: boolean
7+
headingLevel?: `h${number}`
8+
id: string
9+
}
10+
11+
const props = withDefaults(defineProps<Props>(), {
12+
isLoading: false,
13+
headingLevel: 'h2',
14+
})
15+
16+
const appSettings = useSettings()
17+
18+
const buttonId = `${props.id}-collapsible-button`
19+
const contentId = `${props.id}-collapsible-content`
20+
const headingId = `${props.id}-heading`
21+
22+
const isOpen = ref(true)
23+
24+
onPrehydrate(() => {
25+
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
26+
const collapsed: string[] = settings?.sidebar?.collapsed || []
27+
for (const id of collapsed) {
28+
if (!document.documentElement.dataset.collapsed?.includes(id)) {
29+
document.documentElement.dataset.collapsed = (
30+
document.documentElement.dataset.collapsed +
31+
' ' +
32+
id
33+
).trim()
34+
}
35+
}
36+
})
37+
38+
onMounted(() => {
39+
if (document?.documentElement) {
40+
isOpen.value = !(document.documentElement.dataset.collapsed?.includes(props.id) ?? false)
41+
}
42+
})
43+
44+
function toggle() {
45+
isOpen.value = !isOpen.value
46+
47+
const removed = appSettings.settings.value.sidebar.collapsed.filter(c => c !== props.id)
48+
49+
if (isOpen.value) {
50+
appSettings.settings.value.sidebar.collapsed = removed
51+
} else {
52+
removed.push(props.id)
53+
appSettings.settings.value.sidebar.collapsed = removed
54+
}
55+
56+
document.documentElement.dataset.collapsed =
57+
appSettings.settings.value.sidebar.collapsed.join(' ')
58+
}
59+
60+
const ariaLabel = computed(() => {
61+
const action = isOpen.value ? 'Collapse' : 'Expand'
62+
return props.title ? `${action} ${props.title}` : action
63+
})
64+
useHead({
65+
style: [
66+
{
67+
innerHTML: `
68+
:root[data-collapsed~='${props.id}'] section[data-anchor-id='${props.id}'] .collapsible-content {
69+
grid-template-rows: 0fr;
70+
}`,
71+
},
72+
],
73+
})
74+
</script>
75+
76+
<template>
77+
<section class="scroll-mt-20" :data-anchor-id="id">
78+
<div class="flex items-center justify-between mb-3">
79+
<component
80+
:is="headingLevel"
81+
:id="headingId"
82+
class="group text-xs text-fg-subtle uppercase tracking-wider flex items-center gap-2"
83+
>
84+
<button
85+
:id="buttonId"
86+
type="button"
87+
class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg-muted transition-colors duration-200 shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
88+
:aria-expanded="isOpen"
89+
:aria-controls="contentId"
90+
:aria-label="ariaLabel"
91+
@click="toggle"
92+
>
93+
<span
94+
v-if="isLoading"
95+
class="i-carbon:rotate-180 w-3 h-3 motion-safe:animate-spin"
96+
aria-hidden="true"
97+
/>
98+
<span
99+
v-else
100+
class="w-3 h-3 transition-transform duration-200"
101+
:class="isOpen ? 'i-carbon:chevron-down' : 'i-carbon:chevron-right'"
102+
aria-hidden="true"
103+
/>
104+
</button>
105+
106+
<a
107+
:href="`#${id}`"
108+
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
109+
>
110+
{{ title }}
111+
<span
112+
class="i-carbon:link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
113+
aria-hidden="true"
114+
/>
115+
</a>
116+
</component>
117+
118+
<!-- Actions slot for buttons or other elements -->
119+
<slot name="actions" />
120+
</div>
121+
122+
<div
123+
:id="contentId"
124+
class="grid ms-6 transition-[grid-template-rows] duration-200 ease-in-out collapsible-content overflow-hidden"
125+
>
126+
<div class="min-h-0">
127+
<slot />
128+
</div>
129+
</div>
130+
</section>
131+
</template>

app/components/PackageCard.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ const emit = defineEmits<{
123123
<dt class="sr-only">{{ $t('package.card.weekly_downloads') }}</dt>
124124
<dd class="flex items-center gap-1.5">
125125
<span class="i-carbon:chart-line w-3.5 h-3.5 inline-block" aria-hidden="true" />
126-
<span class="font-mono">{{ formatNumber(result.downloads.weekly) }}/w</span>
126+
<span class="font-mono">{{ $n(result.downloads.weekly) }}/w</span>
127127
</dd>
128128
</div>
129129
</dl>
@@ -158,7 +158,7 @@ const emit = defineEmits<{
158158
>
159159
<span class="i-carbon:chart-line w-3.5 h-3.5 inline-block" aria-hidden="true" />
160160
<span class="font-mono text-xs">
161-
{{ formatNumber(result.downloads.weekly) }} {{ $t('common.per_week') }}
161+
{{ $n(result.downloads.weekly) }} {{ $t('common.per_week') }}
162162
</span>
163163
</div>
164164
</div>

app/components/PackageDependencies.vue

Lines changed: 18 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -70,22 +70,11 @@ const sortedOptionalDependencies = computed(() => {
7070
<template>
7171
<div class="space-y-8">
7272
<!-- Dependencies -->
73-
<section id="dependencies" v-if="sortedDependencies.length > 0" class="scroll-mt-20">
74-
<h2
75-
id="dependencies-heading"
76-
class="group text-xs text-fg-subtle uppercase tracking-wider mb-3"
77-
>
78-
<a
79-
href="#dependencies"
80-
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
81-
>
82-
{{ $t('package.dependencies.title', { count: sortedDependencies.length }) }}
83-
<span
84-
class="i-carbon:link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
85-
aria-hidden="true"
86-
/>
87-
</a>
88-
</h2>
73+
<CollapsibleSection
74+
v-if="sortedDependencies.length > 0"
75+
id="dependencies"
76+
:title="$t('package.dependencies.title', { count: sortedDependencies.length })"
77+
>
8978
<ul class="space-y-1 list-none m-0 p-0" :aria-label="$t('package.dependencies.list_label')">
9079
<li
9180
v-for="[dep, version] in sortedDependencies.slice(0, depsExpanded ? undefined : 10)"
@@ -151,33 +140,14 @@ const sortedOptionalDependencies = computed(() => {
151140
</span>
152141
</li>
153142
</ul>
154-
<button
155-
v-if="sortedDependencies.length > 10 && !depsExpanded"
156-
type="button"
157-
class="mt-2 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
158-
@click="depsExpanded = true"
159-
>
160-
{{ $t('package.dependencies.show_all', { count: sortedDependencies.length }) }}
161-
</button>
162-
</section>
143+
</CollapsibleSection>
163144

164145
<!-- Peer Dependencies -->
165-
<section id="peer-dependencies" v-if="sortedPeerDependencies.length > 0" class="scroll-mt-20">
166-
<h2
167-
id="peer-dependencies-heading"
168-
class="group text-xs text-fg-subtle uppercase tracking-wider mb-3"
169-
>
170-
<a
171-
href="#peer-dependencies"
172-
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
173-
>
174-
{{ $t('package.peer_dependencies.title', { count: sortedPeerDependencies.length }) }}
175-
<span
176-
class="i-carbon:link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
177-
aria-hidden="true"
178-
/>
179-
</a>
180-
</h2>
146+
<CollapsibleSection
147+
v-if="sortedPeerDependencies.length > 0"
148+
id="peer-dependencies"
149+
:title="$t('package.peer_dependencies.title', { count: sortedPeerDependencies.length })"
150+
>
181151
<ul
182152
class="space-y-1 list-none m-0 p-0"
183153
:aria-label="$t('package.peer_dependencies.list_label')"
@@ -223,31 +193,16 @@ const sortedOptionalDependencies = computed(() => {
223193
>
224194
{{ $t('package.peer_dependencies.show_all', { count: sortedPeerDependencies.length }) }}
225195
</button>
226-
</section>
196+
</CollapsibleSection>
227197

228198
<!-- Optional Dependencies -->
229-
<section
230-
id="optional-dependencies"
199+
<CollapsibleSection
231200
v-if="sortedOptionalDependencies.length > 0"
232-
class="scroll-mt-20"
201+
id="optional-dependencies"
202+
:title="
203+
$t('package.optional_dependencies.title', { count: sortedOptionalDependencies.length })
204+
"
233205
>
234-
<h2
235-
id="optional-dependencies-heading"
236-
class="group text-xs text-fg-subtle uppercase tracking-wider mb-3"
237-
>
238-
<a
239-
href="#optional-dependencies"
240-
class="inline-flex items-center gap-1.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 no-underline"
241-
>
242-
{{
243-
$t('package.optional_dependencies.title', { count: sortedOptionalDependencies.length })
244-
}}
245-
<span
246-
class="i-carbon:link w-3 h-3 block opacity-0 group-hover:opacity-100 transition-opacity duration-200"
247-
aria-hidden="true"
248-
/>
249-
</a>
250-
</h2>
251206
<ul
252207
class="space-y-1 list-none m-0 p-0"
253208
:aria-label="$t('package.optional_dependencies.list_label')"
@@ -286,6 +241,6 @@ const sortedOptionalDependencies = computed(() => {
286241
$t('package.optional_dependencies.show_all', { count: sortedOptionalDependencies.length })
287242
}}
288243
</button>
289-
</section>
244+
</CollapsibleSection>
290245
</div>
291246
</template>

0 commit comments

Comments
 (0)