Skip to content

Commit c841e6d

Browse files
authored
Merge branch 'main' into chart-improvements
2 parents 6e5af51 + d3cfce5 commit c841e6d

7 files changed

Lines changed: 211 additions & 45 deletions

File tree

app/components/Alert.vue

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script setup lang="ts">
2+
interface Props {
3+
variant: 'warning' | 'error'
4+
title?: string
5+
}
6+
7+
defineProps<Props>()
8+
9+
const WRAPPER_CLASSES: Record<Props['variant'], string> = {
10+
warning: 'border-amber-400/20 bg-amber-500/8',
11+
error: 'border-red-400/20 bg-red-500/8',
12+
}
13+
14+
const TITLE_CLASSES: Record<Props['variant'], string> = {
15+
warning: 'text-amber-800 dark:text-amber-300',
16+
error: 'text-red-800 dark:text-red-300',
17+
}
18+
19+
const BODY_CLASSES: Record<Props['variant'], string> = {
20+
warning: 'text-amber-700 dark:text-amber-400',
21+
error: 'text-red-700 dark:text-red-400',
22+
}
23+
24+
const ROLES: Record<Props['variant'], 'status' | 'alert'> = {
25+
warning: 'status',
26+
error: 'alert',
27+
}
28+
</script>
29+
30+
<template>
31+
<div
32+
:role="ROLES[variant]"
33+
class="border rounded-md px-3 py-2.5"
34+
:class="WRAPPER_CLASSES[variant]"
35+
>
36+
<p v-if="title" class="font-semibold mb-1" :class="TITLE_CLASSES[variant]">{{ title }}</p>
37+
<div class="text-xs" :class="BODY_CLASSES[variant]">
38+
<slot />
39+
</div>
40+
</div>
41+
</template>

app/components/Package/ClaimPackageModal.vue

