fix(ui): web5 cards mobile polish + demo identity avatars
- Identities render as profile cards (banner, overlapping avatar, bio) in narrow containers via container query; wide/desktop row untouched - Demo identities get purpose-tinted SVG avatars (public/demo-avatars) in both mock identity.list/get branches - Node Visibility: description stacks under icon+title on mobile, Refresh is a full-width bottom action on all sizes, discoverable warning line removed, and the demo no longer exposes an onion address Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -61,9 +61,91 @@
|
||||
<div
|
||||
v-for="(identity, idx) in managedIdentities"
|
||||
:key="identity.id"
|
||||
:class="{ 'card-stagger': showStagger }" class="identity-row flex flex-col gap-3 p-4 bg-white/[0.08] rounded-lg"
|
||||
:class="{ 'card-stagger': showStagger }"
|
||||
:style="{ '--stagger-index': idx }"
|
||||
>
|
||||
<!-- Narrow containers (phones): profile-card layout with banner +
|
||||
overlapping avatar. Hidden on wide containers where the classic
|
||||
row below renders instead. -->
|
||||
<div class="identity-card-m rounded-xl overflow-hidden bg-white/[0.08]">
|
||||
<div class="relative h-16" :class="bannerFallback(identity)">
|
||||
<img
|
||||
v-if="identity.profile?.banner"
|
||||
:src="displayableUrl(identity.profile.banner)"
|
||||
class="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="px-4 pb-4">
|
||||
<div class="flex items-end justify-between -mt-7">
|
||||
<button @click="openProfileEditor(identity)" class="relative w-14 h-14 rounded-full overflow-hidden ring-4 ring-black/60 group shrink-0" title="Edit profile">
|
||||
<img
|
||||
v-if="identity.profile?.picture && !listPictureFailed[identity.id]"
|
||||
:src="displayableUrl(identity.profile.picture)"
|
||||
class="w-full h-full object-cover"
|
||||
@error="() => { listPictureFailed[identity.id] = true }"
|
||||
/>
|
||||
<div v-if="!identity.profile?.picture || listPictureFailed[identity.id]" class="w-full h-full flex items-center justify-center" :class="{
|
||||
'bg-blue-500/20': identity.purpose === 'personal',
|
||||
'bg-orange-500/20': identity.purpose === 'business',
|
||||
'bg-purple-500/20': identity.purpose === 'anonymous',
|
||||
}">
|
||||
<span class="text-lg font-bold" :class="{
|
||||
'text-blue-400': identity.purpose === 'personal',
|
||||
'text-orange-400': identity.purpose === 'business',
|
||||
'text-purple-400': identity.purpose === 'anonymous',
|
||||
}">{{ identity.name.charAt(0).toUpperCase() }}</span>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /></svg>
|
||||
</div>
|
||||
</button>
|
||||
<div class="flex items-center gap-1 pt-8">
|
||||
<button @click="openKeyViewer(identity)" class="p-2 rounded-lg text-white/50 hover:text-white hover:bg-white/10 transition-colors" title="View keys">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button v-if="!identity.is_default" @click="setDefaultIdentity(identity.id)" class="p-2 rounded-lg text-white/50 hover:text-yellow-400 hover:bg-white/10 transition-colors" title="Set as default">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="confirmDeleteIdentity(identity)" class="p-2 rounded-lg text-white/50 hover:text-red-400 hover:bg-white/10 transition-colors" title="Delete">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-2 flex-wrap min-w-0">
|
||||
<span class="text-white font-semibold text-sm truncate">{{ identity.profile?.display_name || identity.name }}</span>
|
||||
<span v-if="identity.is_default" class="text-yellow-400 text-xs" title="Default identity">★</span>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full capitalize" :class="{
|
||||
'bg-blue-500/20 text-blue-300': identity.purpose === 'personal',
|
||||
'bg-orange-500/20 text-orange-300': identity.purpose === 'business',
|
||||
'bg-purple-500/20 text-purple-300': identity.purpose === 'anonymous',
|
||||
}">{{ identity.purpose }}</span>
|
||||
</div>
|
||||
<p v-if="identity.profile?.about" class="text-white/60 text-xs mt-1 leading-relaxed">{{ identity.profile.about }}</p>
|
||||
<div class="mt-2 space-y-0.5">
|
||||
<div class="flex items-center gap-1 min-w-0">
|
||||
<p class="text-white/50 text-xs font-mono truncate" :title="identity.did">{{ identity.did }}</p>
|
||||
<button @click="copyIdentityDid(identity.did)" class="shrink-0 p-0.5 rounded text-white/30 hover:text-white/70 transition-colors" title="Copy DID">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="identity.nostr_npub" class="flex items-center gap-1 min-w-0">
|
||||
<p class="text-white/40 text-xs font-mono truncate" :title="identity.nostr_npub">{{ identity.nostr_npub }}</p>
|
||||
<button @click="copyIdentityDid(identity.nostr_npub || '')" class="shrink-0 p-0.5 rounded text-white/30 hover:text-white/70 transition-colors" title="Copy npub">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wide containers: the original row — desktop appearance unchanged -->
|
||||
<div class="identity-row flex flex-col gap-3 p-4 bg-white/[0.08] rounded-lg">
|
||||
<div class="identity-row-main flex items-center gap-4 min-w-0">
|
||||
<!-- Avatar -->
|
||||
<button @click="openProfileEditor(identity)" class="relative flex-shrink-0 w-10 h-10 rounded-full overflow-hidden group" title="Edit profile">
|
||||
@@ -134,6 +216,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -465,6 +548,16 @@ watch(() => profileForm.value.picture, () => { editorPictureFailed.value = false
|
||||
// Rewrite onion-rooted `/blob/<cid>` URLs (with or without capability
|
||||
// query) to same-origin `/blob/<cid>` so they render in this UI. Data
|
||||
// URLs and plain external URLs pass through untouched.
|
||||
// Gradient banner backdrop for the narrow profile card when the identity has
|
||||
// no banner image, tinted by purpose to match the avatar/chip colors.
|
||||
function bannerFallback(identity: ManagedIdentity): string {
|
||||
switch (identity.purpose) {
|
||||
case 'business': return 'bg-gradient-to-br from-orange-500/40 to-amber-500/15'
|
||||
case 'anonymous': return 'bg-gradient-to-br from-purple-500/40 to-fuchsia-500/15'
|
||||
default: return 'bg-gradient-to-br from-blue-500/40 to-indigo-500/15'
|
||||
}
|
||||
}
|
||||
|
||||
function displayableUrl(url: string | null | undefined): string {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('data:') || url.startsWith('/')) return url
|
||||
@@ -710,8 +803,21 @@ defineExpose({ loadIdentities, managedIdentities })
|
||||
container-name: identities-card;
|
||||
}
|
||||
|
||||
/* Narrow containers (default): the profile card renders and the classic row
|
||||
is hidden. Wide containers: the original desktop row — untouched — and the
|
||||
profile card hides. Doubled selectors out-rank the Tailwind utilities on
|
||||
the same elements regardless of stylesheet order. */
|
||||
.identities-card .identity-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@container identities-card (min-width: 560px) {
|
||||
.identity-row {
|
||||
.identities-card .identity-card-m {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.identities-card .identity-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
Reference in New Issue
Block a user