Compare commits

...
6 Commits
Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Fable 5 57b70c8445 Release 0.3.28
Test / test (push) Successful in 27s
Publish release / publish (push) Successful in 1m41s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 20:31:02 +02:00
Bendik Aagaard LynghaugandClaude Fable 5 0d3221c22f Hero description renders inline markdown
The question-level description (hero subtitle) was plain text, so
**bold** showed literal asterisks. Now it runs through
render_inline_markdown like every other description.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y42TyF8Zu7NGRR2893vNcZ
2026-09-01 20:31:02 +02:00
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 34 additions and 13 deletions
Generated
+1 -1
View File
@@ -2948,7 +2948,7 @@ dependencies = [
[[package]]
name = "portal"
version = "0.3.24"
version = "0.3.26"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "portal"
version = "0.3.25"
version = "0.3.28"
edition = "2021"
[lib]
+5 -9
View File
@@ -587,14 +587,10 @@ fn Hero(title: String, description: String, landing: bool, site: SiteConfig, cur
None
};
let has_module = module.is_some();
// Light theme inverts white-stroke wordmarks to ink. The house
// mark and the sibling-site marks are all white-stroke SVGs; a
// raster logo is a client brand and keeps its colors.
let house_wordmark = site
.wordmark
.as_deref()
.map(|w| w.ends_with(".svg"))
.unwrap_or(true);
// Light theme can invert a white-stroke wordmark to ink. Default:
// only the built-in house mark inverts; a content-shipped logo
// keeps its colours unless site.yaml opts in with wordmark_invert.
let house_wordmark = site.wordmark_invert.unwrap_or(site.wordmark.is_none());
let wordmark = site.wordmark.clone().unwrap_or_else(|| "/wordmark.svg".to_string());
// Clamped server-side too, but belt and braces for the inline style.
let wordmark_style = site
@@ -691,7 +687,7 @@ fn Hero(title: String, description: String, landing: bool, site: SiteConfig, cur
<img src=wordmark alt=site_title style=wordmark_style/>
</a>
<h1>{title}</h1>
<p>{description}</p>
<p inner_html=render_inline_markdown(&description)></p>
</div>
</header>
}
+17 -2
View File
@@ -446,6 +446,12 @@ pub struct Requirement {
/// `ResourceSpec` mechanism a `Feature.resource` uses.
#[serde(default)]
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
/// stable id. Defaults to trying `_id` then `id`.
#[serde(default)]
@@ -517,6 +523,12 @@ pub struct SiteConfig {
/// `None` falls back to `/wordmark.svg`.
#[serde(default)]
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
/// stylesheet's default. Raster logos look best at or below their
/// intrinsic pixel height.
@@ -1204,9 +1216,12 @@ pub fn validate_questions(
.collect();
for feature in &alternative.features {
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!(
"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
);
}
+10
View File
@@ -68,6 +68,16 @@ pub async fn get_requirement_options(
.iter()
.find(|r| r.name == requirement_name)
.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
.resource
.as_ref()