16 changed files with 954 additions and 695 deletions
@ -0,0 +1,373 @@
@@ -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