b7093b703a
- Palette: gray/zinc/slate/neutral → stein, red → fire, amber → erde, sky → himmel, green → wald - Footer: replaced raw rgb with stein tokens - Radius: unified to rounded-xs across cards, buttons, inputs, pagination - Buttons: .btn-primary/.btn-secondary now font-medium; added secondary - Card hover: -translate-y-0.5 + shadow + wald-300 border - Header active link: wald border-bottom + wald-700 text - Mobile nav: bigger touch targets + wald active accent - Pagination: prev/next text labels + wald-500 active state with separated shape/color classes to avoid Tailwind conflicts - Lead paragraph: scoped .markdown > p:first-child for top-level MarkdownBlock only via [data-block-type="markdown"] - Section dividers: subtle wald-200 gradient between content rows - Global focus-visible ring (wald-500) - Inline hex → palette tokens (Badge amber, Tag custom-color contrast) - Font weights snapped to design system (300/400/500/600/700) - transition-all → transition-colors where only color changes - Removed em-dashes from user-visible templates Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
179 lines
7.5 KiB
Svelte
179 lines
7.5 KiB
Svelte
<script lang="ts">
|
|
import MarkdownBlock from "./blocks/MarkdownBlock.svelte";
|
|
import HeadlineBlock from "./blocks/HeadlineBlock.svelte";
|
|
import HtmlBlock from "./blocks/HtmlBlock.svelte";
|
|
import IframeBlock from "./blocks/IframeBlock.svelte";
|
|
import ImageBlock from "./blocks/ImageBlock.svelte";
|
|
import ImageGalleryBlock from "./blocks/ImageGalleryBlock.svelte";
|
|
import FilesBlock from "./blocks/FilesBlock.svelte";
|
|
import YoutubeVideoBlock from "./blocks/YoutubeVideoBlock.svelte";
|
|
import YoutubeVideoGalleryBlock from "./blocks/YoutubeVideoGalleryBlock.svelte";
|
|
import QuoteBlock from "./blocks/QuoteBlock.svelte";
|
|
import QuoteCarouselBlock from "./blocks/QuoteCarouselBlock.svelte";
|
|
import LinkListBlock from "./blocks/LinkListBlock.svelte";
|
|
import PostOverviewBlock from "./blocks/PostOverviewBlock.svelte";
|
|
import SearchableTextBlock from "./blocks/SearchableTextBlock.svelte";
|
|
import CalendarBlock from "./blocks/CalendarBlock.svelte";
|
|
import OrganisationsBlock from "./blocks/OrganisationsBlock.svelte";
|
|
import OpnFormBlock from "./blocks/OpnFormBlock.svelte";
|
|
import DeadlineBannerBlock from "./blocks/DeadlineBannerBlock.svelte";
|
|
import StrommixBlock from "./blocks/StrommixBlock.svelte";
|
|
import { isBlockLayoutBreakout, getSpaceBottomClass } from "$lib/block-layout";
|
|
import type { BlockLayout } from "$lib/block-layout";
|
|
import type { Translations } from "$lib/translations";
|
|
import type {
|
|
RowContentLayout,
|
|
ResolvedBlock,
|
|
MarkdownBlockData,
|
|
HeadlineBlockData,
|
|
HtmlBlockData,
|
|
IframeBlockData,
|
|
ImageBlockData,
|
|
ImageGalleryBlockData,
|
|
FilesBlockData,
|
|
YoutubeVideoBlockData,
|
|
YoutubeVideoGalleryBlockData,
|
|
QuoteBlockData,
|
|
QuoteCarouselBlockData,
|
|
LinkListBlockData,
|
|
PostOverviewBlockData,
|
|
SearchableTextBlockData,
|
|
CalendarBlockData,
|
|
OrganisationsBlockData,
|
|
OpnFormBlockData,
|
|
DeadlineBannerBlockData,
|
|
LiveStrommixBlockData,
|
|
} from "$lib/block-types";
|
|
|
|
let { layout, class: className = "", translations = {} }: { layout: RowContentLayout; class?: string; translations?: Translations | null } = $props();
|
|
|
|
function isResolvedBlock(item: unknown): item is ResolvedBlock {
|
|
return typeof item === "object" && item !== null;
|
|
}
|
|
|
|
function blockType(item: ResolvedBlock): string {
|
|
return (item._type as string) ?? "unknown";
|
|
}
|
|
|
|
function justifyToClass(j?: string): string {
|
|
switch (j) {
|
|
case "end": return "justify-end";
|
|
case "center": return "justify-center";
|
|
case "between": return "justify-between";
|
|
case "around": return "justify-around";
|
|
case "evenly": return "justify-evenly";
|
|
default: return "justify-start";
|
|
}
|
|
}
|
|
|
|
function alignToClass(a?: string): string {
|
|
switch (a) {
|
|
case "end": return "items-end";
|
|
case "center": return "items-center";
|
|
case "baseline": return "items-baseline";
|
|
case "stretch": return "items-stretch";
|
|
default: return "items-start";
|
|
}
|
|
}
|
|
|
|
const rows = $derived([
|
|
{
|
|
content: layout.row1Content ?? [],
|
|
justify: justifyToClass(layout.row1JustifyContent),
|
|
align: alignToClass(layout.row1AlignItems),
|
|
},
|
|
{
|
|
content: layout.row2Content ?? [],
|
|
justify: justifyToClass(layout.row2JustifyContent),
|
|
align: alignToClass(layout.row2AlignItems),
|
|
},
|
|
{
|
|
content: layout.row3Content ?? [],
|
|
justify: justifyToClass(layout.row3JustifyContent),
|
|
align: alignToClass(layout.row3AlignItems),
|
|
},
|
|
]);
|
|
</script>
|
|
|
|
<div class="content mb-6 {className}">
|
|
{#each rows as row}
|
|
{#if row.content.length > 0}
|
|
<div
|
|
class="content-row grid grid-cols-12 gap-4 {row.justify} {row.align}"
|
|
role="region"
|
|
data-cf-type="grid-row"
|
|
>
|
|
{#each row.content as item}
|
|
{#if isResolvedBlock(item)}
|
|
{#snippet blockContent()}
|
|
{#if blockType(item) === "markdown"}
|
|
<MarkdownBlock block={item as MarkdownBlockData} />
|
|
{:else if blockType(item) === "headline"}
|
|
<HeadlineBlock block={item as HeadlineBlockData} />
|
|
{:else if blockType(item) === "html"}
|
|
<HtmlBlock block={item as HtmlBlockData} />
|
|
{:else if blockType(item) === "iframe"}
|
|
<IframeBlock block={item as IframeBlockData} translations={translations} />
|
|
{:else if blockType(item) === "image"}
|
|
<ImageBlock block={item as ImageBlockData} />
|
|
{:else if blockType(item) === "image_gallery"}
|
|
<ImageGalleryBlock block={item as ImageGalleryBlockData} />
|
|
{:else if blockType(item) === "files"}
|
|
<FilesBlock block={item as FilesBlockData} />
|
|
{:else if blockType(item) === "youtube_video"}
|
|
<YoutubeVideoBlock block={item as YoutubeVideoBlockData} />
|
|
{:else if blockType(item) === "youtube_video_gallery"}
|
|
<YoutubeVideoGalleryBlock block={item as YoutubeVideoGalleryBlockData} />
|
|
{:else if blockType(item) === "quote"}
|
|
<QuoteBlock block={item as QuoteBlockData} />
|
|
{:else if blockType(item) === "quote_carousel"}
|
|
<QuoteCarouselBlock block={item as QuoteCarouselBlockData} />
|
|
{:else if blockType(item) === "link_list"}
|
|
<LinkListBlock block={item as LinkListBlockData} />
|
|
{:else if blockType(item) === "post_overview"}
|
|
<PostOverviewBlock block={item as PostOverviewBlockData} />
|
|
{:else if blockType(item) === "searchable_text"}
|
|
<SearchableTextBlock block={item as SearchableTextBlockData} translations={translations} />
|
|
{:else if blockType(item) === "calendar"}
|
|
<CalendarBlock block={item as CalendarBlockData} translations={translations} />
|
|
{:else if blockType(item) === "organisations"}
|
|
<OrganisationsBlock block={item as OrganisationsBlockData} />
|
|
{:else if blockType(item) === "opnform"}
|
|
<OpnFormBlock block={item as OpnFormBlockData} />
|
|
{:else if blockType(item) === "deadline_banner"}
|
|
<DeadlineBannerBlock block={item as DeadlineBannerBlockData} />
|
|
{:else if blockType(item) === "live_strommix"}
|
|
<StrommixBlock block={item as LiveStrommixBlockData} />
|
|
{:else}
|
|
<div class="col-span-12 rounded border border-erde-200 bg-erde-50 px-3 py-2 text-sm text-erde-800">
|
|
Unbekannter Block: <code>{blockType(item)}</code>
|
|
</div>
|
|
{/if}
|
|
{/snippet}
|
|
{#if isBlockLayoutBreakout((item as { layout?: BlockLayout }).layout)}
|
|
{@const rowBlockType = blockType(item)}
|
|
<div class="col-span-12">
|
|
<div
|
|
class="component-breakout--{rowBlockType} {getSpaceBottomClass((item as { layout?: BlockLayout }).layout)} w-screen relative left-1/2 -translate-x-1/2 max-w-none {rowBlockType === 'quote'
|
|
? 'bg-wald-50'
|
|
: '[background:var(--color-container-breakout)]'}"
|
|
>
|
|
{#if rowBlockType === 'deadline_banner'}
|
|
{@render blockContent()}
|
|
{:else}
|
|
<div class="container-custom py-8">
|
|
{@render blockContent()}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
{@render blockContent()}
|
|
{/if}
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
</div>
|