0ab2c58eb4
- Header desktop nav: social icons grouped in a wald-tinted pill with labels, active-state (aria-current + bold), opacity hover (no per-item bg) - Mobile overlay: social links as icon+label rows with active-state - Search button: lucide:search, icon-only - Migrate all icons mdi→lucide app-wide (234 refs); brand/language logos (google, whatsapp, mastodon, telegram, language-*) stay on mdi - generate-icons: emit mdi + lucide subsets; add @iconify-json/lucide - iconify-offline: register lucide collection - scripts/icon-map|validate|apply: reusable migration tooling Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
223 lines
9.3 KiB
Svelte
223 lines
9.3 KiB
Svelte
<script lang="ts">
|
||
import Icon from "@iconify/svelte";
|
||
import "$lib/iconify-offline";
|
||
|
||
let {
|
||
open,
|
||
title,
|
||
slug,
|
||
endpointBase,
|
||
downloadPrefix = "social",
|
||
onclose,
|
||
}: {
|
||
open: boolean;
|
||
title: string;
|
||
slug: string;
|
||
/** "/api/social/calendar" oder "/api/social/post" */
|
||
endpointBase: string;
|
||
/** Prefix für Download-Dateinamen, z.B. "termin" oder "beitrag" */
|
||
downloadPrefix?: string;
|
||
onclose: () => void;
|
||
} = $props();
|
||
|
||
let theme = $state<"wald" | "transparent-onlight" | "transparent-ondark">("wald");
|
||
let linkCopied = $state(false);
|
||
|
||
/** Direktlink, der dieses Modal erneut öffnet — abhängig von endpointBase. */
|
||
const directLink = $derived.by(() => {
|
||
if (typeof window === "undefined") return "";
|
||
const origin = window.location.origin;
|
||
if (endpointBase.includes("/calendar")) {
|
||
return `${origin}/kalender/?social=${encodeURIComponent(slug)}`;
|
||
}
|
||
if (endpointBase.includes("/post")) {
|
||
return `${origin}/post/${encodeURIComponent(slug)}?social=1`;
|
||
}
|
||
return `${origin}/`;
|
||
});
|
||
|
||
async function copyDirectLink() {
|
||
try {
|
||
await navigator.clipboard.writeText(directLink);
|
||
linkCopied = true;
|
||
setTimeout(() => (linkCopied = false), 2000);
|
||
} catch {
|
||
linkCopied = false;
|
||
}
|
||
}
|
||
|
||
// Loading-State pro Format. Skeleton wird angezeigt bis das jeweilige
|
||
// Bild fertig geladen ist. Bei Theme-Wechsel resetten.
|
||
let loadedOg = $state(false);
|
||
let loadedSq = $state(false);
|
||
let loadedStory = $state(false);
|
||
$effect(() => {
|
||
// Reset bei Theme-Änderung (neuer Request → neuer Loading-Zustand)
|
||
void theme;
|
||
loadedOg = false;
|
||
loadedSq = false;
|
||
loadedStory = false;
|
||
});
|
||
|
||
const themeQ = $derived(theme === "wald" ? "" : `&theme=${theme}`);
|
||
const ogUrl = $derived(`${endpointBase}/${slug}?format=og${themeQ}`);
|
||
const sqUrl = $derived(`${endpointBase}/${slug}?format=square${themeQ}`);
|
||
const storyUrl = $derived(`${endpointBase}/${slug}?format=story${themeQ}`);
|
||
const previewBg = $derived(
|
||
theme === "wald"
|
||
? "bg-stein-50"
|
||
: theme === "transparent-onlight"
|
||
? "bg-[repeating-linear-gradient(45deg,#f5f5f4,#f5f5f4_10px,#e7e5e4_10px,#e7e5e4_20px)]"
|
||
: "bg-[repeating-linear-gradient(45deg,#292524,#292524_10px,#1c1917_10px,#1c1917_20px)]",
|
||
);
|
||
</script>
|
||
|
||
{#if open}
|
||
<div
|
||
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 p-4"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-label="Social-Bild-Vorschau"
|
||
onclick={onclose}
|
||
onkeydown={(e) => { if (e.key === 'Escape') onclose(); }}
|
||
tabindex="-1"
|
||
>
|
||
<div
|
||
class="relative max-h-[92vh] w-full max-w-5xl overflow-y-auto rounded-lg bg-white p-5 shadow-2xl"
|
||
onclick={(e) => e.stopPropagation()}
|
||
role="presentation"
|
||
>
|
||
<div class="mb-4 flex items-start justify-between gap-3">
|
||
<div class="min-w-0">
|
||
<p class="text-xs uppercase tracking-wider text-stein-500">Social-Bild</p>
|
||
<p class="truncate text-sm font-semibold text-stein-800">{title}</p>
|
||
</div>
|
||
<div class="flex shrink-0 items-center gap-1">
|
||
<button
|
||
type="button"
|
||
onclick={copyDirectLink}
|
||
class="inline-flex items-center gap-1 rounded-md border border-stein-300 bg-white px-2 py-1 text-[11px] font-medium text-stein-700 hover:bg-stein-50"
|
||
title="Direktlink zu dieser Vorschau kopieren"
|
||
aria-label="Direktlink kopieren"
|
||
>
|
||
<Icon icon={linkCopied ? "lucide:check" : "lucide:link"} class="size-3.5" />
|
||
{linkCopied ? "Kopiert" : "Direktlink"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onclick={onclose}
|
||
class="text-stein-400 hover:text-stein-700"
|
||
aria-label="Schließen"
|
||
>
|
||
<Icon icon="lucide:x" class="size-5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-4 rounded-lg border border-stein-200 bg-stein-50 p-3">
|
||
<p class="mb-2 text-[11px] font-medium uppercase tracking-wider text-stein-500">Hintergrund</p>
|
||
<div class="flex flex-wrap gap-2">
|
||
<button
|
||
type="button"
|
||
onclick={() => (theme = "wald")}
|
||
class="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition
|
||
{theme === 'wald' ? 'border-wald-500 bg-wald-100 text-wald-800' : 'border-stein-300 bg-white text-stein-700 hover:bg-stein-50'}"
|
||
>
|
||
<span class="size-3 rounded-full bg-gradient-to-br from-wald-900 to-wald-600"></span>
|
||
Wald (Standard)
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onclick={() => (theme = "transparent-onlight")}
|
||
class="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition
|
||
{theme === 'transparent-onlight' ? 'border-wald-500 bg-wald-100 text-wald-800' : 'border-stein-300 bg-white text-stein-700 hover:bg-stein-50'}"
|
||
title="Transparenter Hintergrund mit dunklem Text"
|
||
>
|
||
<span class="size-3 rounded-full border border-stein-300 bg-[repeating-linear-gradient(45deg,#fff,#fff_2px,#e5e5e5_2px,#e5e5e5_4px)]"></span>
|
||
Transparent · helle Hintergründe
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onclick={() => (theme = "transparent-ondark")}
|
||
class="flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition
|
||
{theme === 'transparent-ondark' ? 'border-wald-500 bg-wald-100 text-wald-800' : 'border-stein-300 bg-white text-stein-700 hover:bg-stein-50'}"
|
||
title="Transparenter Hintergrund mit hellem Text"
|
||
>
|
||
<span class="size-3 rounded-full border border-stein-300 bg-[repeating-linear-gradient(45deg,#1c1917,#1c1917_2px,#44403c_2px,#44403c_4px)]"></span>
|
||
Transparent · dunkle Hintergründe
|
||
</button>
|
||
</div>
|
||
{#if theme !== "wald"}
|
||
<p class="mt-2 text-[10px] text-stein-500">
|
||
Vorschau-Hintergrund zeigt nur, wie der Text aussieht — heruntergeladene PNG hat echten Alpha-Kanal.
|
||
</p>
|
||
{/if}
|
||
</div>
|
||
|
||
<div class="grid gap-4 sm:grid-cols-3">
|
||
<div class="flex flex-col gap-2">
|
||
<p class="text-xs font-medium text-stein-600">Quer · 1200 × 630</p>
|
||
<div class="relative w-full aspect-[1200/630] rounded-sm border border-stein-200 overflow-hidden {previewBg}">
|
||
{#if !loadedOg}
|
||
<div class="absolute inset-0 animate-pulse bg-gradient-to-br from-stein-200 via-stein-300 to-stein-200"></div>
|
||
{/if}
|
||
<img
|
||
src={ogUrl}
|
||
alt="Social-Bild Quer"
|
||
class="absolute inset-0 h-full w-full transition-opacity duration-200 {loadedOg ? 'opacity-100' : 'opacity-0'}"
|
||
loading="eager"
|
||
onload={() => (loadedOg = true)}
|
||
/>
|
||
</div>
|
||
<a href={ogUrl} download={`${downloadPrefix}-${slug}-og-${theme}.png`} class="btn-outline btn-sm justify-center no-underline!">
|
||
<Icon icon="lucide:download" class="size-3.5" />
|
||
Quer herunterladen
|
||
</a>
|
||
</div>
|
||
<div class="flex flex-col gap-2">
|
||
<p class="text-xs font-medium text-stein-600">Quadrat · 1080 × 1080</p>
|
||
<div class="relative mx-auto w-full aspect-square rounded-sm border border-stein-200 overflow-hidden {previewBg}">
|
||
{#if !loadedSq}
|
||
<div class="absolute inset-0 animate-pulse bg-gradient-to-br from-stein-200 via-stein-300 to-stein-200"></div>
|
||
{/if}
|
||
<img
|
||
src={sqUrl}
|
||
alt="Social-Bild Quadrat"
|
||
class="absolute inset-0 h-full w-full transition-opacity duration-200 {loadedSq ? 'opacity-100' : 'opacity-0'}"
|
||
loading="eager"
|
||
onload={() => (loadedSq = true)}
|
||
/>
|
||
</div>
|
||
<a href={sqUrl} download={`${downloadPrefix}-${slug}-square-${theme}.png`} class="btn-outline btn-sm justify-center no-underline!">
|
||
<Icon icon="lucide:download" class="size-3.5" />
|
||
Quadrat herunterladen
|
||
</a>
|
||
</div>
|
||
<div class="flex flex-col gap-2">
|
||
<p class="text-xs font-medium text-stein-600">Story · 1080 × 1920</p>
|
||
<div class="relative mx-auto h-[280px] w-[158px] rounded-sm border border-stein-200 overflow-hidden {previewBg}">
|
||
{#if !loadedStory}
|
||
<div class="absolute inset-0 animate-pulse bg-gradient-to-br from-stein-200 via-stein-300 to-stein-200"></div>
|
||
{/if}
|
||
<img
|
||
src={storyUrl}
|
||
alt="Social-Bild Story"
|
||
class="absolute inset-0 h-full w-full transition-opacity duration-200 {loadedStory ? 'opacity-100' : 'opacity-0'}"
|
||
loading="eager"
|
||
onload={() => (loadedStory = true)}
|
||
/>
|
||
</div>
|
||
<a href={storyUrl} download={`${downloadPrefix}-${slug}-story-${theme}.png`} class="btn-outline btn-sm justify-center no-underline!">
|
||
<Icon icon="lucide:download" class="size-3.5" />
|
||
Story herunterladen
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="mt-4 text-[11px] text-stein-400">
|
||
Quer: Facebook/Twitter/Link-Preview. Quadrat: Instagram-Feed. Story: Instagram/WhatsApp Story.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{/if}
|