Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions components/entities/manager/ManagerEntityLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<script setup lang="ts">
import type { EulerLabelEntity } from '~/entities/euler/labels'
import { getEulerLabelEntityLogo } from '~/entities/euler/labels'
import { getEulerLabelEntityDisplayName, getEulerLabelEntitySlug, getManagerProfilePath } from '~/utils/manager-profile'

const props = withDefaults(defineProps<{
entities: EulerLabelEntity[]
label?: string
showAvatar?: boolean
spanLink?: boolean
textClass?: string
avatarClass?: string
dataKey?: string
dataField?: string
disabled?: boolean
}>(), {
label: '',
showAvatar: true,
spanLink: false,
textClass: 'text-p2 text-content-primary hover:text-accent-600 underline transition-colors',
avatarClass: 'icon--20',
dataKey: '',
dataField: '',
disabled: false,
})

const { entities: entityMap } = useEulerLabels()
const route = useRoute()

const displayName = computed(() => props.label || getEulerLabelEntityDisplayName(props.entities))
const entityLogos = computed(() => props.entities.map(entity => getEulerLabelEntityLogo(entity.logo)))
const primarySlug = computed(() => {
const first = props.entities[0]
return first ? getEulerLabelEntitySlug(entityMap, first) : ''
})
const to = computed(() => ({
path: getManagerProfilePath(primarySlug.value),
query: { network: route.query.network },
}))
const isLinked = computed(() => Boolean(primarySlug.value) && props.entities.length === 1 && !props.disabled)
const fallbackTextClass = computed(() =>
props.textClass.replace('hover:text-accent-600 underline transition-colors', ''),
)

const goToManager = () => {
if (!isLinked.value) return
void navigateTo(to.value)
}

const onSpanKeydown = (event: KeyboardEvent) => {
if (event.key !== 'Enter' && event.key !== ' ') return
event.preventDefault()
event.stopPropagation()
goToManager()
}
</script>

<template>
<span class="inline-flex min-w-0 items-center gap-6">
<BaseAvatar
v-if="showAvatar"
:class="avatarClass"
:label="displayName"
:src="entityLogos"
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion: spanLink makes the manager destination mouse-clickable, but it renders as a plain <span> with no href, role="link", tabindex, or keyboard handler. Most of the new vault-card call sites use this mode to avoid nested anchors, so keyboard and screen-reader users do not get the same navigation affordance as mouse users. Consider making the span mode an accessible link-like control (for example role="link", tabindex="0", and Enter/Space handling) or otherwise restructuring the card/link relationship so the shared component stays accessible everywhere.

<span
v-if="spanLink && isLinked"
role="link"
tabindex="0"
:class="textClass"
:data-id="dataKey && dataField ? 'data-point' : undefined"
:data-key="dataKey || undefined"
:data-field="dataField || undefined"
:data-value="displayName"
@click.stop.prevent="goToManager"
@keydown="onSpanKeydown"
>{{ displayName }}</span>
Comment on lines +66 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Clickable span is not keyboard accessible.

In the spanLink && isLinked branch the navigation lives on a <span> with only a mouse @click handler, so keyboard users can't focus or activate it. Since this branch exists to avoid nested anchors inside link rows, consider adding role="link", tabindex="0", and a keydown handler (Enter/Space).

♿ Suggested accessibility attributes
     <span
       v-if="spanLink && isLinked"
       :class="textClass"
+      role="link"
+      tabindex="0"
       :data-id="dataKey && dataField ? 'data-point' : undefined"
       :data-key="dataKey || undefined"
       :data-field="dataField || undefined"
       :data-value="displayName"
       `@click.stop.prevent`="goToManager"
+      `@keydown.enter.stop.prevent`="goToManager"
+      `@keydown.space.stop.prevent`="goToManager"
     >{{ displayName }}</span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span
