Skip to content

Commit f599c4b

Browse files
committed
Merge branch 'main' of github.com:nulfrost/npmx.dev into refactor/modal-a11y
2 parents c7fbab0 + 0d06026 commit f599c4b

32 files changed

Lines changed: 2269 additions & 290 deletions

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2026 Daniel Roe
3+
Copyright (c) 2026 npmx team and contributors
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

app/components/AppHeader.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ onKeyStroke(
185185
<!-- Mobile: Menu button (always visible, toggles menu) -->
186186
<button
187187
type="button"
188-
class="sm:hidden p-2 -m-2 text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
188+
class="sm:hidden flex items-center p-2 -m-2 text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
189189
:aria-label="showMobileMenu ? $t('common.close') : $t('nav.open_menu')"
190190
:aria-expanded="showMobileMenu"
191191
@click="showMobileMenu = !showMobileMenu"

app/components/PackageTableRow.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ const emit = defineEmits<{
1717
const pkg = computed(() => props.result.package)
1818
const score = computed(() => props.result.score)
1919
20-
// Get the best available date: prefer result.updated (from packument), fall back to package.date
21-
const updatedDate = computed(() => props.result.updated ?? props.result.package.date)
20+
const updatedDate = computed(() => props.result.package.date)
2221
2322
function formatDownloads(count?: number): string {
2423
if (count === undefined) return '-'

app/components/PackageVulnerabilityTree.vue

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,11 @@ const props = defineProps<{
77
version: string
88
}>()
99
10-
const {
11-
data: vulnTree,
12-
status,
13-
fetch: fetchVulnTree,
14-
} = useDependencyAnalysis(
10+
const { data: vulnTree, status } = useDependencyAnalysis(
1511
() => props.packageName,
1612
() => props.version,
1713
)
1814
19-
onMounted(() => fetchVulnTree())
20-
2115
const isExpanded = shallowRef(false)
2216
const showAllPackages = shallowRef(false)
2317
const showAllVulnerabilities = shallowRef(false)

app/components/Toggle.client.vue

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
label?: string
4+
description?: string
5+
}>()
6+
7+
const checked = defineModel<boolean>({
8+
default: false,
9+
})
10+
</script>
11+
12+
<template>
13+
<button
14+
type="button"
15+
class="w-full flex items-center justify-between gap-4 group"
16+
role="switch"
17+
:aria-checked="checked"
18+
@click="checked = !checked"
19+
>
20+
<span v-if="label" class="text-sm text-fg font-medium text-start">
21+
{{ label }}
22+
</span>
23+
<span
24+
class="relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out motion-reduce:transition-none shadow-sm cursor-pointer"
25+
:class="checked ? 'bg-accent' : 'bg-bg border border-border'"
26+
aria-hidden="true"
27+
>
28+
<span
29+
class="pointer-events-none inline-block h-5 w-5 rounded-full shadow-sm ring-0 transition-transform duration-200 ease-in-out motion-reduce:transition-none"
30+
:class="checked ? 'bg-bg' : 'bg-fg-muted'"
31+
/>
32+
</span>
33+
</button>
34+
<p v-if="description" class="text-sm text-fg-muted">
35+
{{ description }}
36+
</p>
37+
</template>
38+
39+
<style scoped>
40+
button[aria-checked='false'] > span:last-of-type > span {
41+
translate: 0;
42+
}
43+
button[aria-checked='true'] > span:last-of-type > span {
44+
translate: calc(100%);
45+
}
46+
html[dir='rtl'] button[aria-checked='true'] > span:last-of-type > span {
47+
translate: calc(-100%);
48+
}
49+
</style>

app/components/Toggle.server.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
label?: string
4+
description?: string
5+
}>()
6+
</script>
7+
8+
<template>
9+
<div class="w-full flex items-center justify-between gap-4">
10+
<span v-if="label" class="text-sm text-fg font-medium text-start">
11+
{{ label }}
12+
</span>
13+
<span class="skeleton block h-6 w-11 shrink-0 rounded-full" />
14+
</div>
15+
<p v-if="description" class="text-sm text-fg-muted">
16+
{{ description }}
17+
</p>
18+
</template>

app/composables/useCachedFetch.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,10 @@ export function useCachedFetch(): CachedFetchFunction {
3434
if (import.meta.client) {
3535
return async <T = unknown>(
3636
url: string,
37-
options: {
38-
method?: string
39-
body?: unknown
40-
headers?: Record<string, string>
41-
} = {},
37+
options: Parameters<typeof $fetch>[1] = {},
4238
_ttl?: number,
4339
): Promise<CachedFetchResult<T>> => {
44-
const data = (await $fetch(url, options as Parameters<typeof $fetch>[1])) as T
40+
const data = (await $fetch<T>(url, options)) as T
4541
return { data, isStale: false, cachedAt: null }
4642
}
4743
}
@@ -59,14 +55,10 @@ export function useCachedFetch(): CachedFetchFunction {
5955
// (shouldn't happen in normal operation)
6056
return async <T = unknown>(
6157
url: string,
62-
options: {
63-
method?: string
64-
body?: unknown
65-
headers?: Record<string, string>
66-
} = {},
58+
options: Parameters<typeof $fetch>[1] = {},
6759
_ttl?: number,
6860
): Promise<CachedFetchResult<T>> => {
69-
const data = (await $fetch(url, options as Parameters<typeof $fetch>[1])) as T
61+
const data = (await $fetch<T>(url, options)) as T
7062
return { data, isStale: false, cachedAt: null }
7163
}
7264
}
Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { VulnerabilityTreeResult } from '#shared/types/dependency-analysis'
2-
31
/**
42
* Shared composable for dependency analysis data (vulnerabilities, deprecated packages).
53
* Fetches once and caches the result so multiple components can use it.
@@ -9,44 +7,8 @@ export function useDependencyAnalysis(
97
packageName: MaybeRefOrGetter<string>,
108
version: MaybeRefOrGetter<string>,
119
) {
12-
// Build a stable key from the current values
13-
const name = toValue(packageName)
14-
const ver = toValue(version)
15-
const key = `dep-analysis:v1:${name}@${ver}`
16-
17-
// Use useState for SSR-safe caching across components
18-
const data = useState<VulnerabilityTreeResult | null>(key, () => null)
19-
const status = useState<'idle' | 'pending' | 'success' | 'error'>(`${key}:status`, () => 'idle')
20-
const error = useState<Error | null>(`${key}:error`, () => null)
21-
22-
async function fetch() {
23-
const pkgName = toValue(packageName)
24-
const pkgVersion = toValue(version)
25-
26-
if (!pkgName || !pkgVersion) return
27-
28-
// Already fetched or fetching
29-
if (status.value === 'success' || status.value === 'pending') return
30-
31-
status.value = 'pending'
32-
error.value = null
33-
34-
try {
35-
const result = await $fetch<VulnerabilityTreeResult>(
36-
`/api/registry/vulnerabilities/${encodePackageName(pkgName)}/v/${pkgVersion}`,
37-
)
38-
data.value = result
39-
status.value = 'success'
40-
} catch (e) {
41-
error.value = e instanceof Error ? e : new Error('Failed to fetch dependency analysis')
42-
status.value = 'error'
43-
}
44-
}
45-
46-
return {
47-
data: readonly(data),
48-
status: readonly(status),
49-
error: readonly(error),
50-
fetch,
51-
}
10+
return useFetch(
11+
() =>
12+
`/api/registry/vulnerabilities/${encodePackageName(toValue(packageName))}/v/${toValue(version)}`,
13+
)
5214
}

app/composables/useNpmRegistry.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,11 @@ export function usePackage(
185185

186186
const asyncData = useLazyAsyncData(
187187
() => `package:${toValue(name)}:${toValue(requestedVersion) ?? ''}`,
188-
async () => {
188+
async (_nuxtApp, { signal }) => {
189189
const encodedName = encodePackageName(toValue(name))
190-
const { data: r, isStale } = await cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`)
190+
const { data: r, isStale } = await cachedFetch<Packument>(`${NPM_REGISTRY}/${encodedName}`, {
191+
signal,
192+
})
191193
const reqVer = toValue(requestedVersion)
192194
const pkg = transformPackument(r, reqVer)
193195
const resolvedVersion = getResolvedVersion(pkg, reqVer)
@@ -233,10 +235,11 @@ export function usePackageDownloads(
233235

234236
const asyncData = useLazyAsyncData(
235237
() => `downloads:${toValue(name)}:${toValue(period)}`,
236-
async () => {
238+
async (_nuxtApp, { signal }) => {
237239
const encodedName = encodePackageName(toValue(name))
238240
const { data, isStale } = await cachedFetch<NpmDownloadCount>(
239241
`${NPM_API}/downloads/point/${toValue(period)}/${encodedName}`,
242+
{ signal },
240243
)
241244
return { ...data, isStale }
242245
},
@@ -306,7 +309,7 @@ export function useNpmSearch(
306309

307310
const asyncData = useLazyAsyncData(
308311
() => `search:incremental:${toValue(query)}`,
309-
async () => {
312+
async (_nuxtApp, { signal }) => {
310313
const q = toValue(query)
311314
if (!q.trim()) {
312315
return emptySearchResponse
@@ -325,7 +328,7 @@ export function useNpmSearch(
325328

326329
const { data: response, isStale } = await cachedFetch<NpmSearchResponse>(
327330
`${NPM_REGISTRY}/-/v1/search?${params.toString()}`,
328-
{},
331+
{ signal },
329332
60,
330333
)
331334

@@ -509,7 +512,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
509512

510513
const asyncData = useLazyAsyncData(
511514
() => `org-packages:${toValue(orgName)}`,
512-
async () => {
515+
async (_nuxtApp, { signal }) => {
513516
const org = toValue(orgName)
514517
if (!org) {
515518
return emptySearchResponse
@@ -520,6 +523,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
520523
try {
521524
const { data } = await cachedFetch<Record<string, string>>(
522525
`${NPM_REGISTRY}/-/org/${encodeURIComponent(org)}/package`,
526+
{ signal },
523527
)
524528
packageNames = Object.keys(data)
525529
} catch (err) {
@@ -553,6 +557,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
553557
const encoded = encodePackageName(name)
554558
const { data: pkg } = await cachedFetch<MinimalPackument>(
555559
`${NPM_REGISTRY}/${encoded}`,
560+
{ signal },
556561
)
557562
return pkg
558563
} catch {

0 commit comments

Comments
 (0)