Skip to main content

max / makenotwork

Declare the two act glue modules and the follow control Four shapes. Nothing in the loss list moved: every address, every verb, the tone, the prompt, the region a press replaces, and both spellings of a follower count are the same, which is what these modules' own tests already asserted. `follow::region` stays a function. It builds an id rather than a description, and a shape returns a member of the vocabulary.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-03 17:22 UTC
Signed with PGP, not checked
Commit: d1c93add55461709c98c13e7318466aeb2e8688f
Parent: cf80059
4 files changed, +99 insertions, -96 deletions
M server/Cargo.lock +13 -13
@@ -6674,7 +6674,7 @@
6674 6674
6675 6675 [[package]]
6676 6676 name = "quasi-router"
6677 - version = "0.101.1"
6677 + version = "0.101.2"
6678 6678 dependencies = [
6679 6679 "makeover-layout",
6680 6680 ]
@@ -10770,12 +10770,16 @@
10770 10770 ]
10771 10771
10772 10772 [[patch.unused]]
10773 - name = "synckit-client"
10774 - version = "0.10.0"
10773 + name = "kberg"
10774 + version = "0.1.0"
10775 10775
10776 10776 [[patch.unused]]
10777 - name = "synckit-config"
10778 - version = "0.2.0"
10777 + name = "ops-status"
10778 + version = "0.1.0"
10779 +
10780 + [[patch.unused]]
10781 + name = "painhours"
10782 + version = "0.1.0"
10779 10783
10780 10784 [[patch.unused]]
10781 10785 name = "quasi-immediate"
@@ -10794,13 +10798,9 @@
10794 10798 version = "0.101.1"
10795 10799
10796 10800 [[patch.unused]]
10797 - name = "kberg"
10798 - version = "0.1.0"
10801 + name = "synckit-client"
10802 + version = "0.10.0"
10799 10803
10800 10804 [[patch.unused]]
10801 - name = "ops-status"
10802 - version = "0.1.0"
10803 -
10804 - [[patch.unused]]
10805 - name = "painhours"
10806 - version = "0.1.0"
10805 + name = "synckit-config"
10806 + version = "0.2.0"
@@ -39,39 +39,44 @@
39 39 //! Askama on the element around it, where they were always a fact about that
40 40 //! page's layout rather than about the control.
41 41
42 + use quasi_declare::declare;
43 + use quasi_router::Node;
42 44 use quasi_router::screen::Act;
43 - use quasi_router::{Action, Node};
44 45
45 - /// The route each export posts to, and the name the file is kept under.
46 - ///
47 - /// A read would be a link the browser downloads by itself; these are posts,
48 - /// because assembling the file is work the server does rather than a document
49 - /// it already has, and `Action::saves` is independent of the verb for exactly
50 - /// that reason.
51 - ///
52 - /// `awaiting` because the whole file is assembled before any of it comes back.
53 - /// Nothing countable to say about the wait: the row count is often known at the
54 - /// call site and the byte count never is, and `layout::Awaiting` takes a
55 - /// measurement rather than a stand-in for one.
56 - #[must_use]
57 - pub fn act(route: &str, filename: &str) -> Node {
58 - Node::Act(control("Export CSV", route, filename))
46 + declare! {
47 + /// The route each export posts to, and the name the file is kept under.
48 + ///
49 + /// A read would be a link the browser downloads by itself; these are posts,
50 + /// because assembling the file is work the server does rather than a document
51 + /// it already has, and `Action::saves` is independent of the verb for exactly
52 + /// that reason.
53 + ///
54 + /// `awaiting` because the whole file is assembled before any of it comes back.
55 + /// Nothing countable to say about the wait: the row count is often known at the
56 + /// call site and the byte count never is, and `layout::Awaiting` takes a
57 + /// measurement rather than a stand-in for one.
58 + #[must_use]
59 + pub shape act(route: &str, filename: &str) -> Node;
60 +
61 + include control("Export CSV", route, filename);
59 62 }
60 63
61 - /// The same sentence under a caller's own label.
62 - ///
63 - /// `Export CSV` is right for a control standing alone among others, which is
64 - /// every site [`act`] serves. It is wrong on the export portal, where six cards
65 - /// each name their own subject and the format is already in the card's meta
66 - /// line, so the control there reads `Download`. The label is the only thing
67 - /// that varies: what a caller must not restate is the sentence, which is here
68 - /// once and is what this module exists for.
69 - ///
70 - /// Returns the [`Act`] rather than a [`Node`] because the portal's controls sit
71 - /// in a [`quasi_router::Row`], which holds acts rather than nodes.
72 - #[must_use]
73 - pub fn control(label: &str, route: &str, filename: &str) -> Act {
74 - Act::new(label, Action::post(route).saving(filename).awaiting())
64 + declare! {
65 + /// The same sentence under a caller's own label.
66 + ///
67 + /// `Export CSV` is right for a control standing alone among others, which is
68 + /// every site [`act`] serves. It is wrong on the export portal, where six cards
69 + /// each name their own subject and the format is already in the card's meta
70 + /// line, so the control there reads `Download`. The label is the only thing
71 + /// that varies: what a caller must not restate is the sentence, which is here
72 + /// once and is what this module exists for.
73 + ///
74 + /// Returns the [`Act`] rather than a [`Node`] because the portal's controls sit
75 + /// in a [`quasi_router::Row`], which holds acts rather than nodes.
76 + #[must_use]
77 + pub shape control(label: &str, route: &str, filename: &str) -> Act;
78 +
79 + act label to post route saving filename awaiting;
75 80 }
76 81
77 82 /// The same act as a fragment, for a template to drop in place.
@@ -31,7 +31,8 @@
31 31 //! in -- "Following (12)" against "Follow (12)" -- so what is lost is a
32 32 //! seven-tenths opacity on a button that says the same thing in words.
33 33
34 - use quasi_router::{Act, Action, Node, RegionKind, Slot};
34 + use quasi_declare::declare;
35 + use quasi_router::Node;
35 36 use quasi_webview::Webview;
36 37
37 38 /// The region a press on this control replaces.
@@ -46,31 +47,28 @@
46 47 format!("follow-{target_type}-{target_id}")
47 48 }
48 49
49 - /// The control, in whichever of its two states the viewer is in.
50 - ///
51 - /// The count rides in the label rather than beside it, which is what both
52 - /// templates did: a bare number next to a verb reads as a second control.
53 - #[must_use]
54 - pub fn control(
55 - target_type: &str,
56 - target_id: &str,
57 - is_following: bool,
58 - follower_count: i64,
59 - ) -> Node {
50 + declare! {
51 + /// The control, in whichever of its two states the viewer is in.
52 + ///
53 + /// The count rides in the label rather than beside it, which is what both
54 + /// templates did: a bare number next to a verb reads as a second control.
55 + #[must_use]
56 + pub shape control(
57 + target_type: &str,
58 + target_id: &str,
59 + is_following: bool,
60 + follower_count: i64
61 + ) -> Node;
62 +
60 63 let id = region(target_type, target_id);
61 - let route = format!("/api/follow/{target_type}/{target_id}");
62 - let act = if is_following {
63 - Act::new(
64 - format!("Following ({follower_count})"),
65 - Action::delete(route).replacing(&id),
66 - )
67 - } else {
68 - Act::new(
69 - format!("Follow ({follower_count})"),
70 - Action::post(route).replacing(&id),
71 - )
72 - };
73 - Node::Region(Slot::new(id, RegionKind::Group).with(Node::Act(act)))
64 + let route = "/api/follow/{target_type}/{target_id}";
65 +
66 + region id as Group {
67 + given is_following {
68 + true -> act "Following ({follower_count})" to delete route replacing &id;
69 + false -> act "Follow ({follower_count})" to post route replacing &id;
70 + }
71 + }
74 72 }
75 73
76 74 /// The control as a fragment, for the route that answers a press.
@@ -91,21 +89,22 @@
91 89 ))
92 90 }
93 91
94 - /// What a page shows when the viewer cannot follow: the count, or nothing.
95 - ///
96 - /// A signed-out reader, or a creator looking at their own profile. Both
97 - /// templates drew the number as plain text in that case and drew nothing at all
98 - /// when it was zero, which is the right reading: "0 followers" is a fact nobody
99 - /// wants published about them.
100 - #[must_use]
101 - pub fn count_only(follower_count: i64) -> Option<Node> {
102 - (follower_count > 0).then(|| {
103 - Node::text(if follower_count == 1 {
104 - "1 follower".to_owned()
105 - } else {
106 - format!("{follower_count} followers")
107 - })
108 - })
92 + declare! {
93 + /// What a page shows when the viewer cannot follow: the count, or nothing.
94 + ///
95 + /// A signed-out reader, or a creator looking at their own profile. Both
96 + /// templates drew the number as plain text in that case and drew nothing at all
97 + /// when it was zero, which is the right reading: "0 followers" is a fact nobody
98 + /// wants published about them.
99 + #[must_use]
100 + pub shape count_only(follower_count: i64) -> Option<Node>;
101 +
102 + let counted = given follower_count {
103 + 1 -> "1 follower",
104 + otherwise -> "{follower_count} followers",
105 + };
106 +
107 + text counted when follower_count over 0;
109 108 }
110 109
111 110 #[cfg(test)]
@@ -38,22 +38,21 @@
38 38 //! is a plain navigation in `static/item-upload.js` today and becomes an act
39 39 //! whenever the files tab converts.
40 40
41 - use makeover_layout::Tone;
42 - use quasi_router::Action;
41 + use quasi_declare::declare;
43 42 use quasi_router::screen::Act;
44 43
45 - /// The delete button for one version's row.
46 - ///
47 - /// Both ids are needed for the address, which is item and version together.
48 - /// The row it removes is the one it sits in, so nothing has to name it.
49 - #[must_use]
50 - pub fn act(item_id: &str, version_id: &str) -> Act {
51 - Act::new(
52 - "Delete",
53 - Action::delete(format!("/api/items/{item_id}/versions/{version_id}")).replacing_enclosing(),
54 - )
55 - .tone(Tone::Danger)
56 - .confirm("Delete this version?")
44 + declare! {
45 + /// The delete button for one version's row.
46 + ///
47 + /// Both ids are needed for the address, which is item and version together.
48 + /// The row it removes is the one it sits in, so nothing has to name it.
49 + #[must_use]
50 + pub shape act(item_id: &str, version_id: &str) -> Act;
51 +
52 + act "Delete" to delete "/api/items/{item_id}/versions/{version_id}" replacing_enclosing {
53 + tone Danger;
54 + confirm "Delete this version?";
55 + }
57 56 }
58 57
59 58 #[cfg(test)]