max / makenotwork
7 files changed,
+165 insertions,
-217 deletions
| @@ -5317,9 +5317,7 @@ | |||
| 5317 | 5317 | ||
| 5318 | 5318 | [[package]] | |
| 5319 | 5319 | name = "makeover-build" | |
| 5320 | - | version = "0.31.0" | |
| 5321 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5322 | - | checksum = "02f630f22ab60b1f73e45956fc6ff26c31277edbee2aad523200ec96974e5a5a" | |
| 5320 | + | version = "0.32.0" | |
| 5323 | 5321 | dependencies = [ | |
| 5324 | 5322 | "makeover", | |
| 5325 | 5323 | "makeover-geometry", | |
| @@ -5334,15 +5332,11 @@ | |||
| 5334 | 5332 | ||
| 5335 | 5333 | [[package]] | |
| 5336 | 5334 | name = "makeover-layout" | |
| 5337 | - | version = "0.22.0" | |
| 5338 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5339 | - | checksum = "58dad575602dd23c8f69297e9ef774be84e4c6e4c58a54892448175cb6a096da" | |
| 5335 | + | version = "0.23.0" | |
| 5340 | 5336 | ||
| 5341 | 5337 | [[package]] | |
| 5342 | 5338 | name = "makeover-touch" | |
| 5343 | - | version = "0.14.0" | |
| 5344 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5345 | - | checksum = "2d6241ec48fe2dea0d1051b1bfa3c6213bf0e7a75731153c20ca91c501953f24" | |
| 5339 | + | version = "0.15.0" | |
| 5346 | 5340 | dependencies = [ | |
| 5347 | 5341 | "makeover-geometry", | |
| 5348 | 5342 | "makeover-layout", | |
| @@ -5350,9 +5344,7 @@ | |||
| 5350 | 5344 | ||
| 5351 | 5345 | [[package]] | |
| 5352 | 5346 | name = "makeover-webview" | |
| 5353 | - | version = "0.38.0" | |
| 5354 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5355 | - | checksum = "cf73eb60125c7ae695cfce2678b2338f22101ae5c835a080a599a9d52b595f18" | |
| 5347 | + | version = "0.39.0" | |
| 5356 | 5348 | dependencies = [ | |
| 5357 | 5349 | "makeover-geometry", | |
| 5358 | 5350 | "makeover-layout", | |
| @@ -6550,7 +6542,7 @@ | |||
| 6550 | 6542 | ||
| 6551 | 6543 | [[package]] | |
| 6552 | 6544 | name = "quasi-axum" | |
| 6553 | - | version = "0.4.0" | |
| 6545 | + | version = "0.5.0" | |
| 6554 | 6546 | dependencies = [ | |
| 6555 | 6547 | "axum", | |
| 6556 | 6548 | "http 1.4.2", | |
| @@ -6561,7 +6553,7 @@ | |||
| 6561 | 6553 | ||
| 6562 | 6554 | [[package]] | |
| 6563 | 6555 | name = "quasi-basics" | |
| 6564 | - | version = "0.4.0" | |
| 6556 | + | version = "0.5.0" | |
| 6565 | 6557 | dependencies = [ | |
| 6566 | 6558 | "makeover-layout", | |
| 6567 | 6559 | "quasi-router", | |
| @@ -6569,7 +6561,7 @@ | |||
| 6569 | 6561 | ||
| 6570 | 6562 | [[package]] | |
| 6571 | 6563 | name = "quasi-http" | |
| 6572 | - | version = "0.4.0" | |
| 6564 | + | version = "0.5.0" | |
| 6573 | 6565 | dependencies = [ | |
| 6574 | 6566 | "form_urlencoded", | |
| 6575 | 6567 | "http 1.4.2", | |
| @@ -6578,14 +6570,14 @@ | |||
| 6578 | 6570 | ||
| 6579 | 6571 | [[package]] | |
| 6580 | 6572 | name = "quasi-router" | |
| 6581 | - | version = "0.4.0" | |
| 6573 | + | version = "0.5.0" | |
| 6582 | 6574 | dependencies = [ | |
| 6583 | 6575 | "makeover-layout", | |
| 6584 | 6576 | ] | |
| 6585 | 6577 | ||
| 6586 | 6578 | [[package]] | |
| 6587 | 6579 | name = "quasi-webview" | |
| 6588 | - | version = "0.4.0" | |
| 6580 | + | version = "0.5.0" | |
| 6589 | 6581 | dependencies = [ | |
| 6590 | 6582 | "docengine", | |
| 6591 | 6583 | "makeover-layout", | |
| @@ -10696,26 +10688,6 @@ | |||
| 10696 | 10688 | "pkg-config", | |
| 10697 | 10689 | ] | |
| 10698 | 10690 | ||
| 10699 | - | [[patch.unused]] | |
| 10700 | - | name = "quasi-immediate" | |
| 10701 | - | version = "0.4.0" | |
| 10702 | - | ||
| 10703 | - | [[patch.unused]] | |
| 10704 | - | name = "quasi-store" | |
| 10705 | - | version = "0.1.0" | |
| 10706 | - | ||
| 10707 | - | [[patch.unused]] | |
| 10708 | - | name = "quasi-tauri" | |
| 10709 | - | version = "0.4.0" | |
| 10710 | - | ||
| 10711 | - | [[patch.unused]] | |
| 10712 | - | name = "synckit-client" | |
| 10713 | - | version = "0.8.0" | |
| 10714 | - | ||
| 10715 | - | [[patch.unused]] | |
| 10716 | - | name = "synckit-config" | |
| 10717 | - | version = "0.2.0" | |
| 10718 | - | ||
| 10719 | 10691 | [[patch.unused]] | |
| 10720 | 10692 | name = "kberg" | |
| 10721 | 10693 | version = "0.1.0" | |
| @@ -10727,3 +10699,31 @@ | |||
| 10727 | 10699 | [[patch.unused]] | |
| 10728 | 10700 | name = "painhours" | |
| 10729 | 10701 | version = "0.1.0" | |
| 10702 | + | ||
| 10703 | + | [[patch.unused]] | |
| 10704 | + | name = "synckit-client" | |
| 10705 | + | version = "0.8.0" | |
| 10706 | + | ||
| 10707 | + | [[patch.unused]] | |
| 10708 | + | name = "synckit-config" | |
| 10709 | + | version = "0.2.0" | |
| 10710 | + | ||
| 10711 | + | [[patch.unused]] | |
| 10712 | + | name = "quasi-immediate" | |
| 10713 | + | version = "0.5.0" | |
| 10714 | + | ||
| 10715 | + | [[patch.unused]] | |
| 10716 | + | name = "quasi-store" | |
| 10717 | + | version = "0.1.0" | |
| 10718 | + | ||
| 10719 | + | [[patch.unused]] | |
| 10720 | + | name = "quasi-tauri" | |
| 10721 | + | version = "0.5.0" | |
| 10722 | + | ||
| 10723 | + | [[patch.unused]] | |
| 10724 | + | name = "makeover-immediate" | |
| 10725 | + | version = "0.21.0" | |
| 10726 | + | ||
| 10727 | + | [[patch.unused]] | |
| 10728 | + | name = "makeover-tui" | |
| 10729 | + | version = "0.21.0" |
| @@ -158,22 +158,22 @@ | |||
| 158 | 158 | # through quasi beside its Askama equivalent, so the cost of describing a screen | |
| 159 | 159 | # here is measured rather than argued. Not load-bearing for any shipped route. | |
| 160 | 160 | # See wiki look-wave-2, tier G. | |
| 161 | - | quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.4" } | |
| 161 | + | quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.5" } | |
| 162 | 162 | # The description vocabulary quasi's screen types are built from. Pinned here | |
| 163 | 163 | # rather than reached through quasi-router's re-export because a described | |
| 164 | 164 | # screen names FieldKind and Tone directly; it has to track what quasi-router | |
| 165 | 165 | # resolves or the two `layout::` paths are different crates. | |
| 166 | - | makeover-layout = "0.22.0" | |
| 166 | + | makeover-layout = "0.23.0" | |
| 167 | 167 | # For the request head the per-viewer state factory reads. axum re-exports it, | |
| 168 | 168 | # but the factory's signature is quasi-axum's and names `http::request::Parts`. | |
| 169 | 169 | http = "1.3.1" | |
| 170 | - | quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.4" } | |
| 171 | - | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.4" } | |
| 170 | + | quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.5" } | |
| 171 | + | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.5" } | |
| 172 | 172 | # The first-party widget set. The carousel was proved here against three pages | |
| 173 | 173 | # and then moved there; what is left in `quasi/widgets/carousel.rs` is the | |
| 174 | 174 | # Askama glue. Not `makeover-basics`: a widget is an assembly of `Node`s, so it | |
| 175 | 175 | # sits above quasi-router rather than inside the description suite. | |
| 176 | - | quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.4" } | |
| 176 | + | quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.5" } | |
| 177 | 177 | ||
| 178 | 178 | # Git source browser | |
| 179 | 179 | gix = { version = "0.86", default-features = false, features = ["blame", "mailmap", "revision", "sha1", "max-performance-safe"] } | |
| @@ -245,7 +245,7 @@ | |||
| 245 | 245 | # Emits static/geometry.css (makeover-geometry) and static/layout.css | |
| 246 | 246 | # (makeover-webview). The same generator GO and BB run; only the output paths | |
| 247 | 247 | # differ, since the server serves its stylesheets rather than bundling them. | |
| 248 | - | makeover-build = "0.31.0" | |
| 248 | + | makeover-build = "0.32.0" | |
| 249 | 249 | # Read directly for the embeds' own copy of the spacing layer, which they need | |
| 250 | 250 | # because an iframe cannot link a stylesheet. makeover-build does not re-export | |
| 251 | 251 | # it, so the pin lives here and has to track the one makeover-build resolves. |
| @@ -454,19 +454,22 @@ | |||
| 454 | 454 | /// what it describes. | |
| 455 | 455 | const REVIEWED_OVERLAPS: &[(&str, &str)] = &[ | |
| 456 | 456 | ("badge", "color"), | |
| 457 | - | // A carousel showing one frame at a time, 2026-08-14. Makeover says a | |
| 458 | - | // picture is `display: block`, which is what a picture is; the widget says | |
| 459 | - | // the frames that are not current are not shown, which is what a carousel | |
| 460 | - | // is. Two different claims that happen to land on one property, and the | |
| 461 | - | // selector says so -- the override is | |
| 462 | - | // `[data-widget="carousel"][data-ready] > .picture-img`, so it reaches a | |
| 463 | - | // picture only while it is a frame in an enhanced carousel and never a | |
| 464 | - | // picture anywhere else. | |
| 457 | + | // A carousel showing one frame at a time, 2026-08-14. This was | |
| 458 | + | // `("picture-img", "display")` until makeover-layout 0.23.0 described the | |
| 459 | + | // showing itself; the frames are wrapped in `.showing-frame` now and the old | |
| 460 | + | // entry stopped colliding, which this list is built to notice. | |
| 465 | 461 | // | |
| 466 | - | // `[hidden]` was the alternative and does not work: makeover's rule is an | |
| 467 | - | // author rule, layered or not, so `display: block` beats the UA stylesheet | |
| 468 | - | // and the attribute does nothing. | |
| 469 | - | ("picture-img", "display"), | |
| 462 | + | // The two arms are the same rule at two moments, and that is the whole of | |
| 463 | + | // why the overlap is kept. Makeover collapses the stack on `[data-ready]`, | |
| 464 | + | // which is the only honest default for a renderer that cannot know whether | |
| 465 | + | // a page has script: ship every child, take them away once something binds | |
| 466 | + | // them. This site collapses it from first paint instead, because rendering | |
| 467 | + | // three frames and collapsing them was a measured 141px -> 58px jump that | |
| 468 | + | // every visitor with JS paid, and `no-js.css` from a <noscript> opens the | |
| 469 | + | // stack for the few without. That trade is this landing page's to make and | |
| 470 | + | // a generated stylesheet has no way to reach <noscript>. | |
| 471 | + | ("showing-frame", "display"), | |
| 472 | + | ("current", "display"), | |
| 470 | 473 | ("card", "color"), | |
| 471 | 474 | ("chosen", "box-shadow"), | |
| 472 | 475 | ("progress-fill", "background"), |
| @@ -11,11 +11,14 @@ | |||
| 11 | 11 | first paint; with JS off they arrive and open the stack out, because the | |
| 12 | 12 | controls that would page through it are never going to run. | |
| 13 | 13 | ||
| 14 | + | Only the frames. The control row needs nothing said about it here: makeover | |
| 15 | + | hides `.showing` until something sets [data-ready], and with scripting off | |
| 16 | + | nothing ever will. | |
| 17 | + | ||
| 14 | 18 | A separate file rather than an inline <style> because the CSP is | |
| 15 | 19 | `style-src 'self'`. Report-only today, but writing markup that depends on a | |
| 16 | 20 | policy staying lax is how it stops being lax later and quietly breaks. */ | |
| 17 | 21 | ||
| 18 | - | [data-widget="carousel"] > .picture, | |
| 19 | - | [data-widget="carousel"] > .picture-img { | |
| 22 | + | [data-widget="carousel"] > .showing-frame { | |
| 20 | 23 | display: block; | |
| 21 | 24 | } |
| @@ -11522,11 +11522,18 @@ | |||
| 11522 | 11522 | THE FALLBACK RUNS THE OTHER WAY NOW. This used to render frame one and hide | |
| 11523 | 11523 | the rest, because the controls that reach them are the scripted part -- so | |
| 11524 | 11524 | with JS off, two of three screenshots were unreachable. Now the unenhanced | |
| 11525 | - | page is the whole gallery, stacked in order, and the script's job is to | |
| 11526 | - | collapse it to one at a time. It degrades to more content, not less, which | |
| 11527 | - | is why the chrome below is injected by the island rather than rendered | |
| 11528 | - | server-side: prev/next/dots do nothing without script and are not markup | |
| 11529 | - | anyone should be shipping to a reader who has none. | |
| 11525 | + | page is the whole gallery, stacked in order. | |
| 11526 | + | ||
| 11527 | + | THE CHROME IS DESCRIBED TOO, as of makeover-layout 0.23.0. The description | |
| 11528 | + | says `Showing::One` -- an ordered set of pictures with one showing -- and | |
| 11529 | + | quasi-webview derives prev/position/next from that, once, for every widget | |
| 11530 | + | there will ever be. Nothing here or in any renderer matches on the name | |
| 11531 | + | "carousel" to get it. The island stopped injecting chrome and now only binds | |
| 11532 | + | what is already on the page; the dot strip and the overlaid arrows are gone. | |
| 11533 | + | ||
| 11534 | + | The controls are still not shipped live to a reader with no script: makeover | |
| 11535 | + | keeps `.showing` hidden until something sets [data-ready], so the markup no | |
| 11536 | + | longer has to be absent to avoid lying. | |
| 11530 | 11537 | ========================================================================= */ | |
| 11531 | 11538 | ||
| 11532 | 11539 | /* Public gallery carousel sections (item + project pages). */ | |
| @@ -11549,35 +11556,28 @@ | |||
| 11549 | 11556 | everyone with JS paid; `no-js.css`, loaded from a <noscript>, is what opens | |
| 11550 | 11557 | the stack out for a visitor without scripting. | |
| 11551 | 11558 | ||
| 11552 | - | The bottom padding reserves the dots strip. The island injects it after the | |
| 11553 | - | frames, so without a held place its arrival would grow the widget and push | |
| 11554 | - | the page down -- a small shift, but the same kind, and the point of this pass | |
| 11555 | - | is that there are none. Its height: the strip's own 0.6rem plus the gap above | |
| 11556 | - | it. The nav buttons need no reservation; they are absolutely positioned. */ | |
| 11557 | - | [data-widget="carousel"] { | |
| 11558 | - | position: relative; | |
| 11559 | - | padding-bottom: calc(0.6rem + var(--gap-group)); | |
| 11560 | - | } | |
| 11559 | + | This is the one half of the carousel that stays local, and it is host policy | |
| 11560 | + | rather than a gap. makeover's generated sheet collapses the stack on | |
| 11561 | + | [data-ready], which is the honest default for a renderer that cannot know | |
| 11562 | + | whether this page has script: it ships every child and takes them away once | |
| 11563 | + | something binds them. Trading a load-time jump for that is a decision about | |
| 11564 | + | this site's landing page, and <noscript> is a mechanism a generated | |
| 11565 | + | stylesheet has no way to reach. | |
| 11561 | 11566 | ||
| 11562 | - | [data-widget="carousel"] > .picture, | |
| 11563 | - | [data-widget="carousel"] > .picture-img { | |
| 11564 | - | /* respec-ok: makeover says a picture is display:block, which is what a | |
| 11565 | - | picture is; this says the frames that are not current are not shown, which | |
| 11566 | - | is what a carousel is. Nothing in the vocabulary knows that one frame of a | |
| 11567 | - | set is current, so the generated sheet cannot express it. The selector | |
| 11568 | - | keeps the claim narrow: it reaches a picture only while it is a frame in a | |
| 11569 | - | carousel. [hidden] is not the way out either, because makeover's rule is an | |
| 11570 | - | author rule and beats the UA stylesheet. */ | |
| 11567 | + | No padding-bottom and no position: relative any more. Both were for chrome | |
| 11568 | + | that is gone -- the strip that had to be reserved and the arrows that were | |
| 11569 | + | absolutely positioned over the frame. */ | |
| 11570 | + | [data-widget="carousel"] > .showing-frame:not(.current) { | |
| 11571 | + | /* respec-ok: the generated rule is the same claim gated on [data-ready], and | |
| 11572 | + | what it cannot express is <noscript>. A renderer that cannot know whether a | |
| 11573 | + | page has script has to ship every child and take them away once something | |
| 11574 | + | binds them; this page collapses from first paint instead and lets no-js.css | |
| 11575 | + | open it back, which is a trade about this landing page rather than about | |
| 11576 | + | the vocabulary. */ | |
| 11571 | 11577 | display: none; | |
| 11572 | 11578 | } | |
| 11573 | 11579 | ||
| 11574 | - | [data-widget="carousel"] > .picture:first-child, | |
| 11575 | - | [data-widget="carousel"] > .picture-img:first-child, | |
| 11576 | - | [data-widget="carousel"] > .is-active { | |
| 11577 | - | display: block; | |
| 11578 | - | } | |
| 11579 | - | ||
| 11580 | - | [data-widget="carousel"] > .picture { | |
| 11580 | + | [data-widget="carousel"] .picture { | |
| 11581 | 11581 | margin: 0; | |
| 11582 | 11582 | } | |
| 11583 | 11583 | ||
| @@ -11627,7 +11627,7 @@ | |||
| 11627 | 11627 | ||
| 11628 | 11628 | /* --------------------------------------------------------------------------- | |
| 11629 | 11629 | Enhanced. Everything below needs the island, and the island sets | |
| 11630 | - | [data-ready] once it has upgraded and injected the chrome. | |
| 11630 | + | [data-ready] once it has upgraded and bound the controls. | |
| 11631 | 11631 | --------------------------------------------------------------------------- */ | |
| 11632 | 11632 | ||
| 11633 | 11633 | [data-widget="carousel"][data-ready]:focus-visible { | |
| @@ -11635,71 +11635,18 @@ | |||
| 11635 | 11635 | outline-offset: 3px; | |
| 11636 | 11636 | } | |
| 11637 | 11637 | ||
| 11638 | - | /* Prev / next: brutalist offset buttons, vertically centered over the frame. */ | |
| 11639 | - | .carousel-nav { | |
| 11640 | - | position: absolute; | |
| 11641 | - | top: 40%; | |
| 11642 | - | transform: translateY(-50%); | |
| 11643 | - | width: 2.5rem; | |
| 11644 | - | height: 2.5rem; | |
| 11645 | - | display: flex; | |
| 11646 | - | align-items: center; | |
| 11647 | - | justify-content: center; | |
| 11648 | - | font-size: var(--text-head); | |
| 11649 | - | line-height: 1; | |
| 11650 | - | cursor: pointer; | |
| 11651 | - | color: var(--content); | |
| 11652 | - | background: var(--surface-raised); | |
| 11653 | - | border: 1px solid var(--border-strong); | |
| 11654 | - | border-radius: var(--radius-control); | |
| 11655 | - | } | |
| 11638 | + | /* The control row is makeover's `.showing`, rendered by the description rather | |
| 11639 | + | than injected here, and it needs no rules: it is two `.button`s and a muted | |
| 11640 | + | readout, all three of which the generated sheet already styles. What used to | |
| 11641 | + | sit here was `.carousel-nav`, `.carousel-prev`, `.carousel-next`, | |
| 11642 | + | `.carousel-dots` and `.carousel-dot` -- 60 lines for chrome this host drew by | |
| 11643 | + | hand because nothing could describe it. | |
| 11656 | 11644 | ||
| 11657 | - | .carousel-nav:hover { | |
| 11658 | - | background: var(--surface-sunken); | |
| 11659 | - | } | |
| 11645 | + | The arrows were absolutely positioned over the frame, which Max called | |
| 11646 | + | cluttered and which a terminal cannot honestly do at all; the dots had no | |
| 11647 | + | form past a handful of frames, and two of the three galleries here are | |
| 11648 | + | creator uploads of arbitrary length. Both halves of that were the same half. */ | |
| 11660 | 11649 | ||
| 11661 | - | .carousel-nav:active { | |
| 11662 | - | background: var(--surface-sunken); | |
| 11663 | - | transform: translateY(-50%) translateY(1px); | |
| 11664 | - | } | |
| 11665 | - | ||
| 11666 | - | .carousel-nav:focus-visible { | |
| 11667 | - | outline: 2px solid var(--focus-ring); | |
| 11668 | - | outline-offset: 2px; | |
| 11669 | - | } | |
| 11670 | - | ||
| 11671 | - | .carousel-prev { left: 0.6rem; } | |
| 11672 | - | .carousel-next { right: 0.6rem; } | |
| 11673 | - | ||
| 11674 | - | .carousel-dots { | |
| 11675 | - | display: flex; | |
| 11676 | - | justify-content: center; | |
| 11677 | - | gap: var(--gap-peer); | |
| 11678 | - | margin-top: var(--gap-group); | |
| 11679 | - | } | |
| 11680 | - | ||
| 11681 | - | .carousel-dot { | |
| 11682 | - | width: 0.6rem; | |
| 11683 | - | height: 0.6rem; | |
| 11684 | - | padding: 0; | |
| 11685 | - | cursor: pointer; | |
| 11686 | - | border: 1px solid var(--border-strong); | |
| 11687 | - | border-radius: 50%; | |
| 11688 | - | background: transparent; | |
| 11689 | - | } | |
| 11690 | - | ||
| 11691 | - | .carousel-dot.is-active { | |
| 11692 | - | background: var(--content); | |
| 11693 | - | } | |
| 11694 | - | ||
| 11695 | - | .carousel-dot:focus-visible { | |
| 11696 | - | outline: 2px solid var(--focus-ring); | |
| 11697 | - | outline-offset: 2px; | |
| 11698 | - | } | |
| 11699 | - | ||
| 11700 | - | @media (max-width: 599px) { | |
| 11701 | - | .carousel-nav { width: 2rem; height: 2rem; font-size: var(--text-subhead); } | |
| 11702 | - | } | |
| 11703 | 11650 | ||
| 11704 | 11651 | /* --------------------------------------------------------------------------- | |
| 11705 | 11652 | Custom page editor (dashboard/custom_page_editor.html) |
| @@ -6,8 +6,14 @@ | |||
| 6 | 6 | needs more, extend the macro. | |
| 7 | 7 | ||
| 8 | 8 | Click-through, not animated: the viewer advances frames with the prev/next | |
| 9 | - | controls, the dots, or the arrow keys. No autoplay, no auto-advance, no | |
| 10 | - | slide animation, frames swap instantly. | |
| 9 | + | controls or the arrow keys. No autoplay, no auto-advance, no slide animation, | |
| 10 | + | frames swap instantly. | |
| 11 | + | ||
| 12 | + | The dots are gone as of 2026-08-14. A dot strip has no terminal form and no | |
| 13 | + | good webview form past a handful of frames, and only the landing gallery is a | |
| 14 | + | fixed three -- the project and item galleries are creator uploads of any | |
| 15 | + | length. The arrows came off the picture in the same pass; nothing overlays a | |
| 16 | + | frame now. | |
| 11 | 17 | ||
| 12 | 18 | Each frame is a `CarouselFrame { image, alt, caption: Option<_> }` | |
| 13 | 19 | (see `src/templates/mod.rs`). Usage is unchanged: | |
| @@ -35,6 +41,11 @@ | |||
| 35 | 41 | the stack out for a visitor without it. Both properties, no layout shift: | |
| 36 | 42 | collapsing three frames to one after load was a visible jump that everyone | |
| 37 | 43 | paid so that a few could reach frames two and three. | |
| 44 | + | ||
| 45 | + | - The chrome is described too, as of makeover-layout 0.23.0. The description | |
| 46 | + | says one frame is showing and quasi-webview derives prev/position/next from | |
| 47 | + | that, for every widget at once rather than for this one. The island stopped | |
| 48 | + | injecting buttons and now only binds what is already there. | |
| 38 | 49 | #} | |
| 39 | 50 | ||
| 40 | 51 | {% macro carousel(id, frames) -%} |
| @@ -1,18 +1,23 @@ | |||
| 1 | - | // <div data-widget="carousel">, click-through carousel over a described gallery. | |
| 1 | + | // <div data-showing="one">, click-through carousel over a described gallery. | |
| 2 | 2 | // | |
| 3 | - | // The widget tier's browser half. The server no longer renders a carousel; it | |
| 4 | - | // renders an ordered set of pictures carrying `data-widget="carousel"`, and | |
| 5 | - | // this is what "a renderer that recognises the name draws it its own way" | |
| 6 | - | // means on this host. A terminal renders the same description as a stack of | |
| 7 | - | // captions, and neither owes the other a carousel primitive. | |
| 3 | + | // The widget tier's browser half, and it is smaller than it was. The server no | |
| 4 | + | // longer renders a carousel and no longer renders its chrome either: it renders | |
| 5 | + | // an ordered set of pictures that says one of them is showing, and quasi-webview | |
| 6 | + | // derives prev/position/next from that fact alone. So this file stopped | |
| 7 | + | // injecting buttons and stopped building a dot strip. What is left is the one | |
| 8 | + | // thing a description cannot carry: which frame the reader has moved to since | |
| 9 | + | // the page arrived. | |
| 8 | 10 | // | |
| 9 | - | // PROGRESSIVE ENHANCEMENT RUNS THE OTHER WAY NOW. The old element inherited | |
| 10 | - | // server-rendered prev/next/dots and switched them on. Those controls do | |
| 11 | - | // nothing without script, so shipping them to a reader who has none was markup | |
| 12 | - | // that lied. Here the page arrives as the whole gallery stacked in order, and | |
| 13 | - | // this collapses it to one frame at a time and injects the chrome that makes | |
| 14 | - | // that navigable. With no script the reader gets every screenshot instead of | |
| 15 | - | // the first one and two dead buttons. | |
| 11 | + | // NOTHING HERE KNOWS WHAT A CAROUSEL IS. The hook is `data-showing`, not | |
| 12 | + | // `data-widget="carousel"`, and that is deliberate: a script that matched on the | |
| 13 | + | // widget's name would have to be written again for the next assembly showing one | |
| 14 | + | // child at a time. This binds every one of them, including ones that do not | |
| 15 | + | // exist yet. | |
| 16 | + | // | |
| 17 | + | // PROGRESSIVE ENHANCEMENT. The controls are on the page but makeover keeps | |
| 18 | + | // `.showing` hidden until `data-ready` is set, so a reader with no script is | |
| 19 | + | // never offered a button that does nothing. `no-js.css`, loaded from a | |
| 20 | + | // <noscript> in the partial, opens the frame stack out for them instead. | |
| 16 | 21 | ||
| 17 | 22 | import { MnwElement, define } from './base.ts'; | |
| 18 | 23 | import { wrapIndex } from './carousel.logic.ts'; | |
| @@ -24,69 +29,48 @@ | |||
| 24 | 29 | // whole reason islands are custom elements; the description emits a plain | |
| 25 | 30 | // `<div>` because it has no idea this host prefixes its tags with `mnw-`. | |
| 26 | 31 | // So the template puts one around the other and each half stays honest. | |
| 27 | - | const region = this.querySelector<HTMLElement>('[data-widget="carousel"]'); | |
| 32 | + | const region = this.querySelector<HTMLElement>('[data-showing]'); | |
| 28 | 33 | if (!region) return; | |
| 29 | 34 | ||
| 30 | - | // A frame is a direct child of the region. The description emits a figure | |
| 31 | - | // for a captioned frame and a bare image for one without, so the child is | |
| 32 | - | // the frame either way and neither shape needs a class of its own. | |
| 33 | - | // | |
| 34 | - | // Deliberately not selected by class name. Those classes are makeover's and | |
| 35 | - | // arrive with the description, and naming one here would both couple this | |
| 36 | - | // file to the vocabulary's spelling and register as a use in build.rs's | |
| 37 | - | // dead-vocabulary scan, which reads this directory. | |
| 38 | - | const frames = Array.from(region.children).filter( | |
| 39 | - | (el): el is HTMLElement => el instanceof HTMLElement, | |
| 35 | + | // The wrappers the renderer puts around each child, which are what carry | |
| 36 | + | // `current`. Selected by class because this one IS the renderer's own | |
| 37 | + | // structural name rather than a piece of makeover's vocabulary, and the | |
| 38 | + | // description has no other way to mark which child is which. | |
| 39 | + | const frames = Array.from( | |
| 40 | + | region.querySelectorAll<HTMLElement>(':scope > .showing-frame'), | |
| 40 | 41 | ); | |
| 41 | 42 | if (frames.length < 2) return; // nothing to navigate; leave the stack alone | |
| 42 | 43 | ||
| 43 | - | let index = 0; | |
| 44 | - | const dots: HTMLElement[] = []; | |
| 44 | + | let index = Math.max( | |
| 45 | + | frames.findIndex((f) => f.classList.contains('current')), | |
| 46 | + | 0, | |
| 47 | + | ); | |
| 48 | + | ||
| 49 | + | const position = region.querySelector<HTMLElement>('.showing-position'); | |
| 45 | 50 | ||
| 46 | 51 | const show = (next: number): void => { | |
| 47 | 52 | index = wrapIndex(next, frames.length); | |
| 48 | 53 | frames.forEach((f, i) => { | |
| 49 | 54 | const active = i === index; | |
| 50 | - | f.classList.toggle('is-active', active); | |
| 55 | + | f.classList.toggle('current', active); | |
| 51 | 56 | if (active) f.removeAttribute('aria-hidden'); | |
| 52 | 57 | else f.setAttribute('aria-hidden', 'true'); | |
| 53 | 58 | }); | |
| 54 | - | dots.forEach((d, i) => { | |
| 55 | - | const active = i === index; | |
| 56 | - | d.classList.toggle('is-active', active); | |
| 57 | - | d.setAttribute('aria-selected', active ? 'true' : 'false'); | |
| 59 | + | if (position) position.textContent = `${index + 1} / ${frames.length}`; | |
| 60 | + | }; | |
| 61 | + | ||
| 62 | + | // Bind what is already there. `data-shows` is the renderer's word for what | |
| 63 | + | // a control does to the region it sits in, and it takes an index as well as | |
| 64 | + | // the two steps, so a derived tab strip binds through this same loop. | |
| 65 | + | for (const control of region.querySelectorAll<HTMLElement>('[data-shows]')) { | |
| 66 | + | const shows = control.dataset.shows ?? ''; | |
| 67 | + | control.addEventListener('click', () => { | |
| 68 | + | if (shows === 'previous') return show(index - 1); | |
| 69 | + | if (shows === 'next') return show(index + 1); | |
| 70 | + | const at = Number.parseInt(shows, 10); | |
| 71 | + | if (Number.isInteger(at)) show(at); | |
| 58 | 72 | }); | |
| 59 | - | }; | |
| 60 | - | ||
| 61 | - | const button = (cls: string, label: string, glyph: string): HTMLButtonElement => { | |
| 62 | - | const b = document.createElement('button'); | |
| 63 | - | b.type = 'button'; | |
| 64 | - | b.className = cls; | |
| 65 | - | b.setAttribute('aria-label', label); | |
| 66 | - | b.textContent = glyph; | |
| 67 | - | return b; | |
| 68 | - | }; | |
| 69 | - | ||
| 70 | - | // The chrome. Injected rather than rendered, because none of it works | |
| 71 | - | // without this file being here to bind it. | |
| 72 | - | const prev = button('carousel-nav carousel-prev', 'Previous screenshot', '‹'); | |
| 73 | - | const next = button('carousel-nav carousel-next', 'Next screenshot', '›'); | |
| 74 | - | prev.addEventListener('click', () => show(index - 1)); | |
| 75 | - | next.addEventListener('click', () => show(index + 1)); | |
| 76 | - | region.append(prev, next); | |
| 77 | - | ||
| 78 | - | const strip = document.createElement('div'); | |
| 79 | - | strip.className = 'carousel-dots'; | |
| 80 | - | strip.setAttribute('role', 'tablist'); | |
| 81 | - | strip.setAttribute('aria-label', 'Choose screenshot'); | |
| 82 | - | frames.forEach((_, i) => { | |
| 83 | - | const dot = button('carousel-dot', `Screenshot ${i + 1}`, ''); | |
| 84 | - | dot.setAttribute('role', 'tab'); | |
| 85 | - | dot.addEventListener('click', () => show(i)); | |
| 86 | - | dots.push(dot); | |
| 87 | - | strip.append(dot); | |
| 88 | - | }); | |
| 89 | - | region.append(strip); | |
| 73 | + | } | |
| 90 | 74 | ||
| 91 | 75 | // The group semantics the template used to carry. They belong with the | |
| 92 | 76 | // behaviour: a stack of pictures is not a carousel until this runs, and | |
| @@ -112,8 +96,8 @@ | |||
| 112 | 96 | } | |
| 113 | 97 | }); | |
| 114 | 98 | ||
| 115 | - | show(0); | |
| 116 | - | region.setAttribute('data-ready', ''); // CSS hook: collapse to one frame | |
| 99 | + | show(index); | |
| 100 | + | region.setAttribute('data-ready', ''); // reveals the row, collapses the stack | |
| 117 | 101 | } | |
| 118 | 102 | } | |
| 119 | 103 |