Files
windwiderstand/src/lib/block-types.ts
T
Peter Meier 7788e0370e
Deploy / verify (push) Successful in 1m13s
Deploy / deploy (push) Successful in 1m28s
feat(link-list): CMS-Feld columns (auto|single) für Spaltenlayout
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>
2026-06-26 22:40:30 +02:00

529 lines
17 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;
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 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;
/** 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;
}