feat(ui): deadline banner with gradient blob bg, swap Lora→Aleo
- DeadlineBannerBlock: redesign nach Tailwind-UI-Vorlage (gray-50, blurry blobs, dismiss button) - new .gradient-blob-bg utility (radial-gradient blobs via ::before/::after) - apply blob bg to Header - @theme: register --font-sans (Inter) + --font-serif (Aleo) - replace @fontsource-variable/lora with /aleo for blockquote font - Header: simplify active nav-link styling - QuoteBlock: formatting + drop borderClass Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -37,7 +37,7 @@
|
|||||||
"vitest": "^2.1.9"
|
"vitest": "^2.1.9"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/lora": "^5.2.8",
|
"@fontsource-variable/aleo": "^5.2.7",
|
||||||
"@fontsource-variable/rubik": "^5.2.8",
|
"@fontsource-variable/rubik": "^5.2.8",
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"@iconify/svelte": "^5.2.1",
|
"@iconify/svelte": "^5.2.1",
|
||||||
|
|||||||
+44
-6
@@ -6,7 +6,7 @@
|
|||||||
@import "@fontsource/inter/latin-500.css";
|
@import "@fontsource/inter/latin-500.css";
|
||||||
@import "@fontsource/inter/latin-600.css";
|
@import "@fontsource/inter/latin-600.css";
|
||||||
@import "@fontsource/inter/latin-700.css";
|
@import "@fontsource/inter/latin-700.css";
|
||||||
@import "@fontsource-variable/lora";
|
@import "@fontsource-variable/aleo";
|
||||||
|
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@@ -94,12 +94,16 @@
|
|||||||
|
|
||||||
/* Link (Himmel) – für @apply text-link */
|
/* Link (Himmel) – für @apply text-link */
|
||||||
--color-link: var(--color-himmel-500);
|
--color-link: var(--color-himmel-500);
|
||||||
|
|
||||||
|
/* Fonts – Tailwind-Utilities: font-sans (Inter, default), font-serif (Aleo, Zitate) */
|
||||||
|
--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
|
--font-serif: "Aleo Variable", Aleo, Georgia, "Times New Roman", serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Typography – Inter (primary), Lora (Zitate) */
|
/* Typography – Inter (primary), Aleo (Zitate) */
|
||||||
--font-body: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
|
--font-body: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
--font-secondary: "Lora Variable", Lora, Georgia, "Times New Roman", serif;
|
--font-secondary: "Aleo Variable", Aleo, Georgia, "Times New Roman", serif;
|
||||||
|
|
||||||
/* Semantic (nur Light Theme) */
|
/* Semantic (nur Light Theme) */
|
||||||
--bg-primary: #ffffff;
|
--bg-primary: #ffffff;
|
||||||
@@ -149,7 +153,7 @@ main {
|
|||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
Typography – Type Scale (02-typography.md)
|
Typography – Type Scale (02-typography.md)
|
||||||
Erlaubte Gewichte: 300, 400, 500, 600, 700. Inter primary, Lora nur Zitate.
|
Erlaubte Gewichte: 300, 400, 500, 600, 700. Inter primary, Aleo nur Zitate.
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
@@ -331,6 +335,41 @@ main ol:not(.not-prose) li {
|
|||||||
@apply bg-stein-100 border-stein-400;
|
@apply bg-stein-100 border-stein-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dekorativer Hintergrund mit zwei verschwommenen Verlaufs-Blobs (Wald → Erde).
|
||||||
|
Nutzung: Container mit relativer Positionierung; Blobs sitzen in ::before/::after.
|
||||||
|
Inhalt sollte z-Index >= 0 haben (Standard); Blobs liegen unter Stacking-Kontext. */
|
||||||
|
.gradient-blob-bg {
|
||||||
|
@apply relative isolate overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-blob-bg::before,
|
||||||
|
.gradient-blob-bg::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
z-index: -10;
|
||||||
|
width: 36rem;
|
||||||
|
aspect-ratio: 577 / 310;
|
||||||
|
transform: translate3d(0, -50%, 0);
|
||||||
|
background: radial-gradient(
|
||||||
|
ellipse at center,
|
||||||
|
var(--color-himmel-400) 0%,
|
||||||
|
var(--color-himmel-200) 45%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
opacity: 0.35;
|
||||||
|
filter: blur(40px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-blob-bg::before {
|
||||||
|
left: max(-7rem, calc(50% - 52rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-blob-bg::after {
|
||||||
|
left: max(45rem, calc(50% + 8rem));
|
||||||
|
}
|
||||||
|
|
||||||
/* Globaler Focus-Ring – wald-Akzent für Tastatur-Nutzer (a11y + Polish) */
|
/* Globaler Focus-Ring – wald-Akzent für Tastatur-Nutzer (a11y + Polish) */
|
||||||
*:focus-visible {
|
*:focus-visible {
|
||||||
outline: 2px solid var(--color-wald-500);
|
outline: 2px solid var(--color-wald-500);
|
||||||
@@ -508,14 +547,13 @@ main pre code,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Zitate: Lora (Design System – nur für Zitate) */
|
/* Zitate: Aleo (Design System – nur für Zitate) */
|
||||||
blockquote[data-block-type="quote"],
|
blockquote[data-block-type="quote"],
|
||||||
blockquote[data-block-type="quote"] p,
|
blockquote[data-block-type="quote"] p,
|
||||||
blockquote[data-block-type="quote"] cite {
|
blockquote[data-block-type="quote"] cite {
|
||||||
font-family: var(--font-secondary);
|
font-family: var(--font-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
footer a,
|
footer a,
|
||||||
.content-footer a {
|
.content-footer a {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
<header
|
<header
|
||||||
id="horizontal-navigation"
|
id="horizontal-navigation"
|
||||||
class="sticky top-0 z-20 border-b border-white/30 shadow-2xl backdrop-blur-md bg-wald-50"
|
class="gradient-blob-bg sticky top-0 z-20 border-b border-white/30 shadow-2xl backdrop-blur-md bg-wald-50"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between container-custom py-2">
|
<div class="flex items-center justify-between container-custom py-2">
|
||||||
<a
|
<a
|
||||||
@@ -129,9 +129,7 @@
|
|||||||
{@const active = isActiveLink(link.href, $page.url.pathname)}
|
{@const active = isActiveLink(link.href, $page.url.pathname)}
|
||||||
<a
|
<a
|
||||||
href={link.href}
|
href={link.href}
|
||||||
class="text-[.7rem] py-1 transition-colors hover:text-wald-700 {active
|
class="text-[.7rem] py-1 {active ? 'font-bold ' : ''}"
|
||||||
? 'font-bold text-wald-700 border-b-2 border-wald-600'
|
|
||||||
: 'border-b-2 border-transparent'}"
|
|
||||||
aria-current={active ? "page" : undefined}
|
aria-current={active ? "page" : undefined}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
@@ -242,13 +240,16 @@
|
|||||||
aria-label={t(T.header_nav_aria)}
|
aria-label={t(T.header_nav_aria)}
|
||||||
>
|
>
|
||||||
{#each links as link}
|
{#each links as link}
|
||||||
{@const active = isActiveLink(link.href, $page.url.pathname)}
|
{@const active = isActiveLink(
|
||||||
|
link.href,
|
||||||
|
$page.url.pathname,
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
href={link.href}
|
href={link.href}
|
||||||
class="block py-4 px-3 rounded-xs transition-colors text-base {active
|
class="block py-4 px-3 rounded-xs transition-colors text-base {active
|
||||||
? 'font-bold text-wald-300 bg-wald-700/20 border-l-4 border-wald-400'
|
? 'font-bold text-wald-300 bg-wald-700/20 border-l-4 border-wald-400'
|
||||||
: 'text-stein-100 hover:text-stein-0 hover:bg-stein-0/10 border-l-4 border-transparent'}"
|
: 'text-stein-100 hover:text-stein-0 hover:bg-stein-0/10 border-l-4 border-transparent'}"
|
||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? "page" : undefined}
|
||||||
onclick={closeMenu}
|
onclick={closeMenu}
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
let { block }: { block: DeadlineBannerBlockData } = $props();
|
let { block }: { block: DeadlineBannerBlockData } = $props();
|
||||||
|
|
||||||
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
|
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
|
||||||
const variant = $derived(block.variant ?? "accent");
|
|
||||||
const showCountdown = $derived(block.showCountdown !== false);
|
const showCountdown = $derived(block.showCountdown !== false);
|
||||||
|
|
||||||
|
let dismissed = $state(false);
|
||||||
|
|
||||||
type ResolvedItem = {
|
type ResolvedItem = {
|
||||||
title?: string;
|
title?: string;
|
||||||
terminZeit?: string;
|
terminZeit?: string;
|
||||||
@@ -25,17 +26,22 @@
|
|||||||
(x): x is ResolvedItem =>
|
(x): x is ResolvedItem =>
|
||||||
typeof x === "object" && x !== null && "terminZeit" in x,
|
typeof x === "object" && x !== null && "terminZeit" in x,
|
||||||
)
|
)
|
||||||
.map((x): { x: ResolvedItem; ts: number } => ({ x, ts: Date.parse(x.terminZeit ?? "") }))
|
.map((x): { x: ResolvedItem; ts: number } => ({
|
||||||
|
x,
|
||||||
|
ts: Date.parse(x.terminZeit ?? ""),
|
||||||
|
}))
|
||||||
.filter(({ ts }) => !isNaN(ts) && ts >= now)
|
.filter(({ ts }) => !isNaN(ts) && ts >= now)
|
||||||
.sort((a, b) => a.ts - b.ts);
|
.sort((a, b) => a.ts - b.ts);
|
||||||
return items[0]?.x ?? null;
|
return items[0]?.x ?? null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = $derived(
|
const text = $derived(
|
||||||
block.mode === "auto" ? autoItem?.title ?? "" : block.text ?? "",
|
block.mode === "auto" ? (autoItem?.title ?? "") : (block.text ?? ""),
|
||||||
);
|
);
|
||||||
const dateIso = $derived(
|
const dateIso = $derived(
|
||||||
block.mode === "auto" ? autoItem?.terminZeit ?? "" : block.date ?? "",
|
block.mode === "auto"
|
||||||
|
? (autoItem?.terminZeit ?? "")
|
||||||
|
: (block.date ?? ""),
|
||||||
);
|
);
|
||||||
const linkValue = $derived(
|
const linkValue = $derived(
|
||||||
block.mode === "auto" ? autoItem?.link : block.link,
|
block.mode === "auto" ? autoItem?.link : block.link,
|
||||||
@@ -78,72 +84,64 @@
|
|||||||
const newTab = $derived(
|
const newTab = $derived(
|
||||||
linkValue && typeof linkValue === "object" ? !!linkValue.newTab : false,
|
linkValue && typeof linkValue === "object" ? !!linkValue.newTab : false,
|
||||||
);
|
);
|
||||||
|
const ctaLabel = $derived(
|
||||||
|
linkValue && typeof linkValue === "object" && linkValue.linkName
|
||||||
|
? linkValue.linkName
|
||||||
|
: "Mehr erfahren",
|
||||||
|
);
|
||||||
|
|
||||||
const variantClasses = $derived(
|
const sentence = $derived.by(() => {
|
||||||
variant === "urgent"
|
const parts: string[] = [];
|
||||||
? "bg-erde-100 text-erde-900"
|
if (text) parts.push(text);
|
||||||
: variant === "info"
|
if (dateStr) parts.push(`am ${dateStr}`);
|
||||||
? "bg-himmel-100 text-himmel-900"
|
return parts.join(" ");
|
||||||
: "bg-wald-500 text-wald-50",
|
});
|
||||||
);
|
|
||||||
const iconName = $derived(
|
const hasContent = $derived(!!(sentence || countdownStr));
|
||||||
variant === "urgent" ? "mdi:alert-circle" : "mdi:calendar-clock",
|
|
||||||
);
|
|
||||||
const hasContent = $derived(!!(text || dateStr || countdownStr));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if hasContent}
|
{#if hasContent && !dismissed}
|
||||||
<div
|
<div
|
||||||
class="deadline-banner {layoutClasses}"
|
class="deadline-banner {layoutClasses}"
|
||||||
data-block-type="deadline_banner"
|
data-block-type="deadline_banner"
|
||||||
data-block-slug={block._slug}
|
data-block-slug={block._slug}
|
||||||
>
|
>
|
||||||
{#snippet inner()}
|
<div
|
||||||
<div class="container-custom flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-1 px-4 py-2 text-xs sm:text-sm">
|
class="gradient-blob-bg flex items-center gap-x-6 bg-gray-50 px-6 py-2.5 sm:px-3.5"
|
||||||
{#if dateStr}
|
|
||||||
<span class="inline-flex items-center rounded-xs bg-white/50 text-wald-900 px-2 py-0.5 text-sm sm:text-base font-bold whitespace-nowrap shrink-0">
|
|
||||||
<span class="inline-flex items-center gap-1.5">
|
|
||||||
<Icon icon={iconName} class="size-4 shrink-0" aria-hidden="true" />
|
|
||||||
{dateStr}
|
|
||||||
</span>
|
|
||||||
{#if countdownStr}
|
|
||||||
<span
|
|
||||||
class="ml-0 inline-flex min-w-0 max-w-0 scale-[0.96] items-center overflow-hidden whitespace-nowrap rounded-xs border border-transparent py-0.5 text-[0.6rem] font-semibold uppercase tracking-wide opacity-0 transition-[max-width,margin,opacity,transform,padding,border-color] duration-200 ease-out group-hover:ml-2 group-hover:max-w-48 group-hover:border-wald-900 group-hover:px-2 group-hover:opacity-100 group-hover:scale-100 group-focus-within:ml-2 group-focus-within:max-w-48 group-focus-within:border-wald-900 group-focus-within:px-2 group-focus-within:opacity-100 group-focus-within:scale-100"
|
|
||||||
>
|
>
|
||||||
{countdownStr}
|
<span class="hidden flex-1 sm:block" aria-hidden="true"></span>
|
||||||
</span>
|
<p class="my-0! text-sm/6 text-gray-900">
|
||||||
{/if}
|
{sentence}{#if countdownStr}{sentence ? " · " : ""}<span
|
||||||
</span>
|
class="font-semibold">{countdownStr}</span
|
||||||
{:else}
|
>{/if}{#if href}
|
||||||
<span class="inline-flex items-center gap-0 shrink-0">
|
{" "}<a
|
||||||
<Icon icon={iconName} class="size-4 shrink-0" aria-hidden="true" />
|
{href}
|
||||||
{#if countdownStr}
|
|
||||||
<span
|
|
||||||
class="ml-0 inline-flex min-w-0 max-w-0 shrink-0 scale-[0.96] items-center overflow-hidden whitespace-nowrap rounded-full px-0 py-0.5 text-xs font-semibold uppercase tracking-wide opacity-0 transition-[max-width,margin,opacity,transform,padding] duration-200 ease-out group-hover:ml-2 group-hover:max-w-48 group-hover:px-2 group-hover:opacity-100 group-hover:scale-100 group-focus-within:ml-2 group-focus-within:max-w-48 group-focus-within:px-2 group-focus-within:opacity-100 group-focus-within:scale-100"
|
|
||||||
>
|
|
||||||
{countdownStr}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
{#if text}
|
|
||||||
<span class="min-w-0 wrap-break-word line-clamp-2 sm:line-clamp-1 font-semibold text-shadow-sm">{text}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
{#if href}
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
target={newTab ? "_blank" : undefined}
|
target={newTab ? "_blank" : undefined}
|
||||||
rel={newTab ? "noopener noreferrer" : undefined}
|
rel={newTab ? "noopener noreferrer" : undefined}
|
||||||
class="group block no-underline text-inherit hover:no-underline transition-all hover:brightness-95 {variantClasses}"
|
class="font-semibold whitespace-nowrap text-gray-900 hover:text-gray-700"
|
||||||
>
|
>
|
||||||
{@render inner()}
|
{ctaLabel} <Icon
|
||||||
|
icon="mdi:arrow-right"
|
||||||
|
class="inline-block size-4 align-[-0.15em]"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{/if}
|
||||||
<div class="group {variantClasses}">
|
</p>
|
||||||
{@render inner()}
|
<div class="flex flex-1 justify-end">
|
||||||
</div>
|
<button
|
||||||
{/if}
|
type="button"
|
||||||
|
onclick={() => (dismissed = true)}
|
||||||
|
class="-m-3 p-3 focus-visible:-outline-offset-4"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Schließen</span>
|
||||||
|
<Icon
|
||||||
|
icon="mdi:close"
|
||||||
|
class="size-5 text-gray-900"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -6,21 +6,30 @@
|
|||||||
block,
|
block,
|
||||||
bare = false,
|
bare = false,
|
||||||
extraClass = "",
|
extraClass = "",
|
||||||
}: { block: QuoteBlockData; bare?: boolean; extraClass?: string } = $props();
|
}: {
|
||||||
|
block: QuoteBlockData;
|
||||||
|
bare?: boolean;
|
||||||
|
extraClass?: string;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
|
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
|
||||||
const isCenter = $derived(block.variant === "center");
|
const isCenter = $derived(block.variant === "center");
|
||||||
const alignClass = $derived(
|
const alignClass = $derived(
|
||||||
isCenter ? "text-center" : block.variant === "right" ? "text-right" : "text-left",
|
isCenter
|
||||||
|
? "text-center"
|
||||||
|
: block.variant === "right"
|
||||||
|
? "text-right"
|
||||||
|
: "text-left",
|
||||||
);
|
);
|
||||||
const borderClass = $derived(isCenter ? "" : "border-l-4 border-wald-700 pl-4");
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet quoteMarkup()}
|
{#snippet quoteMarkup()}
|
||||||
<blockquote class="{borderClass} {alignClass} {extraClass}">
|
<blockquote class="{alignClass} {extraClass}">
|
||||||
<p class="text-lg text-stein-700 mb-2! ">"{block.quote ?? ""}"</p>
|
<p class="text-lg mb-2! font-serif">
|
||||||
|
{block.quote ?? ""}
|
||||||
|
</p>
|
||||||
{#if block.author}
|
{#if block.author}
|
||||||
<cite class="mt-1 block not-italic text-xs text-stein-400">{block.author}</cite>
|
<cite class="mt-1 block not-italic text-xs">{block.author}</cite>
|
||||||
{/if}
|
{/if}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -28,7 +37,11 @@
|
|||||||
{#if bare}
|
{#if bare}
|
||||||
{@render quoteMarkup()}
|
{@render quoteMarkup()}
|
||||||
{:else}
|
{:else}
|
||||||
<div class={layoutClasses} data-block-type="quote" data-block-slug={block._slug}>
|
<div
|
||||||
|
class={layoutClasses}
|
||||||
|
data-block-type="quote"
|
||||||
|
data-block-slug={block._slug}
|
||||||
|
>
|
||||||
{@render quoteMarkup()}
|
{@render quoteMarkup()}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user