7788e0370e
single = immer untereinander, auto = breitenbasiert mehrspaltig ab >4. Regionalplan-Generatorenliste (w1-w40) auf single. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
529 lines
17 KiB
TypeScript
529 lines
17 KiB
TypeScript
/**
|
||
* 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;
|
||
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;
|
||
/** "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;
|
||
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;
|
||
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;
|
||
/** 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". */
|
||
design?: "list" | "cards" | "carousel";
|
||
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;
|
||
/** 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;
|
||
description?: string;
|
||
/** default = Karten-Grid; compact = engere Karten, Logo links; carousel = horizontaler Slider (3 Desktop / 2 Mobile) */
|
||
presentation?: "default" | "compact" | "carousel";
|
||
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;
|
||
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 1–3 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;
|
||
/** 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;
|
||
layout?: BlockLayout;
|
||
}
|