Skip to main content

max / makenotwork

Take quasi 0.90.0: the document facts move onto the screens quasicoherent ee1882e0. A Screen carries what is true of the document it is drawn into, so this server stops working around not being able to say it. /pricing set Shell::body_class in its renderer. The shell is built once and Arc'd at adapter construction, so that was a constant for every screen the adapter will ever serve -- right for one page and silently wrong for the second, and the conversion is producing the second. It is on the screen now, read off the same Screen::measure declared two lines above it. embeds::document loses its body_class parameter. It existed only because a shell was the one thing that could carry a body class and that function builds one per call, which is why the embeds could vary theirs and the adapter-served screens could not. The four callers say it on the screen they were already passing, and the horizontal card still decides at the route, where knowing which layout was asked for belongs.
Author: Max Johnson <me@maxj.phd> · 2026-08-30 22:03 UTC
Signed with PGP, not checked
Commit: 04284ed582a03e98949d6241fb2d599f231ef250
Parent: b83d46a
7 files changed, +94 insertions, -62 deletions
M server/Cargo.lock +24 -24
@@ -2875,7 +2875,7 @@
2875 2875 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
2876 2876 dependencies = [
2877 2877 "libc",
2878 - "windows-sys 0.59.0",
2878 + "windows-sys 0.52.0",
2879 2879 ]
2880 2880
2881 2881 [[package]]
@@ -4811,7 +4811,7 @@
4811 4811 source = "registry+https://github.com/rust-lang/crates.io-index"
4812 4812 checksum = "82cb6a9f675da968c63b6208c641b9dca58fc0133ae53375736b1767b0cab8bd"
4813 4813 dependencies = [
4814 - "windows-sys 0.59.0",
4814 + "windows-sys 0.61.2",
4815 4815 ]
4816 4816
4817 4817 [[package]]
@@ -5722,7 +5722,7 @@
5722 5722 source = "registry+https://github.com/rust-lang/crates.io-index"
5723 5723 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
5724 5724 dependencies = [
5725 - "windows-sys 0.59.0",
5725 + "windows-sys 0.61.2",
5726 5726 ]
5727 5727
5728 5728 [[package]]
@@ -6636,7 +6636,7 @@
6636 6636
6637 6637 [[package]]
6638 6638 name = "quasi-axum"
6639 - version = "0.89.0"
6639 + version = "0.90.0"
6640 6640 dependencies = [
6641 6641 "axum",
6642 6642 "http 1.4.2",
@@ -6647,7 +6647,7 @@
6647 6647
6648 6648 [[package]]
6649 6649 name = "quasi-basics"
6650 - version = "0.89.0"
6650 + version = "0.90.0"
6651 6651 dependencies = [
6652 6652 "makeover-layout",
6653 6653 "quasi-router",
@@ -6655,7 +6655,7 @@
6655 6655
6656 6656 [[package]]
6657 6657 name = "quasi-http"
6658 - version = "0.89.0"
6658 + version = "0.90.0"
6659 6659 dependencies = [
6660 6660 "form_urlencoded",
6661 6661 "http 1.4.2",
@@ -6664,7 +6664,7 @@
6664 6664
6665 6665 [[package]]
6666 6666 name = "quasi-router"
6667 - version = "0.89.0"
6667 + version = "0.90.0"
6668 6668 dependencies = [
6669 6669 "makeover-layout",
6670 6670 ]
@@ -6687,7 +6687,7 @@
6687 6687
6688 6688 [[package]]
6689 6689 name = "quasi-webview"
6690 - version = "0.89.0"
6690 + version = "0.90.0"
6691 6691 dependencies = [
6692 6692 "docengine",
6693 6693 "makeover-layout",
@@ -6763,7 +6763,7 @@
6763 6763 "once_cell",
6764 6764 "socket2",
6765 6765 "tracing",
6766 - "windows-sys 0.59.0",
6766 + "windows-sys 0.52.0",
6767 6767 ]
6768 6768
6769 6769 [[package]]
@@ -7326,7 +7326,7 @@
7326 7326 "errno",
7327 7327 "libc",
7328 7328 "linux-raw-sys 0.4.15",
7329 - "windows-sys 0.59.0",
7329 + "windows-sys 0.52.0",
7330 7330 ]
7331 7331
7332 7332 [[package]]
@@ -7339,7 +7339,7 @@
7339 7339 "errno",
7340 7340 "libc",
7341 7341 "linux-raw-sys 0.12.1",
7342 - "windows-sys 0.59.0",
7342 + "windows-sys 0.52.0",
7343 7343 ]
7344 7344
7345 7345 [[package]]
@@ -7419,7 +7419,7 @@
7419 7419 "security-framework 3.7.0",
7420 7420 "security-framework-sys",
7421 7421 "webpki-root-certs",
7422 - "windows-sys 0.59.0",
7422 + "windows-sys 0.52.0",
7423 7423 ]
7424 7424
7425 7425 [[package]]
@@ -8545,7 +8545,7 @@
8545 8545 "getrandom 0.4.3",
8546 8546 "once_cell",
8547 8547 "rustix 1.1.4",
8548 - "windows-sys 0.59.0",
8548 + "windows-sys 0.52.0",
8549 8549 ]
8550 8550
8551 8551 [[package]]
@@ -10761,11 +10761,11 @@
10761 10761
10762 10762 [[patch.unused]]
10763 10763 name = "quasi-immediate"
10764 - version = "0.89.0"
10764 + version = "0.90.0"
10765 10765
10766 10766 [[patch.unused]]
10767 10767 name = "quasi-notifs"
10768 - version = "0.89.0"
10768 + version = "0.90.0"
10769 10769
10770 10770 [[patch.unused]]
10771 10771 name = "quasi-store"
@@ -10773,15 +10773,7 @@
10773 10773
10774 10774 [[patch.unused]]
10775 10775 name = "quasi-tauri"
10776 - version = "0.89.0"
10777 -
10778 - [[patch.unused]]
10779 - name = "synckit-client"
10780 - version = "0.10.0"
10781 -
10782 - [[patch.unused]]
10783 - name = "synckit-config"
10784 - version = "0.2.0"
10776 + version = "0.90.0"
10785 10777
10786 10778 [[patch.unused]]
10787 10779 name = "kberg"
@@ -10794,3 +10786,11 @@
10794 10786 [[patch.unused]]
10795 10787 name = "painhours"
10796 10788 version = "0.1.0"
10789 +
10790 + [[patch.unused]]
10791 + name = "synckit-client"
10792 + version = "0.10.0"
10793 +
10794 + [[patch.unused]]
10795 + name = "synckit-config"
10796 + version = "0.2.0"
@@ -163,7 +163,7 @@
163 163 # through quasi beside its Askama equivalent, so the cost of describing a screen
164 164 # here is measured rather than argued. Not load-bearing for any shipped route.
165 165 # See wiki look-wave-2, tier G.
166 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.89" }
166 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.90" }
167 167 # The description vocabulary quasi's screen types are built from. Pinned here
168 168 # rather than reached through quasi-router's re-export because a described
169 169 # screen names FieldKind and Tone directly; it has to track what quasi-router
@@ -179,13 +179,13 @@
179 179 # For the request head the per-viewer state factory reads. axum re-exports it,
180 180 # but the factory's signature is quasi-axum's and names `http::request::Parts`.
181 181 http = "1.3.1"
182 - quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.89" }
183 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.89" }
182 + quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.90" }
183 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.90" }
184 184 # The first-party widget set. The carousel was proved here against three pages
185 185 # and then moved there; what is left in `quasi/widgets/carousel.rs` is the
186 186 # Askama glue. Not `makeover-basics`: a widget is an assembly of `Node`s, so it
187 187 # sits above quasi-router rather than inside the description suite.
188 - quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.89" }
188 + quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.90" }
189 189
190 190 # Git source browser
191 191 gix = { version = "0.86", default-features = false, features = ["blame", "mailmap", "revision", "sha1", "max-performance-safe"] }
@@ -277,7 +277,7 @@
277 277 # Written from the crate rather than copied into the repo for the reason the
278 278 # constants exist: a stale copy in a static directory is a silently dead bulk
279 279 # bar, and the version has to be the one the emitter agrees with.
280 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.89" }
280 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.90" }
281 281
282 282 [dev-dependencies]
283 283 tower = { version = "0.5.3", features = ["util"] }
@@ -47,7 +47,7 @@
47 47 use makeover_layout as layout;
48 48 use quasi_axum::Serves as _;
49 49 use quasi_router::screen::{Act, Picture, Row};
50 - use quasi_router::{Action, Chrome, Node, RegionKind, Screen, Slot};
50 + use quasi_router::{Action, Chrome, Document, Node, RegionKind, Screen, Slot};
51 51 use quasi_webview::{Shell, Webview};
52 52
53 53 use crate::templates::{EMBED_GEOMETRY_CSS, EMBED_TYPOGRAPHY_CSS, embed_theme_css};
@@ -97,13 +97,20 @@
97 97
98 98 /// A whole embed document: the design system, then the described screen.
99 99 ///
100 - /// `body_class` is what the two card layouts and the player differ by. It is
101 - /// [`Shell::body_class`] rather than a second description, because what changes
102 - /// between a horizontal and a vertical card is how one row is laid out in one
103 - /// document, which is this host's question about its own furniture.
100 + /// What the two card layouts and the player differ by is a class on `<body>`,
101 + /// and it rides on the screen ([`Screen::document`]) rather than on this
102 + /// function's shell. It used to be a second parameter, because a shell was the
103 + /// only thing that could carry a body class and this function builds one per
104 + /// call -- which is why the embeds could do it and the adapter-served screens
105 + /// could not. quasicoherent `ee1882e0` put the fact on the screen, so the
106 + /// workaround came out with it.
107 + ///
108 + /// Still this host's question about its own furniture rather than a described
109 + /// property: how one row is laid out in one frame is not something a terminal
110 + /// would have an answer to.
104 111 #[must_use]
105 - pub fn document(screen: &Screen, body_class: Option<&str>) -> String {
106 - let mut shell = Shell::default()
112 + pub fn document(screen: &Screen) -> String {
113 + let shell = Shell::default()
107 114 // An embed calls no route, so it takes no transport and no scripts.
108 115 // A document that does ask fails visibly on the first control pressed,
109 116 // and nothing here asks: every control is an external link.
@@ -129,7 +136,6 @@
129 136 .without_outline()
130 137 .with_chrome(Chrome::new())
131 138 .with_head_first(head_first());
132 - shell.body_class = body_class.map(str::to_owned);
133 139 Webview::new().with_shell(shell).screen(screen)
134 140 }
135 141
@@ -285,7 +291,7 @@
285 291 /// deliberately does not describe — so they are named together.
286 292 #[must_use]
287 293 pub fn player_document(view: &ItemView, preview_url: &str) -> String {
288 - let mut shell = Shell::default()
294 + let shell = Shell::default()
289 295 .without_htmx()
290 296 .without_hyperscript()
291 297 .without_clock()
@@ -297,11 +303,10 @@
297 303 .without_outline()
298 304 .with_chrome(Chrome::new())
299 305 .with_head_first(format!("{}<style>{PLAYER_CSS}</style>", head_first()));
300 - shell.body_class = Some("embed-player".to_owned());
301 306 Webview::new()
302 307 .with_shell(shell)
303 308 .with_fill(PLAYER_REGION, player_markup(preview_url))
304 - .screen(&item_player(view))
309 + .screen(&item_player(view).documented(Document::default().classed("embed-player")))
305 310 }
306 311
307 312 /// The player island, and the script that drives it.
@@ -485,7 +490,8 @@
485 490 /// An embed cannot link a sheet, so every layer has to arrive in the head.
486 491 #[test]
487 492 fn an_embed_document_carries_the_whole_design_system() {
488 - let html = document(&item_button(&item()), Some("embed-button"));
493 + let html =
494 + document(&item_button(&item()).documented(Document::default().classed("embed-button")));
489 495 assert!(html.contains("<style>"), "{html}");
490 496 // Colour, spacing and typography come from the generated files, so the
491 497 // check is that each block is present rather than what is in it.
@@ -500,7 +506,8 @@
500 506 /// takes no transport.
501 507 #[test]
502 508 fn an_embed_document_carries_no_script() {
503 - let html = document(&item_button(&item()), Some("embed-button"));
509 + let html =
510 + document(&item_button(&item()).documented(Document::default().classed("embed-button")));
504 511 assert!(!html.contains("<script"), "{html}");
505 512 assert!(!html.contains("htmx"), "{html}");
506 513 }
@@ -509,7 +516,8 @@
509 516 /// anchor rather than something that asks a route.
510 517 #[test]
511 518 fn every_control_is_a_link_out() {
512 - let html = document(&item_button(&item()), Some("embed-button"));
519 + let html =
520 + document(&item_button(&item()).documented(Document::default().classed("embed-button")));
513 521 assert!(
514 522 html.contains(r#"href="https://makenot.work/buy/one""#),
515 523 "{html}"
@@ -525,7 +533,8 @@
525 533 fn a_title_cannot_open_a_tag() {
526 534 let mut view = item();
527 535 view.title = "<script>alert(1)</script>".into();
528 - let html = document(&item_button(&view), Some("embed-button"));
536 + let html =
537 + document(&item_button(&view).documented(Document::default().classed("embed-button")));
529 538 assert!(!html.contains("<script>alert"), "{html}");
530 539 assert!(html.contains("&lt;script&gt;"), "{html}");
531 540 }
@@ -69,8 +69,8 @@
69 69
70 70 use makeover_layout as layout;
71 71 use quasi_router::{
72 - Action, Cell, Cells, Choice, Column, Consult, Field, Meter, Node, RegionKind, Request,
73 - Response, RouteError, Row, Screen, Slot,
72 + Action, Cell, Cells, Choice, Column, Consult, Document, Field, Meter, Node, RegionKind,
73 + Request, Response, RouteError, Row, Screen, Slot,
74 74 };
75 75 use quasi_webview::Webview;
76 76
@@ -361,9 +361,16 @@
361 361 /// The described document, top to bottom.
362 362 fn page(state: &Pricing, dials: &Dials) -> Screen {
363 363 Screen::list_detail("Pricing Calculator - Makenotwork", false)
364 - // What `pricing.html` said as `class="centered-page"`. The renderer
365 - // turns it back into that class; see `renderer`.
366 364 .measured(MEASURE)
365 + // What `pricing.html` said as `class="centered-page"`, read off the
366 + // measure declared on the line above rather than off a route table.
367 + //
368 + // On the screen and not on the shell (quasicoherent `ee1882e0`). The
369 + // shell is built once and `Arc`'d at adapter construction, so a class
370 + // set there is a constant for every screen that adapter ever serves --
371 + // which is right for one page and silently wrong for the second, and
372 + // the second is what the conversion is producing.
373 + .documented(Document::default().classed(crate::shell::measure(MEASURE)))
367 374 // The whole of what `base.html` put in `<meta name="description">`,
368 375 // including the fee sentence, because this one string is now all three
369 376 // tags: the social pair and the plain one. It carried only the first
@@ -662,7 +669,7 @@
662 669 /// script writes into -- which is what `Shell::with_body_last` is for.
663 670 #[must_use]
664 671 pub fn renderer() -> Webview {
665 - let mut shell = crate::shell::described()
672 + let shell = crate::shell::described()
666 673 .with_body_first(crate::shell::skip_link(PAGE))
667 674 .with_body_last(crate::shell::body_last());
668 675 // The `Shell::head` append that stood here is gone as of quasi 0.80
@@ -672,9 +679,6 @@
672 679 // string the screen already declares. An escape hatch spending itself on
673 680 // something a described property carries is the escape hatch going unused.
674 681 //
675 - // What `pricing.html` wrote as `class="centered-page"`, read off the same
676 - // measure the screen declares rather than off a route table.
677 - shell.body_class = Some(crate::shell::measure(MEASURE).to_owned());
678 682 Webview::new().with_shell(shell)
679 683 }
680 684
@@ -843,6 +847,24 @@
843 847 approx(dials.inputs.other_pct, 0.126, "12.6% as a fraction");
844 848 }
845 849
850 + /// The class `pricing.html` carried, still on `<body>` and now the
851 + /// screen's rather than the adapter's.
852 + ///
853 + /// quasicoherent `ee1882e0`. Asserted on the screen rather than on the
854 + /// emitted markup because that is where the change is: the renderer folding
855 + /// a document into a `<body>` tag is quasi-webview's own test, and this
856 + /// server does not link `quasi-http` to call `Serves::screen` here.
857 + #[test]
858 + fn the_document_carries_the_class_the_template_carried() {
859 + let screen = page(&state(false), &Dials::read(&state(false), &carrying(&[])));
860 +
861 + assert_eq!(
862 + screen.document.body_class.as_deref(),
863 + Some(crate::shell::measure(MEASURE))
864 + );
865 + assert_eq!(screen.document.body_class.as_deref(), Some("centered-page"));
866 + }
867 +
846 868 /// Where the reader sits against the crossover, drawn only when there is a
847 869 /// crossover to sit against.
848 870 #[test]
@@ -14,6 +14,7 @@
14 14 db::{self, ItemId},
15 15 error::{AppError, Result},
16 16 };
17 + use quasi_router::Document;
17 18 use sqlx::PgPool;
18 19
19 20 /// Shared context fetched for all item embeds.
@@ -147,8 +148,7 @@
147 148
148 149 let view = ctx.view(&config);
149 150 let mut response = Html(embeds::document(
150 - &embeds::item_button(&view),
151 - Some("embed-button"),
151 + &embeds::item_button(&view).documented(Document::default().classed("embed-button")),
152 152 ))
153 153 .into_response();
154 154 set_embed_headers(&mut response);
@@ -176,16 +176,17 @@
176 176
177 177 let view = ctx.view(&config);
178 178 // The two layouts are one description in two documents, which is what
179 - // `Shell::body_class` is: the row is the same row, and how it is laid out in
180 - // this frame is a fact about this host's furniture.
179 + // `Screen::document` is: the row is the same row, and how it is laid out in
180 + // this frame is a fact about this host's furniture. Decided here rather
181 + // than inside `item_card`, because which layout was asked for is a fact
182 + // about the request.
181 183 let body_class = if is_horizontal {
182 184 "embed-card embed-card-horizontal"
183 185 } else {
184 186 "embed-card"
185 187 };
186 188 let mut response = Html(embeds::document(
187 - &embeds::item_card(&view),
188 - Some(body_class),
189 + &embeds::item_card(&view).documented(Document::default().classed(body_class)),
189 190 ))
190 191 .into_response();
191 192 set_embed_headers(&mut response);
@@ -11,6 +11,7 @@
11 11 db::{self},
12 12 error::{AppError, Result},
13 13 };
14 + use quasi_router::Document;
14 15 use sqlx::PgPool;
15 16
16 17 use super::item::set_embed_headers;
@@ -61,8 +62,7 @@
61 62 category_label: category_label.to_string(),
62 63 };
63 64 let mut response = Html(embeds::document(
64 - &embeds::project_card(&view),
65 - Some("embed-card"),
65 + &embeds::project_card(&view).documented(Document::default().classed("embed-card")),
66 66 ))
67 67 .into_response();
68 68 set_embed_headers(&mut response);
@@ -11,6 +11,7 @@
11 11 db::{self, Username},
12 12 error::{AppError, Result},
13 13 };
14 + use quasi_router::Document;
14 15 use sqlx::PgPool;
15 16
16 17 use super::item::set_embed_headers;
@@ -45,8 +46,7 @@
45 46 avatar_url: user.avatar_url,
46 47 };
47 48 let mut response = Html(embeds::document(
48 - &embeds::tip_button(&view),
49 - Some("embed-tip"),
49 + &embeds::tip_button(&view).documented(Document::default().classed("embed-tip")),
50 50 ))
51 51 .into_response();
52 52 set_embed_headers(&mut response);