Skip to main content

max / makenotwork

Say the confirm and the danger where the act is, at thirteen more sites Shape 8 through the glue-module ruling, eight modules under src/quasi/: sessions, clips, promo codes, license keys, git repositories, library rows, the cart line and the profile link row. Each names its route, where the answer lands, that it destroys something, and what to ask first, in one sentence every renderer reads. clip_acts holds the ruling's proof: deleting a shared clip asks, removing one from an item does not, and both carry Tone::Danger. link_row leaves the unsayable column. Action::replacing_enclosing says "the row I am in" without the relative selector a description cannot hold, so the row says data-row and the target follows. blog_delete_act goes: both templates it served are gone and the described content tab spells the same act against its own route. HIGH_WATER 43 to 30, with the three classes that remain named in its doc.
Author: Max Johnson <me@maxj.phd> · 2026-09-01 15:42 UTC
Signed with PGP, not checked
Commit: 2787e4dad58c83215cf7fcf80638c1b6ee576ce3
Parent: fdf85e7
20 files changed, +820 insertions, -172 deletions
@@ -37,16 +37,50 @@
37 37 /// without describing anything. Read it against what moved before treating it
38 38 /// as a progress figure.
39 39 ///
40 - /// Two sites are deliberately not converted: the Remove buttons in
41 - /// `partials/link_row.html` and `partials/tag.html` both target
42 - /// `hx-target="closest .<class>"`, and a relative selector is not something
43 - /// `Action::replaces` can say. Filed rather than approximated.
40 + /// 43 to 30: thirteen sites left through glue modules, which is the whole of
41 + /// what a described act can say against a route the description layer does not
42 + /// serve. `session_acts` took the three in `tabs/user_sessions.html`,
43 + /// `promo_code_acts` the two in `partials/promo_codes_list.html`, `repo_acts`
44 + /// the two in `tabs/project_code.html`, `library_acts` the two in
45 + /// `tabs/library_collections.html`, and `clip_acts`, `license_key_act`,
46 + /// `cart_act` and `link_remove_act` one each from `partials/insertion_list.html`,
47 + /// `partials/item_license_keys.html`, `pages/cart.html` and
48 + /// `partials/link_row.html`. `clip_acts` also took the Remove in
49 + /// `partials/placement_list.html`, which the seal never counted: it is
50 + /// destructive with no confirm, and describing it beside its asking twin is
51 + /// where the ruling's separation is proved.
52 + ///
53 + /// `link_row.html` was one of the two the shape recorded as unsayable.
54 + /// `Action::replacing_enclosing` says it now. The other, `partials/tag.html`,
55 + /// stays: its button reads a glyph and takes its name from `aria-label`, and a
56 + /// described act renders its label as text, so converting it would move a
57 + /// prompt and lose a name.
58 + ///
59 + /// What is left splits four ways, none of it a matter of effort. Measured
60 + /// 2026-09-01 by reading the verb, target and swap at each of the 30:
61 + ///
62 + /// - Ten carry no `hx-target` and refresh, reload or navigate through
63 + /// `data-after` glue. `Action::invalidating` emits `data-replaces` and
64 + /// nothing under `frontend/src` performs it, so describing one today would
65 + /// be describing a control that stops working.
66 + /// - Ten answer into a region with `hx-swap="innerHTML"`: the four admin
67 + /// entry partials, both admin dashboards, the broadcast result, and the two
68 + /// dual-serve fragments below. Every described act emits
69 + /// `hx-swap="outerMorph"`, so the container would be replaced by its own
70 + /// contents and the next press would find no target.
71 + /// - Five are submit buttons inside a form that carries the verb. An act is a
72 + /// control with an action of its own, so these convert with their form.
73 + /// - Five are neither: two tag glyphs (above), the two domain rows in
74 + /// `tabs/user_profile.html`, which target `closest .form-section` and also
75 + /// run a `data-after` verb, and the Remove in `tabs/library_purchases.html`,
76 + /// which is a row of a context menu rather than a button standing among
77 + /// others.
44 78 ///
45 79 /// `partials/ssh_keys_list.html` and `partials/git_tokens_list.html` are not
46 80 /// this program's to remove either: they are not tab renderings but the
47 81 /// fragments `routes::api::ssh_keys` and the git-token routes answer with, and
48 82 /// the described screens still call those.
49 - const HIGH_WATER: usize = 43;
83 + const HIGH_WATER: usize = 30;
50 84
51 85 /// Every template, recursively. Walked rather than listed for the reason the
52 86 /// frontend-globals seal walks `frontend/src`: a fence that has to be told about
@@ -38,8 +38,9 @@
38 38 use crate::AppState;
39 39 use crate::auth::SessionUser;
40 40
41 - pub mod blog_delete_act;
42 41 pub mod buyer_contacts;
42 + pub mod cart_act;
43 + pub mod clip_acts;
43 44 pub mod collections;
44 45 pub mod creators;
45 46 pub mod custom_page;
@@ -55,8 +56,11 @@
55 56 pub mod item_files;
56 57 pub mod item_sales;
57 58 pub mod item_tabs;
59 + pub mod library_acts;
58 60 pub mod library_contacts;
59 61 pub mod library_tabs;
62 + pub mod license_key_act;
63 + pub mod link_remove_act;
60 64 pub mod media_picker;
61 65 pub mod payout_summary;
62 66 pub mod policy;
@@ -66,8 +70,11 @@
66 70 pub mod project_members;
67 71 pub mod project_overview;
68 72 pub mod project_tabs;
73 + pub mod promo_code_acts;
74 + pub mod repo_acts;
69 75 pub mod rich_field;
70 76 pub mod schedule_field;
77 + pub mod session_acts;
71 78 pub mod settings_tabs;
72 79 pub mod shortcuts;
73 80 pub mod ssh_keys;
@@ -84,11 +84,7 @@
84 84 {% endif %}
85 85 </td>
86 86 <td>
87 - <button class="btn-secondary small cart-row-btn"
88 - hx-delete="/api/cart/{{ item.item_id }}"
89 - hx-target="#cart-row-{{ item.item_id }}"
90 - hx-swap="outerHTML"
91 - hx-confirm="Remove this item from your cart?">Remove</button>
87 + {{ crate::quasi::cart_act::html(item.item_id)|safe }}
92 88 </td>
93 89 </tr>
94 90 {% endfor %}
@@ -29,11 +29,7 @@
29 29 <td>{{ ins.created_at }}</td>
30 30 <td class="insertion-list-actions">
31 31 <button type="button" class="btn btn-xs insertion-list-action-btn" data-title="{{ ins.title }}" data-action="onInsertionRename" data-arg="{{ ins.id }}">Rename</button>
32 - <button type="button" class="btn btn-xs btn-danger insertion-list-action-btn"
33 - hx-delete="/api/insertions/{{ ins.id }}"
34 - hx-target="#insertion-library"
35 - hx-swap="outerHTML"
36 - hx-confirm="Delete this clip? It will be removed from all items using it.">Delete</button>
32 + {{ crate::quasi::clip_acts::delete_html(ins.id)|safe }}
37 33 </td>
38 34 </tr>
39 35 {% endfor %}
@@ -34,13 +34,7 @@
34 34 <td>{{ key.created_at }}</td>
35 35 <td>
36 36 {% if !key.is_revoked %}
37 - <button class="btn btn-sm btn-danger"
38 - hx-post="/api/keys/{{ key.id }}/revoke"
39 - hx-target="#license-keys-list"
40 - hx-swap="outerHTML"
41 - hx-confirm="Revoke this license key? All activations will be deactivated.">
42 - Revoke
43 - </button>
37 + {{ crate::quasi::license_key_act::html(key.id)|safe }}
44 38 {% endif %}
45 39 </td>
46 40 </tr>
@@ -1,4 +1,4 @@
1 - <div class="link-row" data-id="{{ id }}">
1 + <div class="link-row" data-row data-id="{{ id }}">
2 2 <div class="link-order-buttons">
3 3 <button type="button" class="order-btn" data-action="onMoveLink" data-arg="-1" title="Move up">&#9650;</button>
4 4 <button type="button" class="order-btn" data-action="onMoveLink" data-arg="1" title="Move down">&#9660;</button>
@@ -8,9 +8,5 @@
8 8 <button class="btn-secondary link-edit-btn" data-action="onEditLink">Edit</button>
9 9 <button class="btn-primary link-save-btn hidden" data-action="onSaveLink">Save</button>
10 10 <button class="btn-secondary link-cancel-btn hidden" data-action="onCancelLink">Cancel</button>
11 - <button class="btn-danger link-remove-btn"
12 - hx-delete="/api/links/{{ id }}"
13 - hx-target="closest .link-row"
14 - hx-swap="outerHTML"
15 - hx-confirm="Remove this link?">Remove</button>
11 + <span class="link-remove-btn">{{ crate::quasi::link_remove_act::html(id)|safe }}</span>
16 12 </div>
@@ -23,10 +23,7 @@
23 23 <td>{{ p.position }}</td>
24 24 <td>{% if let Some(offset) = p.offset_display %}{{ offset }}{% else %}-{% endif %}</td>
25 25 <td class="placement-list-actions">
26 - <button type="button" class="btn btn-xs btn-danger placement-list-remove"
27 - hx-delete="/api/item-insertions/{{ p.id }}"
28 - hx-target="#placement-list"
29 - hx-swap="outerHTML">Remove</button>
26 + {{ crate::quasi::clip_acts::remove_html(p.id)|safe }}
30 27 </td>
31 28 </tr>
32 29 {% endfor %}
@@ -63,13 +63,7 @@
63 63 <td class="promo-codes-actions">
64 64 <button class="btn-secondary small promo-codes-edit-btn"
65 65 data-action="togglePromoEditRow">Edit</button>
66 - <button class="btn btn-sm btn-danger"
67 - hx-delete="/api/promo-codes/{{ code.id }}"
68 - hx-target="#promo-codes-list"
69 - hx-swap="outerHTML"
70 - hx-confirm="Delete this code? It will no longer be usable.">
71 - Delete
72 - </button>
66 + {{ crate::quasi::promo_code_acts::html(code.id)|safe }}
73 67 </td>
74 68 </tr>
75 69 <tr class="hidden promo-codes-edit-row">
@@ -105,11 +99,7 @@
105 99 </table>
106 100
107 101 <div class="promo-codes-bulk-row">
108 - <button class="btn-secondary small promo-codes-bulk-btn"
109 - hx-delete="/api/promo-codes/expired"
110 - hx-target="#promo-codes-list"
111 - hx-swap="outerHTML"
112 - hx-confirm="Delete all expired promo codes?">Delete all expired</button>
102 + {{ crate::quasi::promo_code_acts::expired_html()|safe }}
113 103 </div>
114 104 {% endif %}
115 105 </div>
@@ -17,17 +17,13 @@
17 17 </thead>
18 18 <tbody>
19 19 {% for c in collections %}
20 - <tr>
20 + <tr data-row>
21 21 <td><a href="/c/{{ username }}/{{ c.slug }}">{{ c.title }}</a></td>
22 22 <td>{{ c.item_count }}</td>
23 23 <td>{% if c.is_public %}<span class="badge">Public</span>{% else %}<span class="badge is-faded">Private</span>{% endif %}</td>
24 24 <td>{{ c.created_at }}</td>
25 25 <td>
26 - <button class="btn-secondary small btn-compact"
27 - hx-delete="/api/collections/{{ c.id }}"
28 - hx-confirm="Delete this collection?"
29 - hx-target="closest tr"
30 - hx-swap="outerHTML">Delete</button>
26 + {{ crate::quasi::library_acts::delete_collection_html(c.id)|safe }}
31 27 </td>
32 28 </tr>
33 29 {% endfor %}
@@ -97,11 +93,7 @@
97 93 hx-post="/api/cart/{{ item.item_id }}"
98 94 hx-swap="none"
99 95 data-after="mark-added">Add to Cart</button>
100 - <button class="btn-secondary small btn-compact"
101 - hx-post="/api/wishlists/{{ item.item_id }}"
102 - hx-target="#wishlist-row-{{ item.item_id }}"
103 - hx-swap="outerHTML swap:0.2s"
104 - hx-confirm="Remove from wishlist?">Remove</button>
96 + {{ crate::quasi::library_acts::remove_from_wishlist_html(item.item_id)|safe }}
105 97 </td>
106 98 </tr>
107 99 {% endfor %}
@@ -13,11 +13,7 @@
13 13 <div class="proj-code-repo">
14 14 <div class="proj-code-repo-row">
15 15 <a href="/git/{{ repo.name }}" class="proj-code-repo-link">{{ repo.name }}</a>
16 - <button class="btn-danger"
17 - hx-delete="/api/projects/{{ project.id }}/repos/{{ repo.name }}"
18 - hx-target="#git-repos-section"
19 - hx-swap="outerHTML"
20 - hx-confirm="Unlink '{{ repo.name }}' from this project?">Unlink</button>
16 + {{ crate::quasi::repo_acts::unlink_html(project.id, repo.name)|safe }}
21 17 </div>
22 18 {% if !repo.collaborators.is_empty() %}
23 19 <div class="proj-code-collab-block">
@@ -25,11 +21,7 @@
25 21 {% for c in &repo.collaborators %}
26 22 <div class="proj-code-collab-row">
27 23 <span class="proj-code-collab-name">@{{ c.username }}{% if !c.can_push %} <span class="proj-code-collab-readonly">(read only)</span>{% endif %}</span>
28 - <button class="btn-danger small"
29 - hx-delete="/api/repos/{{ repo.id }}/collaborators/{{ c.user_id }}"
30 - hx-target="#git-repos-section"
31 - hx-swap="outerHTML"
32 - hx-confirm="Remove @{{ c.username }} from {{ repo.name }}?">Remove</button>
24 + {{ crate::quasi::repo_acts::remove_collaborator_html(repo.id, repo.name, c.user_id, c.username)|safe }}
33 25 </div>
34 26 {% endfor %}
35 27 </div>
@@ -31,18 +31,10 @@
31 31 <td>
32 32 {% if let Some(current_id) = current_session_id %}
33 33 {% if s.id != *current_id %}
34 - <button class="btn-secondary small"
35 - hx-delete="/api/users/me/sessions/{{ s.id }}"
36 - hx-target="#sessions-list"
37 - hx-swap="outerHTML"
38 - hx-confirm="Sign out this device?">Sign out</button>
34 + {{ crate::quasi::session_acts::html(s.id)|safe }}
39 35 {% endif %}
40 36 {% else %}
41 - <button class="btn-secondary small"
42 - hx-delete="/api/users/me/sessions/{{ s.id }}"
43 - hx-target="#sessions-list"
44 - hx-swap="outerHTML"
45 - hx-confirm="Sign out this device?">Sign out</button>
37 + {{ crate::quasi::session_acts::html(s.id)|safe }}
46 38 {% endif %}
47 39 </td>
48 40 </tr>
@@ -52,11 +44,7 @@
52 44
53 45 {% if sessions.len() > 1 %}
54 46 <div class="mt-section">
55 - <button class="btn-secondary"
56 - hx-delete="/api/users/me/sessions"
57 - hx-target="#sessions-list"
58 - hx-swap="outerHTML"
59 - hx-confirm="Sign out all other devices?">Sign out all other devices</button>
47 + {{ crate::quasi::session_acts::all_others_html()|safe }}
60 48 </div>
61 49 {% endif %}
62 50 {% endif %}
@@ -1,91 +1,0 @@
1 - //! The Askama entry point for a described "delete this blog post" button.
2 - //!
3 - //! Shape 8's first conversion that did not have to wait for a screen
4 - //! (`b279b9eb`), and the second thing the glue-module ruling bought
5 - //! (`27d5e5b8`, reasoning in [`super::widgets`]). The same button was written
6 - //! twice, in `templates/partials/tabs/project_content.html` and
7 - //! `templates/partials/tabs/project_blog.html`, over the same route with the
8 - //! same prompt, in two tabs of the same dashboard. Described once, called from
9 - //! both.
10 - //!
11 - //! **One caller today**, which is not a reason to inline it back:
12 - //! `project_content.html`'s own copy becomes a described element in its turn,
13 - //! and this is where it goes.
14 - //!
15 - //! # What the description says that the markup did not
16 - //!
17 - //! That the act is destructive, and what to ask before doing it.
18 - //! [`Act::confirm`] is that, and `hx-confirm` was it: one of the 50 sites the
19 - //! shape exists to move. The difference is where the sentence lives. A prompt
20 - //! in an attribute is a fact about one template; a prompt on the act is a fact
21 - //! about the call, so a terminal asks y/n and a touch host raises an action
22 - //! sheet without either being told about this button.
23 - //!
24 - //! `Tone::Danger` for the same reason. The templates said it with
25 - //! `class="danger-text"`, which is the same claim in a spelling no other
26 - //! renderer can read.
27 - //!
28 - //! # Why the row id is a parameter
29 - //!
30 - //! The answer replaces the row it was pressed in, and `/api/blog/{id}` is a
31 - //! plain API route the description layer does not serve — it answers an
32 - //! `HX-Trigger` toast and an empty body — so it cannot name what it changed and
33 - //! the control has to. See [`Action::replaces`]: this is exactly the case that
34 - //! member exists for, rather than the misuse it warns about.
35 - //!
36 - //! Both templates already give the row a real id (`post-row-{id}`), which is
37 - //! what makes these two describable when their neighbours are not. The Remove
38 - //! buttons in `partials/link_row.html` and `partials/tag.html` carry
39 - //! `hx-target="closest .link-row"` and `hx-target="closest .tag"`, and a
40 - //! relative selector is not something [`Action::replaces`] can say; the tag one
41 - //! additionally reads `×` and gets its name from `aria-label`, which is
42 - //! quasicoherent `aad33ecc`. Both are left hand-written and filed rather than
43 - //! approximated.
44 -
45 - use makeover_layout::Tone;
46 - use quasi_router::screen::Act;
47 - use quasi_router::{Action, Node};
48 -
49 - /// The delete button for one post's row.
50 - ///
51 - /// `id` is the blog post's, and it names both the route and the row: the two
52 - /// are the same fact, so the caller states it once.
53 - #[must_use]
54 - pub fn html(id: &str) -> String {
55 - use quasi_axum::Serves as _;
56 -
57 - let act = Act::new(
58 - "Delete",
59 - Action::delete(format!("/api/blog/{id}")).replacing(format!("post-row-{id}")),
60 - )
61 - .tone(Tone::Danger)
62 - .confirm("Delete this blog post?");
63 -
64 - quasi_webview::Webview::new().fragment(&Node::Act(act))
65 - }
66 -
67 - #[cfg(test)]
68 - mod tests {
69 - /// The three things the two templates said between them, said once: where
70 - /// it goes, what it replaces, and that it is destructive.
71 - #[test]
72 - fn a_delete_names_its_route_its_row_and_its_prompt() {
73 - let html = super::html("42");
74 -
75 - assert!(html.contains(r#"hx-delete="/api/blog/42""#), "{html}");
76 - assert!(html.contains(r##"hx-target="#post-row-42""##), "{html}");
77 - assert!(
78 - html.contains(r#"hx-confirm="Delete this blog post?""#),
79 - "{html}"
80 - );
81 - }
82 -
83 - /// The tone, which is the half that could not survive before `35756077`.
84 - /// `danger-text` in a class attribute said this to the stylesheet only.
85 - #[test]
86 - fn a_delete_says_it_is_dangerous_in_a_word_every_renderer_reads() {
87 - let html = super::html("42");
88 -
89 - assert!(html.contains(r#"data-tone="danger""#), "{html}");
90 - }
91 - }