max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
5 files changed,
+201 insertions,
-29 deletions
| @@ -5390,9 +5390,9 @@ | |||
| 5390 | 5390 | ||
| 5391 | 5391 | [[package]] | |
| 5392 | 5392 | name = "makeover-build" | |
| 5393 | - | version = "0.56.0" | |
| 5393 | + | version = "0.57.0" | |
| 5394 | 5394 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5395 | - | checksum = "56c8903ef617932723aea44f7ada31c7ce4f89d008e8d25ebaee956b2258bb91" | |
| 5395 | + | checksum = "0f8d7b3e487f34397f239cd34820a6f0a02fdd57bdd8c72af851c4e0dd8810ef" | |
| 5396 | 5396 | dependencies = [ | |
| 5397 | 5397 | "makeover", | |
| 5398 | 5398 | "makeover-geometry", | |
| @@ -5433,9 +5433,9 @@ | |||
| 5433 | 5433 | ||
| 5434 | 5434 | [[package]] | |
| 5435 | 5435 | name = "makeover-webview" | |
| 5436 | - | version = "0.63.0" | |
| 5436 | + | version = "0.64.0" | |
| 5437 | 5437 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5438 | - | checksum = "9149f06aa7b8f4660609a680169d76075d91def63c23127bd84af9b183be22fa" | |
| 5438 | + | checksum = "6b91db6e169bc8b7c7225e17b6bc83f9c6f1879eb3bc6f1554e96cb621c84e5c" | |
| 5439 | 5439 | dependencies = [ | |
| 5440 | 5440 | "makeover-geometry", | |
| 5441 | 5441 | "makeover-layout", | |
| @@ -6636,7 +6636,7 @@ | |||
| 6636 | 6636 | ||
| 6637 | 6637 | [[package]] | |
| 6638 | 6638 | name = "quasi-axum" | |
| 6639 | - | version = "0.72.0" | |
| 6639 | + | version = "0.73.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.72.0" | |
| 6650 | + | version = "0.73.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.72.0" | |
| 6658 | + | version = "0.73.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.72.0" | |
| 6667 | + | version = "0.73.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.72.0" | |
| 6690 | + | version = "0.73.0" | |
| 6691 | 6691 | dependencies = [ | |
| 6692 | 6692 | "docengine", | |
| 6693 | 6693 | "makeover-layout", | |
| @@ -10781,11 +10781,11 @@ | |||
| 10781 | 10781 | ||
| 10782 | 10782 | [[patch.unused]] | |
| 10783 | 10783 | name = "quasi-immediate" | |
| 10784 | - | version = "0.72.0" | |
| 10784 | + | version = "0.73.0" | |
| 10785 | 10785 | ||
| 10786 | 10786 | [[patch.unused]] | |
| 10787 | 10787 | name = "quasi-notifs" | |
| 10788 | - | version = "0.72.0" | |
| 10788 | + | version = "0.73.0" | |
| 10789 | 10789 | ||
| 10790 | 10790 | [[patch.unused]] | |
| 10791 | 10791 | name = "quasi-store" | |
| @@ -10793,4 +10793,4 @@ | |||
| 10793 | 10793 | ||
| 10794 | 10794 | [[patch.unused]] | |
| 10795 | 10795 | name = "quasi-tauri" | |
| 10796 | - | version = "0.72.0" | |
| 10796 | + | version = "0.73.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.72" } | |
| 166 | + | quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.73" } | |
| 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 | |
| @@ -175,17 +175,17 @@ | |||
| 175 | 175 | # through quasi-webview for makeover-layout's reason: two `form::` paths from | |
| 176 | 176 | # two resolutions are two crates, and the `data-format="markdown"` rules in | |
| 177 | 177 | # `static/layout.css` are generated from this one. | |
| 178 | - | makeover-webview = "0.63.0" | |
| 178 | + | makeover-webview = "0.64.0" | |
| 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.72" } | |
| 183 | - | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.72" } | |
| 182 | + | quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.73" } | |
| 183 | + | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.73" } | |
| 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.72" } | |
| 188 | + | quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.73" } | |
| 189 | 189 | ||
| 190 | 190 | # Git source browser | |
| 191 | 191 | gix = { version = "0.86", default-features = false, features = ["blame", "mailmap", "revision", "sha1", "max-performance-safe"] } | |
| @@ -254,7 +254,7 @@ | |||
| 254 | 254 | # Emits static/geometry.css (makeover-geometry) and static/layout.css | |
| 255 | 255 | # (makeover-webview). The same generator GO and BB run; only the output paths | |
| 256 | 256 | # differ, since the server serves its stylesheets rather than bundling them. | |
| 257 | - | makeover-build = "0.56.0" | |
| 257 | + | makeover-build = "0.57.0" | |
| 258 | 258 | # Read directly for the embeds' own copy of the spacing layer, which they need | |
| 259 | 259 | # because an iframe cannot link a stylesheet. makeover-build does not re-export | |
| 260 | 260 | # it, so the pin lives here and has to track the one makeover-build resolves. | |
| @@ -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.72" } | |
| 280 | + | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.73" } | |
| 281 | 281 | ||
| 282 | 282 | [dev-dependencies] | |
| 283 | 283 | tower = { version = "0.5.3", features = ["util"] } |
| @@ -510,12 +510,22 @@ | |||
| 510 | 510 | /// field messages of its own. It stops being dead the moment a described | |
| 511 | 511 | /// screen here carries a note. | |
| 512 | 512 | /// | |
| 513 | + | /// 46 to 48, 2026-08-28: makeover-webview 0.64.0 added `.run-overflow` and | |
| 514 | + | /// `.run-overflow-items`, the control a menu run's shed members move into. They | |
| 515 | + | /// arrive dead here by construction and will stay dead permanently, which is | |
| 516 | + | /// new: the two classes above are written by a Rust dependency and these are | |
| 517 | + | /// written by a *script* one, `quasi-webview`'s `menu.js`, which builds the | |
| 518 | + | /// control at the moment it measures the run as tight. No markup anywhere can | |
| 519 | + | /// spell them, because which members no longer fit is not a fact any markup | |
| 520 | + | /// holds. See `makeover_webview::RUN_CLASSES`, which carries them for exactly | |
| 521 | + | /// this reason. | |
| 522 | + | /// | |
| 513 | 523 | /// So a rising number here is not automatically a regression. Read it against | |
| 514 | 524 | /// what moved: markup deleted in favour of a description will raise it, and a | |
| 515 | 525 | /// class that genuinely lost its writer will too. The list the failure prints | |
| 516 | 526 | /// is what tells them apart -- if a name on it is one this server still means | |
| 517 | 527 | /// to render, that is the bug. | |
| 518 | - | const DEAD_VOCABULARY_HIGH_WATER: usize = 46; | |
| 528 | + | const DEAD_VOCABULARY_HIGH_WATER: usize = 48; | |
| 519 | 529 | ||
| 520 | 530 | /// The scripts this build script generates into `static/`. | |
| 521 | 531 | /// | |
| @@ -547,7 +557,7 @@ | |||
| 547 | 557 | /// and unserved -- a 404 per document, with nothing in the log -- and | |
| 548 | 558 | /// `quasi-awaiting.js` would have joined them the moment it shipped. Found | |
| 549 | 559 | /// adding the third, which is the only reason the other two were. | |
| 550 | - | const GENERATED_STATIC_JS: [(&str, &str); 9] = [ | |
| 560 | + | const GENERATED_STATIC_JS: [(&str, &str); 10] = [ | |
| 551 | 561 | ("quasi-selection.js", quasi_webview::SELECTION_JS), | |
| 552 | 562 | ("quasi-clock.js", quasi_webview::CLOCK_JS), | |
| 553 | 563 | ("quasi-download.js", quasi_webview::DOWNLOAD_JS), | |
| @@ -557,6 +567,7 @@ | |||
| 557 | 567 | ("quasi-repeat.js", quasi_webview::REPEAT_JS), | |
| 558 | 568 | ("quasi-awaiting.js", quasi_webview::AWAITING_JS), | |
| 559 | 569 | ("quasi-instant.js", quasi_webview::INSTANT_JS), | |
| 570 | + | ("quasi-menu.js", quasi_webview::MENU_JS), | |
| 560 | 571 | ]; | |
| 561 | 572 | ||
| 562 | 573 | /// Every file that can carry a class name. |
| @@ -110,14 +110,15 @@ | |||
| 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 | - | // 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. | |
| 113 | + | // Eight `without_` calls now. quasi 0.54.0 added the fill script, | |
| 114 | + | // 0.59.0 and 0.60.0 the reveal and repeat scripts, 0.68.0 the copy | |
| 115 | + | // script and 0.73.0 the menu script. All five are independent of htmx by | |
| 116 | + | // design — what they read is attributes on a control, a region or a | |
| 117 | + | // fieldset, or the room a run has — so dropping the transport does not | |
| 118 | + | // drop them, and each has to be refused by name. An embed names no | |
| 119 | + | // destination field, no conditional region, no repeating question, no | |
| 120 | + | // act that copies and no run that folds, so all five have nothing to | |
| 121 | + | // read here. | |
| 121 | 122 | .without_htmx() | |
| 122 | 123 | .without_hyperscript() | |
| 123 | 124 | .without_clock() | |
| @@ -125,6 +126,7 @@ | |||
| 125 | 126 | .without_reveal() | |
| 126 | 127 | .without_repeat() | |
| 127 | 128 | .without_copy() | |
| 129 | + | .without_menu() | |
| 128 | 130 | .with_chrome(Chrome::new()) | |
| 129 | 131 | .with_head_first(head_first()); | |
| 130 | 132 | shell.body_class = body_class.map(str::to_owned); | |
| @@ -293,6 +295,7 @@ | |||
| 293 | 295 | .without_reveal() | |
| 294 | 296 | .without_repeat() | |
| 295 | 297 | .without_copy() | |
| 298 | + | .without_menu() | |
| 296 | 299 | .with_chrome(Chrome::new()) | |
| 297 | 300 | .with_head_first(format!("{}<style>{PLAYER_CSS}</style>", head_first())); | |
| 298 | 301 | shell.body_class = Some("embed-player".to_owned()); |
| @@ -1,0 +1,158 @@ | |||
| 1 | + | // Folding a run's overflow into a menu, because only a browser can measure it. | |
| 2 | + | // | |
| 3 | + | // `makeover_layout::Fallback::Menu` says the members a `Shed` would drop move | |
| 4 | + | // into one overflow control instead. That needs to know the run is out of room, | |
| 5 | + | // and CSS cannot ask: `@container` compares against a `<length>`, and there is | |
| 6 | + | // no `@container (inline-size < min-content)`. Every honest CSS spelling needs | |
| 7 | + | // an authored breakpoint, which is the thing the room ruling exists to forbid. | |
| 8 | + | // So the measuring is here, where the browser already knows. | |
| 9 | + | // | |
| 10 | + | // It reads what makeover-webview emits and nothing else: `.run-menu` for a run | |
| 11 | + | // that asked, and `.cell-drops-first` / `.cell-drops-next` for what a member is | |
| 12 | + | // worth. The control it builds carries that crate's `.run-overflow` classes, so | |
| 13 | + | // the surface and the shadow are the design system's and only the measuring is | |
| 14 | + | // this file's. | |
| 15 | + | // | |
| 16 | + | // Degrading is the point of the mark. A page that never runs this keeps | |
| 17 | + | // `.run-menu`'s own `flex-wrap: wrap` and every member stays reachable, which is | |
| 18 | + | // rule 1; `data-menu` is what switches the run back to `nowrap`, and it is | |
| 19 | + | // written from here, so the fallback is the default rather than the failure. | |
| 20 | + | // | |
| 21 | + | // The MNW server carried a copy of this for its four Askama tab strips | |
| 22 | + | // (`core/tabs.ts`'s `tabOverflow`), which looked for `.tabs` and never saw the | |
| 23 | + | // described strips at all. | |
| 24 | + | (() => { | |
| 25 | + | "use strict"; | |
| 26 | + | ||
| 27 | + | // What a member is worth when room runs out, lowest first. Read off the | |
| 28 | + | // classes rather than off an attribute of our own: these are what | |
| 29 | + | // `Ranked::priority` already reaches the markup as, and inventing a second | |
| 30 | + | // spelling is how two encodings of one fact stop agreeing. | |
| 31 | + | const TIERS = ["cell-drops-first", "cell-drops-next"]; | |
| 32 | + | ||
| 33 | + | // A tab strip is one member of its run and its tabs are what fold. The five | |
| 34 | + | // described MNW strips are all this shape: `across(Fallback::Menu)` on a | |
| 35 | + | // tab group whose run holds no members of its own, so a reading that only | |
| 36 | + | // looked at the run's children would find one strip that always fits and | |
| 37 | + | // fold nothing. | |
| 38 | + | const STRIP = '[role="tablist"]'; | |
| 39 | + | ||
| 40 | + | /** | |
| 41 | + | * The element whose children fold, and the children in shedding order. | |
| 42 | + | * | |
| 43 | + | * Order is by priority tier and then last-first within a tier. Last-first | |
| 44 | + | * is what "shed by priority, not by document order" leaves once priority is | |
| 45 | + | * spent: among members worth the same, the ones at the end are the ones a | |
| 46 | + | * reader reaches for least. For a strip, where no tab carries a priority at | |
| 47 | + | * all, that is the whole rule and it is the one the description means -- | |
| 48 | + | * the last tabs fold away and the first stay. | |
| 49 | + | */ | |
| 50 | + | const shedding = (run) => { | |
| 51 | + | const strip = run.querySelector(STRIP); | |
| 52 | + | const host = strip ?? run; | |
| 53 | + | const members = [...host.children].filter( | |
| 54 | + | (el) => !el.classList.contains("run-overflow"), | |
| 55 | + | ); | |
| 56 | + | const order = []; | |
| 57 | + | for (const tier of TIERS) { | |
| 58 | + | order.push(...members.filter((el) => el.classList.contains(tier)).reverse()); | |
| 59 | + | } | |
| 60 | + | order.push( | |
| 61 | + | ...members | |
| 62 | + | .filter((el) => !TIERS.some((tier) => el.classList.contains(tier))) | |
| 63 | + | .reverse(), | |
| 64 | + | ); | |
| 65 | + | return { host, order }; | |
| 66 | + | }; | |
| 67 | + | ||
| 68 | + | /** The control the shed members move into, built once per host. */ | |
| 69 | + | const control = (host) => { | |
| 70 | + | const existing = host.querySelector(":scope > .run-overflow"); | |
| 71 | + | if (existing) return existing; | |
| 72 | + | ||
| 73 | + | const wrap = document.createElement("div"); | |
| 74 | + | wrap.className = "run-overflow"; | |
| 75 | + | ||
| 76 | + | const button = document.createElement("button"); | |
| 77 | + | button.type = "button"; | |
| 78 | + | button.className = "button"; | |
| 79 | + | button.setAttribute("aria-expanded", "false"); | |
| 80 | + | button.setAttribute("aria-haspopup", "true"); | |
| 81 | + | // A word rather than a glyph. The strips this replaces said "More" and | |
| 82 | + | // a screen reader has something to read either way. | |
| 83 | + | button.textContent = "More"; | |
| 84 | + | ||
| 85 | + | const items = document.createElement("div"); | |
| 86 | + | items.className = "run-overflow-items"; | |
| 87 | + | items.hidden = true; | |
| 88 | + | ||
| 89 | + | button.addEventListener("click", () => { | |
| 90 | + | const open = items.hidden; | |
| 91 | + | items.hidden = !open; | |
| 92 | + | button.setAttribute("aria-expanded", String(open)); | |
| 93 | + | }); | |
| 94 | + | ||
| 95 | + | wrap.append(button, items); | |
| 96 | + | host.append(wrap); | |
| 97 | + | return wrap; | |
| 98 | + | }; | |
| 99 | + | ||
| 100 | + | /** Put everything back, so a measurement starts from the whole run. */ | |
| 101 | + | const restore = (host, wrap) => { | |
| 102 | + | const items = wrap.querySelector(".run-overflow-items"); | |
| 103 | + | while (items.firstChild) host.insertBefore(items.firstChild, wrap); | |
| 104 | + | wrap.hidden = true; | |
| 105 | + | items.hidden = true; | |
| 106 | + | wrap.querySelector("button")?.setAttribute("aria-expanded", "false"); | |
| 107 | + | }; | |
| 108 | + | ||
| 109 | + | // The measurement. `scrollWidth > clientWidth` is the browser answering | |
| 110 | + | // "does this overflow" directly, which is the question CSS could not be | |
| 111 | + | // asked. It is read after a restore, so what is compared is always the | |
| 112 | + | // whole run rather than a run already missing what a previous pass took. | |
| 113 | + | const tight = (host) => host.scrollWidth > host.clientWidth + 1; | |
| 114 | + | ||
| 115 | + | /** Fold one run until what is left fits. */ | |
| 116 | + | const fold = (run) => { | |
| 117 | + | const { host, order } = shedding(run); | |
| 118 | + | const wrap = control(host); | |
| 119 | + | restore(host, wrap); | |
| 120 | + | ||
| 121 | + | // Marked before measuring: the mark is what turns wrapping off, and a | |
| 122 | + | // wrapped run always fits, so measuring first would always find room. | |
| 123 | + | run.setAttribute("data-menu", ""); | |
| 124 | + | ||
| 125 | + | const items = wrap.querySelector(".run-overflow-items"); | |
| 126 | + | let shed = 0; | |
| 127 | + | // Never the last one. A control that folded everything into itself | |
| 128 | + | // would leave a strip with nothing on it, which is worse than an | |
| 129 | + | // overflowing strip and is not what the description asks for. | |
| 130 | + | while (tight(host) && shed < order.length - 1) { | |
| 131 | + | items.prepend(order[shed]); | |
| 132 | + | shed += 1; | |
| 133 | + | } | |
| 134 | + | wrap.hidden = shed === 0; | |
| 135 | + | }; | |
| 136 | + | ||
| 137 | + | const all = () => { | |
| 138 | + | for (const run of document.querySelectorAll(".run-menu")) fold(run); | |
| 139 | + | }; | |
| 140 | + | ||
| 141 | + | // Three occasions, and each is a different way the answer changes: the room | |
| 142 | + | // changed, the members changed, or the page just arrived. The walk is over | |
| 143 | + | // the document every time rather than over a list captured at load, which is | |
| 144 | + | // the selection script's reason -- a swap brings new runs and takes old ones | |
| 145 | + | // away, and both are found by the next pass. | |
| 146 | + | if (typeof ResizeObserver === "function") { | |
| 147 | + | const observer = new ResizeObserver(() => all()); | |
| 148 | + | observer.observe(document.documentElement); | |
| 149 | + | } else { | |
| 150 | + | window.addEventListener("resize", all); | |
| 151 | + | } | |
| 152 | + | document.addEventListener("htmx:after:settle", all); | |
| 153 | + | if (document.readyState === "loading") { | |
| 154 | + | document.addEventListener("DOMContentLoaded", all); | |
| 155 | + | } else { | |
| 156 | + | all(); | |
| 157 | + | } | |
| 158 | + | })(); |