Skip to main content

max / goingson

Split the events table in two, and fix the column it had been off by The upcoming events list rendered six cells into five tracks. Every cell sat one column left of its label: the checkbox took the date's 100px, the time took the event title's 1fr, the title took the location's 150px, and the location ended up in the 40px track meant for the kebab. The cause is that these were two tables wearing one class. The recurring list leads with a recurrence pattern and cannot be selected; the upcoming list leads with a date and has a bulk checkbox. Both emitted .event-header-row and .event-row-virtual, so both took the single five-track grid, and only the recurring one fitted it. So each gets its own column description and its own modifier, and the grid splits while everything the two genuinely share -- padding, hover, the row border -- stays on .event-row-virtual. The upcoming header gains the select column it never had. .event-cell--shrink is deleted. It set `flex: 0 0 auto` on a child of a grid container, so it had never done anything; it is a leftover from when these rows were flex, and it is the reason the extra cell went unnoticed. Both tables narrow by priority now, like the task table: select, location and actions drop on a narrow screen and the tracks shorten to match, out of the same call, so the two cannot drift apart again.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-30 13:58 UTC
Signed with PGP, not checked
Commit: bd63f7db9565ea659eca131374fbe6a2ed7326ad
Parent: ca0d396
4 files changed, +159 insertions, -44 deletions
M src-tauri/build.rs +129 -13
@@ -53,7 +53,81 @@
53 53 },
54 54 ];
55 55
56 - /// Generate the two tables' column CSS.
56 + /// The upcoming-events table, left to right.
57 + ///
58 + /// Separate from [`RECURRING_COLUMNS`] because these are two tables and not
59 + /// one. They had been sharing a class and therefore a grid, which is what put
60 + /// the upcoming row's six cells into five tracks: the checkbox took the date's
61 + /// track, the title took the location's, and the location took the 40px one
62 + /// meant for the kebab. Only the recurring table ever fitted.
63 + const UPCOMING_COLUMNS: &[Column<'static>] = &[
64 + // Bulk selection. Goes first on a narrow screen, like the kebab does.
65 + Column {
66 + name: "select",
67 + width: Width::Fixed,
68 + priority: Priority::Optional,
69 + },
70 + Column {
71 + name: "date",
72 + width: Width::Fixed,
73 + priority: Priority::Essential,
74 + },
75 + Column {
76 + name: "time",
77 + width: Width::Fixed,
78 + priority: Priority::Secondary,
79 + },
80 + Column {
81 + name: "title",
82 + width: Width::Fill,
83 + priority: Priority::Essential,
84 + },
85 + Column {
86 + name: "location",
87 + width: Width::Fixed,
88 + priority: Priority::Optional,
89 + },
90 + Column {
91 + name: "actions",
92 + width: Width::Fixed,
93 + priority: Priority::Optional,
94 + },
95 + ];
96 +
97 + /// The recurring-events table, left to right.
98 + ///
99 + /// Its first column is the recurrence pattern where the upcoming table's is a
100 + /// date, and it has no bulk selection, which is the other half of why one
101 + /// description could never have served both.
102 + const RECURRING_COLUMNS: &[Column<'static>] = &[
103 + Column {
104 + name: "pattern",
105 + width: Width::Fixed,
106 + priority: Priority::Essential,
107 + },
108 + Column {
109 + name: "time",
110 + width: Width::Fixed,
111 + priority: Priority::Secondary,
112 + },
113 + Column {
114 + name: "title",
115 + width: Width::Fill,
116 + priority: Priority::Essential,
117 + },
118 + Column {
119 + name: "location",
120 + width: Width::Fixed,
121 + priority: Priority::Optional,
122 + },
123 + Column {
124 + name: "actions",
125 + width: Width::Fixed,
126 + priority: Priority::Optional,
127 + },
128 + ];
129 +
130 + /// Generate the tables' column CSS.
57 131 ///
58 132 /// Both halves of narrowing come out of one call per breakpoint: the track list
59 133 /// and the hiding. They used to be written apart and kept in step by hand,
@@ -82,6 +156,37 @@
82 156 fallback: "",
83 157 };
84 158
159 + let upcoming_wide = Sizing {
160 + lengths: &[
161 + ("select", "40px"),
162 + ("date", "100px"),
163 + ("time", "80px"),
164 + ("title", "0"),
165 + ("location", "150px"),
166 + ("actions", "40px"),
167 + ],
168 + fallback: "",
169 + };
170 + let upcoming_narrow = Sizing {
171 + lengths: &[("date", "90px"), ("time", "70px"), ("title", "0")],
172 + fallback: "",
173 + };
174 +
175 + let recurring_wide = Sizing {
176 + lengths: &[
177 + ("pattern", "140px"),
178 + ("time", "80px"),
179 + ("title", "0"),
180 + ("location", "150px"),
181 + ("actions", "40px"),
182 + ],
183 + fallback: "",
184 + };
185 + let recurring_narrow = Sizing {
186 + lengths: &[("pattern", "110px"), ("time", "70px"), ("title", "0")],
187 + fallback: "",
188 + };
189 +
85 190 let mut css = String::from(
86 191 "/* Generated by makeover-webview from the column descriptions in\n \
87 192 build.rs. Do not edit. Columns narrow by priority, never by position:\n \
@@ -89,18 +194,29 @@
89 194 silently disappears. */\n",
90 195 );
91 196
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 - )] {
197 + // The two event selectors carry a modifier rather than a class of their
198 + // own, so everything the two tables genuinely share -- padding, hover, the
199 + // row border -- stays on .event-row-virtual and only the grid splits.
200 + for (selector, columns, wide, narrow) in [
201 + (
202 + ".task-header-row, .task-row",
203 + TASK_COLUMNS,
204 + &task_wide,
205 + &task_narrow,
206 + ),
207 + (
208 + ".event-header-row.event-header-upcoming, .event-row-virtual.event-upcoming",
209 + UPCOMING_COLUMNS,
210 + &upcoming_wide,
211 + &upcoming_narrow,
212 + ),
213 + (
214 + ".event-header-row.event-header-recurring, .event-row-virtual.event-recurring",
215 + RECURRING_COLUMNS,
216 + &recurring_wide,
217 + &recurring_narrow,
218 + ),
219 + ] {
104 220 css.push('\n');
105 221 css.push_str(&narrowing_css(
106 222 columns,
@@ -415,24 +415,25 @@
415 415 <span class="past-events-count" id="recurring-events-count">0</span>
416 416 </summary>
417 417 <div class="event-table-virtual" id="recurring-event-table">
418 - <div class="event-header-row" role="row">
419 - <div class="event-cell" role="columnheader">Pattern</div>
420 - <div class="event-cell" role="columnheader">Time</div>
421 - <div class="event-cell" role="columnheader">Event</div>
422 - <div class="event-cell" role="columnheader">Location</div>
423 - <div class="event-cell" role="columnheader"></div>
418 + <div class="event-header-row event-header-recurring" role="row">
419 + <div class="event-cell col-pattern" role="columnheader">Pattern</div>
420 + <div class="event-cell col-time" role="columnheader">Time</div>
421 + <div class="event-cell col-title" role="columnheader">Event</div>
422 + <div class="event-cell col-location" role="columnheader">Location</div>
423 + <div class="event-cell col-actions" role="columnheader"></div>
424 424 </div>
425 425 <div class="event-list-container" id="recurring-event-list-container"></div>
426 426 </div>
427 427 </details>
428 428 <h3 class="events-section-heading hidden" id="future-events-heading">Upcoming</h3>
429 429 <div class="event-table-virtual" id="event-table" role="grid" aria-label="Upcoming events">
430 - <div class="event-header-row" role="row">
431 - <div class="event-cell" role="columnheader">Date</div>
432 - <div class="event-cell" role="columnheader">Time</div>
433 - <div class="event-cell" role="columnheader">Event</div>
434 - <div class="event-cell" role="columnheader">Location</div>
435 - <div class="event-cell" role="columnheader"></div>
430 + <div class="event-header-row event-header-upcoming" role="row">
431 + <div class="event-cell col-select" role="columnheader"><span class="sr-only">Select</span></div>
432 + <div class="event-cell col-date" role="columnheader">Date</div>
433 + <div class="event-cell col-time" role="columnheader">Time</div>
434 + <div class="event-cell col-title" role="columnheader">Event</div>
435 + <div class="event-cell col-location" role="columnheader">Location</div>
436 + <div class="event-cell col-actions" role="columnheader"></div>
436 437 </div>
437 438 <div class="event-list-container well" id="event-list-container" aria-live="polite">
438 439 <div class="skeleton-shimmer" aria-label="Loading events">
@@ -1298,11 +1298,14 @@
1298 1298 box-shadow: var(--bevel-raised);
1299 1299 }
1300 1300
1301 - /* Event Grid Column Widths */
1301 + /* Event Grid Column Widths
1302 + grid-template-columns comes from css/tables.css. There are two tables here,
1303 + not one: the upcoming list leads with a select checkbox and a date, the
1304 + recurring list with a pattern and no selection. They shared this rule until
1305 + 2026-07-30, which put the upcoming row's six cells into five tracks. */
1302 1306 .event-header-row,
1303 1307 .event-row-virtual {
1304 1308 display: grid;
1305 - grid-template-columns: 100px 80px 1fr 150px 40px;
1306 1309 align-items: center;
1307 1310 gap: var(--gap-group);
1308 1311 }
@@ -2687,11 +2690,6 @@
2687 2690 .error-state--padded {
2688 2691 padding: var(--gap-group);
2689 2692 }
2690 - .event-cell--shrink {
2691 - flex: 0 0 auto;
2692 - padding-right: 0;
2693 - }
2694 -
2695 2693 .hr-soft {
2696 2694 border: none;
2697 2695 border-top: var(--border-width) solid var(--border);
@@ -542,11 +542,11 @@
542 542 data-act="events.open" data-a1="${escAttr(e.id)}"
543 543 data-contextmenu="contextMenus.showEvent" data-a1="@event" data-a2="${escAttr(e.id)}"
544 544 tabindex="0" role="row">
545 - <div class="event-cell event-cell-date"><span class="event-recurrence-pattern">${esc(patternLabel)}</span></div>
546 - <div class="event-cell row-secondary event-cell-time">${e.timeFormatted}</div>
547 - <div class="event-cell event-cell-title">${esc(displayTitle)}</div>
548 - <div class="event-cell row-secondary event-cell-location">${e.location ? esc(e.location) : '-'}</div>
549 - <div class="event-cell" style="text-align: right;" data-act="ui.noop">
545 + <div class="event-cell col-pattern event-cell-date"><span class="event-recurrence-pattern">${esc(patternLabel)}</span></div>
546 + <div class="event-cell col-time row-secondary event-cell-time">${e.timeFormatted}</div>
547 + <div class="event-cell col-title event-cell-title">${esc(displayTitle)}</div>
548 + <div class="event-cell col-location row-secondary event-cell-location">${e.location ? esc(e.location) : '-'}</div>
549 + <div class="event-cell col-actions" style="text-align: right;" data-act="ui.noop">
550 550 <button class="button--icon row-actions kebab-btn" data-act="contextMenus.showEvent" data-a1="@event" data-a2="${escAttr(e.id)}" title="Actions" aria-label="Event actions">&#x22EE;</button>
551 551 </div>
552 552 </div>
@@ -573,24 +573,24 @@
573 573
574 574 return `
575 575 ${dateHeader}
576 - <div class="row event-row-virtual ${e.isPast || isPast ? 'event-past' : ''}"
576 + <div class="row event-row-virtual event-upcoming ${e.isPast || isPast ? 'event-past' : ''}"
577 577 data-id="${escAttr(e.id)}"
578 578 data-act="events.open" data-a1="${escAttr(e.id)}"
579 579 data-contextmenu="contextMenus.showEvent" data-a1="@event" data-a2="${escAttr(e.id)}"
580 580 tabindex="0" role="row">
581 - <div class="event-cell event-cell--shrink">
581 + <div class="event-cell col-select">
582 582 <input type="checkbox" class="bulk-checkbox event-select-cb" data-id="${escAttr(e.id)}"
583 583 data-act="events.toggleEventSelection" data-a1="${escAttr(e.id)}" data-a2="@event"
584 584 aria-label="Select event">
585 585 </div>
586 - <div class="event-cell event-cell-date">
586 + <div class="event-cell col-date event-cell-date">
587 587 <span class="row-primary event-date-num">${startDate.getDate()} ${monthName}</span>
588 588 <span class="event-date-badge event-proximity-${e.proximityClass || 'default'}">${e.proximityLabel || ''}</span>
589 589 </div>
590 - <div class="event-cell row-secondary event-cell-time">${e.timeFormatted}</div>
591 - <div class="event-cell event-cell-title">${esc(displayTitle)}</div>
592 - <div class="event-cell row-secondary event-cell-location">${e.location ? esc(e.location) : '-'}</div>
593 - <div class="event-cell" style="text-align: right;" data-act="ui.noop">
590 + <div class="event-cell col-time row-secondary event-cell-time">${e.timeFormatted}</div>
591 + <div class="event-cell col-title event-cell-title">${esc(displayTitle)}</div>
592 + <div class="event-cell col-location row-secondary event-cell-location">${e.location ? esc(e.location) : '-'}</div>
593 + <div class="event-cell col-actions" style="text-align: right;" data-act="ui.noop">
594 594 <button class="button--icon row-actions kebab-btn" data-act="contextMenus.showEvent" data-a1="@event" data-a2="${escAttr(e.id)}" title="Actions" aria-label="Event actions">&#x22EE;</button>
595 595 </div>
596 596 </div>