Files
windwiderstand/src/lib/components/TopBanner.svelte
T
Peter Meier d327798ce6
Deploy / verify (push) Failing after 1m40s
Deploy / deploy (push) Has been skipped
feat: add class-variance-authority for card styling and enhance color palettes
- Introduced `class-variance-authority` to manage card component styles, allowing for flexible variant and layout options.
- Added a new color palette for "Fire" to the CSS for improved theming.
- Updated `Card` and `PostCard` components to utilize the new styling system, enhancing visual consistency and responsiveness.
- Refined layout and styling in various components for better user experience.
2026-04-20 22:09:43 +02:00

178 lines
6.3 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
/**
* TopBanner: Fullwidth-Banner mit Bild (über /cms-images aufgelöst), optionalem Text
* und Titel/Subtitle der aktuellen Page (Markdown). Bevorzugt `<picture>` mit AVIF/WebP-Sources
* und srcset, wenn `responsive` geliefert wird; sonst Fallback auf einzelnes `<img>` (legacy).
*/
import { marked } from "marked";
import type { FullwidthBannerEntry } from "$lib/cms";
marked.setOptions({ gfm: true });
interface ResponsiveBannerImage {
sources: { type: string; srcset: string }[];
fallback: string;
width: number;
height: number;
/** Optionaler LQIP (data-URL) als Unschärfe-Placeholder beim Laden. */
lqip?: string;
}
interface Props {
banner: FullwidthBannerEntry | null;
/** Einfache (legacy) einzelne URL(s) — wenn `responsive` fehlt. */
resolvedImages?: string[];
/** Bevorzugter Pfad: fertig berechneter `<picture>`-Inhalt fürs erste Banner-Bild. */
responsive?: ResponsiveBannerImage | null;
/** `sizes`-Attribut fürs srcset (Default: volle Viewport-Breite). */
sizes?: string;
/** Focal-Point als CSS-`object-position` (z. B. "69.5% 40.38%"). */
focalCss?: string | null;
/** Headline der Page (Markdown). */
headline?: string;
/** Subheadline der Page (Markdown). */
subheadline?: string;
}
let {
banner = null,
resolvedImages = [],
responsive = null,
sizes = "100vw",
focalCss = null,
headline,
subheadline,
}: Props = $props();
const imgStyle = $derived(
focalCss ? `object-position: ${focalCss};` : undefined,
);
/**
* LQIP: Blurhash-Placeholder als `background-image` auf dem Wrapper.
* Kein Blur-Filter nötig — der aus 32×32 hochskalierte JPEG ist intrinsisch weich.
* `background-position` folgt dem Focal, damit der Placeholder identisch zum Image croppt.
*/
const wrapperStyle = $derived.by(() => {
const parts: string[] = [];
if (responsive?.lqip) {
parts.push(`background-image: url("${responsive.lqip}")`);
parts.push("background-size: cover");
parts.push(`background-position: ${focalCss ?? "center"}`);
}
return parts.length > 0 ? parts.join("; ") : undefined;
});
const headlineHtml = $derived(
headline?.trim() ? (marked.parseInline(headline) as string) : "",
);
const subheadlineHtml = $derived(
subheadline?.trim() ? (marked.parseInline(subheadline) as string) : "",
);
const hasResponsive = $derived(
responsive != null && responsive.fallback.trim().length > 0,
);
const hasImage = $derived(
hasResponsive || (resolvedImages.length > 0 && Boolean(resolvedImages[0]?.trim())),
);
const hasText = $derived(banner?.text != null && banner.text !== "");
const hasPageTitle = $derived((headline != null && headline !== "") || (subheadline != null && subheadline !== ""));
const showBanner = $derived(hasImage || hasText || hasPageTitle);
const altText = $derived(banner?.headline ?? headline ?? "");
let imgEl = $state<HTMLImageElement | null>(null);
let imgLoaded = $state(false);
const currentSrc = $derived(responsive?.fallback ?? resolvedImages[0] ?? "");
$effect(() => {
// reset on src change, then probe cache
void currentSrc;
imgLoaded = false;
queueMicrotask(() => {
if (imgEl?.complete && imgEl.naturalWidth > 0) imgLoaded = true;
});
});
</script>
{#if showBanner}
<div class="w-full">
{#if hasImage}
<div
class="w-full overflow-hidden relative flex justify-center items-center shadow-lg bg-zinc-200"
style={wrapperStyle}
>
{#if hasResponsive && responsive}
{#key responsive.fallback}
<picture class="block w-full">
{#each responsive.sources as s (s.type)}
<source type={s.type} srcset={s.srcset} sizes={sizes} />
{/each}
<img
src={responsive.fallback}
alt={altText}
class="w-full object-cover max-h-[400px] lg:max-h-[600px] max-w-[1920px] aspect-square sm:aspect-video lg:aspect-24/9 text-transparent transition-opacity duration-500"
class:opacity-0={!imgLoaded}
class:opacity-100={imgLoaded}
width={responsive.width}
height={responsive.height}
style={imgStyle}
loading="eager"
fetchpriority="high"
decoding="async"
bind:this={imgEl}
onload={() => (imgLoaded = true)}
/>
</picture>
{/key}
{:else}
{#key resolvedImages[0]}
<img
src={resolvedImages[0]}
alt={altText}
class="w-full object-cover max-h-[400px] lg:max-h-[600px] max-w-[1920px] aspect-square sm:aspect-video lg:aspect-24/9 text-transparent transition-opacity duration-500"
class:opacity-0={!imgLoaded}
class:opacity-100={imgLoaded}
width={1920}
height={600}
style={imgStyle}
loading="eager"
fetchpriority="high"
onload={() => (imgLoaded = true)}
/>
{/key}
{/if}
{#if hasPageTitle}
<div class="absolute inset-0 flex items-center justify-start ">
<div class="container-custom mx-auto px-6 relative">
{#if headlineHtml}
<h1 class="bg-red-400/70 inline-block py-1 px-2 -ml-2 backdrop-blur">{@html headlineHtml}</h1>
{/if}
<div></div>
{#if subheadlineHtml}
<h2 class="bg-white/50 inline-block py-1 px-2 -ml-2 backdrop-blur font-light! text-xl!">{@html subheadlineHtml}</h2>
{/if}
</div>
</div>
{/if}
</div>
{:else if hasPageTitle}
<div class="border-b border-zinc-200 container mx-auto px-6 py-4">
{#if headlineHtml}
<h1 class="text-2xl font-bold text-zinc-900">{@html headlineHtml}</h1>
{/if}
{#if subheadlineHtml}
<h2 class="text-lg text-zinc-600 mt-1">{@html subheadlineHtml}</h2>
{/if}
</div>
{/if}
{#if hasText}
<div class="w-full bg-green-700 text-white text-center py-2 px-4">
<div class="container mx-auto">
<div class="text-sm font-medium">{banner!.text}</div>
</div>
</div>
{/if}
</div>
{/if}