Skip to main content

max / goingson

Adopt the makeover cascade layer and the generated interaction states Pins to makeover-build 0.7.0, makeover-webview 0.13, makeover-layout 0.7 and makeover-geometry 0.6. All three generated stylesheets emit into @layer makeover now, and index.html declares the order ahead of every link so it is a statement rather than an accident of link order. tables.css is wrapped here rather than upstream. The columns are this app's, so makeover never sees that file, and left unlayered it would go on outranking every layered rule in styles.css. DELETED, because layout.css supplies them: - The focus rings for .tab, .button, .field and .card in section 28. What is left there is this app's own focusables at the same 2px and offset, so the app has one ring rather than two. - .field:focus entirely. That was the third of three rings the three apps had each invented, and the only one using `outline: none` plus a composed box-shadow. That form also killed the generated .field:focus-visible, so keeping it would have left fields with no ring at all rather than with two. - .button:disabled:hover / :active, which existed to out-specify the generated hover and pressed rules in those words. Disabled is emitted after both now and restates the rest depth, so source order settles it without anything reaching (0,3,0). - .button:hover and .card:hover from the touch block. layout.css gates its own hover on (hover: hover) and (pointer: fine), so there is nothing to take back. - .row-actions { opacity: 1 } from the touch block, after makeover-webview 0.13.0 moved the hide inside the same query as the reveal. - The background on .bulk-actions-bar .button and its :hover sibling, which restated what layout.css already sets and hovered to --surface-overlay where everything else in the app now hovers to --hover-surface. - The duplicated cursor on .pagination-controls .button:disabled. KEPT, with the reason written down, because two of these look deletable: - .button:disabled keeps colour and cursor. Layers resolve before specificity, so the generated rule in the `makeover` layer loses to `.button` in this file however specific it is. The rule that falls out: an app must state the disabled variant of every property it sets at rest. That is the cascade working as designed, not the app fighting a primitive. - body.is-touch .row-actions stays where its sibling went. makeover gates on what the platform reports and this exists for platforms that report it wrong, which a design system cannot know about. FIXED while here: a disabled .field never greyed its text, for the same layer-ordering reason. Found by adopting, not reported. Conflict surface measured before and after with the overlap parse: 8 exact and 13 distinct specificity-dependent rules down to .tab.chosen plus the seven badge status classes, both filed separately. Two upstream defects went back to makeover on the way: the 1px focus ring (0.12.0) and the ungated row-action hide (0.13.0).
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-02 17:19 UTC
Signed with PGP, not checked
Commit: 15dac71ff29a8950e695b16737e533f100a3f780
Parent: 349e460
5 files changed, +168 insertions, -68 deletions
M Cargo.lock +27 -9
@@ -2250,7 +2250,7 @@
2250 2250 "makeover",
2251 2251 "makeover-build",
2252 2252 "makeover-geometry",
2253 - "makeover-layout",
2253 + "makeover-layout 0.7.0",
2254 2254 "makeover-webview",
2255 2255 "notify",
2256 2256 "notify-debouncer-mini",
@@ -3376,9 +3376,9 @@
3376 3376
3377 3377 [[package]]
3378 3378 name = "makeover-build"
3379 - version = "0.3.1"
3379 + version = "0.7.0"
3380 3380 source = "registry+https://github.com/rust-lang/crates.io-index"
3381 - checksum = "d9b46dc1c83e6855486eeb765a895febd2f0f4a5abde5e44cc9760f419339bf5"
3381 + checksum = "6e0f93fd97d0ce7e3854c0dd8edddefac444de8197bbaa215dfced38cf7a03b6"
3382 3382 dependencies = [
3383 3383 "makeover",
3384 3384 "makeover-geometry",
@@ -3387,9 +3387,9 @@
3387 3387
3388 3388 [[package]]
3389 3389 name = "makeover-geometry"
3390 - version = "0.4.0"
3390 + version = "0.6.0"
3391 3391 source = "registry+https://github.com/rust-lang/crates.io-index"
3392 - checksum = "ec63a50eb9934c2cd416912048c9437a61657064c1947e40571f5fd37a22ffe6"
3392 + checksum = "b2466b8ca54dba9ef62f3679b33c90a0554e507c23e7b725c178ce9d277f542e"
3393 3393
3394 3394 [[package]]
3395 3395 name = "makeover-layout"
@@ -3397,6 +3397,22 @@
3397 3397 source = "registry+https://github.com/rust-lang/crates.io-index"
3398 3398 checksum = "ad745601b5220b2244d73a5da3f3854aefe4cdc955dd45c6d3aa5781ad645f5a"
3399 3399
3400 + [[package]]
3401 + name = "makeover-layout"
3402 + version = "0.7.0"
3403 + source = "registry+https://github.com/rust-lang/crates.io-index"
3404 + checksum = "00d6235015ee1a15bc6b3936c5265229de3454524053eb67bf95fc6b741299b0"
3405 +
3406 + [[package]]
3407 + name = "makeover-touch"
3408 + version = "0.2.0"
3409 + source = "registry+https://github.com/rust-lang/crates.io-index"
3410 + checksum = "064bb24217e6d49ccafbefbcc29c7c42572c164feb98e5876e9ca49e77d58808"
3411 + dependencies = [
3412 + "makeover-geometry",
3413 + "makeover-layout 0.7.0",
3414 + ]
3415 +
3400 3416 [[package]]
3401 3417 name = "makeover-tui"
3402 3418 version = "0.7.0"
@@ -3404,17 +3420,19 @@
3404 3420 checksum = "e0174c5f2e03f2fc856589d061f5718c2bce77e2d15d1ac6d4bdcb0f53134d63"
3405 3421 dependencies = [
3406 3422 "makeover",
3407 - "makeover-layout",
3423 + "makeover-layout 0.6.0",
3408 3424 "ratatui",
3409 3425 ]
3410 3426
3411 3427 [[package]]
3412 3428 name = "makeover-webview"
3413 - version = "0.9.0"
3429 + version = "0.13.0"
3414 3430 source = "registry+https://github.com/rust-lang/crates.io-index"
3415 - checksum = "d4ae8b0857cfa95b5d698e22d5df17eb19bae978839d568ae5de02344892962b"
3431 + checksum = "ac7fbadaa7cf1e4a074a0b0c62a9ea3b1616f9de4e40990fab209560b807046f"
3416 3432 dependencies = [
3417 - "makeover-layout",
3433 + "makeover-geometry",
3434 + "makeover-layout 0.7.0",
3435 + "makeover-touch",
3418 3436 ]
3419 3437
3420 3438 [[package]]
@@ -16,16 +16,16 @@
16 16 [build-dependencies]
17 17 tauri-build = { workspace = true }
18 18 # Materialises all three generated files: themes/, geometry.css, layout.css.
19 - makeover-build = "0.3.1"
19 + makeover-build = "0.7.0"
20 20 # The table CSS is generated here too: the columns are this app's, so the
21 21 # shared helper cannot know them, but the tracks and the narrowing rules come
22 22 # from the description rather than from hand-written nth-child cuts.
23 - makeover-webview = "0.9.0"
24 - makeover-layout = "0.6.0"
23 + makeover-webview = "0.13"
24 + makeover-layout = "0.7"
25 25 # Width. Direct rather than through makeover-webview, because the narrow table
26 26 # pass keys off SizeClass::Compact and a boundary reached transitively is a
27 27 # boundary nobody pinned.
28 - makeover-geometry = "0.4.0"
28 + makeover-geometry = "0.6"
29 29
30 30 [dependencies]
31 31 goingson-core = { workspace = true }
@@ -97,8 +97,8 @@
97 97 # The forms emitter and the description it renders. Runtime deps, not build:
98 98 # form markup depends on the value, the error and the options at the moment a
99 99 # modal opens, so it cannot be materialised the way the stylesheet is.
100 - makeover-webview = "0.9.0"
101 - makeover-layout = "0.6.0"
100 + makeover-webview = "0.13"
101 + makeover-layout = "0.7"
102 102 # Browser opening
103 103 open = { workspace = true }
104 104
@@ -270,12 +270,7 @@
270 270 fallback: "",
271 271 };
272 272
273 - let mut css = String::from(
274 - "/* Generated by makeover-webview from the column descriptions in\n \
275 - build.rs. Do not edit. Columns narrow by priority, never by position:\n \
276 - inserting one changes what is emitted rather than changing which one\n \
277 - silently disappears. */\n",
278 - );
273 + let mut css = String::new();
279 274
280 275 // Zipped rather than carried in TABLES: a length is a CSS answer and the
281 276 // description deliberately holds none, which is the split Sizing exists for.
@@ -328,7 +323,22 @@
328 323 ));
329 324 css.push_str("}\n");
330 325
331 - css
326 + // Same cascade layer as layout.css and geometry.css. These rules are as
327 + // generated as those are, and makeover cannot put them there on our behalf
328 + // because the columns are this app's, so it never sees this file. Left
329 + // unlayered, tables.css would go on outranking every layered rule in
330 + // styles.css and the layer adoption would look broken in exactly the place
331 + // it was supposed to help.
332 + //
333 + // Banner outside the layer: a comment participates in no cascade, and a
334 + // reader opening the file should see what it is before seeing an at-rule.
335 + format!(
336 + "/* Generated by makeover-webview from the column descriptions in\n \
337 + build.rs. Do not edit. Columns narrow by priority, never by position:\n \
338 + inserting one changes what is emitted rather than changing which one\n \
339 + silently disappears. */\n{}",
340 + makeover_webview::in_css_layer(&css)
341 + )
332 342 }
333 343
334 344 /// Widths that are tuning inside the wide shell, not a shell boundary.
@@ -29,8 +29,24 @@
29 29 See the TWO AXES section at the top of css/styles.css. -->
30 30 <script src="js/bootstrap-uimode.js"></script>
31 31
32 - <!-- Generated from makeover-geometry by src-tauri/build.rs. First, so the
33 - stylesheet can read --gap-* and --step-* off :root. -->
32 + <!-- Cascade layer order, declared before any stylesheet so it is a
33 + statement rather than an accident of link order. A layer's position is
34 + fixed where the name is FIRST seen, so without this the three generated
35 + files below would establish `makeover` simply by loading first, and
36 + reordering the links would silently reorder the cascade.
37 +
38 + Earlier in the list = lower priority. `makeover` is first because the
39 + design system is what the app overrides, never the reverse. Unlayered
40 + rules still beat every named layer, which is why styles.css works today
41 + with no layers of its own; the other three names are declared ahead of
42 + the styles.css layer adoption (GoingsOn task 298aa3b8) and are empty
43 + until then. Declaring an empty layer costs nothing. -->
44 + <style>@layer makeover, base, components, responsive;</style>
45 +
46 + <!-- Generated from makeover-geometry, makeover-webview and the column
47 + descriptions in src-tauri/build.rs. All three emit into `@layer
48 + makeover`. Geometry first, so the stylesheet can read --gap-* and
49 + --step-* off :root. -->
34 50 <link rel="stylesheet" href="css/geometry.css">
35 51 <link rel="stylesheet" href="css/layout.css">
36 52 <link rel="stylesheet" href="css/tables.css">
@@ -736,24 +736,32 @@
736 736 }
737 737
738 738 /* Disabled keeps its bevel. The object is still there, its label is just
739 - unavailable, which is what greying the content says. */
739 + unavailable, which is what greying the content says.
740 +
741 + The fill and the edge come from layout.css now. The colour and the cursor
742 + do NOT, and the reason generalises: layers are resolved before specificity,
743 + so a lower layer loses to a higher one no matter how specific it is. The
744 + generated `.button:disabled` sits in `makeover`, and `.button` above sets
745 + `color` and `cursor` in this file, which is either unlayered or in a later
746 + layer. So the base rule wins and the generated disabled values never apply.
747 +
748 + The rule that falls out of that, and it is worth knowing before deleting
749 + anything else: an app must state the disabled variant of every property it
750 + sets at rest. makeover can only supply what the app is silent about. This
751 + is not the app out-specifying a primitive; it is the cascade working as
752 + designed.
753 +
754 + The `.button:disabled:hover, .button:disabled:active` pair that used to sit
755 + below IS gone. That one existed to out-specify the generated hover and
756 + pressed rules, in those words, and makeover emits disabled after both now
757 + and restates the rest depth, so source order settles it at equal
758 + specificity. Nothing here has to reach (0,3,0) to win any more. */
740 759 .button:disabled {
741 760 color: var(--content-muted);
742 761 border-color: var(--content-muted);
743 762 cursor: not-allowed;
744 763 }
745 764
746 - /* Inert on both, and both properties on both selectors. Overriding only
747 - box-shadow on :active left a disabled button taking the pressed fill while
748 - keeping the raised edge, which is the fill-and-edge disagreement the
749 - generated rules exist to prevent. Higher specificity than the generated
750 - .button:hover and .button:active, so these win. */
751 - .button:disabled:hover,
752 - .button:disabled:active {
753 - background: var(--surface-raised);
754 - box-shadow: var(--bevel-raised);
755 - }
756 -
757 765 .button--primary {
758 766 background-color: var(--action);
759 767 color: var(--content-on-action);
@@ -1937,13 +1945,36 @@
1937 1945 font-size: var(--font-size-lg);
1938 1946 }
1939 1947
1940 - /* The focus ring sits outside the well, so the inset bevel stays under it.
1941 - Composed rather than replacing, because box-shadow is not additive. */
1942 - .field:focus {
1943 - outline: none;
1944 - box-shadow: var(--bevel-inset), 0 0 0 2px var(--action);
1948 + /* A disabled field never greyed its text. layout.css supplies the muted
1949 + colour, and `.field` above sets `color` at rest in a later layer, so the
1950 + base rule was winning and the generated one never applied. Same rule as
1951 + `.button:disabled`: an app must state the disabled variant of every
1952 + property it sets at rest.
1953 +
1954 + Found by adopting the generated states, not reported. The fill and the edge
1955 + come from layout.css and are not restated here. */
1956 + .field:disabled,
1957 + .field[aria-disabled="true"] {
1958 + color: var(--content-muted);
1945 1959 }
1946 1960
1961 + /* The field's focus ring comes from layout.css now. This rule was the third
1962 + of the three rings the three apps had written independently, and it was
1963 + doing it a third way: `outline: none` plus a composed box-shadow, where the
1964 + others used `outline`. That mattered, because `outline: none` here also
1965 + killed the generated `.field:focus-visible`, so keeping this would have
1966 + left fields with no ring at all rather than with two.
1967 +
1968 + Same 2px, and --focus-ring resolves to the same colour as --action. The
1969 + generated ring sits inside the well (`outline-offset: calc(-1 * 2px)`)
1970 + rather than outside it, which is where a well's ring belongs and which the
1971 + composed box-shadow could not express without restating the bevel beside
1972 + it, as the comment here used to.
1973 +
1974 + `:focus-visible` rather than `:focus` is not a behaviour change for a text
1975 + field: a focused text input always matches `:focus-visible`, however it was
1976 + focused. */
1977 +
1947 1978 /* Kind rides on the element, not on a second class. Every one of the 55 call
1948 1979 sites had the old .form-input class on an <input>, .form-select on a
1949 1980 <select> and .form-textarea on a <textarea>, with no exceptions, so the class
@@ -3498,16 +3529,29 @@
3498 3529 font-size: var(--font-size-base);
3499 3530 }
3500 3531
3532 + /* `cursor: not-allowed` comes from layout.css's disabled rule now. The
3533 + opacity does not: makeover greys a disabled control by moving its content
3534 + to --content-muted rather than by fading the whole box, and this pagination
3535 + pair has always faded. Left as it is rather than folded in, because
3536 + dimming twice is a look question and not a duplication. */
3501 3537 .pagination-controls .button:disabled {
3502 3538 opacity: 0.5;
3503 - cursor: not-allowed;
3504 3539 }
3505 3540
3506 3541 /* 28. Focus & Keyboard Accessibility */
3507 - .tab:focus-visible,
3508 - .button:focus-visible,
3509 - .field:focus-visible,
3510 - .card:focus-visible,
3542 +
3543 + /* .tab, .button, .field and .card take their ring from layout.css and are no
3544 + longer named here. What is left is this app's own focusables, which the
3545 + design system does not know about.
3546 +
3547 + Same width, offset and token as the generated rule on purpose: the point of
3548 + one ring is that a user learns it once, and an app with two rings has the
3549 + problem the design system was fixing, only inside one codebase. Track
3550 + makeover-webview's focus_rule if it ever changes.
3551 +
3552 + Was 3px solid var(--action). The width came down to 2px when makeover took
3553 + the ring, which is the value all three apps had independently chosen; the
3554 + colour is the same, since --focus-ring is derived from action.primary. */
3511 3555 .email-item:focus-visible,
3512 3556 .task-row:focus-visible,
3513 3557 .event-row-virtual:focus-visible,
@@ -3515,9 +3559,8 @@
3515 3559 .modal-close:focus-visible,
3516 3560 .snooze-option:focus-visible,
3517 3561 .unscheduled-task:focus-visible,
3518 - .timeline-item:focus-visible,
3519 - .card:focus-visible {
3520 - outline: 3px solid var(--action);
3562 + .timeline-item:focus-visible {
3563 + outline: 2px solid var(--focus-ring);
3521 3564 outline-offset: 2px;
3522 3565 }
3523 3566
@@ -5088,16 +5131,21 @@
5088 5131 font-family: var(--font-heading);
5089 5132 }
5090 5133
5134 + /* The border and the content colour are this bar's own; the background is
5135 + not, and restating var(--surface-raised) here only repeated what
5136 + layout.css already sets on .button.
5137 +
5138 + The `:hover` sibling went with it. It set var(--surface-overlay), which is
5139 + what this app hovered to before makeover owned the state; the generated
5140 + rule hovers to var(--hover-surface), which is the value makeover derives
5141 + for exactly this and which nothing consumed until phase A. Keeping it
5142 + would have made these buttons the only ones in the app that hover
5143 + differently. */
5091 5144 .bulk-actions-bar .button {
5092 - background: var(--surface-raised);
5093 5145 border: var(--border-width-sm) solid var(--border);
5094 5146 color: var(--content);
5095 5147 }
5096 5148
5097 - .bulk-actions-bar .button:hover {
5098 - background: var(--surface-overlay);
5099 - }
5100 -
5101 5149 .bulk-select-all {
5102 5150 margin-left: auto;
5103 5151 }
@@ -8037,14 +8085,15 @@
8037 8085 background-color: transparent;
8038 8086 }
8039 8087
8040 - .card:hover {
8041 - background-color: var(--surface-raised);
8042 - box-shadow: var(--bevel-raised);
8043 - }
8088 + /* No `.card:hover` or `.button:hover` here any more. layout.css wraps its
8089 + own hover rules in (hover: hover) and (pointer: fine), so on a
8090 + fingertip they never apply and there is nothing left to take back.
8091 + These two only ever existed to out-specify a rule this app does not
8092 + own, which they did by loading last rather than by saying anything.
8044 8093
8045 - .button:hover {
8046 - background: var(--surface-raised);
8047 - }
8094 + The app rules below stay: `.task-row:hover`, `.button--primary:hover`
8095 + and the rest are this app's own hover states, and makeover has no
8096 + opinion about them. */
8048 8097
8049 8098 .button--primary:hover {
8050 8099 background-color: var(--action);
@@ -8075,12 +8124,12 @@
8075 8124 transform: none;
8076 8125 }
8077 8126
8078 - /* Row actions are hover-revealed on desktop; on touch there is no hover, so
8079 - show them all and give the pointer back what .row-actions took away. */
8080 - .row-actions {
8081 - opacity: 1;
8082 - pointer-events: auto;
8083 - }
8127 + /* `.row-actions { opacity: 1 }` is gone from here. It undid a hide that
8128 + layout.css used to emit ungated, which left a fingertip with actions
8129 + and no hover to bring them back. makeover-webview 0.13.0 moved the hide
8130 + inside the capability query with the reveal, so on touch the actions
8131 + are simply never hidden and there is nothing to undo. Both webview apps
8132 + had written the same undo, which is what sent the fix upstream. */
8084 8133
8085 8134 /* Task row actions also take a full touch-target size. */
8086 8135 .task-row .task-row-action {
@@ -8120,7 +8169,14 @@
8120 8169 }
8121 8170
8122 8171 /* Touch-device body class (set by touch.js), equivalent guards for browsers
8123 - that report (hover: hover) but are still touch-first (e.g. some Chromebooks). */
8172 + that report (hover: hover) but are still touch-first (e.g. some Chromebooks).
8173 +
8174 + This one survives where its sibling inside the (hover: none) block did not,
8175 + and the difference is the point: makeover gates on what the platform
8176 + reports, and this exists precisely for the platforms that report it wrong.
8177 + A design system cannot know which browsers lie, so this stays app-local. It
8178 + is not out-specifying the primitive either, it is a narrower condition the
8179 + primitive has no way to express. */
8124 8180 body.is-touch .row-actions {
8125 8181 opacity: 1;
8126 8182 pointer-events: auto;