A slideshow across a page: pictures the full width of the screen, captions over them
Test / test (pull_request) Failing after 1m5s

slideshow on a page: max_height in svh, slides with an image (a path
in the content repo or https), a caption laid over the picture's foot,
and optionally the picture's size so its place is held before it
loads. Swiper upgrades it; before that it is a row that scrolls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-30 17:16:50 +02:00
co-authored by Claude Opus 5.5
parent fc708c3546
commit cfb050d3ef
7 changed files with 226 additions and 2 deletions
+61
View File
@@ -95,6 +95,12 @@ pub struct Question {
/// rather than on the whole world.
#[serde(default)]
pub place: Option<Place>,
/// Pictures across the page, under the question: a strip the full
/// width of the screen that a visitor swipes through, each picture
/// with its caption laid over it. What the business has built,
/// shown rather than described.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slideshow: Option<Slideshow>,
}
/// A point a page is about. `zoom` is a map zoom level, 0 (the globe)
@@ -625,6 +631,50 @@ pub struct Requirement {
/// listing may ask about a record's own contents: one field, one
/// value, compared as strings. Anything more is a query language, and
/// a jq filter is already there for shaping what comes back.
/// A strip of pictures across a page (`Question.slideshow`).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Slideshow {
/// How tall the strip may be, in hundredths of the small viewport
/// height (`svh`): 60 is six tenths of the screen. Every picture is
/// that tall and as wide as its own shape makes it.
#[serde(default = "default_slideshow_height")]
pub max_height: u8,
pub slides: Vec<Slide>,
}
fn default_slideshow_height() -> u8 {
60
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Slide {
/// A path in the content repo (`images/built/01.webp`, served from
/// this origin under `/site/`) or an `https://` address.
pub image: String,
/// Laid over the foot of the picture, and its alternative text.
#[serde(default)]
pub caption: String,
/// The picture's own size in pixels, when known: the browser then
/// holds its place in the strip before it has loaded, so pictures
/// further along can wait until they are swiped to.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
}
impl Slide {
/// Where the browser fetches the picture: https as given, anything
/// else as a content-repo path under `/site/`.
pub fn src(&self) -> String {
if self.image.starts_with("https://") || self.image.starts_with("/site/") {
self.image.clone()
} else {
format!("/site/{}", self.image.trim_start_matches('/'))
}
}
}
/// A link from every listed record to a page about it (`ResourceSpec.link`).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RowLink {
@@ -1904,6 +1954,17 @@ alternatives:
assert_eq!((reqs[1].kind.as_str(), reqs[1].value.as_deref()), ("text", None));
}
#[test]
fn a_slide_is_fetched_from_this_origin_unless_it_is_https() {
let q: Question = serde_yaml::from_str("name: Q?\nslideshow:\n slides:\n - { image: images/built/01.webp, caption: The canteen, width: 3207, height: 1400 }\n - { image: \"https://cdn.example/a.jpg\" }\n").unwrap();
let show = q.slideshow.unwrap();
assert_eq!(show.max_height, 60);
assert_eq!(show.slides[0].src(), "/site/images/built/01.webp");
assert_eq!(show.slides[0].width, Some(3207));
assert_eq!(show.slides[1].src(), "https://cdn.example/a.jpg");
assert_eq!(show.slides[1].caption, "");
}
#[test]
fn markdown_drops_html_and_unsafe_links() {
assert_eq!(render_inline_markdown("x <script>y</script> z"), "x y z");