Skip to main content

max / makenotwork

Give a chosen tab makeover's name for the state, and drop the caret gap Two overrides of the same design system, found by the same check, out in one pass. `.tab.is-selected` set --surface-raised and --bevel-raised, byte for byte what the generated `.tab.chosen` sets. The argument for keeping it was that `.is-selected` is the site's own idiom across git nav links, editor tabs, section tabs, issue tabs and filter buttons, so renaming the tabs alone splits it. True, and still the wrong trade: a second name for a state makeover already names is invisible only while both rules say the same thing. The components makeover has no primitive for keep `.is-selected`; the tab has one, so it takes it. Four templates and core/tabs.ts carry `chosen` now, the More button included, since it is built as `tab tab-more-btn` and is a tab. The chosen arm keeps one declaration: `box-shadow: revert-layer`. This file zeroes the base `button` shadow on `.tab`, and unlayered CSS outranks @layer makeover whatever the specificity, so without it the generated bevel never applies and a chosen tab reads flat. Reverting hands the property back to the layer instead of naming a value, which is the difference between deferring to the design system and copying it. The sort caret's reserved gap goes with makeover-webview 0.31.0, which emits the leading space and the reserved box itself; makeover-build 0.24.0 is what carries that renderer in. Three REVIEWED_OVERLAPS entries come out and one goes in, because the check reads every class in a selector and `.tab.chosen` is two.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-12 14:44 UTC
Signed with PGP, not checked
Commit: 64fd56303ce5487acd4cd71114067669a9fc10dd
Parent: f6fdde0
10 files changed, +69 insertions, -72 deletions
M server/Cargo.lock +12 -12
@@ -5311,9 +5311,9 @@
5311 5311
5312 5312 [[package]]
5313 5313 name = "makeover-build"
5314 - version = "0.23.1"
5314 + version = "0.24.0"
5315 5315 source = "registry+https://github.com/rust-lang/crates.io-index"
5316 - checksum = "09326dad8b7fdbf1ae4359f866e8f4432a86c837f5a8bf1576bf0c298afcd41b"
5316 + checksum = "12bcb5bc04026df9a5f4007e2a0f17ed34d8671573ea6445b4d17d7f44280d69"
5317 5317 dependencies = [
5318 5318 "makeover",
5319 5319 "makeover-geometry",
@@ -5344,9 +5344,9 @@
5344 5344
5345 5345 [[package]]
5346 5346 name = "makeover-webview"
5347 - version = "0.30.1"
5347 + version = "0.31.0"
5348 5348 source = "registry+https://github.com/rust-lang/crates.io-index"
5349 - checksum = "d6f86fc082c5fc0d73192a3f3f5f74840848ba2cc815154ab207b1fc707f7c64"
5349 + checksum = "f9f97c1694d50160ed5c980a17c96b1c8d007dbdb2d6b9472a2a5270e28cb4fd"
5350 5350 dependencies = [
5351 5351 "makeover-geometry",
5352 5352 "makeover-layout",
@@ -10682,6 +10682,14 @@
10682 10682 "pkg-config",
10683 10683 ]
10684 10684
10685 + [[patch.unused]]
10686 + name = "quasi-store"
10687 + version = "0.1.0"
10688 +
10689 + [[patch.unused]]
10690 + name = "quasi-tauri"
10691 + version = "0.1.0"
10692 +
10685 10693 [[patch.unused]]
10686 10694 name = "kberg"
10687 10695 version = "0.1.0"
@@ -10697,11 +10705,3 @@
10697 10705 [[patch.unused]]
10698 10706 name = "synckit-config"
10699 10707 version = "0.2.0"
10700 -
10701 - [[patch.unused]]
10702 - name = "quasi-store"
10703 - version = "0.1.0"
10704 -
10705 - [[patch.unused]]
10706 - name = "quasi-tauri"
10707 - version = "0.1.0"
@@ -236,7 +236,7 @@
236 236 # Emits static/geometry.css (makeover-geometry) and static/layout.css
237 237 # (makeover-webview). The same generator GO and BB run; only the output paths
238 238 # differ, since the server serves its stylesheets rather than bundling them.
239 - makeover-build = "0.23.1"
239 + makeover-build = "0.24.0"
240 240 # Read directly for the embeds' own copy of the spacing layer, which they need
241 241 # because an iframe cannot link a stylesheet. makeover-build does not re-export
242 242 # it, so the pin lives here and has to track the one makeover-build resolves.
M server/build.rs +21 -16
@@ -247,37 +247,42 @@
247 247 /// need a selector matcher and a checker that guesses wrong about specificity
248 248 /// fails correct builds, so the judgement is recorded here.
249 249 ///
250 - /// **A deliberate pairing.** `.table-heading { content }` is the sort caret's
251 - /// reserved gap, declared on the unsorted arm precisely so it cannot blank the
252 - /// generated caret on the sorted one; `.table-heading { display }` and
253 - /// `.progress-fill { background }` are different elements rather than different
254 - /// arms -- the caret pseudo-element, and the media scrubber's fill, which lives
250 + /// **A deliberate pairing.** `.progress-fill { background }` is a different
251 + /// element rather than a different arm: the media scrubber's fill, which lives
255 252 /// inside `.progress-bar` and never matches the generated
256 - /// `.progress > .progress-fill`. That last one already carried a `respec-ok`
257 - /// comment saying so.
253 + /// `.progress > .progress-fill`. It already carried a `respec-ok` comment
254 + /// saying so.
255 + ///
256 + /// `.tab { box-shadow }` and `.chosen { box-shadow }` are the third kind and the
257 + /// awkward one -- one rule, two entries, because the check reads every class in
258 + /// a selector and `.tab.chosen` is both. This file zeroes the base `button`
259 + /// shadow on a tab, and unlayered CSS outranks `@layer makeover` whatever the
260 + /// specificity, so the generated `.tab.chosen` bevel cannot apply on its own.
261 + /// The chosen arm takes `box-shadow: revert-layer`, which hands the property
262 + /// back to the layer rather than naming a value. Two arms, one of which is a
263 + /// deferral rather than an override, so both entries stay.
258 264 ///
259 265 /// **A divergence taken on purpose.** `.table-row { display }` is `grid` here
260 266 /// against makeover's `table-row`, which is the same build-time grid story
261 267 /// goingson is on.
262 268 ///
263 - /// `.tab { background }` and `.tab { box-shadow }` are none of the above and
264 - /// should not stay. `.tab.is-selected` sets `--surface-raised` and
265 - /// `--bevel-raised`, which is byte for byte what the generated `.tab.chosen`
266 - /// sets: a second name for makeover's own state, which is the defect
267 - /// makeover-webview 0.27.0 exists to prevent. Removing it is a template change
268 - /// rather than a stylesheet one, so it is filed rather than done here.
269 + /// `.tab { background }` was here and is gone. `.tab.is-selected` set
270 + /// `--surface-raised` and `--bevel-raised`, byte for byte what the generated
271 + /// `.tab.chosen` sets: a second name for makeover's own state, which is the
272 + /// defect makeover-webview 0.27.0 exists to prevent. The tabs carry `chosen`
273 + /// now, in four templates and in `frontend/src/core/tabs.ts`, and the rule is
274 + /// deleted. The caret's two entries went the same way, one release later:
275 + /// makeover-webview 0.31.0 emits the leading space and the reserved box itself.
269 276 ///
270 277 /// An entry that stops colliding fails the build, so this list cannot outlive
271 278 /// what it describes.
272 279 const REVIEWED_OVERLAPS: &[(&str, &str)] = &[
273 280 ("badge", "color"),
274 281 ("card", "color"),
282 + ("chosen", "box-shadow"),
275 283 ("progress-fill", "background"),
276 - ("tab", "background"),
277 284 ("tab", "box-shadow"),
278 285 ("tab", "color"),
279 286 ("tab", "cursor"),
280 - ("table-heading", "content"),
281 - ("table-heading", "display"),
282 287 ("table-row", "display"),
283 288 ];
@@ -113,7 +113,7 @@
113 113 | Upload status block | `.upload-status` + `-row` | `-msg.is-success`, `-msg.is-error` | — |
114 114 | Status pill | `.field-status` / `.save-status` | `.success`, `.error`, `.saving` | — |
115 115 | Table | `.data-table` + `.well` (rich), `.compact-table` (small mono) | `.minw-300..800` for horizontal scroll min-width | `.sortable.ascending`, `.sortable.descending` |
116 - | Tabs | `.tabs` + `.tab` | — | `.tab.is-selected` |
116 + | Tabs | `.tabs` + `.tab` | — | `.tab.chosen` (makeover's name, not the site's) |
117 117 | Breadcrumb | `.breadcrumb` | — | — |
118 118 | Pagination | `_ui.html` macro `pagination` → `.pagination` | — | `.active` |
119 119
@@ -162,7 +162,7 @@
162 162
163 163 Hover used to have three options picked by component type, and the second of them was a depth lift: the shadow grew from `var(--shadow-raised)` to `3px 3px var(--shadow-edge)`, reserved for controls that commit a write, so that pressing "Pay" looked different from pressing "Next". That signal is gone rather than moved, and it was spent deliberately. A bevel is an edge and not a distance, so it has no growth axis to extend along; keeping a commit-versus-navigate cue would have meant a local override on top of the `layout.css` every other make-family app takes unmodified. Decided 2026-07-31 (wiki `mnw-platinum`). A control that commits a write is distinguished by its label and its placement, the same way it is in GoingsOn and Balanced Breakfast.
164 164 - **Focus**: `:focus-visible` shows the `--focus-ring` violet outline. Custom interactive containers (`.card--selectable`, sort headers) must opt in by adding `:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }`. A container the browser does not focus on its own also needs `tabindex="0"` in the markup, or the rule never matches and the outline is decoration for a state that cannot happen. The `data-action` dispatcher activates such a container on Enter and Space.
165 - - **Selected / active**: `.is-selected` modifier applies `background: var(--highlight-faint)` plus the focus-ring border. `.tab.active`, `.filter-item.active`, `.view-btn.active`, and the `:checked + .card--selectable-inner` recipe all read it.
165 + - **Selected / active**: `.is-selected` modifier applies `background: var(--highlight-faint)` plus the focus-ring border. `.filter-item.active`, `.view-btn.active`, and the `:checked + .card--selectable-inner` recipe all read it. A tab is the exception and takes `.tab.chosen` instead: makeover names that state and emits its rule, so spelling it `.is-selected` here as well made the site a second name for something the design system already named. Where makeover has a primitive, its name wins; the components it has none for keep `.is-selected`.
166 166 - **Status**: a badge names the status it reports, and the colour follows from the status. The set is `data-tone="success"` (Active, Trialing, Completed, Approved, Published, Connected, Free), `data-tone="warning"` (Pending, Processing, Scheduled, Draft, Incomplete) and `data-tone="danger"` (Failed, Past due, Unpaid, Suspended, Spam). A terminal state nobody has to act on (Refunded, Canceled) takes the plain `.badge` with no tone, which already reads as neutral: colouring it red would ask for an action that does not exist. A badge reports data, not selection, so it never takes `.is-selected` and there is no `.badge.active`. A class that names a colour rather than a state is the same category error as `.mb-4` naming a number.
167 167
168 168 The tone is an attribute rather than a class because that is makeover's spelling: `layout.css` carries `.badge[data-tone="…"]` and the four tone names are the design system's. The badge itself is coloured text, not a filled pill. It was a filled pill until wave 2 tier B2, painted per status by a local `.badge--live/pending/failed` set, and that fill was saying exactly what the generated colour says — keeping it meant the tone could never do anything. `.ai-tier-*` is the one thing that stays local, because makeover's four tones all read as alarm and a disclosure level is not alarm: a handmade item is not a "success".
@@ -55,7 +55,7 @@
55 55 .field-status / .save-status + .success / .error / .saving
56 56 .toast + .toast--success / --error / --warning
57 57 .breadcrumb / .pagination
58 - .tabs + .tab.is-selected
58 + .tabs + .tab.chosen (chosen is makeover's)
59 59
60 60 CONTROL MODIFIERS (compose onto buttons/inputs)
61 61 .small / .btn-compact / .btn-link / .btn-tiny button sizes
@@ -808,17 +808,25 @@
808 808 flex-shrink: 0;
809 809 }
810 810
811 - /* layout.css spells selection `.tab.chosen`; the site spells it
812 - `.is-selected`, and it spells it that way on git nav links, editor tabs,
813 - section tabs, issue tabs and filter buttons as well — one idiom across a
814 - dozen components that makeover has primitives for only some of. Renaming
815 - the tabs alone would split that idiom in two, so the name stays local and
816 - the VALUES are the generated ones: this is `.tab.chosen` under the site's
817 - own selection name. If the description layer ever emits the markup, the
818 - name goes with it and this rule goes away. */
819 - .tab.is-selected {
820 - background: var(--surface-raised);
821 - box-shadow: var(--bevel-raised);
811 + /* A tab's selected state is `.tab.chosen` in layout.css and nowhere else now.
812 + This file used to carry a `.tab.is-selected` rule setting the same two
813 + declarations byte for byte, on the argument that `.is-selected` is the
814 + site's own idiom across git nav links, editor tabs, section tabs, issue tabs
815 + and filter buttons, so renaming the tabs alone would split it. That is true
816 + and it is still the wrong trade: a second name for a state the design system
817 + already names is invisible only while both say the same thing, and stops
818 + being invisible the moment makeover changes what a chosen tab looks like.
819 + The other components keep `.is-selected`, because makeover has no primitive
820 + for a git nav link; the tab has one.
821 +
822 + The chosen arm's box-shadow comes back through the layer rather than being
823 + restated. `.tab` below zeroes the base `button` shadow, and this file is
824 + unlayered, so it outranks @layer makeover whatever the specificity: without
825 + the revert, the generated bevel never applies and a chosen tab reads flat.
826 + `revert-layer` hands the property back instead of naming a value, which is
827 + the difference between deferring to the design system and copying it. */
828 + .tab.chosen {
829 + box-shadow: revert-layer;
822 830 }
823 831
824 832 .tab-more-wrap {
@@ -8694,25 +8702,6 @@
8694 8702 outline-offset: 2px;
8695 8703 }
8696 8704
8697 - /* makeover-tui and makeover-immediate emit their caret with a leading space;
8698 - the webview emitter appends the bare glyph, so the gap is the app's to put
8699 - back. Reserved on every sortable heading and not only the sorted one, or
8700 - pressing a header reflows the row it sits in.
8701 -
8702 - Box only, never `content`. This file outranks the `makeover` layer, so
8703 - declaring `content` on the sorted arm would win over the generated caret and
8704 - put the arrow back to nothing -- the same override this block deletes, one
8705 - property along. The empty box goes on the arm the generator does not match. */
8706 - .table-heading[data-sortable]:not([aria-sort])::after {
8707 - content: '';
8708 - }
8709 -
8710 - .table-heading[data-sortable]::after {
8711 - display: inline-block;
8712 - min-width: 1ch;
8713 - margin-left: var(--gap-bound);
8714 - }
8715 -
8716 8705 /* Data-table column-width utilities (numeric width hints,
8717 8706 kept as classes to avoid inline `style="width: NN%"`) */
8718 8707 .col-3 { width: 3%; }
@@ -38,7 +38,7 @@
38 38 </header>
39 39
40 40 <div class="tabs" role="tablist" aria-label="Item sections">
41 - <button class="tab is-selected"
41 + <button class="tab chosen"
42 42 role="tab"
43 43 aria-selected="true"
44 44 aria-controls="tab-content"
@@ -30,7 +30,7 @@
30 30 </header>
31 31
32 32 <div class="tabs" role="tablist" aria-label="Project sections">
33 - <button class="tab is-selected"
33 + <button class="tab chosen"
34 34 role="tab"
35 35 aria-selected="true"
36 36 aria-controls="tab-content"
@@ -74,7 +74,7 @@
74 74
75 75 <div class="tabs" role="tablist" aria-label="Dashboard sections">
76 76 {% if deactivated %}
77 - <button class="tab is-selected"
77 + <button class="tab chosen"
78 78 role="tab"
79 79 aria-selected="true"
80 80 aria-controls="tab-content"
@@ -96,7 +96,7 @@
96 96 </div>
97 97 {% else %}
98 98 {% if let Some(su) = session_user %}{% if su.can_create_projects %}
99 - <button class="tab is-selected"
99 + <button class="tab chosen"
100 100 role="tab"
101 101 aria-selected="true"
102 102 aria-controls="tab-content"
@@ -108,7 +108,7 @@
108 108 hx-indicator="#tab-spinner"
109 109 data-action="onSetActiveTab">Projects</button>
110 110 {% endif %}{% endif %}
111 - <button class="tab{% if let Some(su) = session_user %}{% if !su.can_create_projects %} is-selected{% endif %}{% endif %}"
111 + <button class="tab{% if let Some(su) = session_user %}{% if !su.can_create_projects %} chosen{% endif %}{% endif %}"
112 112 role="tab"
113 113 aria-selected="{% if let Some(su) = session_user %}{% if !su.can_create_projects %}true{% else %}false{% endif %}{% endif %}"
114 114 aria-controls="tab-content"
@@ -10,7 +10,7 @@
10 10 <h1 class="page-title">Your Library</h1>
11 11
12 12 <div class="tabs" role="tablist" aria-label="Library sections">
13 - <button class="tab is-selected"
13 + <button class="tab chosen"
14 14 role="tab"
15 15 aria-selected="true"
16 16 aria-controls="tab-content"
@@ -17,8 +17,11 @@
17 17 const moreBtn = moreWrap.querySelector('.tab-more-btn');
18 18 const menu = moreWrap.querySelector('.tab-overflow-menu');
19 19 if (!moreBtn || !menu) return;
20 - const hasActive = menu.querySelector('.tab.is-selected');
21 - moreBtn.classList.toggle('is-selected', !!hasActive);
20 + // `chosen` and not the site's `.is-selected`: the More button is built with
21 + // `className = 'tab tab-more-btn'`, so it is a tab and takes the tab's name
22 + // for the state. That name is makeover's, spelled once.
23 + const hasActive = menu.querySelector('.tab.chosen');
24 + moreBtn.classList.toggle('chosen', !!hasActive);
22 25 }
23 26
24 27 function reflow(tabsEl: HTMLElement): void {
@@ -113,10 +116,10 @@
113 116 const container = btn.closest('.tabs');
114 117 if (!container) return;
115 118 container.querySelectorAll('.tab').forEach((tab) => {
116 - tab.classList.remove('is-selected');
119 + tab.classList.remove('chosen');
117 120 tab.setAttribute('aria-selected', 'false');
118 121 });
119 - btn.classList.add('is-selected');
122 + btn.classList.add('chosen');
120 123 btn.setAttribute('aria-selected', 'true');
121 124 const panel = document.getElementById('tab-content');
122 125 if (panel) panel.setAttribute('aria-labelledby', btn.id);