| 1 | <script lang="ts"> |
| 2 | import { messageMediaUrl } from './api'; |
| 3 | import type { LinkPreview } from './types'; |
| 4 | import type { UrlPreview } from './urls'; |
| 5 | |
| 6 | export let link: UrlPreview; |
| 7 | export let preview: LinkPreview | null = null; |
| 8 | export let imageUrl: (mediaId: string) => string = (mediaId) => messageMediaUrl(mediaId, 'Thumbnail'); |
| 9 | |
| 10 | const BANNER_MIN_WIDTH = 320; |
| 11 | const BANNER_MIN_ASPECT = 1.3; |
| 12 | |
| 13 | let imageFailed = false; |
| 14 | |
| 15 | $: image = preview?.image && !imageFailed ? preview.image : null; |
| 16 | $: imageSrc = image ? imageUrl(image.media_id) : null; |
| 17 | $: banner = |
| 18 | image !== null && |
| 19 | image.width !== null && |
| 20 | image.height !== null && |
| 21 | image.width >= BANNER_MIN_WIDTH && |
| 22 | image.width / image.height >= BANNER_MIN_ASPECT; |
| 23 | $: rich = Boolean(preview && (preview.title || preview.description)); |
| 24 | $: if (preview) imageFailed = false; |
| 25 | </script> |
| 26 | |
| 27 | <a class="link-card" class:rich class:banner href={link.href} target="_blank" rel="noopener noreferrer"> |
| 28 | {#if rich && preview} |
| 29 | <span class="body"> |
| 30 | <span class="link-host">{preview.site_name ?? link.hostname}</span> |
| 31 | {#if preview.title}<span class="link-title">{preview.title}</span>{/if} |
| 32 | {#if preview.description}<span class="link-desc">{preview.description}</span>{/if} |
| 33 | </span> |
| 34 | {:else} |
| 35 | <span class="body"> |
| 36 | <span class="link-host">{link.hostname}</span> |
| 37 | {#if link.path}<span class="link-path">{link.path}</span>{/if} |
| 38 | <span class="link-url">{link.display}</span> |
| 39 | </span> |
| 40 | {/if} |
| 41 | {#if imageSrc} |
| 42 | <img class="link-image" src={imageSrc} alt="" loading="lazy" on:error={() => (imageFailed = true)} /> |
| 43 | {/if} |
| 44 | </a> |
| 45 | |
| 46 | <style> |
| 47 | .link-card { |
| 48 | display: flex; gap: 10px; |
| 49 | border: 1px solid var(--border); border-radius: 8px; |
| 50 | background: var(--bg-soft); padding: 8px 10px; |
| 51 | text-decoration: none; color: var(--text); |
| 52 | overflow: hidden; |
| 53 | } |
| 54 | .link-card.rich { border-left: 3px solid var(--accent); } |
| 55 | .link-card.banner { flex-direction: column; } |
| 56 | .link-card:hover { border-color: var(--accent); } |
| 57 | .body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; } |
| 58 | .link-host { font-size: 12.5px; font-weight: 650; } |
| 59 | .rich .link-host { font-size: 11.5px; font-weight: 600; color: var(--muted); } |
| 60 | .link-title { font-size: 14px; font-weight: 650; color: var(--accent); overflow-wrap: anywhere; } |
| 61 | .link-desc { |
| 62 | font-size: 12.5px; color: var(--muted); overflow-wrap: anywhere; |
| 63 | display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; |
| 64 | } |
| 65 | .link-path { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; } |
| 66 | .link-url { font-size: 11.5px; color: var(--accent); overflow-wrap: anywhere; } |
| 67 | .link-image { flex: none; width: 72px; height: 72px; object-fit: cover; border-radius: 6px; } |
| 68 | .banner .link-image { width: 100%; height: auto; max-height: 220px; } |
| 69 | </style> |