Skip to content

Commit d150ec7

Browse files
committed
Merge branch 'main' into feat/convert-npmjs-readme-urls-to-npmx
2 parents db510a4 + b13eeb8 commit d150ec7

45 files changed

Lines changed: 5000 additions & 356 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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,18 @@ import { hasProtocol } from 'ufo'
189189
| Constants | SCREAMING_SNAKE_CASE | `NPM_REGISTRY`, `ALLOWED_TAGS` |
190190
| Types/Interfaces | PascalCase | `NpmSearchResponse` |
191191

192+
> [!TIP]
193+
> Exports in `app/composables/`, `app/utils/`, and `server/utils/` are auto-imported by Nuxt. To prevent [knip](https://knip.dev/) from flagging them as unused, add a `@public` JSDoc annotation:
194+
>
195+
> ```typescript
196+
> /**
197+
> * @public
198+
> */
199+
> export function myAutoImportedFunction() {
200+
> // ...
201+
> }
202+
> ```
203+
192204
### Vue components
193205
194206
- Use Composition API with `<script setup lang="ts">`

app/components/AppFooter.vue

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,55 @@
11
<template>
22
<footer class="border-t border-border mt-auto">
33
<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">
4+
<div class="flex flex-col sm:flex-row items-center justify-start gap-2 sm:gap-4">
55
<p class="font-mono m-0 hidden sm:block">{{ $t('tagline') }}</p>
6-
<div class="flex items-center gap-3 sm:gap-6">
6+
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
7+
<div class="flex items-center justify-start gap-3 sm:gap-6">
78
<NuxtLink
89
to="/about"
910
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center"
1011
>
11-
{{ $t('footer.about') }}
12+
<span>{{ $t('footer.about') }}</span>
1213
</NuxtLink>
1314
<a
1415
href="https://docs.npmx.dev"
1516
target="_blank"
1617
rel="noopener noreferrer"
17-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
18+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center justify-start gap-1"
1819
>
19-
{{ $t('footer.docs') }}
20-
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
20+
<span>{{ $t('footer.docs') }}</span>
21+
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
2122
</a>
2223
<a
2324
href="https://repo.npmx.dev"
2425
target="_blank"
2526
rel="noopener noreferrer"
26-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
27+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex justify-start items-center gap-1"
2728
>
28-
{{ $t('footer.source') }}
29-
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
29+
<span>{{ $t('footer.source') }}</span>
30+
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
3031
</a>
3132
<a
3233
href="https://social.npmx.dev"
3334
target="_blank"
3435
rel="noopener noreferrer"
35-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
36+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center justify-start gap-1"
3637
>
37-
{{ $t('footer.social') }}
38-
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
38+
<span>{{ $t('footer.social') }}</span>
39+
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
3940
</a>
4041
<a
4142
href="https://chat.npmx.dev"
4243
target="_blank"
4344
rel="noopener noreferrer"
44-
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center gap-1"
45+
class="link-subtle font-mono text-xs min-h-8 sm:min-h-11 flex items-center justify-start gap-1"
4546
>
46-
{{ $t('footer.chat') }}
47-
<span class="i-carbon-launch w-3 h-3" aria-hidden="true" />
47+
<span>{{ $t('footer.chat') }}</span>
48+
<span class="i-carbon:launch rtl-flip w-3 h-3" aria-hidden="true" />
4849
</a>
4950
</div>
5051
</div>
51-
<p class="text-xs text-fg-muted text-center sm:text-left m-0">
52+
<p class="text-xs text-fg-muted text-center sm:text-start m-0">
5253
<span class="sm:hidden">{{ $t('non_affiliation_disclaimer') }}</span>
5354
<span class="hidden sm:inline">{{ $t('trademark_disclaimer') }}</span>
5455
</p>

app/components/AppHeader.vue

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ onKeyStroke(',', e => {
5252

5353
<template>
5454
<header class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border">
55-
<nav :aria-label="$t('nav.main_navigation')" class="container h-14 flex items-center">
56-
<!-- Left: Logo -->
55+
<nav
56+
:aria-label="$t('nav.main_navigation')"
57+
class="container h-14 flex items-center justify-start"
58+
>
59+
<!-- Start: Logo -->
5760
<div class="flex-shrink-0">
5861
<NuxtLink
5962
v-if="showLogo"
@@ -79,7 +82,7 @@ onKeyStroke(',', e => {
7982
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
8083
<div class="search-box relative flex items-center">
8184
<span
82-
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+
class="absolute inset-is-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"
8386
>
8487
/
8588
</span>
@@ -91,7 +94,7 @@ onKeyStroke(',', e => {
9194
name="q"
9295
:placeholder="$t('search.placeholder')"
9396
v-bind="noCorrect"
94-
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+
class="w-full bg-bg-subtle border border-border rounded-md ps-7 pe-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"
9598
@input="handleSearchInput"
9699
@focus="isSearchFocused = true"
97100
@blur="isSearchFocused = false"
@@ -115,8 +118,8 @@ onKeyStroke(',', e => {
115118
</ul>
116119
</div>
117120

118-
<!-- Right: User status + GitHub -->
119-
<div class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ml-auto sm:ml-0">
121+
<!-- End: User status + GitHub -->
122+
<div class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ms-auto sm:ms-0">
120123
<NuxtLink
121124
to="/about"
122125
class="sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"

app/components/CodeDirectoryListing.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function formatBytes(bytes: number): string {
7070
:to="parentPath ? `${baseUrl}/${parentPath}` : baseUrl"
7171
class="flex items-center gap-2 font-mono text-sm text-fg-muted hover:text-fg transition-colors"
7272
>
73-
<span class="i-carbon-folder w-4 h-4 text-yellow-600" />
73+
<span class="i-carbon:folder w-4 h-4 text-yellow-600" />
7474
<span>..</span>
7575
</NuxtLink>
7676
</td>
@@ -91,13 +91,13 @@ function formatBytes(bytes: number): string {
9191
>
9292
<span
9393
v-if="node.type === 'directory'"
94-
class="i-carbon-folder w-4 h-4 text-yellow-600"
94+
class="i-carbon:folder w-4 h-4 text-yellow-600"
9595
/>
9696
<span v-else class="w-4 h-4" :class="getFileIcon(node.name)" />
9797
<span>{{ node.name }}</span>
9898
</NuxtLink>
9999
</td>
100-
<td class="py-2 px-4 text-right font-mono text-xs text-fg-subtle">
100+
<td class="py-2 px-4 text-end font-mono text-xs text-fg-subtle">
101101
<span v-if="node.type === 'file' && node.size">
102102
{{ formatBytes(node.size) }}
103103
</span>

app/components/CodeFileTree.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,21 @@ watch(
3939
<template v-if="node.type === 'directory'">
4040
<button
4141
type="button"
42-
class="w-full flex items-center gap-1.5 py-1.5 px-3 text-left font-mono text-sm transition-colors hover:bg-bg-muted"
42+
class="w-full flex items-center gap-1.5 py-1.5 px-3 text-start font-mono text-sm transition-colors hover:bg-bg-muted"
4343
:class="isNodeActive(node) ? 'text-fg' : 'text-fg-muted'"
4444
:style="{ paddingLeft: `${depth * 12 + 12}px` }"
4545
@click="toggleDir(node.path)"
4646
>
4747
<span
4848
class="w-4 h-4 shrink-0 transition-transform"
49-
:class="[isExpanded(node.path) ? 'i-carbon-chevron-down' : 'i-carbon-chevron-right']"
49+
:class="[isExpanded(node.path) ? 'i-carbon:chevron-down' : 'i-carbon:chevron-right']"
5050
/>
5151
<span
5252
class="w-4 h-4 shrink-0"
5353
:class="
5454
isExpanded(node.path)
55-
? 'i-carbon-folder-open text-yellow-500'
56-
: 'i-carbon-folder text-yellow-600'
55+
? 'i-carbon:folder-open text-yellow-500'
56+
: 'i-carbon:folder text-yellow-600'
5757
"
5858
/>
5959
<span class="truncate">{{ node.name }}</span>

app/components/CodeMobileTreeDrawer.vue

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ watch(isOpen, open => (isLocked.value = open))
2727
<!-- Toggle button (mobile only) -->
2828
<button
2929
type="button"
30-
class="md:hidden fixed bottom-4 right-4 z-40 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors"
30+
class="md:hidden fixed bottom-4 inset-ie-4 z-40 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors"
3131
:aria-label="$t('code.toggle_tree')"
3232
@click="isOpen = !isOpen"
3333
>
34-
<span class="w-5 h-5" :class="isOpen ? 'i-carbon-close' : 'i-carbon-folder'" />
34+
<span class="w-5 h-5" :class="isOpen ? 'i-carbon:close' : 'i-carbon:folder'" />
3535
</button>
3636

3737
<!-- Backdrop -->
@@ -57,19 +57,20 @@ watch(isOpen, open => (isLocked.value = open))
5757
>
5858
<aside
5959
v-if="isOpen"
60-
class="md:hidden fixed inset-y-0 left-0 z-50 w-72 bg-bg-subtle border-r border-border overflow-y-auto"
60+
class="md:hidden fixed inset-y-0 inset-is-0 z-50 w-72 bg-bg-subtle border-ie border-border overflow-y-auto"
6161
>
6262
<div
63-
class="sticky top-0 bg-bg-subtle border-b border-border px-4 py-3 flex items-center justify-between"
63+
class="sticky top-0 bg-bg-subtle border-b border-border px-4 py-3 flex items-center justify-start"
6464
>
6565
<span class="font-mono text-sm text-fg-muted">{{ $t('code.files_label') }}</span>
66+
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
6667
<button
6768
type="button"
6869
class="text-fg-muted hover:text-fg transition-colors"
6970
:aria-label="$t('code.close_tree')"
7071
@click="isOpen = false"
7172
>
72-
<span class="i-carbon-close w-5 h-5" />
73+
<span class="i-carbon:close w-5 h-5" />
7374
</button>
7475
</div>
7576
<CodeFileTree :tree="tree" :current-path="currentPath" :base-url="baseUrl" />

app/components/CodeViewer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ watch(
5858
<div class="code-viewer flex min-h-full">
5959
<!-- Line numbers column -->
6060
<div
61-
class="line-numbers shrink-0 bg-bg-subtle border-r border-border text-right select-none"
61+
class="line-numbers shrink-0 bg-bg-subtle border-ie border-border text-end select-none"
6262
aria-hidden="true"
6363
>
6464
<a

app/components/ColumnPicker.vue

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<script setup lang="ts">
2+
import type { ColumnConfig, ColumnId } from '#shared/types/preferences'
3+
4+
const props = defineProps<{
5+
columns: ColumnConfig[]
6+
}>()
7+
8+
const emit = defineEmits<{
9+
toggle: [columnId: ColumnId]
10+
reset: []
11+
}>()
12+
13+
const isOpen = ref(false)
14+
const buttonRef = useTemplateRef('buttonRef')
15+
const menuRef = useTemplateRef('menuRef')
16+
const menuId = useId()
17+
18+
// Close on click outside (check both button and menu)
19+
function handleClickOutside(event: MouseEvent) {
20+
const target = event.target as Node
21+
const isOutsideButton = buttonRef.value && !buttonRef.value.contains(target)
22+
const isOutsideMenu = !menuRef.value || !menuRef.value.contains(target)
23+
if (isOutsideButton && isOutsideMenu) {
24+
isOpen.value = false
25+
}
26+
}
27+
28+
// Close on Escape key
29+
function handleKeydown(event: KeyboardEvent) {
30+
if (event.key === 'Escape' && isOpen.value) {
31+
isOpen.value = false
32+
buttonRef.value?.focus()
33+
}
34+
}
35+
36+
onMounted(() => {
37+
document.addEventListener('click', handleClickOutside)
38+
document.addEventListener('keydown', handleKeydown)
39+
})
40+
41+
onUnmounted(() => {
42+
document.removeEventListener('click', handleClickOutside)
43+
document.removeEventListener('keydown', handleKeydown)
44+
})
45+
46+
// Columns that can be toggled (name is always visible)
47+
const toggleableColumns = computed(() => props.columns.filter(col => col.id !== 'name'))
48+
49+
// Map column IDs to i18n keys
50+
const columnLabelKey: Record<string, string> = {
51+
name: 'filters.columns.name',
52+
version: 'filters.columns.version',
53+
description: 'filters.columns.description',
54+
downloads: 'filters.columns.downloads',
55+
updated: 'filters.columns.updated',
56+
maintainers: 'filters.columns.maintainers',
57+
keywords: 'filters.columns.keywords',
58+
qualityScore: 'filters.columns.quality_score',
59+
popularityScore: 'filters.columns.popularity_score',
60+
maintenanceScore: 'filters.columns.maintenance_score',
61+
combinedScore: 'filters.columns.combined_score',
62+
security: 'filters.columns.security',
63+
}
64+
65+
function getColumnLabel(id: string): string {
66+
const key = columnLabelKey[id]
67+
return key ? $t(key) : id
68+
}
69+
70+
function handleReset() {
71+
emit('reset')
72+
isOpen.value = false
73+
}
74+
</script>
75+
76+
<template>
77+
<div class="relative">
78+
<button
79+
ref="buttonRef"
80+
type="button"
81+
class="btn-ghost inline-flex items-center gap-1.5 px-3 py-1.5 border border-border rounded-md hover:border-border-hover focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
82+
:aria-expanded="isOpen"
83+
aria-haspopup="true"
84+
:aria-controls="menuId"
85+
@click.stop="isOpen = !isOpen"
86+
>
87+
<span class="i-carbon-column w-4 h-4" aria-hidden="true" />
88+
<span class="font-mono text-sm">{{ $t('filters.columns.title') }}</span>
89+
</button>
90+
91+
<Transition name="dropdown">
92+
<div
93+
v-if="isOpen"
94+
ref="menuRef"
95+
:id="menuId"
96+
class="absolute right-0 mt-2 w-56 bg-bg-subtle border border-border rounded-lg shadow-lg z-20"
97+
role="group"
98+
:aria-label="$t('filters.columns.show')"
99+
>
100+
<div class="py-1">
101+
<div
102+
class="px-3 py-2 text-xs font-mono text-fg-subtle uppercase tracking-wider border-b border-border"
103+
aria-hidden="true"
104+
>
105+
{{ $t('filters.columns.show') }}
106+
</div>
107+
108+
<div class="py-1 max-h-64 overflow-y-auto">
109+
<label
110+
v-for="column in toggleableColumns"
111+
:key="column.id"
112+
class="flex items-center px-3 py-2 transition-colors duration-200"
113+
:class="
114+
column.disabled
115+
? 'opacity-50 cursor-not-allowed'
116+
: 'hover:bg-bg-muted cursor-pointer'
117+
"
118+
>
119+
<input
120+
type="checkbox"
121+
:checked="column.visible"
122+
:disabled="column.disabled"
123+
:aria-describedby="column.disabled ? `${column.id}-disabled-reason` : undefined"
124+
class="w-4 h-4 accent-fg bg-bg-muted border-border rounded disabled:opacity-50"
125+
@change="!column.disabled && emit('toggle', column.id)"
126+
/>
127+
<span class="ml-2 text-sm text-fg-muted font-mono flex-1">
128+
{{ getColumnLabel(column.id) }}
129+
</span>
130+
<span
131+
v-if="column.disabled"
132+
:id="`${column.id}-disabled-reason`"
133+
class="text-xs text-fg-subtle italic"
134+
>
135+
{{ $t('filters.columns.coming_soon') }}
136+
</span>
137+
</label>
138+
</div>
139+
140+
<div class="border-t border-border py-1">
141+
<button
142+
type="button"
143+
class="w-full px-3 py-2 text-left text-sm font-mono text-fg-muted hover:bg-bg-muted hover:text-fg transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-inset"
144+
@click="handleReset"
145+
>
146+
{{ $t('filters.columns.reset') }}
147+
</button>
148+
</div>
149+
</div>
150+
</div>
151+
</Transition>
152+
</div>
153+
</template>
154+
155+
<style scoped>
156+
.dropdown-enter-active,
157+
.dropdown-leave-active {
158+
transition: all 0.15s ease;
159+
}
160+
161+
.dropdown-enter-from,
162+
.dropdown-leave-to {
163+
opacity: 0;
164+
transform: translateY(-4px);
165+
}
166+
</style>

0 commit comments

Comments
 (0)