16 changed files with 954 additions and 695 deletions
@ -0,0 +1,373 @@ |
|||||||
|
<script lang="ts"> |
||||||
|
import ProfileBadge from './ProfileBadge.svelte'; |
||||||
|
import type { Snippet } from 'svelte'; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
pubkey: string; |
||||||
|
relativeTime?: string; |
||||||
|
clientName?: string | null; |
||||||
|
topics?: string[]; |
||||||
|
inline?: boolean; // If true, use inline ProfileBadge (no picture) |
||||||
|
showDivider?: boolean; // If true, show divider line below header |
||||||
|
kindLabel?: string; // Optional kind label to show on the right |
||||||
|
badges?: Snippet; |
||||||
|
left?: Snippet; |
||||||
|
actions?: Snippet; |
||||||
|
} |
||||||
|
|
||||||
|
let { |
||||||
|
pubkey, |
||||||
|
relativeTime, |
||||||
|
clientName, |
||||||
|
topics = [], |
||||||
|
inline = false, |
||||||
|
showDivider = false, |
||||||
|
kindLabel, |
||||||
|
badges, |
||||||
|
left, |
||||||
|
actions |
||||||
|
}: Props = $props(); |
||||||
|
|
||||||
|
// Load profile to get NIP-05 for separate display |
||||||
|
let profile = $state<{ name?: string; nip05?: string[] } | null>(null); |
||||||
|
let lastLoadedPubkey = $state<string | null>(null); |
||||||
|
|
||||||
|
// Check if nip05 handle matches the name to avoid duplicate display |
||||||
|
let shouldShowNip05 = $derived.by(() => { |
||||||
|
if (!profile?.nip05 || profile.nip05.length === 0) return false; |
||||||
|
if (!profile?.name) return true; // Show nip05 if no name |
||||||
|
|
||||||
|
const nip05Handle = profile.nip05[0].split('@')[0]; // Extract handle part before @ |
||||||
|
return nip05Handle.toLowerCase() !== profile.name.toLowerCase(); |
||||||
|
}); |
||||||
|
|
||||||
|
$effect(() => { |
||||||
|
if (pubkey && pubkey !== lastLoadedPubkey) { |
||||||
|
profile = null; |
||||||
|
loadProfile(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
async function loadProfile() { |
||||||
|
if (!pubkey || lastLoadedPubkey === pubkey) return; |
||||||
|
const currentPubkey = pubkey; |
||||||
|
try { |
||||||
|
const { fetchProfile } = await import('../../services/user-data.js'); |
||||||
|
const p = await fetchProfile(currentPubkey); |
||||||
|
if (pubkey === currentPubkey) { |
||||||
|
profile = p; |
||||||
|
lastLoadedPubkey = currentPubkey; |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
// Silently fail - profile loading is non-critical |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="card-header" class:with-divider={showDivider}> |
||||||
|
<div class="card-header-left"> |
||||||
|
<div class="profile-badge-wrapper"> |
||||||
|
<ProfileBadge pubkey={pubkey} inline={inline} hideNip05={true} /> |
||||||
|
</div> |
||||||
|
{#if shouldShowNip05 && profile?.nip05} |
||||||
|
<span class="nip05-text text-fog-text-light dark:text-fog-dark-text-light nip05-separate"> |
||||||
|
{profile.nip05[0]} |
||||||
|
</span> |
||||||
|
{/if} |
||||||
|
{#if badges} |
||||||
|
{@render badges()} |
||||||
|
{/if} |
||||||
|
{#if relativeTime} |
||||||
|
<span class="time-text">{relativeTime}</span> |
||||||
|
{/if} |
||||||
|
{#if clientName} |
||||||
|
<span class="client-text">via {clientName}</span> |
||||||
|
{/if} |
||||||
|
{#if topics && topics.length > 0} |
||||||
|
{#each topics.slice(0, 3) as topic} |
||||||
|
<a href="/topics/{topic}" class="topic-badge">{topic}</a> |
||||||
|
{/each} |
||||||
|
{/if} |
||||||
|
{#if left} |
||||||
|
{@render left()} |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
<div class="card-header-right"> |
||||||
|
{#if actions} |
||||||
|
{@render actions()} |
||||||
|
{/if} |
||||||
|
{#if kindLabel} |
||||||
|
<span class="kind-label">{kindLabel}</span> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
{#if showDivider} |
||||||
|
<hr class="card-header-divider" /> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
|
||||||
|
<style> |
||||||
|
.card-header { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
gap: 0.5rem; |
||||||
|
margin-bottom: 0.75rem; |
||||||
|
position: relative; |
||||||
|
min-width: 0; |
||||||
|
flex-wrap: wrap; |
||||||
|
width: 100%; |
||||||
|
max-width: 100%; |
||||||
|
box-sizing: border-box; |
||||||
|
overflow: hidden; |
||||||
|
word-break: break-word; |
||||||
|
overflow-wrap: anywhere; |
||||||
|
line-height: 1.5; |
||||||
|
} |
||||||
|
|
||||||
|
.card-header.with-divider { |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.card-header-left { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 0.5rem; |
||||||
|
flex-wrap: wrap; |
||||||
|
flex: 1; |
||||||
|
min-width: 0; |
||||||
|
max-width: 100%; |
||||||
|
box-sizing: border-box; |
||||||
|
word-break: break-word; |
||||||
|
overflow-wrap: anywhere; |
||||||
|
} |
||||||
|
|
||||||
|
.profile-badge-wrapper { |
||||||
|
min-width: 0 !important; |
||||||
|
max-width: 100% !important; |
||||||
|
flex-shrink: 1 !important; |
||||||
|
overflow: visible !important; |
||||||
|
} |
||||||
|
|
||||||
|
/* Keep profile badge and NIP-05 together, allow time to wrap */ |
||||||
|
.profile-badge-wrapper { |
||||||
|
flex-shrink: 1; |
||||||
|
min-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.nip05-separate { |
||||||
|
flex-shrink: 1; |
||||||
|
min-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
/* Time can wrap to next line if needed */ |
||||||
|
.time-text { |
||||||
|
flex-shrink: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.card-header-left :global(.profile-badge) { |
||||||
|
max-width: 100% !important; |
||||||
|
width: auto !important; |
||||||
|
min-width: 0 !important; |
||||||
|
word-break: break-word !important; |
||||||
|
overflow-wrap: anywhere !important; |
||||||
|
box-sizing: border-box !important; |
||||||
|
flex-shrink: 1 !important; |
||||||
|
flex-wrap: wrap !important; |
||||||
|
} |
||||||
|
|
||||||
|
.nip05-separate { |
||||||
|
font-size: 0.875em; |
||||||
|
white-space: nowrap; |
||||||
|
flex-shrink: 1; |
||||||
|
min-width: 0; |
||||||
|
overflow: hidden; |
||||||
|
text-overflow: ellipsis; |
||||||
|
} |
||||||
|
|
||||||
|
.nip05-separate { |
||||||
|
font-size: 0.875em; |
||||||
|
white-space: nowrap; |
||||||
|
flex-shrink: 1; |
||||||
|
min-width: 0; |
||||||
|
overflow: hidden; |
||||||
|
text-overflow: ellipsis; |
||||||
|
} |
||||||
|
|
||||||
|
:global(.dark) .nip05-separate { |
||||||
|
color: var(--fog-dark-text-light, #a8b8d0); |
||||||
|
} |
||||||
|
|
||||||
|
.time-text, |
||||||
|
.client-text { |
||||||
|
font-size: 0.75em; |
||||||
|
color: var(--fog-text-light, #52667a); |
||||||
|
white-space: nowrap; |
||||||
|
flex-shrink: 0; |
||||||
|
} |
||||||
|
|
||||||
|
:global(.dark) .time-text, |
||||||
|
:global(.dark) .client-text { |
||||||
|
color: var(--fog-dark-text-light, #a8b8d0); |
||||||
|
} |
||||||
|
|
||||||
|
.topic-badge { |
||||||
|
padding: 0.125rem 0.5rem; |
||||||
|
border-radius: 0.25rem; |
||||||
|
background: var(--fog-border, #e5e7eb); |
||||||
|
color: var(--fog-text-light, #52667a); |
||||||
|
text-decoration: none; |
||||||
|
font-size: 0.75em; |
||||||
|
transition: opacity 0.2s; |
||||||
|
flex-shrink: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.topic-badge:hover { |
||||||
|
text-decoration: underline; |
||||||
|
opacity: 0.8; |
||||||
|
} |
||||||
|
|
||||||
|
:global(.dark) .topic-badge { |
||||||
|
background: var(--fog-dark-border, #374151); |
||||||
|
color: var(--fog-dark-text-light, #a8b8d0); |
||||||
|
} |
||||||
|
|
||||||
|
.card-header-right { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 0.5rem; |
||||||
|
flex-shrink: 0; |
||||||
|
flex-wrap: wrap; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.kind-label { |
||||||
|
font-size: 0.75rem; |
||||||
|
color: var(--fog-text-light, #52667a); |
||||||
|
padding: 0.25rem 0.5rem; |
||||||
|
background: var(--fog-highlight, #f3f4f6); |
||||||
|
border-radius: 0.25rem; |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
|
||||||
|
:global(.dark) .kind-label { |
||||||
|
color: var(--fog-dark-text-light, #a8b8d0); |
||||||
|
background: var(--fog-dark-highlight, #374151); |
||||||
|
} |
||||||
|
|
||||||
|
.card-header-divider { |
||||||
|
position: absolute; |
||||||
|
bottom: -0.5rem; |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
margin: 0; |
||||||
|
border: none; |
||||||
|
border-top: 1px solid var(--fog-border, #e5e7eb); |
||||||
|
} |
||||||
|
|
||||||
|
:global(.dark) .card-header-divider { |
||||||
|
border-top-color: var(--fog-dark-border, #374151); |
||||||
|
} |
||||||
|
|
||||||
|
/* On wider screens, keep on one line but allow truncation if needed */ |
||||||
|
@media (min-width: 641px) { |
||||||
|
.card-header-left :global(.nip05-container) { |
||||||
|
flex-wrap: nowrap !important; |
||||||
|
overflow: hidden !important; |
||||||
|
} |
||||||
|
|
||||||
|
.card-header-left :global(.profile-name) { |
||||||
|
white-space: nowrap !important; |
||||||
|
overflow: hidden !important; |
||||||
|
text-overflow: ellipsis !important; |
||||||
|
flex-shrink: 1 !important; |
||||||
|
min-width: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.card-header-left :global(.nip05-text), |
||||||
|
.card-header-left :global(.break-nip05) { |
||||||
|
white-space: nowrap !important; |
||||||
|
overflow: hidden !important; |
||||||
|
text-overflow: ellipsis !important; |
||||||
|
flex-shrink: 1 !important; |
||||||
|
min-width: 0 !important; |
||||||
|
word-break: normal !important; |
||||||
|
overflow-wrap: normal !important; |
||||||
|
word-wrap: normal !important; |
||||||
|
max-width: 100% !important; |
||||||
|
box-sizing: border-box !important; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@media (max-width: 640px) { |
||||||
|
.card-header { |
||||||
|
flex-direction: column; |
||||||
|
align-items: flex-start; |
||||||
|
gap: 0.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
.card-header-left { |
||||||
|
width: 100%; |
||||||
|
flex-wrap: wrap; |
||||||
|
gap: 0.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
.card-header-left > span { |
||||||
|
white-space: normal !important; |
||||||
|
word-break: break-word !important; |
||||||
|
overflow-wrap: anywhere !important; |
||||||
|
max-width: 100%; |
||||||
|
flex-shrink: 1; |
||||||
|
min-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.card-header-right { |
||||||
|
width: 100%; |
||||||
|
justify-content: flex-start; |
||||||
|
flex-wrap: wrap; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.profile-badge-wrapper { |
||||||
|
max-width: 100% !important; |
||||||
|
width: 100% !important; |
||||||
|
flex-shrink: 1 !important; |
||||||
|
min-width: 0 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.card-header-left :global(.profile-badge) { |
||||||
|
max-width: 100% !important; |
||||||
|
width: 100% !important; |
||||||
|
min-width: 0 !important; |
||||||
|
flex-shrink: 1 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.card-header-left :global(.nip05-container) { |
||||||
|
flex-direction: column !important; |
||||||
|
align-items: flex-start !important; |
||||||
|
width: 100% !important; |
||||||
|
} |
||||||
|
|
||||||
|
/* On narrow screens, allow wrapping instead of truncating */ |
||||||
|
.card-header-left :global(.nip05-text), |
||||||
|
.card-header-left :global(.break-nip05), |
||||||
|
.card-header-left :global(.nip05-text.break-all), |
||||||
|
.card-header-left :global(.break-nip05.break-all), |
||||||
|
.card-header-left :global(span.nip05-text), |
||||||
|
.card-header-left :global(span.break-nip05), |
||||||
|
.card-header-left :global(span.nip05-text.break-all), |
||||||
|
.card-header-left :global(span.break-nip05.break-all) { |
||||||
|
max-width: none !important; |
||||||
|
overflow: visible !important; |
||||||
|
text-overflow: clip !important; |
||||||
|
white-space: normal !important; |
||||||
|
word-break: break-word !important; |
||||||
|
overflow-wrap: anywhere !important; |
||||||
|
word-break: normal !important; |
||||||
|
overflow-wrap: normal !important; |
||||||
|
word-wrap: normal !important; |
||||||
|
display: inline-block !important; |
||||||
|
width: auto !important; |
||||||
|
box-sizing: border-box !important; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
Loading…
Reference in new issue