diff --git a/src/lib/block-types.ts b/src/lib/block-types.ts index b9200f1..6091b58 100644 --- a/src/lib/block-types.ts +++ b/src/lib/block-types.ts @@ -337,6 +337,9 @@ export interface OrganisationsBlockData { export interface LiveStrommixBlockData { _type?: "live_strommix"; _slug?: string; + /** `full` = card with argument + breakdown disclosure. `compact` = single + * horizontal strip for sidebar / homepage placement. Default: `full`. */ + variant?: "full" | "compact"; intro_headline?: string; intro_text?: string; threshold_dunkelflaute?: number; @@ -352,6 +355,11 @@ export interface LiveStrommixBlockData { 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 }; layout?: BlockLayout; diff --git a/src/lib/components/blocks/StrommixBlock.svelte b/src/lib/components/blocks/StrommixBlock.svelte index 6679ee8..3f76502 100644 --- a/src/lib/components/blocks/StrommixBlock.svelte +++ b/src/lib/components/blocks/StrommixBlock.svelte @@ -12,6 +12,7 @@ const t = useTranslate(); const layoutClasses = $derived(getBlockLayoutClasses(block.layout)); + const isCompact = $derived(block.variant === "compact"); // Live data + lifecycle let live = $state(null); @@ -89,6 +90,30 @@ block.disclaimer ? (marked.parse(block.disclaimer) as string) : "", ); + /** The default disclaimer is about EEG-Vergütung when prices go + * negative — irrelevant when the day-ahead price sits at 130 €/MWh. + * Show it only when the situation it describes can plausibly arise: + * current price below the threshold (default 10 €/MWh) or already + * negative. Editor can override the cutoff via + * `block.disclaimer_threshold_eur_mwh`; setting it to a very high + * number forces the disclaimer to always show. */ + const disclaimerThreshold = $derived( + typeof block.disclaimer_threshold_eur_mwh === "number" + ? block.disclaimer_threshold_eur_mwh + : 10, + ); + const showDisclaimer = $derived.by(() => { + if (!disclaimerHtml) return false; + const p = live?.priceDeEurMwh; + if (p == null) return false; + return p < disclaimerThreshold; + }); + /** When the negative-price banner shows the disclaimer, the footer + * shouldn't repeat it. Otherwise the same paragraph appears twice. */ + const showFooterDisclaimer = $derived( + showDisclaimer && (live?.priceDeEurMwh ?? 0) >= 0, + ); + const enableFr = $derived(block.enable_france_comparison !== false); function fmtMw(mw: number): string { @@ -137,37 +162,47 @@ : "success"; }); + /** Bucket → windwiderstand-design-palette. fire/erde/himmel/wald + * statt der vorherigen Tailwind-defaults. Dunkelflaute = fire (rot + * warn), niedrig = erde (warm-erdig warnung), mittel = himmel + * (kühl-neutral), hoch = wald (grün, hier wäre erneuerbar + * ausreichend). */ const accentClasses = $derived.by(() => { switch (bucketAccent) { case "danger": return { - box: "border-red-300 bg-red-50", - pct: "text-red-800", - label: "text-red-700", + box: "border-fire-300 bg-fire-50", + pct: "text-fire-800", + label: "text-fire-700", + badge: "bg-fire-700 text-fire-50", }; case "warning": return { - box: "border-amber-300 bg-amber-50", - pct: "text-amber-800", - label: "text-amber-700", + box: "border-erde-300 bg-erde-50", + pct: "text-erde-800", + label: "text-erde-700", + badge: "bg-erde-700 text-erde-50", }; case "info": return { - box: "border-sky-300 bg-sky-50", - pct: "text-sky-800", - label: "text-sky-700", + box: "border-himmel-300 bg-himmel-50", + pct: "text-himmel-800", + label: "text-himmel-700", + badge: "bg-himmel-700 text-himmel-50", }; case "success": return { - box: "border-emerald-300 bg-emerald-50", - pct: "text-emerald-800", - label: "text-emerald-700", + box: "border-wald-300 bg-wald-50", + pct: "text-wald-800", + label: "text-wald-700", + badge: "bg-wald-700 text-wald-50", }; default: return { - box: "border-neutral-200 bg-neutral-50", - pct: "text-neutral-800", - label: "text-neutral-700", + box: "border-stein-200 bg-stein-50", + pct: "text-stein-800", + label: "text-stein-700", + badge: "bg-stein-700 text-stein-50", }; } }); @@ -197,6 +232,54 @@ data-block-type="live_strommix" data-block-slug={block._slug} > + {#if isCompact} + + {@const compactInner = (() => { + const pct = renewablePct != null ? Math.round(renewablePct) : null; + return { pct }; + })()} + +
+ {#if !live} + {t(T.strommix_loading)} + {:else} + {#if compactInner.pct != null} + {#key live.measuredAtUnix} + + {compactInner.pct}% + + {/key} + + {t(T.strommix_wind_solar_share)} + · {statusLabel} + + {:else} + + {t(T.strommix_load_missing)} + + {/if} + + {#if explanationHref} + + + {/if} + {/if} +
+
+ {:else} {#if block.intro_headline || introHtml}
{#if block.intro_headline} @@ -211,11 +294,13 @@
- +
- + {t(T.strommix_header)} - {#if live?.measuredAtUnix} - {t(T.strommix_stand, { - time: fmtTime(live.measuredAtUnix), - })} + {#if live} + + + + {#if live.measuredAtUnix} + + {t(T.strommix_stand, { time: fmtTime(live.measuredAtUnix) })} + + {/if} + {/if}
@@ -238,28 +335,34 @@ {t(T.strommix_load_error, { error: loadError })}
{:else if !live} -
+
{t(T.strommix_loading)}
{:else} -
+
{#if renewablePct != null} -
- {Math.round(renewablePct)}% -
-
+ + {#key live.measuredAtUnix} +
+ {Math.round(renewablePct)}% +
+ {/key} +
{t(T.strommix_wind_solar_share)}
→ {statusLabel}
{#if argumentHtml}
{@html argumentHtml}
@@ -267,144 +370,151 @@ {:else} -
+
{t(T.strommix_load_missing)}
{/if}
- -
-
-
- {t(T.strommix_conventional_now)} -
-
- {fmtMw(live.conventionalMw)} -
-
- {t(T.strommix_conventional_formula)} -
+ {/if} +
+ + + {#if live} +
+
+
+ {t(T.strommix_conventional_now)}
-
-
- {importDirection === "Import" - ? t(T.strommix_import) - : importDirection === "Export" - ? t(T.strommix_export) - : t(T.strommix_saldo)} +
+ {fmtMw(live.conventionalMw)} +
+
+ {t(T.strommix_conventional_formula)} +
+
+
+
+ {importDirection === "Import" + ? t(T.strommix_import) + : importDirection === "Export" + ? t(T.strommix_export) + : t(T.strommix_saldo)} +
+
+ {#if importDirection === "Import"}↓{/if} + {#if importDirection === "Export"}↑{/if} + {fmtGw(live.netFlowGw)} +
+ {#if live.crossborderMeasuredAtUnix} +
+ {t(T.strommix_stand, { + time: fmtTime(live.crossborderMeasuredAtUnix), + })} + {#if cbStale} + · {t(T.strommix_delayed)}{/if}
-
- {#if importDirection === "Import"}↓{/if} - {#if importDirection === "Export"}↑{/if} - {fmtGw(live.netFlowGw)} + {/if} +
+
+ + +
+
+ + {t(T.strommix_price_de, { price: fmtPrice(live.priceDeEurMwh) })} + + {#if enableFr && live.priceFrEurMwh != null} + + {t(T.strommix_price_fr_inline, { price: fmtPrice(live.priceFrEurMwh) })} + + {/if} + {#if live.priceMeasuredAtUnix} + + · {t(T.strommix_slot, { time: fmtTime(live.priceMeasuredAtUnix) })} + + {/if} +
+
+ + + {#if (live.priceDeEurMwh ?? 0) < 0} +
+
+ {/if} {#if live}
{t(T.strommix_check_values)}
-
+
{t(T.strommix_field_wind_onshore)}
{fmtMw(live.windOnshoreMw)}
-
+
{t(T.strommix_field_wind_offshore)}
{fmtMw(live.windOffshoreMw)}
-
{t(T.strommix_field_solar)}
+
{t(T.strommix_field_solar)}
{fmtMw(live.solarMw)}
-
+
{t(T.strommix_field_lignite)}
{fmtMw(live.ligniteMw)}
-
+
{t(T.strommix_field_hard_coal)}
{fmtMw(live.hardCoalMw)}
-
{t(T.strommix_field_gas)}
+
{t(T.strommix_field_gas)}
{fmtMw(live.gasMw)}
-
+
{t(T.strommix_field_nuclear)}
{fmtMw(live.nuclearMw)}
-
+
{t(T.strommix_field_biomass)}
{fmtMw(live.biomassMw)}
-
{t(T.strommix_field_hydro)}
+
{t(T.strommix_field_hydro)}
{fmtMw(live.hydroMw)}
-
+
{t(T.strommix_field_load)}
{fmtMw(live.loadMw)}
-

+

{t(T.strommix_source)} {#if live.dataSource === "smard"} {t(T.strommix_source_smard)} @@ -413,7 +523,7 @@ {:else} {t(T.strommix_source_energy_charts)} {/if} - — {t(T.strommix_source_resolution, { + · {t(T.strommix_source_resolution, { time: fmtTime(live.measuredAtUnix), })} {t(T.strommix_conventional_explanation)} @@ -421,12 +531,14 @@

{/if} - - {#if disclaimerHtml || explanationHref || isStale} + + {#if showFooterDisclaimer || explanationHref || isStale}
- {#if disclaimerHtml} + {#if showFooterDisclaimer}
{@html disclaimerHtml}
@@ -437,7 +549,7 @@ {/if} {#if isStale} - + {/if} + {/if}
+ + diff --git a/src/lib/translations.ts b/src/lib/translations.ts index 97e4583..2ca4c34 100644 --- a/src/lib/translations.ts +++ b/src/lib/translations.ts @@ -204,6 +204,9 @@ const TRANSLATION_KEYS = [ "strommix_conventional_explanation", "strommix_stored_data", "strommix_how_calculated", + // Compact variant for homepage / sidebar. + "strommix_compact_label", // {{pct}}, {{status}} + "strommix_compact_more", ] as const; export type TranslationKey = (typeof TRANSLATION_KEYS)[number];