Skip to main content

max / makenotwork

Take quasi 0.69.0, and make the media tile one control `Act::shows` (quasicoherent `db998898`) is the member the media-picker conversion filed. The tile was a `media-card-<id>` region holding a picture and a button as siblings, and only the file name answered a press: a reader aiming at the thumbnail, which is the whole affordance of a picture picker, hit nothing. `fn card` returns one `Node::Act` now. The per-tile region is gone and so is the id it needed, which is what `Entry::id` existed for; the field stays for the caller's convenience and addresses nothing. `style.css` addresses the button inside the grid rather than a `media-tile` widget, and the comment there that named the gap now names the fix. Also fixes a latent break from taking 0.68.0: the copy script is independent of htmx by design, so `without_htmx` keeps it, and an embed document has to refuse it by name like the other three. `an_embed_document_carries_no_script` caught it. Taking 0.68.0 was checked and not tested, which is why it landed.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-27 18:13 UTC
Signed with PGP, not checked
Commit: 4df80dbc112ddd9ef21cda212f663e5458150b4c
Parent: 66a4ddd
5 files changed, +86 insertions, -87 deletions
M server/Cargo.lock +29 -29
@@ -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.68.0"
6639 + version = "0.69.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.68.0"
6650 + version = "0.69.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.68.0"
6658 + version = "0.69.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.68.0"
6667 + version = "0.69.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.68.0"
6690 + version = "0.69.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]]
@@ -10759,6 +10759,22 @@
10759 10759 "pkg-config",
10760 10760 ]
10761 10761
10762 + [[patch.unused]]
10763 + name = "quasi-immediate"
10764 + version = "0.69.0"
10765 +
10766 + [[patch.unused]]
10767 + name = "quasi-notifs"
10768 + version = "0.69.0"
10769 +
10770 + [[patch.unused]]
10771 + name = "quasi-store"
10772 + version = "0.1.0"
10773 +
10774 + [[patch.unused]]
10775 + name = "quasi-tauri"
10776 + version = "0.69.0"
10777 +
10762 10778 [[patch.unused]]
10763 10779 name = "synckit-client"
10764 10780 version = "0.9.1"
@@ -10778,19 +10794,3 @@
10778 10794 [[patch.unused]]
10779 10795 name = "painhours"
10780 10796 version = "0.1.0"
10781 -
10782 - [[patch.unused]]
10783 - name = "quasi-immediate"
10784 - version = "0.68.0"
10785 -
10786 - [[patch.unused]]
10787 - name = "quasi-notifs"
10788 - version = "0.68.0"
10789 -
10790 - [[patch.unused]]
10791 - name = "quasi-store"
10792 - version = "0.1.0"
10793 -
10794 - [[patch.unused]]
10795 - name = "quasi-tauri"
10796 - version = "0.68.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.68" }
166 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.69" }
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.68" }
183 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.68" }
182 + quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.69" }
183 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.69" }
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.68" }
188 + quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.69" }
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.68" }
280 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.69" }
281 281
282 282 [dev-dependencies]
283 283 tower = { version = "0.5.3", features = ["util"] }
@@ -11949,14 +11949,20 @@
11949 11949 margin-top: var(--gap-section);
11950 11950 }
11951 11951
11952 - [data-widget="media-tile"] {
11952 + /* The tile IS the control, since quasi 0.69.0 (`Act::shows`). It was a
11953 + `media-tile` region holding a picture and a button as siblings, and only the
11954 + button answered a press; a reader aiming at the thumbnail hit nothing. The
11955 + widget name went with the region, so the tile is addressed as the button
11956 + inside the grid. */
11957 + [data-widget="media-picker-grid"] .button {
11953 11958 display: flex;
11954 11959 flex-direction: column;
11955 11960 gap: var(--gap-peer);
11956 - padding: var(--gap-peer);
11957 - border: 1px solid var(--border);
11958 - background: var(--surface-raised);
11959 11961 min-width: 0;
11962 + /* The file name wraps rather than pushing the tile wide. */
11963 + overflow-wrap: anywhere;
11964 + text-align: left;
11965 + font-size: var(--text-note);
11960 11966 }
11961 11967
11962 11968 /* A cap, not a height, and never a bare `img`. The design system already sets
@@ -11965,16 +11971,6 @@
11965 11971 drift check refuses, and it is right to. `max-height` is not one of them, and
11966 11972 it is all this needs: `Fit::Contain` is in the description, so a capped
11967 11973 picture letterboxes inside the cap rather than being cropped or stretched. */
11968 - [data-widget="media-tile"] .picture-img {
11974 + [data-widget="media-picker-grid"] .picture-img {
11969 11975 max-height: 80px;
11970 11976 }
11971 -
11972 - /* The file name is the control, so it wraps rather than pushing the tile wide.
11973 - `Act` is a label and nothing in the vocabulary says a control reads as a
11974 - picture, so the thumbnail above it is a sibling and only this half answers a
11975 - press. Filed rather than faked; see the module header. */
11976 - [data-widget="media-tile"] .button {
11977 - overflow-wrap: anywhere;
11978 - text-align: left;
11979 - font-size: var(--text-note);
11980 - }
@@ -110,19 +110,21 @@
110 110 // A document that does ask fails visibly on the first control pressed,
111 111 // and nothing here asks: every control is an external link.
112 112 //
113 - // Six `without_` calls now. quasi 0.54.0 added the fill script, and
114 - // 0.59.0 and 0.60.0 added the reveal and repeat scripts. All three are
115 - // independent of htmx by design — what they read is attributes on a
116 - // control, a region or a fieldset — so dropping the transport does not
117 - // drop them, and each has to be refused by name. An embed names no
118 - // destination field, no conditional region and no repeating question,
119 - // so all three have nothing to read here.
113 + // Seven `without_` calls now. quasi 0.54.0 added the fill script,
114 + // 0.59.0 and 0.60.0 the reveal and repeat scripts, and 0.68.0 the copy
115 + // script. All four are independent of htmx by design — what they read
116 + // is attributes on a control, a region or a fieldset — so dropping the
117 + // transport does not drop them, and each has to be refused by name. An
118 + // embed names no destination field, no conditional region, no repeating
119 + // question and no act that copies, so all four have nothing to read
120 + // here.
120 121 .without_htmx()
121 122 .without_hyperscript()
122 123 .without_clock()
123 124 .without_fill()
124 125 .without_reveal()
125 126 .without_repeat()
127 + .without_copy()
126 128 .with_chrome(Chrome::new())
127 129 .with_head_first(head_first());
128 130 shell.body_class = body_class.map(str::to_owned);
@@ -290,6 +292,7 @@
290 292 .without_fill()
291 293 .without_reveal()
292 294 .without_repeat()
295 + .without_copy()
293 296 .with_chrome(Chrome::new())
294 297 .with_head_first(format!("{}<style>{PLAYER_CSS}</style>", head_first()));
295 298 shell.body_class = Some("embed-player".to_owned());
@@ -165,9 +165,6 @@
165 165 /// What the grid of tiles is called.
166 166 const GRID_WIDGET: &str = "media-picker-grid";
167 167
168 - /// What one tile is called.
169 - const TILE: &str = "media-tile";
170 -
171 168 /// A named assembly, which is the hook a stylesheet attaches to.
172 169 ///
173 170 /// `RegionKind::Widget` rather than `Group` on all three, because that is what
@@ -313,49 +310,45 @@
313 310 ));
314 311 }
315 312 for entry in entries {
316 - slot = slot.with(Node::Region(card(entry, into)));
313 + slot = slot.with(card(entry, into));
317 314 }
318 315 slot
319 316 }
320 317
321 - /// One tile: the thumbnail, and the control that deposits its reference.
318 + /// One tile: one control, showing the thumbnail and saying the file name.
322 319 ///
323 - /// The control's label is the file name, which is the fact the shipped script
324 - /// lost to a misspelled JSON key. Its deposit is `markdown_ref` verbatim, which
325 - /// is the fact that script wrapped twice.
320 + /// The label is the file name, which is the fact the shipped script lost to a
321 + /// misspelled JSON key. The deposit is `markdown_ref` verbatim, which is the
322 + /// fact that script wrapped twice.
326 323 ///
327 324 /// `Action::local()`, so the press makes no request. The old picker made none
328 325 /// either; what it did instead was 155 lines of DOM building.
329 326 ///
330 - /// # Two members and half a tile, which is the gap this conversion found
327 + /// # This was two members and half a tile until quasi 0.69.0
331 328 ///
332 - /// The shipped card is one pressable box with a picture in it. Here the picture
333 - /// and the control are siblings, and only the control answers a press, because
334 - /// **an [`Act`] is a label and nothing in the vocabulary says a control reads as
335 - /// a picture.** `Cell::new` and `Row::new` both take a string, so a picture
336 - /// cannot sit beside a control in a table or a list either: [`Node::Image`]
337 - /// goes in a region body and nowhere else.
329 + /// The conversion found that an [`Act`] was a label and nothing said a control
330 + /// reads as a picture, so the tile was a region holding the picture and the
331 + /// control as siblings and only the file name answered a press -- a reader
332 + /// aiming at the thumbnail, which is the whole affordance of a picture picker,
333 + /// hit nothing. `Act::shows` (quasicoherent `db998898`) is that member, and the
334 + /// tile is one control again.
338 335 ///
339 - /// So a tile is a region, which is why [`Entry::id`] exists. Filed rather than
340 - /// faked, the same way the caret was: a description that drew a pressable
341 - /// picture by putting the picture in the label would be spelling markup in a
342 - /// string.
343 - fn card(entry: &Entry, into: &str) -> Slot {
344 - let mut slot = Slot::new(format!("media-card-{}", entry.id), widget(TILE));
336 + /// The region went with it, and so did the id it needed. [`Entry::id`] is kept
337 + /// for the caller's convenience and no longer addresses anything.
338 + fn card(entry: &Entry, into: &str) -> Node {
339 + let mut act = Act::new(&entry.filename, Action::local()).filling(into, &entry.reference);
345 340 if entry.image {
346 341 // The alt is the file name, because that is what the tile is for: a
347 342 // reader who cannot see the thumbnail is choosing between file names,
348 - // which is exactly what the control below says.
343 + // which is exactly what the control says.
349 344 let mut picture = Picture::new(&entry.url, &entry.filename).lazy();
350 345 // The letterbox, which is what the shipped tile did with
351 346 // `object-fit: contain`: a thumbnail grid whose files are not all one
352 347 // shape, and the whole picture matters more than filling the square.
353 348 picture.fit = Fit::Contain;
354 - slot = slot.with(Node::Image(picture));
349 + act = act.showing(picture);
355 350 }
356 - slot.with(Node::Act(
357 - Act::new(&entry.filename, Action::local()).filling(into, &entry.reference),
358 - ))
351 + Node::Act(act)
359 352 }
360 353
361 354 /// The typed filter.
@@ -472,13 +465,20 @@
472 465 // owns this picture's width, height and background, and an app rule
473 466 // taking one of those is what `makeover-build`'s drift check refuses.
474 467 assert!(html.contains(r#"data-fit="contain""#), "{html}");
475 - // And the tile is a region, which is the only place a picture can sit
476 - // beside a control. Its id is the file's, so a library of two hundred
477 - // does not have two hundred elements answering to one name.
468 + // And the tile is the control, since quasi 0.69.0: the picture is
469 + // inside the element that answers the press, so aiming at the thumbnail
470 + // picks the file. It was a `media-card-<id>` region holding the picture
471 + // and the button as siblings, and only the button answered.
478 472 assert!(
479 - html.contains(r#"id="media-card-11111111-1111-1111-1111-111111111111""#),
480 - "{html}"
473 + !html.contains("media-card-"),
474 + "the per-tile region is gone: {html}"
481 475 );
476 + let control = html
477 + .split_once("<button")
478 + .and_then(|(_, rest)| rest.split_once("</button>"))
479 + .map_or("", |(inner, _)| inner);
480 + assert!(control.contains("<img"), "{html}");
481 + assert!(control.contains("kick.png"), "{html}");
482 482 }
483 483
484 484 /// A video has no thumbnail to show, so it is the control alone rather than