//! The five public embeds, described.
//!
//! `54d7f8cf`. These were the last hand-written `",
embed_theme_css(),
EMBED_GEOMETRY_CSS,
EMBED_TYPOGRAPHY_CSS,
LAYOUT_CSS,
DOCUMENT_CSS,
)
}
/// A screen with one region, holding one node.
fn one(title: &str, node: Node) -> Screen {
Screen::list_detail(title, false).with(Slot::new(REGION, RegionKind::Pane).with(node))
}
/// A cover picture, cropped to its box.
///
/// `Fit::Cover` because a cover is a fixed square here and the art it holds is
/// any shape: the alternative is letterboxing inside a 40-pixel box, which is
/// the art unreadable and the box the wrong colour.
fn cover(url: &str) -> Node {
let mut picture = Picture::new(url, "");
picture.fit = layout::Fit::Cover;
Node::Image(picture)
}
/// What an item embed is drawn from.
///
/// A view rather than the database row, so a screen can be built in a test
/// without a connection. The same split every described screen here makes.
pub struct ItemView {
/// The item's title.
pub title: String,
/// The price as the canonical formatter writes it.
pub price: String,
/// What the buy control says: "Buy" or "Get".
pub button_text: String,
/// Where the buy control goes, on makenot.work.
pub purchase_url: String,
/// The cover art, when the item has any.
pub cover_image_url: Option,
/// Who made it.
pub creator_display_name: String,
/// Their page, on makenot.work.
pub profile_url: String,
/// The first 150 characters of the description.
pub description_excerpt: String,
}
/// The buy control: a link out to makenot.work.
///
/// [`Destination::External`](quasi_router::Destination::External), which is what
/// makes it an anchor with `rel="noopener noreferrer"` in the webview rather
/// than a button that asks a route. An embed's every control is one of these.
fn buy(view: &ItemView) -> Act {
Act::new(&view.button_text, Action::external(&view.purchase_url))
}
/// The buy button: cover, title, price, and the control, on one line.
///
/// The one embed that is genuinely a [`Row`]: a compact strip where the cover
/// is a thumbnail beside the title rather than the card's own picture. Drawn
/// with the `embed-button` body class, which is what sizes that thumbnail.
#[must_use]
pub fn item_button(view: &ItemView) -> Screen {
let mut row = Row::new("");
if let Some(url) = &view.cover_image_url {
row = row.part(layout::RowPart::Primary, cover(url));
}
let row = row
.part(layout::RowPart::Primary, Node::text(&view.title))
.meta(&view.price)
.part(layout::RowPart::Actions, Node::Act(buy(view)));
one(&view.title, Node::list([row]))
}
/// The product card: the cover, what it is, who made it, and the control.
///
/// Blocks rather than one row, which is the difference between a card and a
/// button. A [`Row`] is an inline run and its parts share a line by role, so a
/// card said as a row would read "coverTitle" with the excerpt and the price
/// crushed in beside it. What a card actually is — a picture, a heading, a line
/// about who made it, a paragraph, a price and a control, each on its own line —
/// is a region holding six nodes, every one of which the vocabulary already
/// names.
#[must_use]
pub fn item_card(view: &ItemView) -> Screen {
let mut slot = Slot::new(REGION, RegionKind::Pane);
if let Some(url) = &view.cover_image_url {
slot = slot.with(cover(url));
}
slot = slot.with(Node::section(&view.title)).with(Node::Link {
text: format!("by {}", view.creator_display_name),
action: Action::external(&view.profile_url),
});
if !view.description_excerpt.is_empty() {
slot = slot.with(Node::text(&view.description_excerpt));
}
let slot = slot
.with(Node::text(&view.price))
.with(Node::Act(buy(view)));
Screen::list_detail(&view.title, false).with(slot)
}
/// The audio player: the card, with the transport in a bespoke region.
///
/// `d86122cf`, ruled by Max 2026-08-18: **bespoke for now, widgets eventually.**
/// A play button, a scrub bar and an elapsed readout are a media transport, and
/// the vocabulary names none of the three on purpose — describing playback would
/// put scrub, rate and chapters into a core two of the three renderers could only
/// degrade. So this screen describes the chrome around the player and leaves the
/// player alone, which is exactly what a [`RegionKind::Bespoke`] is for.
///
/// The markup and the script that fills it are [`player_markup`], unchanged from
/// the template this replaces.
#[must_use]
pub fn item_player(view: &ItemView) -> Screen {
let mut slot = Slot::new(REGION, RegionKind::Pane);
if let Some(url) = &view.cover_image_url {
slot = slot.with(cover(url));
}
let slot = slot
.with(Node::section(&view.title))
.with(Node::text(format!("by {}", view.creator_display_name)))
.with(Node::text(&view.price))
.with(Node::Act(buy(view)));
Screen::list_detail(&view.title, false)
.with(slot)
.with(Slot::new(
PLAYER_REGION,
RegionKind::Bespoke {
name: "media-transport".into(),
},
))
}
/// The bespoke region the transport is mounted in.
pub const PLAYER_REGION: &str = "transport";
/// The player document: the described chrome, with the transport mounted.
///
/// Its own function rather than [`document`] with an argument, because the
/// player is the one embed whose renderer carries a fill and whose head carries
/// a second sheet. Both are about the same one thing — the island this screen
/// deliberately does not describe — so they are named together.
#[must_use]
pub fn player_document(view: &ItemView, preview_url: &str) -> String {
let mut shell = Shell::default()
.without_htmx()
.without_hyperscript()
.without_clock()
.without_fill()
.with_chrome(Chrome::new())
.with_head_first(format!("{}", head_first()));
shell.body_class = Some("embed-player".to_owned());
Webview::new()
.with_shell(shell)
.with_fill(PLAYER_REGION, player_markup(preview_url))
.screen(&item_player(view))
}
/// The player island, and the script that drives it.
///
/// Verbatim from `templates/embed/item_player.html`, which is the whole point of
/// a bespoke region: the behaviour is already implemented once and tested, and
/// converting the page around it must not rewrite it. The classes are this
/// host's own and are styled by [`PLAYER_CSS`].
///
/// `preview_url` is the one value from outside, and it is escaped here: a
/// bespoke fill is markup and nothing downstream escapes it.
#[must_use]
pub fn player_markup(preview_url: &str) -> String {
format!(
r#"
Preview
"#,
crate::helpers::escape_html(preview_url)
)
}
/// The transport's own rules, which are about a control the design system does
/// not name.
///
/// Kept out of [`DOCUMENT_CSS`] because it applies to one embed, and kept in
/// this crate because the markup it styles is this crate's. Colour is tokens
/// throughout, the same rule the rest of the document keeps.
pub const PLAYER_CSS: &str = "\
.transport { display: flex; align-items: center; gap: var(--step-base); }
.play-btn {
width: 32px; height: 32px; border-radius: 50%;
background: var(--action); color: var(--content-on-action); border: none;
cursor: pointer; display: flex; align-items: center; justify-content: center;
flex: none;
}
.play-btn:hover { background: var(--action-hover); }
.progress-bar {
flex: 1; height: 4px; background: var(--surface-sunken);
border-radius: 2px; cursor: pointer; position: relative;
}
.progress-fill { height: 100%; background: var(--action); border-radius: 2px; width: 0%; }
.time { font-family: var(--font-mono); color: var(--content-muted); white-space: nowrap; }
.preview-label { color: var(--content-muted); }
";
/// What a project embed is drawn from.
pub struct ProjectView {
/// The project's title.
pub title: String,
/// Who made it.
pub creator_display_name: String,
/// Their page, on makenot.work.
pub profile_url: String,
/// The project's page, on makenot.work.
pub project_url: String,
/// The cover art, when the project has any.
pub cover_image_url: Option,
/// The first 150 characters of the description.
pub description_excerpt: String,
/// How many items it holds.
pub item_count: usize,
/// What kind of project it is.
pub category_label: String,
}
/// The project card: [`item_card`]'s shape, about a project.
#[must_use]
pub fn project_card(view: &ProjectView) -> Screen {
let mut slot = Slot::new(REGION, RegionKind::Pane);
if let Some(url) = &view.cover_image_url {
slot = slot.with(cover(url));
}
slot = slot.with(Node::section(&view.title)).with(Node::Link {
text: format!("by {}", view.creator_display_name),
action: Action::external(&view.profile_url),
});
if !view.description_excerpt.is_empty() {
slot = slot.with(Node::text(&view.description_excerpt));
}
// The count and the kind read together and neither stands on its own, so
// they are one line rather than two nodes.
let slot = slot
.with(Node::text(format!(
"{} {} \u{b7} {}",
view.item_count,
if view.item_count == 1 {
"item"
} else {
"items"
},
view.category_label
)))
.with(Node::Act(Act::new(
"View project",
Action::external(&view.project_url),
)));
Screen::list_detail(&view.title, false).with(slot)
}
/// What a tip embed is drawn from.
pub struct TipView {
/// The creator's display name, for the document title.
pub display_name: String,
/// Their handle, which is what the label reads.
pub username: String,
/// Where the support control goes, on makenot.work.
pub tip_url: String,
/// Their avatar, when they have one.
pub avatar_url: Option,
}
/// The tip button.
#[must_use]
pub fn tip_button(view: &TipView) -> Screen {
let mut row = Row::new("");
if let Some(url) = &view.avatar_url {
row = row.part(layout::RowPart::Primary, cover(url));
}
let row = row
.part(
layout::RowPart::Primary,
Node::text(format!("Support @{}", view.username)),
)
.part(
layout::RowPart::Actions,
Node::Act(Act::new("Support", Action::external(&view.tip_url))),
);
one(&format!("Support {}", view.display_name), Node::list([row]))
}
#[cfg(test)]
mod tests {
use super::*;
fn item() -> ItemView {
ItemView {
title: "Item".into(),
price: "$9".into(),
button_text: "Buy".into(),
purchase_url: "https://makenot.work/buy/one".into(),
cover_image_url: Some("https://makenot.work/cover.png".into()),
creator_display_name: "Creator".into(),
profile_url: "https://makenot.work/u/creator".into(),
description_excerpt: "About it.".into(),
}
}
fn hex_literals(css: &str) -> Vec {
css.split('#')
.skip(1)
.map(|tail| {
tail.chars()
.take_while(char::is_ascii_hexdigit)
.collect::()
})
.filter(|run| run.len() == 3 || run.len() == 6)
.map(|run| format!("#{run}"))
.collect()
}
/// The regression guard the templates carried, kept: `#5a4bd6` sat in all
/// five of them as a hover violet matching no token in the tree, and nothing
/// was looking. What this host still writes by hand is two constants, so
/// this is now a check on two strings rather than on five rendered pages.
#[test]
fn this_host_writes_no_colour_of_its_own() {
for (name, css) in [("document", DOCUMENT_CSS), ("player", PLAYER_CSS)] {
let found = hex_literals(css);
assert!(
found.is_empty(),
"{name} writes its own colour: {found:?}. Use the token instead; \
a literal here drifts from the theme and nothing will report it.",
);
}
}
/// An embed cannot link a sheet, so every layer has to arrive in the head.
#[test]
fn an_embed_document_carries_the_whole_design_system() {
let html = document(&item_button(&item()), Some("embed-button"));
assert!(html.contains("