feat(strommix): variant system + wald palette + UX polish
Add full/compact variants, prominent share, source badge, big negative-price banner with conditional disclaimer, cards-style context row, fade-in animation, and remove em-dashes from visible strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<StrommixResponse | null>(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}
|
||||
<!-- Compact variant — single horizontal strip for sidebars or
|
||||
homepage placement. No argument, no breakdown disclosure, no
|
||||
quelle-attribution. Optional `explanation_page` makes the whole
|
||||
strip linkable so visitors can jump to the full version. -->
|
||||
{@const compactInner = (() => {
|
||||
const pct = renewablePct != null ? Math.round(renewablePct) : null;
|
||||
return { pct };
|
||||
})()}
|
||||
<svelte:element
|
||||
this={explanationHref ? "a" : "div"}
|
||||
href={explanationHref || undefined}
|
||||
class="block rounded-md border border-l-4 bg-white shadow-sm transition-colors {accentClasses.box} {explanationHref ? 'no-underline! hover:brightness-95' : ''}"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3 text-sm">
|
||||
{#if !live}
|
||||
<span class="text-stein-500">{t(T.strommix_loading)}</span>
|
||||
{:else}
|
||||
{#if compactInner.pct != null}
|
||||
{#key live.measuredAtUnix}
|
||||
<span class="strommix-pct text-3xl sm:text-4xl font-extrabold tabular-nums leading-none {accentClasses.pct}">
|
||||
{compactInner.pct}%
|
||||
</span>
|
||||
{/key}
|
||||
<span class="text-xs sm:text-sm text-stein-700">
|
||||
{t(T.strommix_wind_solar_share)}
|
||||
<span class="font-semibold {accentClasses.label}"> · {statusLabel}</span>
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-sm text-fire-700">
|
||||
{t(T.strommix_load_missing)}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="ml-auto hidden sm:inline-flex items-center gap-3 text-xs text-stein-600 tabular-nums">
|
||||
<span>{t(T.strommix_conventional_now)}: {fmtMw(live.conventionalMw)}</span>
|
||||
<span>·</span>
|
||||
<span>{fmtPrice(live.priceDeEurMwh)}</span>
|
||||
</span>
|
||||
{#if explanationHref}
|
||||
<span class="text-xs text-stein-500 inline-flex items-center gap-1">
|
||||
<Icon icon="mdi:chevron-right" class="size-3.5" aria-hidden="true" />
|
||||
{t(T.strommix_compact_more)}
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:element>
|
||||
{:else}
|
||||
{#if block.intro_headline || introHtml}
|
||||
<header class="mb-4">
|
||||
{#if block.intro_headline}
|
||||
@@ -211,11 +294,13 @@
|
||||
<div
|
||||
class="rounded-xs border-2 shadow-sm overflow-hidden {accentClasses.box}"
|
||||
>
|
||||
<!-- Header bar -->
|
||||
<!-- Header bar — title + source badge + last-updated stamp.
|
||||
Source-Badge zeigt klar woher die zahlen kommen ohne dass
|
||||
man die Detail-Aufklappkachel öffnen muss. -->
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-2 border-b border-current/10 text-xs uppercase tracking-wide font-semibold {accentClasses.label}"
|
||||
class="flex flex-wrap items-center gap-x-3 gap-y-1 justify-between px-4 py-2 border-b border-current/10 text-xs font-semibold {accentClasses.label}"
|
||||
>
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<span class="inline-flex items-center gap-1.5 uppercase tracking-wide">
|
||||
<Icon
|
||||
icon="mdi:transmission-tower"
|
||||
class="size-4"
|
||||
@@ -223,12 +308,24 @@
|
||||
/>
|
||||
{t(T.strommix_header)}
|
||||
</span>
|
||||
{#if live?.measuredAtUnix}
|
||||
<span class="font-normal normal-case"
|
||||
>{t(T.strommix_stand, {
|
||||
time: fmtTime(live.measuredAtUnix),
|
||||
})}</span
|
||||
>
|
||||
{#if live}
|
||||
<span class="inline-flex items-center gap-2 normal-case font-normal">
|
||||
<span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] uppercase tracking-wide {accentClasses.badge}">
|
||||
<Icon icon="mdi:database-outline" class="size-3" aria-hidden="true" />
|
||||
{#if live.dataSource === "smard"}
|
||||
SMARD
|
||||
{:else if live.dataSource === "smard+energy-charts"}
|
||||
SMARD + EC
|
||||
{:else}
|
||||
Energy-Charts
|
||||
{/if}
|
||||
</span>
|
||||
{#if live.measuredAtUnix}
|
||||
<span>
|
||||
{t(T.strommix_stand, { time: fmtTime(live.measuredAtUnix) })}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -238,28 +335,34 @@
|
||||
{t(T.strommix_load_error, { error: loadError })}
|
||||
</div>
|
||||
{:else if !live}
|
||||
<div class="p-6 text-center text-neutral-500 text-sm">
|
||||
<div class="p-6 text-center text-stein-500 text-sm">
|
||||
{t(T.strommix_loading)}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-6 text-center">
|
||||
<div class="p-6 sm:p-8 text-center">
|
||||
{#if renewablePct != null}
|
||||
<div
|
||||
class="text-6xl sm:text-7xl font-extrabold tabular-nums {accentClasses.pct}"
|
||||
>
|
||||
{Math.round(renewablePct)}%
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-neutral-700">
|
||||
<!-- Key change keys the % display so a fresh fetch
|
||||
triggers a brief CSS fade-in transition. The
|
||||
number is the dominant visual; everything else
|
||||
supports it. -->
|
||||
{#key live.measuredAtUnix}
|
||||
<div
|
||||
class="strommix-pct text-7xl sm:text-8xl font-extrabold tabular-nums leading-none {accentClasses.pct}"
|
||||
>
|
||||
{Math.round(renewablePct)}%
|
||||
</div>
|
||||
{/key}
|
||||
<div class="mt-2 text-sm sm:text-base text-stein-700">
|
||||
{t(T.strommix_wind_solar_share)}
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 text-base font-semibold {accentClasses.label}"
|
||||
class="mt-3 text-lg font-semibold {accentClasses.label}"
|
||||
>
|
||||
→ {statusLabel}
|
||||
</div>
|
||||
{#if argumentHtml}
|
||||
<div
|
||||
class="prose prose-sm mt-3 max-w-prose mx-auto text-neutral-800"
|
||||
class="prose prose-xs mt-2 max-w-md mx-auto text-xs leading-snug text-stein-700 [&_p]:my-1"
|
||||
>
|
||||
{@html argumentHtml}
|
||||
</div>
|
||||
@@ -267,144 +370,151 @@
|
||||
{:else}
|
||||
<!-- Production data arrived but load is missing → no percentage,
|
||||
but we still show what we have so the widget isn't blank. -->
|
||||
<div class="text-sm text-amber-700">
|
||||
<div class="text-sm text-fire-700">
|
||||
{t(T.strommix_load_missing)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Context row -->
|
||||
<div
|
||||
class="grid grid-cols-2 divide-x divide-current/10 border-t border-current/10 text-sm"
|
||||
>
|
||||
<div class="px-4 py-3">
|
||||
<div
|
||||
class="text-xs text-neutral-600 uppercase tracking-wide"
|
||||
>
|
||||
{t(T.strommix_conventional_now)}
|
||||
</div>
|
||||
<div class="text-lg font-semibold tabular-nums">
|
||||
{fmtMw(live.conventionalMw)}
|
||||
</div>
|
||||
<div class="text-[11px] text-neutral-500 mt-0.5">
|
||||
{t(T.strommix_conventional_formula)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Context cards: konventionell + saldo, je eigenes box-block.
|
||||
Stack mobile, side-by-side ab sm. Kein farbakzent damit haupt-
|
||||
karte mit %-zahl optisch dominiert. -->
|
||||
{#if live}
|
||||
<div class="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-xs border border-stein-200 bg-white px-4 py-3 text-sm shadow-sm">
|
||||
<div class="text-xs text-stein-600 uppercase tracking-wide">
|
||||
{t(T.strommix_conventional_now)}
|
||||
</div>
|
||||
<div class="px-4 py-3">
|
||||
<div
|
||||
class="text-xs text-neutral-600 uppercase tracking-wide"
|
||||
>
|
||||
{importDirection === "Import"
|
||||
? t(T.strommix_import)
|
||||
: importDirection === "Export"
|
||||
? t(T.strommix_export)
|
||||
: t(T.strommix_saldo)}
|
||||
<div class="text-2xl font-semibold tabular-nums text-stein-900">
|
||||
{fmtMw(live.conventionalMw)}
|
||||
</div>
|
||||
<div class="text-[11px] text-stein-500 mt-1">
|
||||
{t(T.strommix_conventional_formula)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xs border border-stein-200 bg-white px-4 py-3 text-sm shadow-sm">
|
||||
<div class="text-xs text-stein-600 uppercase tracking-wide">
|
||||
{importDirection === "Import"
|
||||
? t(T.strommix_import)
|
||||
: importDirection === "Export"
|
||||
? t(T.strommix_export)
|
||||
: t(T.strommix_saldo)}
|
||||
</div>
|
||||
<div class="text-2xl font-semibold tabular-nums text-stein-900">
|
||||
{#if importDirection === "Import"}↓{/if}
|
||||
{#if importDirection === "Export"}↑{/if}
|
||||
{fmtGw(live.netFlowGw)}
|
||||
</div>
|
||||
{#if live.crossborderMeasuredAtUnix}
|
||||
<div class="text-[11px] text-stein-500 mt-1">
|
||||
{t(T.strommix_stand, {
|
||||
time: fmtTime(live.crossborderMeasuredAtUnix),
|
||||
})}
|
||||
{#if cbStale}<span class="text-fire-700">
|
||||
· {t(T.strommix_delayed)}</span
|
||||
>{/if}
|
||||
</div>
|
||||
<div class="text-lg font-semibold tabular-nums">
|
||||
{#if importDirection === "Import"}↓{/if}
|
||||
{#if importDirection === "Export"}↑{/if}
|
||||
{fmtGw(live.netFlowGw)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price card — separater block, full-width damit der vergleich
|
||||
DE/FR + slot-info nicht in den 2-spaltigen flow oben gepresst
|
||||
werden muss. -->
|
||||
<div class="mt-3 rounded-xs border border-stein-200 bg-white px-4 py-3 text-sm shadow-sm">
|
||||
<div class="flex flex-wrap items-baseline gap-x-4 gap-y-1">
|
||||
<span class="font-semibold">
|
||||
{t(T.strommix_price_de, { price: fmtPrice(live.priceDeEurMwh) })}
|
||||
</span>
|
||||
{#if enableFr && live.priceFrEurMwh != null}
|
||||
<span class="text-stein-600">
|
||||
{t(T.strommix_price_fr_inline, { price: fmtPrice(live.priceFrEurMwh) })}
|
||||
</span>
|
||||
{/if}
|
||||
{#if live.priceMeasuredAtUnix}
|
||||
<span class="text-[11px] text-stein-500">
|
||||
· {t(T.strommix_slot, { time: fmtTime(live.priceMeasuredAtUnix) })}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Negativpreis-warnung als eigener prominenter banner statt
|
||||
inline-badge. Greift wenn Day-Ahead-Preis negativ — sehr
|
||||
aussagekräftiger marktzustand der nicht versteckt werden
|
||||
sollte. -->
|
||||
{#if (live.priceDeEurMwh ?? 0) < 0}
|
||||
<div class="mt-3 flex items-start gap-3 rounded-xs border-2 border-fire-500 bg-fire-50 px-4 py-3 text-sm shadow-sm">
|
||||
<Icon
|
||||
icon="mdi:alert-circle"
|
||||
class="size-6 shrink-0 text-fire-700"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="text-base font-bold text-fire-800">
|
||||
{t(T.strommix_negative_price)}: {fmtPrice(live.priceDeEurMwh)}
|
||||
</div>
|
||||
{#if live.crossborderMeasuredAtUnix}
|
||||
<div class="text-[11px] text-neutral-500 mt-0.5">
|
||||
{t(T.strommix_stand, {
|
||||
time: fmtTime(live.crossborderMeasuredAtUnix),
|
||||
})}
|
||||
{#if cbStale}<span class="text-amber-700">
|
||||
· {t(T.strommix_delayed)}</span
|
||||
>{/if}
|
||||
{#if showDisclaimer}
|
||||
<div class="mt-1 text-xs leading-snug text-fire-900/80 prose prose-xs max-w-none [&_p]:my-0">
|
||||
{@html disclaimerHtml}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price row -->
|
||||
<div class="border-t border-current/10 px-4 py-3 text-sm">
|
||||
<div class="flex flex-wrap items-baseline gap-x-4 gap-y-1">
|
||||
<span class="font-semibold"
|
||||
>{t(T.strommix_price_de, {
|
||||
price: fmtPrice(live.priceDeEurMwh),
|
||||
})}</span
|
||||
>
|
||||
{#if enableFr && live.priceFrEurMwh != null}
|
||||
<span class="text-neutral-600"
|
||||
>{t(T.strommix_price_fr_inline, {
|
||||
price: fmtPrice(live.priceFrEurMwh),
|
||||
})}</span
|
||||
>
|
||||
{/if}
|
||||
{#if live.priceMeasuredAtUnix}
|
||||
<span class="text-[11px] text-neutral-500">
|
||||
· {t(T.strommix_slot, {
|
||||
time: fmtTime(live.priceMeasuredAtUnix),
|
||||
})}
|
||||
</span>
|
||||
{/if}
|
||||
{#if (live.priceDeEurMwh ?? 0) < 0}
|
||||
<span
|
||||
class="ml-auto inline-flex items-center gap-1 text-amber-800 font-semibold"
|
||||
>
|
||||
<Icon
|
||||
icon="mdi:alert-circle-outline"
|
||||
class="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(T.strommix_negative_price)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Production breakdown: aufklappbar damit user die Summe nachvollziehen
|
||||
kann. Werte sind aufeinander abgestimmt (gleicher SMARD-15-min-Slot). -->
|
||||
{#if live}
|
||||
<details class="mt-3 text-xs">
|
||||
<summary
|
||||
class="cursor-pointer text-neutral-600 hover:text-neutral-900"
|
||||
class="cursor-pointer text-stein-600 hover:text-stein-900"
|
||||
>
|
||||
{t(T.strommix_check_values)}
|
||||
</summary>
|
||||
<div
|
||||
class="mt-2 grid grid-cols-2 gap-x-4 gap-y-1 rounded-md border border-neutral-200 bg-neutral-50 px-3 py-2 tabular-nums sm:grid-cols-3"
|
||||
class="mt-2 grid grid-cols-2 gap-x-4 gap-y-1 rounded-md border border-stein-200 bg-stein-50 px-3 py-2 tabular-nums sm:grid-cols-3"
|
||||
>
|
||||
<div class="text-neutral-500">
|
||||
<div class="text-stein-500">
|
||||
{t(T.strommix_field_wind_onshore)}
|
||||
</div>
|
||||
<div>{fmtMw(live.windOnshoreMw)}</div>
|
||||
<div class="text-neutral-500">
|
||||
<div class="text-stein-500">
|
||||
{t(T.strommix_field_wind_offshore)}
|
||||
</div>
|
||||
<div>{fmtMw(live.windOffshoreMw)}</div>
|
||||
<div class="text-neutral-500">{t(T.strommix_field_solar)}</div>
|
||||
<div class="text-stein-500">{t(T.strommix_field_solar)}</div>
|
||||
<div>{fmtMw(live.solarMw)}</div>
|
||||
<div class="text-neutral-500">
|
||||
<div class="text-stein-500">
|
||||
{t(T.strommix_field_lignite)}
|
||||
</div>
|
||||
<div>{fmtMw(live.ligniteMw)}</div>
|
||||
<div class="text-neutral-500">
|
||||
<div class="text-stein-500">
|
||||
{t(T.strommix_field_hard_coal)}
|
||||
</div>
|
||||
<div>{fmtMw(live.hardCoalMw)}</div>
|
||||
<div class="text-neutral-500">{t(T.strommix_field_gas)}</div>
|
||||
<div class="text-stein-500">{t(T.strommix_field_gas)}</div>
|
||||
<div>{fmtMw(live.gasMw)}</div>
|
||||
<div class="text-neutral-500">
|
||||
<div class="text-stein-500">
|
||||
{t(T.strommix_field_nuclear)}
|
||||
</div>
|
||||
<div>{fmtMw(live.nuclearMw)}</div>
|
||||
<div class="text-neutral-500">
|
||||
<div class="text-stein-500">
|
||||
{t(T.strommix_field_biomass)}
|
||||
</div>
|
||||
<div>{fmtMw(live.biomassMw)}</div>
|
||||
<div class="text-neutral-500">{t(T.strommix_field_hydro)}</div>
|
||||
<div class="text-stein-500">{t(T.strommix_field_hydro)}</div>
|
||||
<div>{fmtMw(live.hydroMw)}</div>
|
||||
<div class="text-neutral-500 font-semibold">
|
||||
<div class="text-stein-500 font-semibold">
|
||||
{t(T.strommix_field_load)}
|
||||
</div>
|
||||
<div class="font-semibold">{fmtMw(live.loadMw)}</div>
|
||||
</div>
|
||||
<p class="mt-1 text-[11px] text-neutral-500">
|
||||
<p class="mt-1 text-[11px] text-stein-500">
|
||||
{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 @@
|
||||
</details>
|
||||
{/if}
|
||||
|
||||
<!-- Footer: disclaimer + explanation link + stale flag -->
|
||||
{#if disclaimerHtml || explanationHref || isStale}
|
||||
<!-- Footer: disclaimer (only when the low-price condition it describes
|
||||
actually applies AND not already shown in the negative-price
|
||||
banner) + explanation link + stale flag. -->
|
||||
{#if showFooterDisclaimer || explanationHref || isStale}
|
||||
<footer
|
||||
class="mt-3 text-xs text-neutral-600 flex flex-wrap items-start gap-x-4 gap-y-1"
|
||||
class="mt-3 text-xs text-stein-600 flex flex-wrap items-start gap-x-4 gap-y-1"
|
||||
>
|
||||
{#if disclaimerHtml}
|
||||
{#if showFooterDisclaimer}
|
||||
<div class="flex-1 min-w-0 prose prose-xs">
|
||||
{@html disclaimerHtml}
|
||||
</div>
|
||||
@@ -437,7 +549,7 @@
|
||||
</a>
|
||||
{/if}
|
||||
{#if isStale}
|
||||
<span class="inline-flex items-center gap-1 text-amber-700">
|
||||
<span class="inline-flex items-center gap-1 text-fire-700">
|
||||
<Icon
|
||||
icon="mdi:cloud-off-outline"
|
||||
class="size-3.5"
|
||||
@@ -448,4 +560,32 @@
|
||||
{/if}
|
||||
</footer>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Smooth fade-in when the % value gets re-keyed on a new fetch.
|
||||
Subtle — the visitor notices the update without being
|
||||
distracted. */
|
||||
.strommix-pct {
|
||||
animation: strommix-pct-in 250ms ease-out both;
|
||||
}
|
||||
@keyframes strommix-pct-in {
|
||||
from { opacity: 0; transform: scale(0.96); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.strommix-pct {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
/* Color/border transitions on the bucket-tinted card so the
|
||||
accent shifts smoothly when the bucket flips between fetches. */
|
||||
.live-strommix [class*="border-fire"],
|
||||
.live-strommix [class*="border-erde"],
|
||||
.live-strommix [class*="border-himmel"],
|
||||
.live-strommix [class*="border-wald"],
|
||||
.live-strommix [class*="border-stein"] {
|
||||
transition: background-color 350ms ease, border-color 350ms ease, color 350ms ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user