Lines changed: 19 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -199,28 +199,26 @@ const previewPackageJson = computed(() => {
199199
</div>
200200

201201
<!-- Validation errors -->
202-
<div
202+
<Alert
203203
v-if="checkResult.validationErrors?.length"
204-
class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
205-
role="alert"
204+
variant="error"
205+
:title="$t('claim.modal.invalid_name')"
206206
>
207-
<p class="font-medium mb-1">{{ $t('claim.modal.invalid_name') }}</p>
208207
<ul class="list-disc list-inside space-y-1">
209208
<li v-for="err in checkResult.validationErrors" :key="err">{{ err }}</li>
210209
</ul>
211-
</div>
210+
</Alert>
212211

213212
<!-- Validation warnings -->
214-
<div
213+
<Alert
215214
v-if="checkResult.validationWarnings?.length"
216-
class="p-3 text-sm text-yellow-400 bg-yellow-500/10 border border-yellow-500/20 rounded-md"
217-
role="alert"
215+
variant="warning"
216+
:title="$t('common.warnings')"
218217
>
219-
<p class="font-medium mb-1">{{ $t('common.warnings') }}</p>
220218
<ul class="list-disc list-inside space-y-1">
221219
<li v-for="warn in checkResult.validationWarnings" :key="warn">{{ warn }}</li>
222220
</ul>
223-
</div>
221+
</Alert>
224222

225223
<!-- Availability status -->
226224
<template v-if="checkResult.valid">
@@ -305,39 +303,23 @@ const previewPackageJson = computed(() => {
305303
</template>
306304

307305
<!-- Error message -->
308-
<div
309-
v-if="mergedError"
310-
role="alert"
311-
class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
312-
>
313-
{{ mergedError }}
314-
</div>
306+
<Alert v-if="mergedError" variant="error">{{ mergedError }}</Alert>
315307

316308
<!-- Actions -->
317309
<div v-if="checkResult.available && checkResult.valid" class="space-y-3">
318310
<!-- Warning for unscoped packages -->
319-
<div
320-
v-if="!isScoped"
321-
class="p-3 text-sm text-yellow-400 bg-yellow-500/10 border border-yellow-500/20 rounded-md"
322-
>
323-
<p class="font-medium mb-1">{{ $t('claim.modal.scope_warning_title') }}</p>
324-
<p class="text-xs text-yellow-400/80">
325-
{{
326-
$t('claim.modal.scope_warning_text', {
327-
username: npmUser || 'username',
328-
name: packageName,
329-
})
330-
}}
331-
</p>
332-
</div>
311+
<Alert v-if="!isScoped" variant="warning" :title="$t('claim.modal.scope_warning_title')">
312+
{{
313+
$t('claim.modal.scope_warning_text', {
314+
username: npmUser || 'username',
315+
name: packageName,
316+
})
317+
}}
318+
</Alert>
333319

334320
<!-- Not connected warning -->
335321
<div v-if="!isConnected" class="space-y-3">
336-
<div
337-
class="p-3 text-sm text-yellow-400 bg-yellow-500/10 border border-yellow-500/20 rounded-md"
338-
>
339-
<p>{{ $t('claim.modal.connect_required') }}</p>
340-
</div>
322+
<Alert variant="warning">{{ $t('claim.modal.connect_required') }}</Alert>
341323
<button
342324
type="button"
343325
class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90 focus-visible:outline-accent/70"
@@ -389,12 +371,7 @@ const previewPackageJson = computed(() => {
389371

390372
<!-- Error state -->
391373
<div v-else-if="mergedError" class="space-y-4">
392-
<div
393-
role="alert"
394-
class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md"
395-
>
396-
{{ mergedError }}
397-
</div>
374+
<Alert variant="error">{{ mergedError }}</Alert>
398375
<button
399376
type="button"
400377
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-accent/70"

app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
PackageFileTreeResponse,
55
PackageFileContentResponse,
66
} from '#shared/types'
7+
import { isBinaryFilePath } from '~/utils/file-types'
78
89
definePageMeta({
910
name: 'code',
@@ -105,6 +106,9 @@ const isViewingFile = computed(() => currentNode.value?.type === 'file')
105106
106107
// Maximum file size we'll try to load (500KB) - must match server
107108
const MAX_FILE_SIZE = 500 * 1024
109+
110+
const isBinaryFile = computed(() => !!filePath.value && isBinaryFilePath(filePath.value))
111+
108112
const isFileTooLarge = computed(() => {
109113
const size = currentNode.value?.size
110114
return size !== undefined && size > MAX_FILE_SIZE
@@ -113,7 +117,13 @@ const isFileTooLarge = computed(() => {
113117
// Fetch file content when a file is selected (and not too large)
114118
const fileContentUrl = computed(() => {
115119
// Don't fetch if no file path, file tree not loaded, file is too large, or it's a directory
116-
if (!filePath.value || !fileTree.value || isFileTooLarge.value || !isViewingFile.value) {
120+
if (
121+
!filePath.value ||
122+
!fileTree.value ||
123+
isFileTooLarge.value ||
124+
!isViewingFile.value ||
125+
isBinaryFile.value
126+
) {
117127
return null
118128
}
119129
return `/api/registry/file/${packageName.value}/v/${version.value}/${filePath.value}`
@@ -426,7 +436,7 @@ defineOgImageComponent('Default', {
426436
ref="contentContainer"
427437
>
428438
<!-- File viewer -->
429-
<template v-if="isViewingFile && fileContent">
439+
<template v-if="isViewingFile && !isBinaryFile && fileContent">
430440
<div
431441
class="sticky z-10 top-0 bg-bg border-b border-border px-4 py-2 flex items-center justify-between"
432442
>
@@ -519,6 +529,19 @@ defineOgImageComponent('Default', {
519529
/>
520530
</template>
521531

532+
<!-- Binary file warning -->
533+
<div v-else-if="isViewingFile && isBinaryFile" class="py-20 text-center">
534+
<div class="i-lucide:binary w-12 h-12 mx-auto text-fg-subtle mb-4" />
535+
<p class="text-fg-muted mb-2">{{ $t('code.binary_file') }}</p>
536+
<p class="text-fg-subtle text-sm mb-4">{{ $t('code.binary_rendering_warning') }}</p>
537+
<LinkBase
538+
variant="button-secondary"
539+
:to="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`"
540+
>
541+
{{ $t('code.view_raw') }}
542+
</LinkBase>
543+
</div>
544+
522545
<!-- File too large warning -->
523546
<div v-else-if="isViewingFile && isFileTooLarge" class="py-20 text-center">
524547
<div class="i-lucide:file-text w-12 h-12 mx-auto text-fg-subtle mb-4" />

app/utils/file-types.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Extensions that are binary and cannot be meaningfully displayed as text
2+
const BINARY_EXTENSIONS = new Set([
3+
// Images
4+
'png',
5+
'jpg',
6+
'jpeg',
7+
'gif',
8+
'webp',
9+
'ico',
10+
'bmp',
11+
'tiff',
12+
'tif',
13+
'avif',
14+
'heic',
15+
'heif',
16+
// Fonts
17+
'woff',
18+
'woff2',
19+
'ttf',
20+
'otf',
21+
'eot',
22+
// Archives
23+
'zip',
24+
'tar',
25+
'gz',
26+
'tgz',
27+
'bz2',
28+
'xz',
29+
'7z',
30+
'rar',
31+
// Executables / compiled
32+
'exe',
33+
'dll',
34+
'so',
35+
'dylib',
36+
'node',
37+
'wasm',
38+
'pyc',
39+
'class',
40+
// Media
41+
'mp3',
42+
'mp4',
43+
'ogg',
44+
'wav',
45+
'avi',
46+
'mov',
47+
'webm',
48+
'flac',
49+
'aac',
50+
'mkv',
51+
// Documents
52+
'pdf',
53+
'doc',
54+
'docx',
55+
'xls',
56+
'xlsx',
57+
'ppt',
58+
'pptx',
59+
// Data
60+
'bin',
61+
'dat',
62+
'db',
63+
'sqlite',
64+
'sqlite3',
65+
])
66+
67+
export function isBinaryFilePath(filePath: string): boolean {
68+
const dotIndex = filePath.lastIndexOf('.')
69+
const ext = dotIndex > -1 ? filePath.slice(dotIndex + 1).toLowerCase() : ''
70+
return BINARY_EXTENSIONS.has(ext)
71+
}

i18n/locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,9 @@
792792
"code": "code"
793793
},
794794
"file_path": "File path",
795-
"scroll_to_top": "Scroll to top"
795+
"scroll_to_top": "Scroll to top",
796+
"binary_file": "Binary file",
797+
"binary_rendering_warning": "File type not supported for preview."
796798
},
797799
"badges": {
798800
"provenance": {

i18n/schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2382,6 +2382,12 @@
23822382
},
23832383
"scroll_to_top": {
23842384
"type": "string"
2385+
},
2386+
"binary_file": {
2387+
"type": "string"
2388+
},
2389+
"binary_rendering_warning": {
2390+
"type": "string"
23852391
}
23862392
},
23872393
"additionalProperties": false

test/nuxt/a11y.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ vi.mock('vue-data-ui/vue-ui-xy', () => {
116116
// Import components from #components where possible
117117
// For server/client variants, we need to import directly to test the specific variant
118118
import {
119+
Alert,
119120
AppFooter,
120121
AppHeader,
121122
AppLogo,
@@ -3529,6 +3530,35 @@ describe('component accessibility audits', () => {
35293530
expect(results.violations).toEqual([])
35303531
})
35313532
})
3533+
3534+
describe('Alert', () => {
3535+
it('should have no accessibility violations for warning variant', async () => {
3536+
const component = await mountSuspended(Alert, {
3537+
props: { variant: 'warning', title: 'Warning title' },
3538+
slots: { default: 'This is a warning message.' },
3539+
})
3540+
const results = await runAxe(component)
3541+
expect(results.violations).toEqual([])
3542+
})
3543+
3544+
it('should have no accessibility violations for error variant', async () => {
3545+
const component = await mountSuspended(Alert, {
3546+
props: { variant: 'error', title: 'Error title' },
3547+
slots: { default: 'This is an error message.' },
3548+
})
3549+
const results = await runAxe(component)
3550+
expect(results.violations).toEqual([])
3551+
})
3552+
3553+
it('should have no accessibility violations without title', async () => {
3554+
const component = await mountSuspended(Alert, {
3555+
props: { variant: 'warning' },
3556+
slots: { default: 'This is a warning message.' },
3557+
})
3558+
const results = await runAxe(component)
3559+
expect(results.violations).toEqual([])
3560+
})
3561+
})
35323562
})
35333563

35343564
function applyTheme(colorMode: string, bgTheme: string | null) {
@@ -3575,6 +3605,22 @@ describe('background theme accessibility', () => {
35753605
}
35763606

35773607
const components = [
3608+
{
3609+
name: 'AlertWarning',
3610+
mount: () =>
3611+
mountSuspended(Alert, {
3612+
props: { variant: 'warning', title: 'Warning title' },
3613+
slots: { default: '<p>Warning body</p>' },
3614+
}),
3615+
},
3616+
{
3617+
name: 'AlertError',
3618+
mount: () =>
3619+
mountSuspended(Alert, {
3620+
props: { variant: 'error', title: 'Error title' },
3621+
slots: { default: '<p>Error body</p>' },
3622+
}),
3623+
},
35783624
{ name: 'AppHeader', mount: () => mountSuspended(AppHeader) },
35793625
{ name: 'AppFooter', mount: () => mountSuspended(AppFooter) },
35803626
{ name: 'HeaderSearchBox', mount: () => mountSuspended(HeaderSearchBox) },

0 commit comments

Comments
 (0)