Skip to main content

max / goingson

Take quasi 0.79.0, and ship the outline script with the rest quasicoherent `ccaa7e4b`. `Row::depth` and `Row::open` are a semver break on the description layer, forward-fixed here in the same pass; nothing this app describes is an outline yet, so the only edit is the pin and the new script. `quasi-outline.js` joins the loop for the reason `quasi-download.js` is in it: the shell links every one of these by default, so a script the renderer ships and this file forgets is a 404 per document with nothing in the log.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_0153yKAYUmoC9bZz2eV6cA88
Author: Max Johnson <me@maxj.phd> · 2026-08-29 16:51 UTC
Signed with PGP, not checked
Commit: 1ef5d2b63f18bf695b7e51853a1f986f40999040
Parent: 7bb4113
3 files changed, +112 insertions, -23 deletions
M Cargo.lock +23 -23
@@ -3444,9 +3444,9 @@
3444 3444
3445 3445 [[package]]
3446 3446 name = "makeover-build"
3447 - version = "0.60.0"
3447 + version = "0.61.0"
3448 3448 source = "registry+https://github.com/rust-lang/crates.io-index"
3449 - checksum = "347bc87ae657975fb73638f028d7df91cfd8c6486f56f38682a73395011bf201"
3449 + checksum = "a89ef584bdb9d949fcefe00251a27d01d39cde43aecca05aa96e5d65ae2a8f8a"
3450 3450 dependencies = [
3451 3451 "makeover",
3452 3452 "makeover-geometry",
@@ -3499,9 +3499,9 @@
3499 3499
3500 3500 [[package]]
3501 3501 name = "makeover-webview"
3502 - version = "0.67.0"
3502 + version = "0.68.0"
3503 3503 source = "registry+https://github.com/rust-lang/crates.io-index"
3504 - checksum = "87b1f05ef8adcc840f118da789b73ffb25d4111103113e597ee6e9d858d02183"
3504 + checksum = "d8ca6d6b1e336b2b55a7b5ee96e15901300081e9bd7e2331e89c1bcd9bdadfbe"
3505 3505 dependencies = [
3506 3506 "makeover-geometry",
3507 3507 "makeover-layout",
@@ -4717,7 +4717,7 @@
4717 4717
4718 4718 [[package]]
4719 4719 name = "quasi-http"
4720 - version = "0.78.0"
4720 + version = "0.79.0"
4721 4721 dependencies = [
4722 4722 "form_urlencoded",
4723 4723 "http",
@@ -4726,7 +4726,7 @@
4726 4726
4727 4727 [[package]]
4728 4728 name = "quasi-notifs"
4729 - version = "0.78.0"
4729 + version = "0.79.0"
4730 4730 dependencies = [
4731 4731 "quasi-router",
4732 4732 "synckit-config",
@@ -4734,14 +4734,14 @@
4734 4734
4735 4735 [[package]]
4736 4736 name = "quasi-router"
4737 - version = "0.78.0"
4737 + version = "0.79.0"
4738 4738 dependencies = [
4739 4739 "makeover-layout",
4740 4740 ]
4741 4741
4742 4742 [[package]]
4743 4743 name = "quasi-tauri"
4744 - version = "0.78.0"
4744 + version = "0.79.0"
4745 4745 dependencies = [
4746 4746 "http",
4747 4747 "quasi-http",
@@ -4767,7 +4767,7 @@
4767 4767
4768 4768 [[package]]
4769 4769 name = "quasi-webview"
4770 - version = "0.78.0"
4770 + version = "0.79.0"
4771 4771 dependencies = [
4772 4772 "docengine",
4773 4773 "makeover-layout",
@@ -8490,21 +8490,21 @@
8490 8490 ]
8491 8491
8492 8492 [[patch.unused]]
8493 - name = "quasi-axum"
8494 - version = "0.78.0"
8495 -
8496 - [[patch.unused]]
8497 - name = "quasi-basics"
8498 - version = "0.78.0"
8499 -
8500 - [[patch.unused]]
8501 - name = "quasi-immediate"
8502 - version = "0.78.0"
8503 -
8504 - [[patch.unused]]
8505 - name = "quasi-store"
8493 + name = "ops-status"
8506 8494 version = "0.1.0"
8507 8495
8508 8496 [[patch.unused]]
8509 - name = "ops-status"
8497 + name = "quasi-axum"
8498 + version = "0.79.0"
8499 +
8500 + [[patch.unused]]
8501 + name = "quasi-basics"
8502 + version = "0.79.0"
8503 +
8504 + [[patch.unused]]
8505 + name = "quasi-immediate"
8506 + version = "0.79.0"
8507 +
8508 + [[patch.unused]]
8509 + name = "quasi-store"
8510 8510 version = "0.1.0"
@@ -618,6 +618,7 @@
618 618 ("quasi-awaiting.js", quasi_webview::AWAITING_JS),
619 619 ("quasi-instant.js", quasi_webview::INSTANT_JS),
620 620 ("quasi-menu.js", quasi_webview::MENU_JS),
621 + ("quasi-outline.js", quasi_webview::OUTLINE_JS),
621 622 ] {
622 623 fs::write(frontend.join("js").join(name), source)
623 624 .unwrap_or_else(|error| panic!("write js/{name}: {error}"));
@@ -1,0 +1,88 @@
1 + // Folding a branch of an outline, which is the reader tidying their own view.
2 + //
3 + // `Row::depth` and `Row::open` describe a hierarchy as a flat list of rows,
4 + // each saying how deep it sits. Which rows a shut branch hides falls out of
5 + // that reading, and the renderer already did it once: every row is in the
6 + // document and the ones under a shut branch arrived `hidden`. What is left is
7 + // the gesture, and the gesture is a browser's.
8 + //
9 + // Nothing here asks the app anything. Folding changes what the reader is
10 + // looking at and nothing anyone else can observe -- see `Row::open` for why
11 + // that is not a write -- so a round trip would be a request for rows the page
12 + // is already holding.
13 + //
14 + // It reads two hooks the renderer emits and nothing else: `[data-disclose]` on
15 + // a branch's chevron, and `data-depth` on the rows. A page that never runs this
16 + // keeps the outline in the shape the description gave it, with every row still
17 + // reachable -- the shut branches simply stay shut.
18 + (() => {
19 + "use strict";
20 +
21 + const DISCLOSE = "[data-disclose]";
22 + const ROW = "[data-row]";
23 +
24 + /** How deep a row sits. Absent means top level, which is what it means. */
25 + const depth = (row) => Number(row.getAttribute("data-depth") ?? 0);
26 +
27 + /** The row a chevron belongs to. */
28 + const rowOf = (el) => el.closest(ROW);
29 +
30 + /**
31 + * The rows under `row`: the ones after it that are deeper, up to the first
32 + * one no deeper than it.
33 + *
34 + * The same reading `quasi_router::folded` makes on the other side, and it
35 + * has to be: a reader who folds a branch here and reloads the page must see
36 + * the list the server would have folded.
37 + */
38 + const under = (row) => {
39 + const rows = [...row.parentElement.querySelectorAll(`:scope > ${ROW}`)];
40 + const start = rows.indexOf(row);
41 + const level = depth(row);
42 + const kin = [];
43 + for (const next of rows.slice(start + 1)) {
44 + if (depth(next) <= level) break;
45 + kin.push(next);
46 + }
47 + return kin;
48 + };
49 +
50 + /**
51 + * Draw the outline under `row` as its chevrons say it should be.
52 + *
53 + * Walked rather than toggled row by row: a branch inside a shut branch
54 + * keeps its own state and stays shut when the outer one opens, which is
55 + * what a reader who left it shut expects to come back to. `folded` is the
56 + * same one-variable walk the Rust side makes.
57 + */
58 + const settle = (row) => {
59 + let shut = null;
60 + for (const next of under(row)) {
61 + const level = depth(next);
62 + if (shut !== null && level <= shut) shut = null;
63 + const hidden = shut !== null;
64 + next.hidden = hidden;
65 + const chevron = next.querySelector(DISCLOSE);
66 + if (!hidden && chevron?.getAttribute("aria-expanded") === "false") {
67 + shut = level;
68 + }
69 + }
70 + };
71 +
72 + document.addEventListener("click", (event) => {
73 + const chevron = event.target.closest(DISCLOSE);
74 + if (!chevron) return;
75 + const row = rowOf(chevron);
76 + if (!row) return;
77 + // Never the row's own act. A chevron is a separate hit target from the
78 + // label precisely so that pressing it is not pressing the row, and a
79 + // row that activates on click would otherwise navigate underneath the
80 + // fold.
81 + event.preventDefault();
82 + event.stopPropagation();
83 + const open = chevron.getAttribute("aria-expanded") === "true";
84 + chevron.setAttribute("aria-expanded", String(!open));
85 + chevron.setAttribute("aria-label", open ? "Expand" : "Collapse");
86 + settle(row);
87 + });
88 + })();