v-if="spanLink && isLinked"
:class="textClass"
:data-id="dataKey && dataField ? 'data-point' : undefined"
:data-key="dataKey || undefined"
:data-field="dataField || undefined"
:data-value="displayName"
@click.stop.prevent="goToManager"
>{{ displayName }}</span>
<span
v-if="spanLink && isLinked"
:class="textClass"
role="link"
tabindex="0"
:data-id="dataKey && dataField ? 'data-point' : undefined"
:data-key="dataKey || undefined"
:data-field="dataField || undefined"
:data-value="displayName"
`@click.stop.prevent`="goToManager"
`@keydown.enter.stop.prevent`="goToManager"
`@keydown.space.stop.prevent`="goToManager"
>{{ displayName }}</span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/entities/manager/ManagerEntityLink.vue` around lines 59 - 67, The
clickable `span` in `ManagerEntityLink.vue` is mouse-only and needs keyboard
support. Update the `spanLink && isLinked` branch on the `<span>` used for
`goToManager` to be accessible by adding link semantics and keyboard activation,
such as `role="link"`, `tabindex="0"`, and a keydown handler for Enter/Space,
while keeping the existing click behavior and avoiding nested anchors.

<NuxtLink
v-else-if="isLinked"
:to="to"
:class="textClass"
:data-id="dataKey && dataField ? 'data-point' : undefined"
:data-key="dataKey || undefined"
:data-field="dataField || undefined"
:data-value="displayName"
>{{ displayName }}</NuxtLink>
<span
v-else
:class="fallbackTextClass"
:data-id="dataKey && dataField ? 'data-point' : undefined"
:data-key="dataKey || undefined"
:data-field="dataField || undefined"
:data-value="displayName"
>{{ displayName }}</span>
</span>
</template>
27 changes: 9 additions & 18 deletions components/entities/vault/SecuritizeVaultItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { formatAssetValue } from '~/utils/sdk-prices'
import { isVaultBlockedByCountry } from '~/composables/useGeoBlock'
import { useEulerProductOfVault, useEulerEntitiesOfVault } from '~/composables/useEulerLabels'
import { isVaultGovernanceLimited } from '~/utils/eulerLabelsUtils'
import { getEulerLabelEntityLogo } from '~/entities/euler/labels'
import { withVaultIntrinsicApy, getVaultIntrinsicApy, getVaultIntrinsicApyInfo } from '~/utils/vault-intrinsic-apy'
import { formatNumber, formatCompactUsdValue } from '~/utils/string-utils'
import { VaultSupplyApyModal, UiModalPreviewTrigger } from '#components'
Expand All @@ -30,10 +29,6 @@ const entityName = computed(() => {
if (entities.length === 2) return `${entities[0].name} & ${entities[1].name}`
return `${entities[0].name} & others`
})
const entityLogos = computed(() => {
if (!entityName.value || entities.length === 0) return []
return entities.map(e => getEulerLabelEntityLogo(e.logo))
})
const displayName = computed(() => product.name || vault.shares.name)
const isGeoBlocked = computed(() => isVaultBlockedByCountry(vault.address))

Expand Down Expand Up @@ -197,18 +192,14 @@ watchEffect(async () => {
class="flex items-center gap-6"
:class="{ 'opacity-20': isGovernanceLimited }"
>
<BaseAvatar
class="icon--20"
<ManagerEntityLink
:entities="entities"
:label="entityName"
:src="entityLogos"
/>
<span
class="text-p2 text-content-primary truncate"
data-id="data-point"
span-link
text-class="text-p2 text-content-primary hover:text-accent-600 underline transition-colors truncate"
:data-key="vault.address.toLowerCase()"
data-field="risk-manager"
:data-value="entityName"
>{{ entityName }}</span>
/>
</div>
<div
v-else
Expand Down Expand Up @@ -274,12 +265,12 @@ watchEffect(async () => {
class="flex items-center gap-8"
:class="{ 'opacity-20': isGovernanceLimited }"
>
<BaseAvatar
class="icon--20"
<ManagerEntityLink
:entities="entities"
:label="entityName"
:src="entityLogos"
span-link
text-class="text-p2 text-content-primary hover:text-accent-600 underline transition-colors truncate"
/>
<span class="text-p2 text-content-primary truncate">{{ entityName }}</span>
</div>
<div
v-else
Expand Down
27 changes: 11 additions & 16 deletions components/entities/vault/VaultBorrowItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { withVaultIntrinsicApy, getVaultIntrinsicApy, getVaultIntrinsicApyInfo }
import { getVaultAvailableLiquidity, getVaultUtilization } from '~/utils/vault-display'
import { useEulerProductOfVault } from '~/composables/useEulerLabels'
import { isVaultGovernanceLimited, isVaultRecentlyAdded, isVaultKeyring, isVaultCyclicalNote, getUniqueEntitiesByVaults } from '~/utils/eulerLabelsUtils'
import { getEulerLabelEntityLogo } from '~/entities/euler/labels'
import { isAnyVaultBlockedByCountry, isVaultRestrictedByCountry } from '~/composables/useGeoBlock'
import { VaultBorrowApyModal, VaultMaxRoeModal, VaultNetApyPairModal, VaultSupplyApyModal, UiModalPreviewTrigger } from '#components'
import { isSecuritizeBorrowPair, type AnyBorrowVaultPair } from '~/types/borrow-pair'
Expand All @@ -32,15 +31,15 @@ const isAnyGovernorUnverified = computed(() => {

const entityDisplay = computed(() => {
const all = getUniqueEntitiesByVaults([pair.collateral, pair.borrow])
if (all.length === 0) return { name: '', logos: [] }
if (all.length === 0) return { name: '', entities: [] }
const name = all.length === 1
? all[0].name
: all.length === 2
? `${all[0].name} & ${all[1].name}`
: `${all[0].name} & others`
return {
name,
logos: all.map(e => getEulerLabelEntityLogo(e.logo)),
entities: all,
}
})

Expand Down Expand Up @@ -469,18 +468,14 @@ const linkPath = computed(() => ({
class="flex items-center gap-6"
:class="{ 'opacity-20': isAnyGovernanceLimited }"
>
<BaseAvatar
class="icon--20"
<ManagerEntityLink
:entities="entityDisplay.entities"
:label="entityDisplay.name"
:src="entityDisplay.logos"
/>
<span
class="text-p2 text-content-primary truncate"
data-id="data-point"
span-link
text-class="text-p2 text-content-primary hover:text-accent-600 underline transition-colors truncate"
:data-key="pairKey"
data-field="risk-manager"
:data-value="entityDisplay.name"
>{{ entityDisplay.name }}</span>
/>
</div>
<div
v-else
Expand Down Expand Up @@ -670,12 +665,12 @@ const linkPath = computed(() => ({
class="flex items-center gap-8"
:class="{ 'opacity-20': isAnyGovernanceLimited }"
>
<BaseAvatar
class="icon--20"
<ManagerEntityLink
:entities="entityDisplay.entities"
:label="entityDisplay.name"
:src="entityDisplay.logos"
span-link
text-class="text-p2 text-content-primary hover:text-accent-600 underline transition-colors truncate"
/>
<span class="text-p2 text-content-primary truncate">{{ entityDisplay.name }}</span>
</div>
<div
v-else
Expand Down
34 changes: 12 additions & 22 deletions components/entities/vault/VaultEarnItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { computeSupplyApyBreakdown, type EulerEarn } from '@eulerxyz/euler-v2-sd
import { formatAssetValue } from '~/utils/sdk-prices'
import { useEulerProductOfVault, useEulerEntitiesOfEarnVault } from '~/composables/useEulerLabels'
import { isVaultRecentlyAdded, getEarnVaultDescription } from '~/utils/eulerLabelsUtils'
import { getEulerLabelEntityLogo } from '~/entities/euler/labels'
import { getVaultIntrinsicApyInfo } from '~/utils/vault-intrinsic-apy'
import { isVaultBlockedByCountry } from '~/composables/useGeoBlock'
import { formatNumber, formatCompactUsdValue } from '~/utils/string-utils'
Expand All @@ -25,10 +24,6 @@ const entityName = computed(() => {
if (entities.length === 2) return `${entities[0].name} & ${entities[1].name}`
return `${entities[0].name} & others`
})
const entityLogos = computed(() => {
if (!entityName.value || entities.length === 0) return []
return entities.map(e => getEulerLabelEntityLogo(e.logo))
})
const { getBalance, isLoading: isBalancesLoading } = useWallets()
const { settings } = useUserSettings()
const enableIntrinsicApy = computed(() => settings.value.enableIntrinsicApy)
Expand Down Expand Up @@ -221,18 +216,14 @@ const supplyApyModalData = computed(() => ({
v-else-if="entityName"
class="flex items-center gap-6"
>
<BaseAvatar
class="icon--20"
<ManagerEntityLink
:entities="entities"
:label="entityName"
:src="entityLogos"
/>
<span
class="text-p2 text-content-primary truncate"
data-id="data-point"
span-link
text-class="text-p2 text-content-primary hover:text-accent-600 underline transition-colors truncate"
:data-key="vault.address.toLowerCase()"
data-field="capital-allocator"
:data-value="entityName"
>{{ entityName }}</span>
/>
</div>
<div
v-else
Expand Down Expand Up @@ -321,14 +312,13 @@ const supplyApyModalData = computed(() => ({
/>
Unknown
</div>
<template v-else-if="entityName">
<BaseAvatar
class="icon--20"
:label="entityName"
:src="entityLogos"
/>
<span class="text-p2 text-content-primary truncate">{{ entityName }}</span>
</template>
<ManagerEntityLink
v-else-if="entityName"
:entities="entities"
:label="entityName"
span-link
text-class="text-p2 text-content-primary hover:text-accent-600 underline transition-colors truncate"
/>
<div
v-else
class="text-p2 text-content-primary"
Expand Down
27 changes: 9 additions & 18 deletions components/entities/vault/VaultItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { formatAssetValue } from '~/utils/sdk-prices'
import { useEulerProductOfVault, useEulerEntitiesOfVault } from '~/composables/useEulerLabels'
import { isVaultGovernanceLimited, isVaultRecentlyAdded, isVaultKeyring, isVaultCyclicalNote } from '~/utils/eulerLabelsUtils'
import { withVaultIntrinsicApy, getVaultIntrinsicApy, getVaultIntrinsicApyInfo } from '~/utils/vault-intrinsic-apy'
import { getEulerLabelEntityLogo } from '~/entities/euler/labels'
import { isVaultBlockedByCountry } from '~/composables/useGeoBlock'
import { formatNumber, compactNumber, formatCompactUsdValue } from '~/utils/string-utils'
import BaseLoadableContent from '~/components/base/BaseLoadableContent.vue'
Expand All @@ -32,10 +31,6 @@ const entityName = computed(() => {
if (entities.length === 2) return `${entities[0].name} & ${entities[1].name}`
return `${entities[0].name} & others`
})
const entityLogos = computed(() => {
if (!entityName.value || entities.length === 0) return []
return entities.map(e => getEulerLabelEntityLogo(e.logo))
})
const isEscrow = computed(() => getVaultCategory(vault.address) === 'escrow')
const isBorrowable = computed(() => isVaultBorrowable(vault))
const displayName = computed(() => {
Expand Down Expand Up @@ -303,18 +298,14 @@ watchEffect(async () => {
class="flex items-center gap-6"
:class="{ 'opacity-20': isGovernanceLimited }"
>
<BaseAvatar
class="icon--20"
<ManagerEntityLink
:entities="entities"
:label="entityName"
:src="entityLogos"
/>
<span
class="text-p2 text-content-primary truncate"
data-id="data-point"
span-link
text-class="text-p2 text-content-primary hover:text-accent-600 underline transition-colors truncate"
:data-key="vault.address.toLowerCase()"
data-field="risk-manager"
:data-value="entityName"
>{{ entityName }}</span>
/>
Comment on lines +301 to +308

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the manager-profile affordance keyboard accessible.

Using span-link here routes through ManagerEntityLink’s click-only <span> branch, so the new profile destination is not tabbable or activatable from the keyboard inside these vault cards. Please fix the shared span-link path before rolling this pattern out across the other list items in this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/entities/vault/VaultItem.vue` around lines 301 - 308, The
manager-profile link is using the click-only span branch in ManagerEntityLink,
so it is not keyboard accessible from the vault cards. Update the shared
span-link path in ManagerEntityLink so it renders a tabbable,
keyboard-activatable control with proper link semantics, and make the VaultItem
usage continue through that fixed path rather than relying on the non-accessible
span behavior.

