Skip to main content

max / goingson

Take quasi 0.72 and the makeover suite behind it makeover-layout 0.37.0 added Field::as_instant and its links key makes the whole suite move together: layout 0.37.0, tui 0.37.0, webview 0.63.0, build 0.56.0. Two things beyond the pins. The runtime form model says as_instant: false for the reason it says nothing to every other constraint — the JS form spec has no way to ask for it, and these fields post to Tauri commands rather than to a route storing an instant. And quasi-instant.js joins the scripts written out at build time, because the shell links every one of them and 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>
Author: Max Johnson <me@maxj.phd> · 2026-08-28 15:29 UTC
Signed with PGP, not checked
Commit: 0eaa7185f824360e20a14a71c3601efd9eaddc6b
Parent: 71cf68f
6 files changed, +106 insertions, -32 deletions
M Cargo.lock +19 -19
@@ -3444,9 +3444,9 @@
3444 3444
3445 3445 [[package]]
3446 3446 name = "makeover-build"
3447 - version = "0.55.0"
3447 + version = "0.56.0"
3448 3448 source = "registry+https://github.com/rust-lang/crates.io-index"
3449 - checksum = "725a4e9fad30d4d9fa840c408a70dfea583f0d5ada611a95a131a63429b4391a"
3449 + checksum = "56c8903ef617932723aea44f7ada31c7ce4f89d008e8d25ebaee956b2258bb91"
3450 3450 dependencies = [
3451 3451 "makeover",
3452 3452 "makeover-geometry",
@@ -3462,9 +3462,9 @@
3462 3462
3463 3463 [[package]]
3464 3464 name = "makeover-layout"
3465 - version = "0.36.0"
3465 + version = "0.37.0"
3466 3466 source = "registry+https://github.com/rust-lang/crates.io-index"
3467 - checksum = "9ffed28fd6f311d8603b9fe02cffc14bacf29b840c486f84e8ff4866a4748514"
3467 + checksum = "369bc3c3e0ee91d24b3552f48f1ecb1764bca953725fb817df33b2dc80f2d34f"
3468 3468
3469 3469 [[package]]
3470 3470 name = "makeover-timing"
@@ -3477,9 +3477,9 @@
3477 3477
3478 3478 [[package]]
3479 3479 name = "makeover-touch"
3480 - version = "0.26.0"
3480 + version = "0.27.0"
3481 3481 source = "registry+https://github.com/rust-lang/crates.io-index"
3482 - checksum = "941676f114fdb8b8e2dbd2e35dd0149d409212982b37139de831d9865f664d77"
3482 + checksum = "b180e22917189e05d291bfec0e930b5e6c2952dc7bf6e2f7a2262b903431cc72"
3483 3483 dependencies = [
3484 3484 "makeover-geometry",
3485 3485 "makeover-layout",
@@ -3487,9 +3487,9 @@
3487 3487
3488 3488 [[package]]
3489 3489 name = "makeover-tui"
3490 - version = "0.36.0"
3490 + version = "0.37.0"
3491 3491 source = "registry+https://github.com/rust-lang/crates.io-index"
3492 - checksum = "6ecee186af0fc104be9237defc437be2bfd8512cf5064ba0ff7a9298b01de081"
3492 + checksum = "b5e2ea93b84f2cb85fde760d360ff4c095320db242a0d0c08cbaf2f396a8de8e"
3493 3493 dependencies = [
3494 3494 "makeover",
3495 3495 "makeover-layout",
@@ -3499,9 +3499,9 @@
3499 3499
3500 3500 [[package]]
3501 3501 name = "makeover-webview"
3502 - version = "0.62.0"
3502 + version = "0.63.0"
3503 3503 source = "registry+https://github.com/rust-lang/crates.io-index"
3504 - checksum = "546fb283399000bcd303b1d4d59b7d4c560eea8d6c40c4bed172f0c32a50c8e1"
3504 + checksum = "9149f06aa7b8f4660609a680169d76075d91def63c23127bd84af9b183be22fa"
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.71.0"
4720 + version = "0.72.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.71.0"
4729 + version = "0.72.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.71.0"
4737 + version = "0.72.0"
4738 4738 dependencies = [
4739 4739 "makeover-layout",
4740 4740 ]
4741 4741
4742 4742 [[package]]
4743 4743 name = "quasi-tauri"
4744 - version = "0.71.0"
4744 + version = "0.72.0"
4745 4745 dependencies = [
4746 4746 "http",
4747 4747 "quasi-http",
@@ -4751,7 +4751,7 @@
4751 4751
4752 4752 [[package]]
4753 4753 name = "quasi-type"
4754 - version = "0.1.0"
4754 + version = "0.1.1"
4755 4755 dependencies = [
4756 4756 "brotli",
4757 4757 "font-types",
@@ -4767,7 +4767,7 @@
4767 4767
4768 4768 [[package]]
4769 4769 name = "quasi-webview"
4770 - version = "0.71.0"
4770 + version = "0.72.0"
4771 4771 dependencies = [
4772 4772 "docengine",
4773 4773 "makeover-layout",
@@ -8506,15 +8506,15 @@
8506 8506
8507 8507 [[patch.unused]]
8508 8508 name = "quasi-axum"
8509 - version = "0.71.0"
8509 + version = "0.72.0"
8510 8510
8511 8511 [[patch.unused]]
8512 8512 name = "quasi-basics"
8513 - version = "0.71.0"
8513 + version = "0.72.0"
8514 8514
8515 8515 [[patch.unused]]
8516 8516 name = "quasi-immediate"
8517 - version = "0.71.0"
8517 + version = "0.72.0"
8518 8518
8519 8519 [[patch.unused]]
8520 8520 name = "quasi-store"
M Cargo.toml +1 -1
@@ -105,7 +105,7 @@
105 105 notify = "8.2"
106 106 notify-debouncer-mini = "0.7"
107 107 makeover = "3.0"
108 - makeover-tui = "0.36.0"
108 + makeover-tui = "0.37.0"
109 109 toml = "1.1"
110 110
111 111 # Enums
@@ -18,25 +18,25 @@
18 18 # Materialises all three generated files: themes/, geometry.css, layout.css,
19 19 # and holds both drift checks this build.rs used to carry itself: touch density
20 20 # as of 0.17, breakpoints as of 0.18.
21 - makeover-build = "0.55.0"
21 + makeover-build = "0.56.0"
22 22 # Cuts the two house faces into frontend/fonts/. A git dependency because
23 23 # quasi-type is `publish = false`, which is also why makeover-build cannot do
24 24 # this and only emits the CSS that fetches the result. The rev is the one
25 25 # shop's `shop-font`, Balanced Breakfast and Alloy's `QUASI_TYPE_REV` name: the
26 26 # house face is one face, so they move together or a user sees two Quasi Monos.
27 - quasi-type = { git = "https://makenot.work/git/max/quasi-type.git", version = "0.1", rev = "e40330602e68fbdd2a9d2917bdec33a7ce573a1f" }
27 + quasi-type = { git = "https://makenot.work/git/max/quasi-type.git", version = "0.1.1", rev = "e57a57f35563252b5ed00a8caf1262517f3d51f4" }
28 28 # The table CSS is generated here too: the columns are this app's, so the
29 29 # shared helper cannot know them, but the tracks and the narrowing rules come
30 30 # from the description rather than from hand-written nth-child cuts.
31 - makeover-webview = "0.62.0"
32 - makeover-layout = "0.36.0"
31 + makeover-webview = "0.63.0"
32 + makeover-layout = "0.37.0"
33 33 # Width. Direct rather than through makeover-webview, because the narrow table
34 34 # pass keys off SizeClass::Compact and a boundary reached transitively is a
35 35 # boundary nobody pinned.
36 36 makeover-geometry = "0.7"
37 37 # Only to write its selection and clock scripts into the frontend. Those two are
38 38 # the whole of `frontend/js/` since the swap.
39 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.71" }
39 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.72" }
40 40
41 41 [dependencies]
42 42 goingson-core = { workspace = true }
@@ -54,10 +54,10 @@
54 54 # dependency's source even with the feature off -- so a public clone could not
55 55 # build at all against a URL it could not reach. quasi went public that day and
56 56 # the URL moved here with it.
57 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.71" }
58 - quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.71" }
59 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.71" }
60 - quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.71" }
57 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.72" }
58 + quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.72" }
59 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.72" }
60 + quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.72" }
61 61
62 62 # The notification declaration, and it is NOT behind the `quasi` feature: the
63 63 # watcher is desktop app behaviour rather than a described screen, and the
@@ -74,7 +74,7 @@
74 74 # before it has ported its screens cannot take it. `notifications::Notifier` is
75 75 # the eight lines meanwhile, and it goes when the flip makes quasi-router
76 76 # unconditional here.
77 - quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.71", features = ["synckit", "describe"] }
77 + quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.72", features = ["synckit", "describe"] }
78 78
79 79 # Tauri
80 80 tauri = { workspace = true, features = ["image-png", "devtools"] }
@@ -139,8 +139,8 @@
139 139 # The forms emitter and the description it renders. Runtime deps, not build:
140 140 # form markup depends on the value, the error and the options at the moment a
141 141 # modal opens, so it cannot be materialised the way the stylesheet is.
142 - makeover-webview = "0.62.0"
143 - makeover-layout = "0.36.0"
142 + makeover-webview = "0.63.0"
143 + makeover-layout = "0.37.0"
144 144 # Browser opening
145 145 open = { workspace = true }
146 146
@@ -616,6 +616,7 @@
616 616 ("quasi-copy.js", quasi_webview::COPY_JS),
617 617 ("quasi-repeat.js", quasi_webview::REPEAT_JS),
618 618 ("quasi-awaiting.js", quasi_webview::AWAITING_JS),
619 + ("quasi-instant.js", quasi_webview::INSTANT_JS),
619 620 ] {
620 621 fs::write(frontend.join("js").join(name), source)
621 622 .unwrap_or_else(|error| panic!("write js/{name}: {error}"));
@@ -224,6 +224,12 @@
224 224 // would be inventing a question the app never asked.
225 225 upper_name: None,
226 226 extended: spec.extended,
227 + // makeover-layout 0.37.0's instant flag, on the same footing again:
228 + // the JS form spec has no way to say a datetime is submitted as the
229 + // moment it names, and this runtime form posts to Tauri commands
230 + // rather than to a route storing an instant. Saying it here would
231 + // convert a value nothing asked to have converted.
232 + as_instant: false,
227 233 };
228 234
229 235 let value = match kind {
@@ -1,0 +1,67 @@
1 + // Submitting the moment a reader meant, rather than the words they typed.
2 + //
3 + // A `datetime-local` box asks for a time the way a person says one -- "the 14th
4 + // at half past two" -- and that names a different moment in Denver than it does
5 + // in Berlin. A route storing an instant needs the moment, and nothing in HTML
6 + // converts one to the other: the box submits the characters. So somebody
7 + // converts, and in this renderer that is here.
8 + //
9 + // It is here rather than above the renderer because the browser is the only
10 + // party that knows what "your computer's time zone" means. A description cannot
11 + // answer it; a server can only guess; a terminal and an egui host would each
12 + // answer it their own way. `makeover_layout::Field::as_instant` is the
13 + // description saying it wants the conversion and declining to say how, and this
14 + // file is the how for one host.
15 + //
16 + // It reads one attribute the form renderer writes, `data-instant`, and it is
17 + // the whole of the per-app publish-at plumbing this stack exists to delete. The
18 + // MNW server carried its own copy under a private `data-config` vocabulary.
19 + (() => {
20 + "use strict";
21 +
22 + // The mark makeover-webview puts on a datetime input whose description
23 + // asked for an instant. Only that kind is marked: a date and a time are
24 + // each half a moment and cannot name one on their own.
25 + const MARK = "data-instant";
26 +
27 + /**
28 + * The absolute instant a local wall-clock string names, as RFC 3339.
29 + *
30 + * `new Date(v)` reads a bare `YYYY-MM-DDTHH:mm` in the browser's own zone,
31 + * which is exactly the reading wanted. A value the browser cannot parse is
32 + * left alone rather than replaced with `Invalid Date`: an unparseable value
33 + * is a validation failure the route should see and answer, and rewriting it
34 + * here would turn a legible error into an unreadable one.
35 + */
36 + const instant = (local) => {
37 + const at = new Date(local);
38 + return Number.isNaN(at.getTime()) ? null : at.toISOString();
39 + };
40 +
41 + // Rewrite on the way out, on the request htmx is about to make, so nothing
42 + // the reader can see changes: the box keeps showing the time they typed and
43 + // the wire carries the moment it names.
44 + //
45 + // The walk is over the submitting element's own marked inputs rather than
46 + // over the document, so a page with two forms on it converts the one being
47 + // submitted. `htmx:config:request` is htmx 4's name; 2.x spelled it
48 + // `htmx:configRequest`.
49 + document.body.addEventListener("htmx:config:request", (event) => {
50 + const source = event.detail?.ctx?.sourceElement;
51 + const body = event.detail?.ctx?.request?.body;
52 + if (!source?.querySelectorAll || !body) return;
53 +
54 + const marked =
55 + source.matches?.(`[${MARK}]`) === true
56 + ? [source]
57 + : source.querySelectorAll(`[${MARK}]`);
58 + for (const input of marked) {
59 + const name = input.getAttribute("name");
60 + if (!name) continue;
61 + const local = body.get(name);
62 + if (typeof local !== "string" || !local) continue;
63 + const moment = instant(local);
64 + if (moment !== null) body.set(name, moment);
65 + }
66 + });
67 + })();