+ :owner="view.owner"
+ :size
+ @avatar-error="emit('avatarError')"
+ />
@@ -87,28 +70,10 @@ const where = computed(() => view.author ? view.repo : view.source)
gap: 0.75rem;
}
-.skill-id__avatar {
- flex: none;
- inline-size: var(--skill-id-size);
- block-size: var(--skill-id-size);
- border: 1px solid var(--ui-border);
- border-radius: 9999px;
- background: var(--ui-bg-muted);
-}
-
.skill-id--28 {
- --skill-id-size: 1.75rem;
gap: 0.625rem;
}
-.skill-id--32 {
- --skill-id-size: 2rem;
-}
-
-.skill-id--36 {
- --skill-id-size: 2.25rem;
-}
-
.skill-id__text {
min-inline-size: 0;
flex: 1;
diff --git a/app/types/skill-card.ts b/app/types/skill-card.ts
index f480c072..ca678ed9 100644
--- a/app/types/skill-card.ts
+++ b/app/types/skill-card.ts
@@ -80,8 +80,6 @@ export interface SkillCardView {
/** `owner/repo`. */
source: string
byline: SkillCardByline
- /** Square GitHub avatar URL at twice the CSS size. */
- avatar: (cssSize: number) => string
description: string | null
/** The metric the context asked for, so a row can drop its metric column when it is `none`. */
metricKind: SkillCardMetric
diff --git a/app/utils/skill-card-view.ts b/app/utils/skill-card-view.ts
index 0658e136..de2a258e 100644
--- a/app/utils/skill-card-view.ts
+++ b/app/utils/skill-card-view.ts
@@ -8,7 +8,6 @@ import type {
SkillCardSkill,
SkillCardView,
} from '~/types/skill-card'
-import { githubAvatarProxyUrl } from '#shared/image-proxy'
import { skillRunCmd } from '#shared/skill-commands'
import { formatGithubStars } from './github-stars'
import { resolveAuthorName } from './skill-byline'
@@ -99,7 +98,6 @@ export function buildSkillCardView(skill: SkillCardSkill, options: SkillCardOpti
author,
source: `${skill.owner}/${skill.repo}`,
byline: options.byline,
- avatar: cssSize => githubAvatarProxyUrl(skill.owner, cssSize * 2),
description: (options.description ?? defaultSkillCardDescription(options.layout)) ? (skill.description?.trim() || null) : null,
metricKind: options.metric,
metric: metricView(skill, options.metric),
diff --git a/layers/identity/app/pages/me/index.vue b/layers/identity/app/pages/me/index.vue
index 0aa702af..a9cfefb7 100644
--- a/layers/identity/app/pages/me/index.vue
+++ b/layers/identity/app/pages/me/index.vue
@@ -428,7 +428,7 @@ async function deleteAccount() {
Email
-
+
{{ me?.digest_email || me?.email || 'No email set' }}
+
-
+
{{ source.owner }}/{{ source.repo }}
diff --git a/layers/identity/app/pages/onboarding/discover.vue b/layers/identity/app/pages/onboarding/discover.vue
index 693e52fa..967ab545 100644
--- a/layers/identity/app/pages/onboarding/discover.vue
+++ b/layers/identity/app/pages/onboarding/discover.vue
@@ -208,9 +208,10 @@ useSeoMeta({ title: 'Discover skills', robots: 'noindex' })
class="mt-1 size-4 accent-primary"
@change="toggle(`${r.owner}/${r.repo}`)"
>
+
- {{ r.owner }}/{{ r.repo }}
+ {{ r.owner }}/{{ r.repo }}
Already watching
diff --git a/test/unit/github-avatar.nuxt.test.ts b/test/unit/github-avatar.nuxt.test.ts
new file mode 100644
index 00000000..0af9fe9c
--- /dev/null
+++ b/test/unit/github-avatar.nuxt.test.ts
@@ -0,0 +1,26 @@
+import { mountSuspended } from '@nuxt/test-utils/runtime'
+import GitHubAvatar from '../../app/components/GitHubAvatar.vue'
+
+beforeEach(() => {
+ vi.spyOn(HTMLImageElement.prototype, 'complete', 'get').mockReturnValue(false)
+})
+
+afterEach(() => vi.restoreAllMocks())
+
+it('replaces a failed avatar with owner initials and reports the failure', async () => {
+ const wrapper = await mountSuspended(GitHubAvatar, { props: { owner: 'cloudflare' } })
+ await wrapper.get('img').trigger('error')
+ expect(wrapper.find('img').exists()).toBe(false)
+ expect(wrapper.text()).toBe('CL')
+ expect(wrapper.emitted('avatarError')).toHaveLength(1)
+ wrapper.unmount()
+})
+
+it('loads the new owner after a previous avatar failed', async () => {
+ const wrapper = await mountSuspended(GitHubAvatar, { props: { owner: 'cloudflare' } })
+ await wrapper.get('img').trigger('error')
+ await wrapper.setProps({ owner: 'vuejs' })
+ expect(wrapper.find('img').exists()).toBe(true)
+ expect(wrapper.get('img').attributes('src')).toContain('vuejs.png')
+ wrapper.unmount()
+})
From 3817566729422de7749c9f95b1405429aea86b82 Mon Sep 17 00:00:00 2001
From: Harlan Wilton
Date: Mon, 12 Oct 2026 17:38:03 +1100
Subject: [PATCH 4/4] test(a11y): cover shared avatar in Repository selection
---
test/a11y.nuxt.spec.ts | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/test/a11y.nuxt.spec.ts b/test/a11y.nuxt.spec.ts
index ba90b35f..d5baca5b 100644
--- a/test/a11y.nuxt.spec.ts
+++ b/test/a11y.nuxt.spec.ts
@@ -88,6 +88,22 @@ afterEach(() => {
})
describe('accessibility: components', () => {
+ it('keeps Repository selection accessible with GitHubAvatar', async () => {
+ const Avatar = await loadComponent('GitHubAvatar')
+ const container = createIsolatedContainer()
+ const Selection = defineComponent({
+ setup: () => () => h('label', [
+ h('input', { type: 'checkbox' }),
+ h(Avatar, { owner: 'cloudflare' }),
+ h('span', 'cloudflare/security-audit-skill'),
+ ]),
+ })
+ const wrapper = await mountSuspended(Selection, { attachTo: container })
+ const results = await runAxe(container)
+ expect(formatViolations(results)).toBe('')
+ wrapper.unmount()
+ })
+
it('writing demos select files and toggle between updated documents and highlighted diffs', async () => {
const container = createIsolatedContainer()
const wrapper = await mountSuspended(await loadComponent('home-demos/_DemoWriting'), {