</div>
<div
v-else
Expand Down Expand Up @@ -475,12 +466,12 @@ watchEffect(async () => {
class="flex items-center gap-8"
:class="{ 'opacity-20': isGovernanceLimited }"
>
<BaseAvatar
class="icon--20"
<ManagerEntityLink
:entities="entities"
:label="entityName"
:src="entityLogos"
span-link
text-class="text-p2 text-content-primary hover:text-accent-600 underline transition-colors truncate"
/>
<span class="text-p2 text-content-primary truncate">{{ entityName }}</span>
</div>
<div
v-else
Expand Down
14 changes: 3 additions & 11 deletions components/entities/vault/overview/SecuritizeVaultOverview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { EVault, EVaultCollateral, SecuritizeCollateralVault } from '@euler
import { useEulerEntitiesOfVault } from '~/composables/useEulerLabels'
import { getProductByVault, getProductKeyByVault, isVaultGovernanceLimited } from '~/utils/eulerLabelsUtils'
import { useVaultRegistry } from '~/composables/useVaultRegistry'
import { getEulerLabelEntityLogo } from '~/entities/euler/labels'
import { isVaultBlockedByCountry } from '~/composables/useGeoBlock'
import { autoLink } from '~/utils/autoLink'
import { getExplorerLink } from '~/utils/block-explorer'
Expand Down Expand Up @@ -217,17 +216,10 @@ const supplyCapPercentageDisplay = computed(() => {
class="flex items-center gap-8"
:class="{ 'opacity-20': isGovernanceLimited }"
>
<BaseAvatar
:label="entity.name"
:src="getEulerLabelEntityLogo(entity.logo)"
class="!w-28 !h-28"
<ManagerEntityLink
:entities="[entity]"
avatar-class="!w-28 !h-28"
/>
<a
:href="entity.url"
target="_blank"
rel="noopener noreferrer"
class="text-p2 text-content-primary underline"
>{{ entity.name }}</a>
</div>
</div>
<VaultTypeChip
Expand Down
Loading