Files
windwiderstand/src/lib/block-types.ts
T
Peter Meier 5a1a49a97b
Deploy / verify (push) Successful in 1m47s
Deploy / deploy (push) Successful in 1m32s
feat(calendar): section_header (kicker/number/action/title) im Schema + wired
calendar-Schema extends section_header + title-Feld. CalendarBlockData +
kicker/number/action. CalendarCompact reicht kicker/number an SectionHeader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:20:33 +02:00

633 lines
21 KiB
TypeScript
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.
/**
* Gemeinsame Typen für Content-Blöcke (Rows, Markdown, …).
* Wird von SvelteKit-Seiten und Svelte-Komponenten genutzt.
* Kalender-Typen basieren auf der OpenAPI-Spec (cms-api.generated).
*/
import type { BlockLayout } from "./block-layout";
import type { components } from "./cms-api.generated";
/** Aufgelöster Block vom CMS (mit _type, _slug + typ-spezifische Felder). */
export type ResolvedBlock = {
_type?: string;
_slug?: string;
[key: string]: unknown;
};
/** Gemeinsames Row-Layout (page, post, footer erben content_layout). */
export interface RowContentLayout {
row1Content?: unknown[];
row1JustifyContent?: string;
row1AlignItems?: string;
row2Content?: unknown[];
row2JustifyContent?: string;
row2AlignItems?: string;
row3Content?: unknown[];
row3JustifyContent?: string;
row3AlignItems?: string;
}
/** Resolved Markdown-Block vom CMS (_type: "markdown"). resolvedContent = HTML mit transformierten Bildern (von resolveContentImages). */
export interface MarkdownBlockData {
_type?: "markdown";
_slug?: string;
name?: string;
content?: string;
/** Gesetzt von resolveContentImages: HTML (Bilder transformiert, in Links eingewickelt). */
resolvedContent?: string;
alignment?: "left" | "center" | "right";
layout?: BlockLayout;
}
/** Headline (_type: "headline"). */
export interface HeadlineBlockData {
_type?: "headline";
_slug?: string;
tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
text?: string;
/** SectionHeader: Label über dem Titel (uppercase). Gesetzt → Headline rendert als Sektionskopf. */
kicker?: string;
/** SectionHeader: Index vor dem Kicker, z.B. "03". */
number?: string;
/** SectionHeader: CTA-Link (aufgelöst: url, linkName). */
action?: string | { url?: string; linkName?: string; newTab?: boolean };
align?: "left" | "center" | "right";
layout?: BlockLayout;
}
/** HTML (_type: "html"). */
export interface HtmlBlockData {
_type?: "html";
_slug?: string;
html?: string;
layout?: BlockLayout;
}
/** Iframe (_type: "iframe"). */
export interface IframeBlockData {
_type?: "iframe";
_slug?: string;
iframe?: string;
content?: string;
layout?: BlockLayout;
}
/** Image (_type: "image"). image oder img: Slug, URL-String oder aufgelöstes Objekt (src/description oder file.url). */
export interface ImageBlockData {
_type?: "image";
_slug?: string;
image?:
| string
| {
_type?: "img";
_slug?: string;
src?: string;
description?: string;
file?: { url?: string };
title?: string;
};
/** Alternative zu image (manche CMS-Responses liefern img). */
img?:
| string
| {
_type?: "img";
_slug?: string;
src?: string;
description?: string;
file?: { url?: string };
title?: string;
};
/** Nach resolveContentImages: Proxy-URL zum transformierten Bild. */
resolvedImageSrc?: string;
caption?: string;
aspectRatio?: number;
layout?: BlockLayout;
}
/** Eintrag in einer image_gallery (img mit src/description). */
export interface ImageGalleryImage {
_type?: "img";
_slug?: string;
src?: string;
description?: string;
/** Optionaler Hover-Titel (image-Type: alt/caption/title). */
alt?: string;
caption?: string;
title?: string;
file?: { url?: string };
/** Nach resolveContentImages: Proxy-URL zum transformierten Bild (16:9). */
resolvedSrc?: string;
}
/** Bildergalerie (_type: "image_gallery"). images: Array von img-Objekten (src, description). */
export interface ImageGalleryBlockData {
_type?: "image_gallery";
_slug?: string;
/** Optionaler Einleitungstext (Markdown). */
description?: string;
/** Optional: oberhalb des Galerie-Modals als Header. */
title?: string;
/** SectionHeader: Label über dem Titel (uppercase). */
kicker?: string;
/** SectionHeader: Index vor dem Kicker, z.B. "03". */
number?: string;
/** SectionHeader: CTA-Link (aufgelöst: url, linkName). */
action?: string | { url?: string; linkName?: string; newTab?: boolean };
/** "standard" = Carousel (Default), "grid" = Thumbnail-Grid mit Modal. */
variant?: "standard" | "grid";
images?: ImageGalleryImage[];
layout?: BlockLayout;
}
/** Einzelne Datei in einem files-Block (CMS file-Field). */
export interface FilesBlockItem {
_type?: "file";
src?: string;
title?: string;
description?: string;
/** Aus Sidecar gemerged (size in Bytes). */
size?: number;
mime?: string;
filename?: string;
}
/** Datei-Block (_type: "files"). items = Array von file-Feldern. */
export interface FilesBlockData {
_type?: "files";
_slug?: string;
headline?: string;
/** SectionHeader: Label über dem Titel (uppercase). */
kicker?: string;
/** SectionHeader: Index vor dem Kicker, z.B. "03". */
number?: string;
/** SectionHeader: CTA-Link (aufgelöst: url, linkName). */
action?: string | { url?: string; linkName?: string; newTab?: boolean };
description?: string;
items?: Array<string | FilesBlockItem>;
forceDownload?: boolean;
layout?: BlockLayout;
}
/** YouTube-Video (_type: "youtube_video"). */
export interface YoutubeVideoBlockData {
_type?: "youtube_video";
_slug?: string;
youtubeId?: string;
/**
* Playlist-ID (PLxxxxx). Wenn ohne youtubeId gesetzt, lädt die Playlist
* von vorne; mit youtubeId startet das Video in der Playlist.
* Akzeptiert auch direkte URLs wie
* https://www.youtube.com/watch?v=...&list=PL... (wird parsed).
*/
playlistId?: string;
title?: string;
description?: string;
params?: string;
layout?: BlockLayout;
}
/** Galerie aus YouTube-Video-Referenzen (_type: "youtube_video_gallery"). */
export interface YoutubeVideoGalleryBlockData {
_type?: "youtube_video_gallery";
_slug?: string;
title?: string;
subtitle?: string;
/** SectionHeader: Label über dem Titel (uppercase). */
kicker?: string;
/** SectionHeader: Index vor dem Kicker, z.B. "03". */
number?: string;
/** SectionHeader: CTA-Link (aufgelöst: url, linkName). */
action?: string | { url?: string; linkName?: string; newTab?: boolean };
description?: string;
variant?: "carousel" | "grid";
videos?: YoutubeVideoBlockData[];
layout?: BlockLayout;
}
/** Zitat (_type: "quote"). */
export interface QuoteBlockData {
_type?: "quote";
_slug?: string;
quote?: string;
author?: string;
variant?: "left" | "right" | "center";
layout?: BlockLayout;
}
/** Zitat-Karussell (_type: "quote_carousel"). Rotiert durch aufgelöste quote-Einträge. */
export interface QuoteCarouselBlockData {
_type?: "quote_carousel";
_slug?: string;
headline?: string;
quotes?: Array<
| string
| {
_slug?: string;
quote?: string;
author?: string;
variant?: "left" | "right" | "center";
}
>;
autoRotate?: boolean;
intervalSeconds?: number;
showArrows?: boolean;
layout?: BlockLayout;
}
/** Link-Liste (_type: "link_list"). links können Slugs oder aufgelöste link-Objekte sein. */
export interface LinkListBlockData {
_type?: "link_list";
_slug?: string;
headline?: string;
links?: Array<string | { url?: string; linkName?: string; newTab?: boolean }>;
/** Spaltenlayout: "auto" = mehrspaltig bei >4 Links, "single" = immer untereinander. */
columns?: "auto" | "single";
layout?: BlockLayout;
}
/** Post-Übersicht (_type: "post_overview"). Nach resolvePostOverviewBlocks: postsResolved gesetzt. */
export interface PostOverviewBlockData {
_type?: "post_overview";
_slug?: string;
headline?: string;
/** SectionHeader: kleines Label über dem Titel (uppercase). */
kicker?: string;
/** SectionHeader: optionaler Index vor dem Kicker, z.B. "03". */
number?: string;
/** SectionHeader: CTA-Link (aufgelöst aus link-Collection: url, linkName). */
action?: string | { url?: string; linkName?: string; newTab?: boolean };
/** Einleitungstext (Markdown). */
text?: string;
/** true = alle Posts laden; false = posts (Slugs/Objekte) aus dem Block nutzen. */
allPosts?: boolean;
/** Nur wenn allPosts false: feste Post-Liste (Slugs oder aufgelöste Einträge). */
posts?: unknown[];
/** Tag-Slugs, nach denen gefiltert wird (nur bei allPosts true). */
filterByTag?: string[];
/** Tag-Slugs, die ausgeschlossen werden (optional, nach filterByTag angewendet). */
excludeTag?: string[];
/** Max. Anzahl Einträge. */
numberItems?: number;
/** "list" | "cards" | "carousel" | "featured" (1. Post groß links, Rest kompakt rechts). */
design?: "list" | "cards" | "carousel" | "featured";
layout?: BlockLayout;
/** Nach resolvePostOverviewBlocks: geladene/gefilterte Posts (PostEntry[]). */
postsResolved?: unknown[];
}
/** Deadline-Banner (_type: "deadline_banner"). Schmaler Balken unter Hero. */
export interface DeadlineBannerBlockData {
_type?: "deadline_banner";
_slug?: string;
mode?: "manual" | "auto";
label?: string;
text?: string;
date?: string;
items?: Array<
| string
| {
_slug?: string;
title?: string;
terminZeit?: string;
description?: string;
link?: string | { url?: string; linkName?: string; newTab?: boolean; _slug?: string };
}
>;
link?: string | { url?: string; linkName?: string; newTab?: boolean; _slug?: string };
variant?: "accent" | "urgent" | "info";
showCountdown?: boolean;
layout?: BlockLayout;
}
/** Tag-Referenz (API liefert oft { _slug, _type, name }). */
export type SearchableTextTagRef = string | { _slug?: string; name?: string };
/** Ein Text-Fragment (aus text_fragment), für SearchableTextBlock. */
export interface SearchableTextFragment {
_slug?: string;
title?: string;
text?: string;
tags?: SearchableTextTagRef[];
}
/** Durchsuchbarer Text (_type: "searchable_text"). textFragments können Slugs oder aufgelöste Objekte sein. */
export interface SearchableTextBlockData {
_type?: "searchable_text";
_slug?: string;
/** Titel der Suchkomponente. */
title?: string;
/** SectionHeader: Label über dem Titel (uppercase). */
kicker?: string;
/** SectionHeader: Index vor dem Kicker, z.B. "03". */
number?: string;
/** SectionHeader: CTA-Link (aufgelöst: url, linkName). */
action?: string | { url?: string; linkName?: string; newTab?: boolean };
/** Einleitung (Markdown). */
description?: string;
/** Slugs oder aufgelöste text_fragment-Einträge. */
textFragments?: (string | SearchableTextFragment)[];
layout?: BlockLayout;
}
/** OpnForm embed (_type: "opnform"). */
export interface OpnFormBlockData {
_type?: "opnform";
_slug?: string;
id?: string;
formId?: string;
baseUrl?: string;
iframeTitle?: string;
layout?: BlockLayout;
}
/** Einzelne Organisation (aufgelöst aus organisation-Collection). */
export interface OrganisationEntry {
_type?: "organisation";
_slug?: string;
name?: string;
logo?: string | { src?: string; description?: string };
/** Nach resolveContentImages: Proxy-URL zum Logo (WebP). */
resolvedLogoSrc?: string;
description?: string;
link?: string | { url?: string; linkName?: string; newTab?: boolean };
badge?: string | { label?: string; color?: string };
whatsapp?: string;
youtube?: string;
location?: { text?: string; lat?: number; lng?: number };
contacts?: (string | ContactEntry)[];
}
/** Organisations-Block (_type: "organisations"). */
export interface OrganisationsBlockData {
_type?: "organisations";
_slug?: string;
headline?: string;
/** SectionHeader: Label über dem Titel (uppercase). */
kicker?: string;
/** SectionHeader: Index vor dem Kicker, z.B. "03". */
number?: string;
/** SectionHeader: CTA-Link (aufgelöst: url, linkName). */
action?: string | { url?: string; linkName?: string; newTab?: boolean };
description?: string;
/** default = Karten-Grid; compact = engere Karten, Logo links; carousel = horizontaler Slider (3 Desktop / 2 Mobile) */
presentation?: "default" | "compact" | "carousel" | "grid";
addOrganisationTitle?: string;
addOrganisationMarkdown?: string;
/** Referenz auf link-Collection (aufgelöst: url, linkName, newTab) */
addOrganisationLink?: string | { url?: string; linkName?: string; newTab?: boolean };
organisations?: (string | OrganisationEntry)[];
layout?: BlockLayout;
}
export interface OrganisationsMapBlockData {
_type?: "organisations_map";
_slug?: string;
headline?: string;
/** SectionHeader: Label über dem Titel (uppercase). */
kicker?: string;
/** SectionHeader: Index vor dem Kicker, z.B. "03". */
number?: string;
/** SectionHeader: CTA-Link (aufgelöst: url, linkName). */
action?: string | { url?: string; linkName?: string; newTab?: boolean };
organisations?: (string | OrganisationEntry)[];
layout?: BlockLayout;
}
export interface ContactEntry {
_type?: "contact";
_slug?: string;
name?: string;
role?: string;
organisations?: (string | { _slug?: string; name?: string })[];
email?: string;
phone?: string;
note?: string;
}
export interface ContactsBlockData {
_type?: "contacts_block";
_slug?: string;
headline?: string;
contacts?: (string | ContactEntry)[];
layout?: BlockLayout;
}
export interface AdressbuchBlockData {
_type?: "adressbuch";
_slug?: string;
excludeContacts?: (string | ContactEntry)[];
layout?: BlockLayout;
/** Server-side injected — not in CMS schema */
resolvedContacts?: ContactEntry[];
}
/** Live-Strommix-Widget (_type: "live_strommix"). Editorial-Konfig + Platzierungs-Instanz.
* Fetched live data from /api/strommix (aggregator) and renders status bucket
* derived from `(wind_onshore + wind_offshore + solar) / load * 100`. */
export interface LiveStrommixBlockData {
_type?: "live_strommix";
_slug?: string;
/** `full` = card with argument + breakdown disclosure + history accordion.
* `hero` = pct + status + sparkline + trend, no sub-cards or accordion.
* `compact` = single horizontal strip for sidebar / homepage placement.
* Default: `full`. */
variant?: "full" | "hero" | "compact";
intro_headline?: string;
intro_text?: string;
threshold_dunkelflaute?: number;
threshold_niedrig?: number;
threshold_mittel?: number;
label_dunkelflaute?: string;
label_niedrig?: string;
label_mittel?: string;
label_hoch?: string;
argument_dunkelflaute?: string;
argument_niedrig?: string;
argument_mittel?: string;
argument_hoch?: string;
enable_france_comparison?: boolean;
disclaimer?: string;
/** Day-ahead price (€/MWh) below which `disclaimer` is rendered.
* Default 10 — disclaimer about EEG-Vergütung at negative prices is
* irrelevant at 130 €/MWh and gets hidden. Set very high (e.g. 9999)
* to always show. */
disclaimer_threshold_eur_mwh?: number;
/** Reference (resolved: { _slug, _type, ...page-fields } or just slug string). */
explanation_page?: string | { _slug?: string; title?: string };
// ---------------------------------------------------------------------
// Stammdaten (BNetzA Marktstammdatenregister, manuell gepflegt). Treiben
// den Live-vs-Installiert-Auslastungs-Bereich. Stand quartalsweise
// aktualisieren — die Werte bewegen sich um 13 GW pro Quartal.
// ---------------------------------------------------------------------
installed_wind_onshore_gw?: number;
installed_wind_offshore_gw?: number;
installed_solar_gw?: number;
/** Free-form Stand-Marker für die Stammdaten, z. B. "2025-Q4". */
installed_as_of?: string;
/** Optional: Hand-gepflegte EinsMan-Entschädigungssumme YTD (Mio €).
* BNetzA liefert das nur quartalsweise per PDF. Wenn nicht gesetzt,
* zeigt das Widget den Counter ohne Eurosumme. */
einsman_costs_ytd_eur_mio?: number;
einsman_costs_as_of?: string;
layout?: BlockLayout;
}
/** Kalender-Item (calendar_item) aus OpenAPI; terminZeit = ISO date-time.
* Lokale Erweiterungen sind im RustyCMS-Schema ergänzt und werden bis zur
* OpenAPI-Regen hier inline mitgeführt. */
export type CalendarItemData = components["schemas"]["calendar_item"] & {
/** Optionales Endedatum für Mehrtagesveranstaltungen (ISO date-time). */
terminEnde?: string;
/** Optionaler Veranstaltungsort (string oder {text, lat, lng}). */
location?: string | { text?: string; lat?: number; lng?: number };
/** Optionale Tags zur Filterung. */
tags?: string[];
/** Optionales Veranstaltungsbild (aufgelöstes image-Feld oder Slug). */
image?:
| string
| {
_type?: "img";
_slug?: string;
src?: string;
description?: string;
file?: { url?: string };
};
/** Optionaler Dateianhang (z.B. PDF-Flyer). */
attachment?: {
src?: string;
label?: string;
};
};
/** Kalender-Block (_type: "calendar"). items nach Resolve: CalendarItemData[]. Basis aus OpenAPI (calendar). */
export type CalendarBlockData = Omit<
components["schemas"]["calendar"],
"items"
> & {
_type?: "calendar";
/** Optionaler Titel über dem Kalender. */
title?: string;
/** SectionHeader: Label über dem Titel (uppercase). */
kicker?: string;
/** SectionHeader: Index vor dem Kicker, z.B. "03". */
number?: string;
/** SectionHeader: CTA-Link (aufgelöst: url, linkName). */
action?: string | { url?: string; linkName?: string; newTab?: boolean };
/** Slugs oder aufgelöste calendar_item-Einträge. */
items?: (string | CalendarItemData)[];
layout?: BlockLayout;
/** Variante: "default" voller Kalender, "compact" Anteaser. */
variant?: "default" | "compact";
/** Compact: Ziel-Page-Referenz hinter dem "Alle Termine"-Link. */
linkTo?: string | { _slug?: string;[key: string]: unknown };
/** Compact: Anzahl Termine (default 3). */
limit?: number;
};
/** Internal component block (_type: "internal_component"). */
export type InternalComponentId = "form-contact" | "form-mitmachen" | "form-newsletter" | "form-newsletter-inline";
export interface InternalComponentBlockData {
_type?: "internal_component";
_slug?: string;
component?: InternalComponentId;
layout?: BlockLayout;
}
export interface WindMapBlockData {
_type?: "wind_map";
_slug?: string;
name?: string;
title?: string;
subtitle?: string;
/** Explizite Auswahl von wind_area-Slugs. Leer/null = alle published areas. */
areas?: (string | { _slug: string;[key: string]: unknown })[];
/** Optionales Disclaimer-Accordion oberhalb der Karte. HTML/Markdown aus
* dem CMS — Komponente rendert es via {@html}. */
hinweis_text?: string;
/** Beschreibender Fließtext direkt unter dem Titel (Statistik, Interaktions-
* Hinweise, Methodik). HTML aus dem CMS. */
info_text?: string;
layout?: BlockLayout;
}
import type { TextFragment, WindArea } from "./windkarte";
/** Stellungnahme-Generator (_type: "stellungnahme_generator").
* Platzierbare Instanz, rendert Einwendungs-Assistenten für ein Vorranggebiet. */
export interface StellingnahmeGeneratorBlockData {
_type?: "stellungnahme_generator";
_slug?: string;
id?: string;
/** Referenz auf wind_area; nach Resolve vollständiges WindArea-Objekt inkl.
* aufgelöster stellungnahme_kriterien (TextFragment[]). */
windArea?: string | (WindArea & {
stellungnahme_kriterien?: TextFragment[];
});
headline?: string;
intro?: string;
/** Referenz auf tag; nach Resolve: { _slug, name }. */
allgemeineArgumenteTag?: string | { _slug: string; name?: string };
/** Vom Resolver befüllt: allgemeine (gebietsunabhängige) Fragmente via Tag. */
allgemeineArgumente?: TextFragment[];
/** Einleitungssatz; {gebiet} wird ersetzt. */
defaultOpeningText?: string;
/** Abschlussforderung vor "Mit freundlichen Grüßen". */
defaultClosingText?: string;
/** ISO-Datum: Einwendungsfrist-Ende (z.B. "2026-07-04"). */
deadline?: string;
/** E-Mail der Planungsstelle — aktiviert Mail-Button. */
recipientEmail?: string;
/** Vom Resolver befüllt: zentrales KI-Regelwerk aus stellungnahme_config (Singleton). */
aiRules?: string;
layout?: BlockLayout;
}
/** Einzelne Kachel (info_card): Icon/Bild + Titel + Beschreibung + optionaler Link. */
export interface InfoCardData {
_type?: "info_card";
_slug?: string;
/** Iconify-String ("mdi:email") oder Bild-URL — Komponente erkennt automatisch. */
icon?: string;
title?: string;
description?: string;
linkLabel?: string;
linkHref?: string;
external?: boolean;
/** Farbton der Kachel: default (weiß) oder green (grün getönt). */
tone?: "default" | "green";
}
/** Sektions-Grid (_type: "section_grid"): SectionHeader + info_card-Kacheln. */
export interface SectionGridBlockData {
_type?: "section_grid";
_slug?: string;
headline?: string;
subtitle?: string;
/** SectionHeader: Label über dem Titel (uppercase). */
kicker?: string;
/** SectionHeader: Index vor dem Kicker, z.B. "01". */
number?: string;
/** SectionHeader: CTA-Link (aufgelöst: url, linkName). */
action?: string | { url?: string; linkName?: string; newTab?: boolean };
columns?: "2" | "3" | "4";
/** Slugs oder aufgelöste info_card-Einträge. */
cards?: (string | InfoCardData)[];
layout?: BlockLayout;
}
/** Zweispaltiger Formular-Block (_type: "contact_form"): Form links, Aside rechts. */
export interface ContactFormBlockData {
_type?: "contact_form";
_slug?: string;
headline?: string;
kicker?: string;
number?: string;
action?: string | { url?: string; linkName?: string; newTab?: boolean };
form?: "form-contact" | "form-mitmachen" | "form-newsletter" | "form-newsletter-inline";
asideTitle?: string;
asideText?: string;
asideCards?: (string | InfoCardData)[];
layout?: BlockLayout;
}