From 3ccc50248f9f8c02b76e8857fe512e504561060c Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Wed, 12 Aug 2026 17:34:07 +0200 Subject: [PATCH] Add optional Alternative.image and Feature.color/icon for richer layouts image: a banner url rendered as , client-fetched directly (never server-side, so none of ResourceSource::Url's SSRF concern applies). color: any CSS color, set as the feature's own --feature-accent custom property (never interpolated into a stylesheet) - draws a left-border accent; unset means no border, not a fallback to the global --accent. icon: an Iconify name (lucide:star etc.), rendered via Iconify's public SVG API - no icon library bundled, matching this app's zero-JS- dependency content otherwise keeps to. Co-Authored-By: Claude Sonnet 5 --- src/app.rs | 18 +++++++++++++++++- src/content.rs | 24 ++++++++++++++++++++++++ style/main.css | 21 +++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index 2416136..f6fb8e0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -600,6 +600,9 @@ fn AlternativeCard( view! {
+ {alternative.image.clone().map(|src| view! { + + })}

{alternative.name.clone()}

{alternative.description.clone()}

@@ -625,8 +628,13 @@ fn AlternativeCard( let alt_name = alt_name.clone(); let resource = feature.resource.clone(); let feature_name = feature.name.clone(); + let color = feature.color.clone(); + let icon = feature.icon.clone(); view! { -
+
+ {icon.filter(|i| is_safe_icon_name(i)).map(|icon| view! { + + })} bool { + !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == ':') +} + fn format_ms(ms: i64) -> String { #[cfg(feature = "ssr")] { diff --git a/src/content.rs b/src/content.rs index 3daa4df..a1a1012 100644 --- a/src/content.rs +++ b/src/content.rs @@ -62,6 +62,13 @@ pub struct Alternative { pub consequence: Vec, #[serde(default)] pub encouragements: Vec, + /// A banner image url, rendered above the description - purely + /// decorative, no upload/hosting mechanism of its own, just an + /// already-hosted url the browser fetches directly (unlike + /// `ResourceSource::Url`, this is never fetched server-side, so it + /// carries none of that variant's SSRF concern). + #[serde(default)] + pub image: Option, #[serde(default)] pub features: Vec, /// Names a NATS KV bucket to also durably store this submission @@ -99,6 +106,23 @@ pub struct Feature { pub name: String, #[serde(default)] pub description: String, + /// Any valid CSS color (hex, named, rgb()/oklch()/...) - set as this + /// feature's own `--feature-accent` custom property rather than + /// interpolated into a stylesheet, so a bad value just fails to + /// apply instead of being live CSS content could inject arbitrary + /// rules into. Unset means no accent border at all (see + /// style/main.css's `.feature`), not a silent fallback to the + /// global `--accent` - a feature that never asked for a color + /// shouldn't suddenly gain a visible border. + #[serde(default)] + pub color: Option, + /// An Iconify icon name (`{prefix}:{name}`, e.g. `lucide:star`), + /// rendered via Iconify's public SVG API + /// (`https://api.iconify.design/{icon}.svg`) - no icon library + /// bundled here, matching this app's zero-JS-dependency content + /// otherwise keeps to. + #[serde(default)] + pub icon: Option, #[serde(default)] pub requirements: Vec, /// Live data this feature pulls in. Read-only unless `transitions` diff --git a/style/main.css b/style/main.css index 369c7af..d5910ae 100644 --- a/style/main.css +++ b/style/main.css @@ -280,6 +280,15 @@ main.not-found { padding: 1.75rem 1.9rem; } +.alt-image { + display: block; + width: 100%; + max-height: 14rem; + object-fit: cover; + border-radius: calc(var(--radius) - 0.3rem); + margin-bottom: 1.1rem; +} + .alt-card h2 { font-size: 1.3rem; } @@ -316,6 +325,18 @@ main.not-found { margin-top: 0.6rem; } +.feature { + border-left: 3px solid var(--feature-accent, transparent); + padding-left: 0.9rem; +} + +.feature-icon { + float: left; + width: 1.3rem; + height: 1.3rem; + margin: 0.1rem 0.6rem 0.4rem 0; +} + .feature h3 { font-size: 1rem; margin-bottom: 0.15em;