Skip to content

Commit b66749e

Browse files
authored
feat(ui): show maintainers avatars (#2055)
1 parent a482999 commit b66749e

File tree

7 files changed

+73
-14
lines changed

7 files changed

+73
-14
lines changed

app/components/Package/Maintainers.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,13 @@ watch(
189189
class="link-subtle text-sm shrink-0"
190190
dir="ltr"
191191
>
192-
~{{ maintainer.name }}
192+
<i18n-t keypath="package.maintainers.maintainer_template">
193+
<template #avatar>
194+
<UserAvatar :username="maintainer.name" size="xs" aria-hidden="true" />
195+
</template>
196+
<template #char126>~</template>
197+
<template #name>{{ maintainer.name }}</template>
198+
</i18n-t>
193199
</LinkBase>
194200
<span v-else class="font-mono text-sm text-fg-muted" dir="ltr">{{
195201
maintainer.email

app/components/User/Avatar.vue

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,36 @@
11
<script setup lang="ts">
22
const props = defineProps<{
33
username: string
4+
size: 'xs' | 'lg'
45
}>()
56
7+
const sizePixels = computed(() => {
8+
switch (props.size) {
9+
case 'xs':
10+
return 24
11+
case 'lg':
12+
return 64
13+
}
14+
})
15+
16+
const sizeClass = computed(() => {
17+
switch (props.size) {
18+
case 'xs':
19+
return 'size-6'
20+
case 'lg':
21+
return 'size-16'
22+
}
23+
})
24+
25+
const textClass = computed(() => {
26+
switch (props.size) {
27+
case 'xs':
28+
return 'text-xs'
29+
case 'lg':
30+
return 'text-2xl'
31+
}
32+
})
33+
634
const { data: gravatarUrl } = useLazyFetch(() => `/api/gravatar/${props.username}`, {
735
transform: res => (res.hash ? `/_avatar/${res.hash}?s=128&d=404` : null),
836
getCachedData(key, nuxtApp) {
@@ -14,7 +42,8 @@ const { data: gravatarUrl } = useLazyFetch(() => `/api/gravatar/${props.username
1442
<template>
1543
<!-- Avatar -->
1644
<div
17-
class="size-16 shrink-0 rounded-full bg-bg-muted border border-border flex items-center justify-center overflow-hidden"
45+
class="shrink-0 rounded-full bg-bg-muted border border-border flex items-center justify-center overflow-hidden"
46+
:class="sizeClass"
1847
role="img"
1948
:aria-label="`Avatar for ${username}`"
2049
>
@@ -23,13 +52,22 @@ const { data: gravatarUrl } = useLazyFetch(() => `/api/gravatar/${props.username
2352
v-if="gravatarUrl"
2453
:src="gravatarUrl"
2554
alt=""
26-
width="64"
27-
height="64"
55+
:width="sizePixels"
56+
:height="sizePixels"
2857
class="w-full h-full object-cover"
2958
/>
30-
<!-- Else fallback to initials -->
31-
<span v-else class="text-2xl text-fg-subtle font-mono" aria-hidden="true">
32-
{{ username.charAt(0).toUpperCase() }}
33-
</span>
59+
<!-- Else fallback to initials (use svg to avoid underline styling) -->
60+
<svg
61+
v-else
62+
xmlns="http://www.w3.org/2000/svg"
63+
:width="sizePixels"
64+
:height="sizePixels"
65+
class="text-fg-subtle"
66+
:class="textClass"
67+
>
68+
<text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" fill="currentColor">
69+
{{ username.charAt(0).toUpperCase() }}
70+
</text>
71+
</svg>
3472
</div>
3573
</template>

app/pages/~[username]/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ defineOgImageComponent('Default', {
140140
<!-- Header -->
141141
<header class="mb-8 pb-8 border-b border-border">
142142
<div class="flex flex-wrap items-center gap-4">
143-
<UserAvatar :username="username" />
143+
<UserAvatar :username="username" size="lg" />
144144
<div>
145145
<h1 class="font-mono text-2xl sm:text-3xl font-medium">~{{ username }}</h1>
146146
<p v-if="results?.total" class="text-fg-muted text-sm mt-1">

app/pages/~[username]/orgs.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ defineOgImageComponent('Default', {
126126
<!-- Header -->
127127
<header class="mb-8 pb-8 border-b border-border">
128128
<div class="flex flex-wrap items-center gap-4 mb-4">
129-
<UserAvatar :username="username" />
129+
<UserAvatar :username="username" size="lg" />
130130
<div>
131131
<h1 class="font-mono text-2xl sm:text-3xl font-medium">~{{ username }}</h1>
132132
<p class="text-fg-muted text-sm mt-1">{{ $t('user.orgs_page.title') }}</p>

i18n/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,8 @@
447447
"cancel_add": "Cancel adding owner",
448448
"add_owner": "+ Add owner",
449449
"show_more": "(show {count} more)",
450-
"show_less": "(show fewer)"
450+
"show_less": "(show fewer)",
451+
"maintainer_template": "{avatar} {char126}{name}"
451452
},
452453
"trends": {
453454
"granularity": "Granularity",

i18n/schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,9 @@
13471347
},
13481348
"show_less": {
13491349
"type": "string"
1350+
},
1351+
"maintainer_template": {
1352+
"type": "string"
13501353
}
13511354
},
13521355
"additionalProperties": false

test/nuxt/a11y.spec.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2914,27 +2914,38 @@ describe('component accessibility audits', () => {
29142914
describe('UserAvatar', () => {
29152915
it('should have no accessibility violations', async () => {
29162916
const component = await mountSuspended(UserAvatar, {
2917-
props: { username: 'testuser' },
2917+
props: { username: 'testuser', size: 'lg' },
29182918
})
29192919
const results = await runAxe(component)
29202920
expect(results.violations).toEqual([])
29212921
})
29222922

29232923
it('should have no accessibility violations with short username', async () => {
29242924
const component = await mountSuspended(UserAvatar, {
2925-
props: { username: 'a' },
2925+
props: { username: 'a', size: 'lg' },
29262926
})
29272927
const results = await runAxe(component)
29282928
expect(results.violations).toEqual([])
29292929
})
29302930

29312931
it('should have no accessibility violations with long username', async () => {
29322932
const component = await mountSuspended(UserAvatar, {
2933-
props: { username: 'verylongusernameexample' },
2933+
props: { username: 'verylongusernameexample', size: 'lg' },
29342934
})
29352935
const results = await runAxe(component)
29362936
expect(results.violations).toEqual([])
29372937
})
2938+
2939+
it('should have no accessibility violations in all sizes', async () => {
2940+
const sizes = ['xs', 'lg'] as const
2941+
for (const size of sizes) {
2942+
const component = await mountSuspended(UserAvatar, {
2943+
props: { username: 'testuser', size },
2944+
})
2945+
const results = await runAxe(component)
2946+
expect(results.violations).toEqual([])
2947+
}
2948+
})
29382949
})
29392950

29402951
// Diff components

0 commit comments

Comments
 (0)