feat(frist): Deadline-Banner im AnnouncementBanner-Look (rot), nur ein Balken
Deploy / verify (push) Successful in 1m11s
Deploy / deploy (push) Successful in 1m29s

- Look wie AnnouncementBanner: kompakte zentrierte Leiste, weiße Schrift, 0.7rem,
  variant-Farbe (urgent=rot), flex-wrap → fließt/stackt sauber mobil
- Countdown als dunkles Badge (bg-black/20)
- Layout: aktive Frist verdrängt NEU-Banner → nie 2 Balken über der Navi

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-07-01 15:32:40 +02:00
parent f7529d3b47
commit 7a490b1ae1
2 changed files with 44 additions and 54 deletions
@@ -1,30 +1,27 @@
<script lang="ts">
import { getBlockLayoutClasses } from "$lib/block-layout";
import type { DeadlineBannerBlockData } from "$lib/block-types";
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
let { block }: { block: DeadlineBannerBlockData } = $props();
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
const showCountdown = $derived(block.showCountdown !== false);
// Farbgebung nach variant — "urgent" fällt auf (rot), damit die Frist wirkt.
const styles = $derived.by(() => {
// Look wie AnnouncementBanner (kompakte, zentrierte Leiste) — nur farbig.
// "urgent" = rot, damit die Frist wirkt.
const barClass = $derived.by(() => {
switch (block.variant) {
case "urgent":
return { bar: "bg-fire-600", text: "text-white", link: "text-white hover:text-white/80" };
case "accent":
return { bar: "bg-himmel-100", text: "text-himmel-900", link: "text-himmel-900 hover:text-himmel-700" };
return "bg-himmel-600";
case "info":
return { bar: "bg-wald-50", text: "text-wald-900", link: "text-wald-900 hover:text-wald-700" };
return "bg-wald-600";
case "urgent":
return "bg-fire-600";
default:
return { bar: "bg-gray-50", text: "text-gray-900", link: "text-gray-900 hover:text-gray-700" };
return "bg-stein-700";
}
});
let dismissed = $state(false);
type ResolvedItem = {
title?: string;
terminZeit?: string;
@@ -116,56 +113,37 @@
const sentence = $derived.by(() => {
const parts: string[] = [];
if (text) parts.push(text);
if (dateStr) parts.push(`am ${dateStr}`);
// Datum nur im Auto-Modus anhängen — im Manual-Text steht es meist schon.
if (dateStr && block.mode === "auto") parts.push(`am ${dateStr}`);
return parts.join(" ");
});
const hasContent = $derived(!!(sentence || countdownStr));
</script>
{#if hasContent && !dismissed}
{#if hasContent}
<div
class="deadline-banner {layoutClasses}"
class="deadline-banner {layoutClasses} w-full {barClass} text-white print:hidden"
data-block="DeadlineBanner" data-block-type="deadline_banner"
data-block-slug={block._slug}
role="note"
style="font-size: 0.7rem;"
>
<div
class="gradient-blob-bg {styles.bar} py-2.5"
>
<div class="container-custom flex items-center gap-x-6">
<p class="my-0! flex-1 text-sm/6 {styles.text}">
{sentence}{#if countdownStr}{sentence ? " · " : ""}<span
class="font-semibold">{countdownStr}</span
>{/if}{#if href}
{" "}<a
{href}
target={newTab ? "_blank" : undefined}
rel={newTab ? "noopener noreferrer" : undefined}
class="font-semibold whitespace-nowrap {styles.link}"
>
{ctaLabel}&nbsp;<Icon
icon="lucide:arrow-right"
class="inline-block size-4 align-[-0.15em]"
aria-hidden="true"
/>
</a>
{/if}
</p>
<div class="shrink-0">
<button
type="button"
onclick={() => (dismissed = true)}
class="-m-3 p-3 focus-visible:-outline-offset-4"
>
<span class="sr-only">Schließen</span>
<Icon
icon="lucide:x"
class="size-5 {styles.text}"
aria-hidden="true"
/>
</button>
</div>
</div>
<div class="container-custom flex flex-wrap items-center justify-center gap-x-3 gap-y-1.5 py-2 text-center">
{#if sentence}
<span>{sentence}</span>
{/if}
{#if countdownStr}
<span class="whitespace-nowrap rounded-full bg-black/20 px-2 py-0.5 font-bold">{countdownStr}</span>
{/if}
{#if href}
<a
{href}
target={newTab ? "_blank" : undefined}
rel={newTab ? "noopener noreferrer" : undefined}
class="whitespace-nowrap font-semibold underline underline-offset-2 hover:no-underline"
>{ctaLabel}</a>
{/if}
</div>
</div>
{/if}
+15 -3
View File
@@ -26,6 +26,19 @@
*/
const isEmbed = $derived(($page.route.id ?? '').startsWith('/(embed)'));
// Nur EIN Balken über der Navi: aktive Frist hat Vorrang, sonst NEU-Banner.
const deadlineActive = $derived.by(() => {
const b = data.deadlineBanner as { mode?: string; date?: string; items?: unknown[] } | null;
if (!b) return false;
if (b.mode === 'auto') return (b.items ?? []).length > 0;
if (!b.date) return false;
const d = new Date(b.date);
if (Number.isNaN(d.getTime())) return false;
const today = new Date(); today.setHours(0, 0, 0, 0);
const t = new Date(d); t.setHours(0, 0, 0, 0);
return t.getTime() >= today.getTime();
});
// Live-preview reload bridge: when the page was loaded with
// `?preview_draft=...`, accept `rustycms:reload` postMessage events from the
// admin iframe parent and re-run the SvelteKit `load()` so the iframe
@@ -329,10 +342,9 @@
class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:bg-white focus:px-3 focus:py-2 focus:shadow focus:outline focus:outline-2 focus:outline-wald-600 focus:rounded-xs"
>Zum Inhalt springen</a>
<div class="flex min-h-screen flex-col text-stein-900">
{#if data.deadlineBanner}
{#if deadlineActive && data.deadlineBanner}
<DeadlineBannerBlock block={data.deadlineBanner} />
{/if}
{#if data.announcementBanner}
{:else if data.announcementBanner}
<AnnouncementBanner banner={data.announcementBanner} />
{/if}
{#if pageData?.preview || pageData?.previewDraft}