Compare commits

..
4 Commits
Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Fable 5 013fd7151e Release 0.3.27
Test / test (push) Successful in 26s
Publish release / publish (push) Successful in 1m40s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 20:26:34 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 99b29bb09f wordmark_invert: explicit, not inferred from .svg
A content-shipped colour logo (Klingenberg's red tile) was being
inverted to teal in light theme by the .svg heuristic. Invert is now
an explicit site.yaml flag defaulting to house-mark-only; content
logos keep their colours unless they opt in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 20:26:34 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 b08e1af139 Release 0.3.26
Test / test (push) Successful in 26s
Publish release / publish (push) Successful in 1m42s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 20:20:50 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 c1cdc53187 Select fields: inline static options
A select requirement can now declare a plain options: [A, B, C] list
instead of a resource - each string is both value and label. Restores
the ancestor question format's inline enums (used by the klingenberg
port) without needing a resource endpoint for a fixed list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 20:20:50 +02:00
5 changed files with 33 additions and 12 deletions
Generated
+1 -1
View File
@@ -2948,7 +2948,7 @@ dependencies = [
[[package]] [[package]]
name = "portal" name = "portal"
version = "0.3.24" version = "0.3.26"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "portal" name = "portal"
version = "0.3.25" version = "0.3.27"
edition = "2021" edition = "2021"
[lib] [lib]
+4 -8
View File
@@ -587,14 +587,10 @@ fn Hero(title: String, description: String, landing: bool, site: SiteConfig, cur
None None
}; };
let has_module = module.is_some(); let has_module = module.is_some();
// Light theme inverts white-stroke wordmarks to ink. The house // Light theme can invert a white-stroke wordmark to ink. Default:
// mark and the sibling-site marks are all white-stroke SVGs; a // only the built-in house mark inverts; a content-shipped logo
// raster logo is a client brand and keeps its colors. // keeps its colours unless site.yaml opts in with wordmark_invert.
let house_wordmark = site let house_wordmark = site.wordmark_invert.unwrap_or(site.wordmark.is_none());
.wordmark
.as_deref()
.map(|w| w.ends_with(".svg"))
.unwrap_or(true);
let wordmark = site.wordmark.clone().unwrap_or_else(|| "/wordmark.svg".to_string()); let wordmark = site.wordmark.clone().unwrap_or_else(|| "/wordmark.svg".to_string());
// Clamped server-side too, but belt and braces for the inline style. // Clamped server-side too, but belt and braces for the inline style.
let wordmark_style = site let wordmark_style = site
+17 -2
View File
@@ -446,6 +446,12 @@ pub struct Requirement {
/// `ResourceSpec` mechanism a `Feature.resource` uses. /// `ResourceSpec` mechanism a `Feature.resource` uses.
#[serde(default)] #[serde(default)]
pub resource: Option<ResourceSpec>, pub resource: Option<ResourceSpec>,
/// `type: select` only - a static list of options, as an
/// alternative to a `resource`. Each string is both the option's
/// stored value and its label. Faithful to the ancestor format's
/// inline enums.
#[serde(default)]
pub options: Vec<String>,
/// `type: select` only - which field in each item is the option's /// `type: select` only - which field in each item is the option's
/// stable id. Defaults to trying `_id` then `id`. /// stable id. Defaults to trying `_id` then `id`.
#[serde(default)] #[serde(default)]
@@ -517,6 +523,12 @@ pub struct SiteConfig {
/// `None` falls back to `/wordmark.svg`. /// `None` falls back to `/wordmark.svg`.
#[serde(default)] #[serde(default)]
pub wordmark: Option<String>, pub wordmark: Option<String>,
/// Whether to invert the wordmark in light theme. `None` inverts
/// only the built-in house wordmark (a white-stroke SVG); a
/// content-shipped logo keeps its own colours unless it sets this
/// true (e.g. a sibling site's white-stroke mark).
#[serde(default)]
pub wordmark_invert: Option<bool>,
/// Wordmark display height in rem (0.56.0). `None` keeps the /// Wordmark display height in rem (0.56.0). `None` keeps the
/// stylesheet's default. Raster logos look best at or below their /// stylesheet's default. Raster logos look best at or below their
/// intrinsic pixel height. /// intrinsic pixel height.
@@ -1204,9 +1216,12 @@ pub fn validate_questions(
.collect(); .collect();
for feature in &alternative.features { for feature in &alternative.features {
for requirement in &feature.requirements { for requirement in &feature.requirements {
if requirement.kind == "select" && requirement.resource.is_none() { if requirement.kind == "select"
&& requirement.resource.is_none()
&& requirement.options.is_empty()
{
anyhow::bail!( anyhow::bail!(
"question {:?} alternative {:?} feature {:?}: requirement {:?} is type: select but declares no resource to select from", "question {:?} alternative {:?} feature {:?}: requirement {:?} is type: select but declares neither a resource nor inline options",
question.id, alternative.name, feature.name, requirement.name question.id, alternative.name, feature.name, requirement.name
); );
} }
+10
View File
@@ -68,6 +68,16 @@ pub async fn get_requirement_options(
.iter() .iter()
.find(|r| r.name == requirement_name) .find(|r| r.name == requirement_name)
.ok_or_else(|| ServerFnError::new("unknown requirement"))?; .ok_or_else(|| ServerFnError::new("unknown requirement"))?;
// Inline options: return them as {id,label} objects, the shape the
// SelectField renders, without touching a resource.
if !requirement.options.is_empty() {
let items: Vec<serde_json::Value> = requirement
.options
.iter()
.map(|o| serde_json::json!({ "id": o, "label": o }))
.collect();
return Ok(serde_json::Value::Array(items));
}
let resource = requirement let resource = requirement
.resource .resource
.as_ref() .as_ref()