Skip to content

Commit 52ee98c

Browse files
Merge branch 'main' into ux/make-package-name-selectable
2 parents 13fbd23 + 2317d7c commit 52ee98c

31 files changed

Lines changed: 1823 additions & 275 deletions

CONTRIBUTING.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,10 +409,10 @@ describe('featureName', () => {
409409
410410
### Component accessibility tests
411411

412-
All new components should have a basic accessibility test in `test/nuxt/components.spec.ts`. These tests use [axe-core](https://github.com/dequelabs/axe-core) to catch common accessibility violations.
412+
All Vue components should have accessibility tests in `test/nuxt/a11y.spec.ts`. These tests use [axe-core](https://github.com/dequelabs/axe-core) to catch common accessibility violations and run in a real browser environment via Playwright.
413413

414414
```typescript
415-
import MyComponent from '~/components/MyComponent.vue'
415+
import { MyComponent } from '#components'
416416

417417
describe('MyComponent', () => {
418418
it('should have no accessibility violations', async () => {
@@ -429,6 +429,8 @@ describe('MyComponent', () => {
429429

430430
The `runAxe` helper handles DOM isolation and disables page-level rules that don't apply to isolated component testing.
431431

432+
A coverage test in `test/unit/a11y-component-coverage.spec.ts` ensures all components are either tested or explicitly skipped with justification. When you add a new component, this test will fail until you add accessibility tests for it.
433+
432434
> [!IMPORTANT]
433435
> Just because axe-core doesn't find any obvious issues, it does not mean a component is accessible. Please do additional checks and use best practices.
434436

app/components/AuthButton.vue

Lines changed: 0 additions & 18 deletions
This file was deleted.

app/components/ClaimPackageModal.vue

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
<script setup lang="ts">
2-
import type { CheckNameResult } from '~/utils/package-name'
32
import { checkPackageName } from '~/utils/package-name'
43
54
const props = defineProps<{
@@ -16,25 +15,34 @@ const {
1615
refreshState,
1716
} = useConnector()
1817
19-
// Fetch name availability when modal opens
20-
const checkResult = shallowRef<CheckNameResult | null>(null)
21-
22-
const isChecking = shallowRef(false)
2318
const isPublishing = shallowRef(false)
24-
const publishError = shallowRef<string | null>(null)
2519
const publishSuccess = shallowRef(false)
20+
const publishError = shallowRef<string | null>(null)
2621
27-
async function checkAvailability() {
28-
isChecking.value = true
29-
publishError.value = null
30-
try {
31-
checkResult.value = await checkPackageName(props.packageName)
32-
} catch (err) {
33-
publishError.value = err instanceof Error ? err.message : $t('claim.modal.failed_to_check')
34-
} finally {
35-
isChecking.value = false
36-
}
37-
}
22+
const {
23+
data: checkResult,
24+
refresh: checkAvailability,
25+
status,
26+
error: checkError,
27+
} = useAsyncData(
28+
(_nuxtApp, { signal }) => {
29+
return checkPackageName(props.packageName, { signal })
30+
},
31+
{ default: () => null, immediate: false },
32+
)
33+
34+
const isChecking = computed(() => {
35+
return status.value === 'pending'
36+
})
37+
38+
const mergedError = computed(() => {
39+
return (
40+
publishError.value ??
41+
(checkError.value instanceof Error
42+
? checkError.value.message
43+
: $t('claim.modal.failed_to_check'))
44+
)
45+
})
3846
3947
const connectorModal = useModal('connector-modal')
4048
@@ -92,7 +100,6 @@ const dialogRef = ref<HTMLDialogElement>()
92100
93101
function open() {
94102
// Reset state and check availability each time modal is opened
95-
checkResult.value = null
96103
publishError.value = null
97104
publishSuccess.value = false
98105
checkAvailability()
@@ -287,11 +294,11 @@ const previewPackageJson = computed(() => {
287294

288295
<!-- Error message -->
289296
<div
290-
v-if="publishError"
297+
v-if="mergedError"
291298
role="alert"
292299
class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
293300
>
294-
{{ publishError }}
301+
{{ mergedError }}
295302
</div>
296303

297304
<!-- Actions -->
@@ -369,17 +376,17 @@ const previewPackageJson = computed(() => {
369376
</div>
370377

371378
<!-- Error state -->
372-
<div v-else-if="publishError" class="space-y-4">
379+
<div v-else-if="mergedError" class="space-y-4">
373380
<div
374381
role="alert"
375382
class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
376383
>
377-
{{ publishError }}
384+
{{ mergedError }}
378385
</div>
379386
<button
380387
type="button"
381388
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
382-
@click="checkAvailability"
389+
@click="() => checkAvailability()"
383390
>
384391
{{ $t('common.retry') }}
385392
</button>

app/components/PackageDependencies.vue

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,15 @@ const sortedOptionalDependencies = computed(() => {
7979
<li
8080
v-for="[dep, version] in sortedDependencies.slice(0, depsExpanded ? undefined : 10)"
8181
:key="dep"
82-
class="flex items-center justify-start py-1 text-sm gap-2"
82+
class="flex items-center justify-between py-1 text-sm gap-2"
8383
>
8484
<NuxtLink
8585
:to="{ name: 'package', params: { package: dep.split('/') } }"
86-
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate min-w-0"
86+
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate min-w-0 flex-1"
8787
>
8888
{{ dep }}
8989
</NuxtLink>
90-
<span class="flex items-center gap-1">
90+
<span class="flex items-center gap-1 max-w-[40%]">
9191
<span
9292
v-if="outdatedDeps[dep]"
9393
class="shrink-0"
@@ -97,7 +97,6 @@ const sortedOptionalDependencies = computed(() => {
9797
>
9898
<span class="i-carbon:warning-alt w-3 h-3 block" />
9999
</span>
100-
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
101100
<NuxtLink
102101
v-if="getVulnerableDepInfo(dep)"
103102
:to="{
@@ -174,9 +173,9 @@ const sortedOptionalDependencies = computed(() => {
174173
<li
175174
v-for="peer in sortedPeerDependencies.slice(0, peerDepsExpanded ? undefined : 10)"
176175
:key="peer.name"
177-
class="flex items-center justify-start py-1 text-sm gap-2"
176+
class="flex items-center justify-between py-1 text-sm gap-1 min-w-0"
178177
>
179-
<div class="flex items-center gap-2 min-w-0">
178+
<div class="flex items-center gap-1 min-w-0 flex-1">
180179
<NuxtLink
181180
:to="{
182181
name: 'package',
@@ -194,13 +193,12 @@ const sortedOptionalDependencies = computed(() => {
194193
{{ $t('package.dependencies.optional') }}
195194
</span>
196195
</div>
197-
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
198196
<NuxtLink
199197
:to="{
200198
name: 'package',
201199
params: { package: [...peer.name.split('/'), 'v', peer.version] },
202200
}"
203-
class="font-mono text-xs text-fg-subtle max-w-[40%] text-end truncate"
201+
class="font-mono text-xs text-fg-subtle max-w-[40%] truncate"
204202
:title="peer.version"
205203
>
206204
{{ peer.version }}
@@ -241,21 +239,20 @@ const sortedOptionalDependencies = computed(() => {
241239
optionalDepsExpanded ? undefined : 10,
242240
)"
243241
:key="dep"
244-
class="flex items-center justify-start py-1 text-sm gap-2"
242+
class="flex items-center justify-between py-1 text-sm gap-2"
245243
>
246244
<NuxtLink
247245
:to="{ name: 'package', params: { package: dep.split('/') } }"
248-
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate min-w-0"
246+
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate min-w-0 flex-1"
249247
>
250248
{{ dep }}
251249
</NuxtLink>
252-
<span aria-hidden="true" class="flex-shrink-1 flex-grow-1" />
253250
<NuxtLink
254251
:to="{
255252
name: 'package',
256253
params: { package: [...dep.split('/'), 'v', version] },
257254
}"
258-
class="font-mono text-xs text-fg-subtle max-w-[50%] text-end truncate"
255+
class="font-mono text-xs text-fg-subtle max-w-[40%] text-end truncate"
259256
:title="version"
260257
>
261258
{{ version }}

app/components/PackageDownloadAnalytics.vue

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,26 @@ const rootEl = shallowRef<HTMLElement | null>(null)
1919
2020
const { width } = useElementSize(rootEl)
2121
22+
const chartKey = ref(0)
23+
24+
let chartRemountTimeoutId: ReturnType<typeof setTimeout> | null = null
25+
2226
onMounted(() => {
2327
rootEl.value = document.documentElement
2428
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
29+
30+
// If the chart is painted too early, built-in auto-sizing does not adapt to the final container size
31+
chartRemountTimeoutId = setTimeout(() => {
32+
chartKey.value += 1
33+
chartRemountTimeoutId = null
34+
}, 10)
35+
})
36+
37+
onBeforeUnmount(() => {
38+
if (chartRemountTimeoutId !== null) {
39+
clearTimeout(chartRemountTimeoutId)
40+
chartRemountTimeoutId = null
41+
}
2542
})
2643
2744
const { colors } = useCssVariables(
@@ -543,6 +560,7 @@ const config = computed(() => {
543560
show: false, // As long as a single package is displayed
544561
},
545562
tooltip: {
563+
teleportTo: '#chart-modal',
546564
borderColor: 'transparent',
547565
backdropFilter: false,
548566
backgroundColor: 'transparent',
@@ -687,7 +705,12 @@ const config = computed(() => {
687705
</div>
688706

689707
<ClientOnly v-if="inModal && chartData.dataset">
690-
<VueUiXy :dataset="chartData.dataset" :config="config" class="[direction:ltr]">
708+
<VueUiXy
709+
:dataset="chartData.dataset"
710+
:config="config"
711+
class="[direction:ltr]"
712+
:key="chartKey"
713+
>
691714
<template #menuIcon="{ isOpen }">
692715
<span v-if="isOpen" class="i-carbon:close w-6 h-6" aria-hidden="true" />
693716
<span v-else class="i-carbon:overflow-menu-vertical w-6 h-6" aria-hidden="true" />
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 type { SkillListItem } from '#shared/types'
3+
4+
defineProps<{
5+
skills: SkillListItem[]
6+
packageName: string
7+
version?: string
8+
}>()
9+
10+
const skillsModal = useModal('skills-modal')
11+
</script>
12+
13+
<template>
14+
<section v-if="skills.length" id="skills" class="scroll-mt-20">
15+
<h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
16+
{{ $t('package.skills.title') }}
17+
</h2>
18+
<button
19+
type="button"
20+
class="w-full flex items-center gap-2 px-3 py-2 text-sm font-mono bg-bg-muted border border-border rounded-md hover:border-border-hover hover:bg-bg-elevated focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-hover transition-colors duration-200"
21+
@click="skillsModal.open()"
22+
>
23+
<span class="i-custom:agent-skills w-4 h-4 shrink-0 text-fg-muted" aria-hidden="true" />
24+
<span class="text-fg-muted">{{
25+
$t('package.skills.skills_available', { count: skills.length }, skills.length)
26+
}}</span>
27+
</button>
28+
</section>
29+
</template>

0 commit comments

Comments
 (0)