Skip to main content

max / goingson

Generate the task table's columns from a description The tracks and the narrowing rules come out of build.rs now, from a column list that says what each column is worth rather than where it sits. The bug this fixes: mobile columns were hidden with nth-child(n+5) against a seven-column table, plus a separate nth-child(3), plus two class-based rules. One fact said three ways, two of them positional, so inserting a column anywhere left of the cut hid the wrong one and nothing said so. It was already wrong. The mobile rule declared four tracks while three cells survived, because the priority column is hidden by that separate nth-child(3) and a display:none cell occupies no track. So the due date was landing in the 40px track meant for the priority letter. Emitting both halves from one call is what stops the track list and the hiding drifting apart. Desktop output is byte-identical to the rules it replaces. Mobile is three tracks for the three surviving columns. Cells carry a col-<name> class now, which is what makes them addressable without counting. The old per-cell classes stay: they still carry padding and alignment that has nothing to do with which column it is. The events table is deliberately not generated. It is not describable as one column set yet: the two row builders disagree, the upcoming row emits six cells and the recurring row five, against a five-cell header and the five-track grid they share, so the upcoming row's cells sit one track off their labels. .event-cell--shrink was meant to handle it and sets a flex property inside a grid, which does nothing. That wants deciding, not guessing.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-30 13:48 UTC
Signed with PGP, not checked
Commit: ca0d3963edef835f6e57e352e5d45e5f70efb3f4
Parent: c7660fb
8 files changed, +171 insertions, -39 deletions
M .gitignore +1
@@ -52,3 +52,4 @@
52 52 # Generated by src-tauri/build.rs from makeover-geometry's scale
53 53 /src-tauri/frontend/css/geometry.css
54 54 /src-tauri/frontend/css/layout.css
55 + /src-tauri/frontend/css/tables.css
M Cargo.lock -4
@@ -3393,14 +3393,10 @@
3393 3393 [[package]]
3394 3394 name = "makeover-layout"
3395 3395 version = "0.5.0"
3396 - source = "registry+https://github.com/rust-lang/crates.io-index"
3397 - checksum = "0a733d3943b405611b466caab47159ae11fd0d8f01ab4e50bdeabd0b66b477e8"
3398 3396
3399 3397 [[package]]
3400 3398 name = "makeover-webview"
3401 3399 version = "0.8.0"
3402 - source = "registry+https://github.com/rust-lang/crates.io-index"
3403 - checksum = "111306ceb7c46e0f6c7c69885dcb7e49e3ee9ac517c56d235dcc47e98f0f2561"
3404 3400 dependencies = [
3405 3401 "makeover-layout",
3406 3402 ]
M Cargo.toml +7
@@ -154,3 +154,10 @@
154 154 match_same_arms = "allow"
155 155 unnecessary_wraps = "allow"
156 156 type_complexity = "allow"
157 +
158 + # Temporary, for the lists-half adoption: makeover-webview gained the list
159 + # module and goingson is the first consumer. Released once when the wiring
160 + # stops finding gaps.
161 + [patch.crates-io]
162 + makeover-layout = { path = "../../Libraries/makeover-layout" }
163 + makeover-webview = { path = "../../Libraries/makeover-webview" }
@@ -18,6 +18,11 @@
18 18 # The build script needs nothing else; makeover and makeover-geometry reach it
19 19 # through here. Path dep while unpublished.
20 20 makeover-build = { path = "../../../Libraries/makeover-build" }
21 + # The table CSS is generated here too: the columns are this app's, so the
22 + # shared helper cannot know them, but the tracks and the narrowing rules come
23 + # from the description rather than from hand-written nth-child cuts.
24 + makeover-webview = "0.8.0"
25 + makeover-layout = "0.5.0"
21 26
22 27 [dependencies]
23 28 goingson-core = { workspace = true }
@@ -1,3 +1,134 @@
1 + use std::fs;
2 + use std::path::Path;
3 +
4 + use makeover_layout::{Column, Priority, Width};
5 + use makeover_webview::Emit;
6 + use makeover_webview::list::{Sizing, narrowing_css};
7 +
8 + /// The tasks table, left to right.
9 + ///
10 + /// Priority is what a column is worth when there is not room for all of them,
11 + /// and it is the whole reason this description exists. The stylesheet used to
12 + /// hide mobile columns with `nth-child(n+5)` against a seven-column table plus a
13 + /// separate `nth-child(3)`, so inserting a column anywhere left of the cut hid
14 + /// the wrong one and nothing said so.
15 + const TASK_COLUMNS: &[Column<'static>] = &[
16 + // Without it the row does not identify itself.
17 + Column {
18 + name: "description",
19 + width: Width::Fill,
20 + priority: Priority::Essential,
21 + },
22 + Column {
23 + name: "project",
24 + width: Width::Fixed,
25 + priority: Priority::Secondary,
26 + },
27 + // A single letter, and the row already carries its priority in the left
28 + // border colour, so it is the first thing that can go.
29 + Column {
30 + name: "priority",
31 + width: Width::Fixed,
32 + priority: Priority::Optional,
33 + },
34 + Column {
35 + name: "due",
36 + width: Width::Fixed,
37 + priority: Priority::Secondary,
38 + },
39 + Column {
40 + name: "recurrence",
41 + width: Width::Fixed,
42 + priority: Priority::Optional,
43 + },
44 + Column {
45 + name: "progress",
46 + width: Width::Fixed,
47 + priority: Priority::Optional,
48 + },
49 + Column {
50 + name: "actions",
51 + width: Width::Fixed,
52 + priority: Priority::Optional,
53 + },
54 + ];
55 +
56 + /// Generate the two tables' column CSS.
57 + ///
58 + /// Both halves of narrowing come out of one call per breakpoint: the track list
59 + /// and the hiding. They used to be written apart and kept in step by hand,
60 + /// which they were not. The mobile rule declared four tracks while three cells
61 + /// survived, so the due date landed in the 40px track meant for the priority
62 + /// letter.
63 + fn table_css() -> String {
64 + let opts = Emit::default();
65 +
66 + let task_wide = Sizing {
67 + lengths: &[
68 + ("description", "200px"),
69 + ("project", "140px"),
70 + ("priority", "80px"),
71 + ("due", "110px"),
72 + ("recurrence", "90px"),
73 + ("progress", "100px"),
74 + ("actions", "90px"),
75 + ],
76 + fallback: "",
77 + };
78 + // The narrow pass carries its own lengths: the columns that survive are not
79 + // the same size on a phone as on a desktop.
80 + let task_narrow = Sizing {
81 + lengths: &[("description", "0"), ("project", "80px"), ("due", "80px")],
82 + fallback: "",
83 + };
84 +
85 + let mut css = String::from(
86 + "/* Generated by makeover-webview from the column descriptions in\n \
87 + build.rs. Do not edit. Columns narrow by priority, never by position:\n \
88 + inserting one changes what is emitted rather than changing which one\n \
89 + silently disappears. */\n",
90 + );
91 +
92 + // Tasks only for now. The events table is not describable as one column
93 + // set yet: its two row builders disagree, the upcoming row emits six cells
94 + // and the recurring row five, against a header of five and a five-track
95 + // grid they share. Emitting narrowing rules for a table whose markup does
96 + // not match them would hide nothing and shorten the tracks, which is worse
97 + // than the hand-written rules it would replace.
98 + for (selector, columns, wide, narrow) in [(
99 + ".task-header-row, .task-row",
100 + TASK_COLUMNS,
101 + &task_wide,
102 + &task_narrow,
103 + )] {
104 + css.push('\n');
105 + css.push_str(&narrowing_css(
106 + columns,
107 + selector,
108 + wide,
109 + Priority::Optional,
110 + &opts,
111 + ));
112 +
113 + // Scoped under the mode class the geometry half already keys touch
114 + // density on, so one signal decides both.
115 + css.push('\n');
116 + let narrow_selector = selector
117 + .split(", ")
118 + .map(|s| format!(".ui-mode-mobile {s}"))
119 + .collect::<Vec<_>>()
120 + .join(", ");
121 + css.push_str(&narrowing_css(
122 + columns,
123 + &narrow_selector,
124 + narrow,
125 + Priority::Secondary,
126 + &opts,
127 + ));
128 + }
129 + css
130 + }
131 +
1 132 fn main() {
2 133 // All three generated files: themes/, geometry.css, layout.css. The
3 134 // geometry emitter moved out too once density selection was settled:
@@ -8,5 +139,12 @@
8 139 &makeover_build::Emit::default(),
9 140 Some(".ui-mode-mobile"),
10 141 );
142 +
143 + // The columns are this app's, so the shared helper cannot know them, but
144 + // what is done with them comes from the description all the same.
145 + let tables = Path::new(env!("CARGO_MANIFEST_DIR")).join("frontend/css/tables.css");
146 + fs::write(&tables, table_css()).expect("write tables.css");
147 + println!("cargo:rerun-if-changed=build.rs");
148 +
11 149 tauri_build::build();
12 150 }
@@ -32,6 +32,7 @@
32 32 stylesheet can read --gap-* and --step-* off :root. -->
33 33 <link rel="stylesheet" href="css/geometry.css">
34 34 <link rel="stylesheet" href="css/layout.css">
35 + <link rel="stylesheet" href="css/tables.css">
35 36 <link rel="stylesheet" href="css/styles.min.css">
36 37 </head>
37 38 <body>
@@ -138,21 +139,21 @@
138 139 </div>
139 140 <div class="task-table" id="task-table" role="grid" aria-label="Tasks list">
140 141 <div class="task-header-row" role="row">
141 - <div class="task-cell sortable" data-sort="description" data-act="tasks.sort" data-a1="description" role="columnheader" tabindex="0">
142 + <div class="task-cell col-description sortable" data-sort="description" data-act="tasks.sort" data-a1="description" role="columnheader" tabindex="0">
142 143 Description <span class="sort-arrow"></span>
143 144 </div>
144 - <div class="task-cell sortable" data-sort="project" data-act="tasks.sort" data-a1="project" role="columnheader" tabindex="0">
145 + <div class="task-cell col-project sortable" data-sort="project" data-act="tasks.sort" data-a1="project" role="columnheader" tabindex="0">
145 146 Project <span class="sort-arrow"></span>
146 147 </div>
147 - <div class="task-cell sortable" data-sort="priority" data-act="tasks.sort" data-a1="priority" role="columnheader" tabindex="0" aria-label="Priority">
148 + <div class="task-cell col-priority sortable" data-sort="priority" data-act="tasks.sort" data-a1="priority" role="columnheader" tabindex="0" aria-label="Priority">
148 149 Priority <span class="sort-arrow"></span>
149 150 </div>
150 - <div class="task-cell sortable" data-sort="due" data-act="tasks.sort" data-a1="due" role="columnheader" tabindex="0">
151 + <div class="task-cell col-due sortable" data-sort="due" data-act="tasks.sort" data-a1="due" role="columnheader" tabindex="0">
151 152 Due <span class="sort-arrow"></span>
152 153 </div>
153 - <div class="task-cell" role="columnheader">Recurs</div>
154 - <div class="task-cell" role="columnheader">Progress</div>
155 - <div class="task-cell task-actions-header" role="columnheader"><span class="sr-only">Actions</span></div>
154 + <div class="task-cell col-recurrence" role="columnheader">Recurs</div>
155 + <div class="task-cell col-progress" role="columnheader">Progress</div>
156 + <div class="task-cell col-actions task-actions-header" role="columnheader"><span class="sr-only">Actions</span></div>
156 157 </div>
157 158 <div class="task-list-container well" id="task-list-container" aria-live="polite">
158 159 <div class="skeleton-shimmer" aria-label="Loading tasks">
@@ -1076,14 +1076,14 @@
1076 1076 box-shadow: var(--bevel-raised);
1077 1077 }
1078 1078
1079 - /* Task Grid Column Widths */
1079 + /* Task Grid Column Widths
1080 + grid-template-columns comes from css/tables.css, generated from the column
1081 + descriptions in build.rs. The floors live there too: the description column
1082 + must not collapse below its "Description" label, and the priority column is
1083 + widened just enough to fit the word. */
1080 1084 .task-header-row,
1081 1085 .task-row {
1082 1086 display: grid;
1083 - /* Column floors keep every header label readable: the description column
1084 - won't collapse below its "Description" label, and the priority column
1085 - (a single-letter H/M/L body) is widened just enough to fit "Priority". */
1086 - grid-template-columns: minmax(200px, 1fr) 140px 80px 110px 90px 100px 90px;
1087 1087 align-items: center;
1088 1088 gap: var(--gap-group);
1089 1089 }
@@ -3391,16 +3391,6 @@
3391 3391 font-size: var(--font-size-base);
3392 3392 }
3393 3393
3394 - /* Hide recurrence, progress, and actions columns on mobile */
3395 - .ui-mode-mobile .task-header-row,
3396 - .ui-mode-mobile .task-row {
3397 - grid-template-columns: 1fr 80px 40px 80px;
3398 - }
3399 -
3400 - .ui-mode-mobile .task-header-row .task-cell:nth-child(n+5),
3401 - .ui-mode-mobile .task-row .task-cell:nth-child(n+5) {
3402 - display: none;
3403 - }
3404 3394
3405 3395 .ui-mode-mobile .filter-bar {
3406 3396 flex-direction: column;
@@ -7678,12 +7668,6 @@
7678 7668 content: none;
7679 7669 }
7680 7670
7681 - /* Hide priority letter (shown via left border color), recurrence, progress columns */
7682 - .ui-mode-mobile .task-row .task-cell:nth-child(3),
7683 - .ui-mode-mobile .task-cell.task-recurrence,
7684 - .ui-mode-mobile .task-cell.task-progress {
7685 - display: none !important;
7686 - }
7687 7671
7688 7672 /* Task meta row: project + due side-by-side */
7689 7673 .ui-mode-mobile .task-cell.task-project {
@@ -148,7 +148,7 @@
148 148 data-id="${escAttr(t.id)}"
149 149 data-contextmenu="contextMenus.showTask" data-a1="@event" data-a2="${escAttr(t.id)}"
150 150 tabindex="0" role="row">
151 - <div class="task-cell task-description" data-act="taskOverview.open" data-a1="${escAttr(t.id)}">
151 + <div class="task-cell col-description task-description" data-act="taskOverview.open" data-a1="${escAttr(t.id)}">
152 152 ${isStarted ? '<span class="task-started-icon" title="Started" aria-hidden="true"></span>' : ''}
153 153 <span class="task-description-text">${esc(displayDesc)}</span>
154 154 ${renderTokenDot(t)}
@@ -157,11 +157,11 @@
157 157 ${t.contactName ? `<span class="contact-badge" title="${escAttrVal(t.contactName)}">${esc(t.contactName)}</span>` : ''}
158 158 ${t.isSnoozed ? `<span class="snooze-badge" title="Snoozed until ${escAttr(t.snoozedUntilFormatted || '')}" aria-label="Snoozed until ${escAttr(t.snoozedUntilFormatted || '')}">Snoozed</span>` : ''}
159 159 </div>
160 - <div class="task-cell row-secondary task-project">${esc(t.projectName) || '-'}${GoingsOn.groups.taskSharedBadge(t.projectId)}</div>
161 - <div class="task-cell priority-${t.priority.toLowerCase()}" aria-label="Priority ${t.priority}">${t.priority.charAt(0)}</div>
162 - <div class="task-cell row-meta task-due">${t.dueFormatted || '-'}</div>
163 - <div class="task-cell row-secondary task-recurrence">${formatRecurrence(t)}</div>
164 - <div class="task-cell task-progress">
160 + <div class="task-cell col-project row-secondary task-project">${esc(t.projectName) || '-'}${GoingsOn.groups.taskSharedBadge(t.projectId)}</div>
161 + <div class="task-cell col-priority priority-${t.priority.toLowerCase()}" aria-label="Priority ${t.priority}">${t.priority.charAt(0)}</div>
162 + <div class="task-cell col-due row-meta task-due">${t.dueFormatted || '-'}</div>
163 + <div class="task-cell col-recurrence row-secondary task-recurrence">${formatRecurrence(t)}</div>
164 + <div class="task-cell col-progress task-progress">
165 165 ${t.subtaskCount > 0 ? `
166 166 <div class="progress" title="${t.subtaskCompleted}/${t.subtaskCount} subtasks"
167 167 role="progressbar" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100"
@@ -170,7 +170,7 @@
170 170 </div>
171 171 ` : '<span class="row-secondary no-subtasks">-</span>'}
172 172 </div>
173 - <div class="task-cell task-actions-cell">
173 + <div class="task-cell col-actions task-actions-cell">
174 174 ${renderRowActions(t, isStarted)}
175 175 <input type="checkbox" class="bulk-checkbox" data-id="${escAttr(t.id)}"
176 176 ${isSelected ? 'checked' : ''}