max / goingson
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
21 files changed,
+504 insertions,
-31 deletions
| @@ -9718,6 +9718,84 @@ | |||
| 9718 | 9718 | ||
| 9719 | 9719 | /* 63. Chrome Panel (the described side's running-timer band) */ | |
| 9720 | 9720 | ||
| 9721 | + | ||
| 9722 | + | /* --- The described shell's navigation ------------------------------------ */ | |
| 9723 | + | /* quasi's Chrome::nav says what the places are and declines to say where they | |
| 9724 | + | sit, because a terminal has no header. A browser has a stylesheet, and this | |
| 9725 | + | is it. The two levels are drawn as the shipped header draws them: a tab row, | |
| 9726 | + | then an underline strip, which is the call made on 2026-07-28 about not | |
| 9727 | + | putting two competing "this contains what follows" claims on one screen. | |
| 9728 | + | ||
| 9729 | + | The markup is one nested list, because that is the structure the description | |
| 9730 | + | stated. The outer list is the tabs and each item's inner list is its places, | |
| 9731 | + | so only the current tab's strip is on screen and the rest are folded away by | |
| 9732 | + | the :has() below rather than by anything the app said. | |
| 9733 | + | ||
| 9734 | + | No positioning: the renderer emits the nav before <main>, so it is already | |
| 9735 | + | the first thing in the document and does not have to be lifted there. That | |
| 9736 | + | is the difference from .chrome-panel below, which is emitted after the | |
| 9737 | + | content because it is a band over it. */ | |
| 9738 | + | .chrome-nav { | |
| 9739 | + | border-bottom: var(--border-width) solid var(--border); | |
| 9740 | + | padding: 0 var(--gap-page); | |
| 9741 | + | } | |
| 9742 | + | ||
| 9743 | + | .chrome-nav ul { | |
| 9744 | + | display: flex; | |
| 9745 | + | align-items: flex-end; | |
| 9746 | + | gap: var(--gap-bound); | |
| 9747 | + | margin: 0; | |
| 9748 | + | padding: 0; | |
| 9749 | + | list-style: none; | |
| 9750 | + | } | |
| 9751 | + | ||
| 9752 | + | .chrome-place { | |
| 9753 | + | display: block; | |
| 9754 | + | padding: var(--gap-peer) var(--gap-section); | |
| 9755 | + | text-decoration: none; | |
| 9756 | + | color: var(--content-secondary); | |
| 9757 | + | font-weight: 600; | |
| 9758 | + | } | |
| 9759 | + | ||
| 9760 | + | .chrome-place:hover { | |
| 9761 | + | color: var(--content); | |
| 9762 | + | } | |
| 9763 | + | ||
| 9764 | + | .chrome-place[aria-current="page"] { | |
| 9765 | + | color: var(--content); | |
| 9766 | + | } | |
| 9767 | + | ||
| 9768 | + | /* The second level, and only under the tab the user is on. A tab whose places | |
| 9769 | + | are not showing keeps them in the document: the description gave one tree and | |
| 9770 | + | hiding a branch is this stylesheet's answer, not a second description. */ | |
| 9771 | + | .chrome-nav > ul > li > ul { | |
| 9772 | + | display: none; | |
| 9773 | + | } | |
| 9774 | + | ||
| 9775 | + | .chrome-nav > ul > li:has(> .chrome-place[aria-current="page"]) > ul { | |
| 9776 | + | display: flex; | |
| 9777 | + | gap: var(--gap-section); | |
| 9778 | + | border-top: var(--border-width) solid var(--border); | |
| 9779 | + | margin-top: var(--gap-peer); | |
| 9780 | + | } | |
| 9781 | + | ||
| 9782 | + | .chrome-nav > ul > li > ul .chrome-place { | |
| 9783 | + | padding: var(--gap-bound) var(--gap-peer); | |
| 9784 | + | border-bottom: calc(var(--border-width) * 2) solid transparent; | |
| 9785 | + | margin-bottom: calc(-1 * var(--border-width)); | |
| 9786 | + | font-weight: 400; | |
| 9787 | + | } | |
| 9788 | + | ||
| 9789 | + | .chrome-nav > ul > li > ul .chrome-place[aria-current="page"] { | |
| 9790 | + | border-bottom-color: var(--action); | |
| 9791 | + | } | |
| 9792 | + | ||
| 9793 | + | /* Settings has no places inside it, so it is the one tab that can go to the | |
| 9794 | + | far end without leaving a gap in the strip under the others. */ | |
| 9795 | + | .chrome-nav > ul > li:last-child { | |
| 9796 | + | margin-left: auto; | |
| 9797 | + | } | |
| 9798 | + | ||
| 9721 | 9799 | /* Where the app's persistent panel sits. quasi's description declines to say, | |
| 9722 | 9800 | because a terminal has no floating; a browser has a stylesheet, and this is | |
| 9723 | 9801 | it. Same furniture as .timer-widget above, which is the JS widget this | |
| @@ -9729,7 +9807,7 @@ | |||
| 9729 | 9807 | an empty band must not draw a bar across the bottom of the screen. The clock | |
| 9730 | 9808 | is the marker because it is what the running state is made of: no timer, no | |
| 9731 | 9809 | readout, no panel. */ | |
| 9732 | - | .chrome-panel { | |
| 9810 | + | .chrome-panel[data-role="activity"] { | |
| 9733 | 9811 | position: fixed; | |
| 9734 | 9812 | bottom: 0; | |
| 9735 | 9813 | left: 0; | |
| @@ -9741,7 +9819,7 @@ | |||
| 9741 | 9819 | padding: var(--gap-peer) var(--gap-group); | |
| 9742 | 9820 | } | |
| 9743 | 9821 | ||
| 9744 | - | .chrome-panel:not(:has([data-clock])) { | |
| 9822 | + | .chrome-panel[data-role="activity"]:not(:has([data-clock])) { | |
| 9745 | 9823 | display: none; | |
| 9746 | 9824 | } | |
| 9747 | 9825 |
| @@ -216,7 +216,11 @@ | |||
| 216 | 216 | ||
| 217 | 217 | /// The whole board. | |
| 218 | 218 | fn board(state: &AppState, _request: quasi_router::Request) -> Result<Response, RouteError> { | |
| 219 | + | // The board is a mode of the Tasks place rather than a place of its | |
| 220 | + | // own: `index.html` draws it behind a `data-mode="board"` toggle inside | |
| 221 | + | // Tasks, so the Tasks tab stays lit while it is showing. | |
| 219 | 222 | Ok(Screen::list_detail("Board", false) | |
| 223 | + | .at_place(super::shell::TASKS) | |
| 220 | 224 | .with(board_region(state)?) | |
| 221 | 225 | .into()) | |
| 222 | 226 | } |
| @@ -220,6 +220,7 @@ | |||
| 220 | 220 | } | |
| 221 | 221 | ||
| 222 | 222 | Ok(Screen::list_detail("Contacts", false) | |
| 223 | + | .at_place(super::shell::CONTACTS) | |
| 223 | 224 | .with(band) | |
| 224 | 225 | .with(Slot::new("contacts-grid", RegionKind::Pane).with(grid(state, search, tag)?)) | |
| 225 | 226 | .with(Slot::new("contacts-detail", RegionKind::Pane).with(Node::empty("Nothing selected"))) |
| @@ -1028,7 +1028,10 @@ | |||
| 1028 | 1028 | ||
| 1029 | 1029 | /// The whole screen. | |
| 1030 | 1030 | fn screen(state: &AppState) -> Result<Screen, RouteError> { | |
| 1031 | + | // Reached from the settings sidebar, which is how a person gets here, | |
| 1032 | + | // so Settings is the place that stays lit. See `settings::Section::at`. | |
| 1031 | 1033 | Ok(Screen::list_detail("Import & Export", false) | |
| 1034 | + | .at_place(super::shell::SETTINGS) | |
| 1032 | 1035 | .with(Slot::new("data-band", RegionKind::Band).with(Node::page("Import & Export"))) | |
| 1033 | 1036 | .with(import_region()) | |
| 1034 | 1037 | .with(Slot::new(PREVIEW, RegionKind::Pane).with(no_preview())) |
| @@ -302,6 +302,7 @@ | |||
| 302 | 302 | axis = axis.with(timeline(&response, date)); | |
| 303 | 303 | ||
| 304 | 304 | Ok(Screen::list_detail("Day", false) | |
| 305 | + | .at_place(super::shell::DAY) | |
| 305 | 306 | .with(band(date, &response)) | |
| 306 | 307 | .with(axis) | |
| 307 | 308 | // The sidebar's tracked-time panel, which is `super::time_tracking`'s |
| @@ -686,6 +686,7 @@ | |||
| 686 | 686 | }; | |
| 687 | 687 | ||
| 688 | 688 | Ok(Screen::list_detail("Emails", false) | |
| 689 | + | .at_place(super::shell::EMAILS) | |
| 689 | 690 | .selecting(SELECTION) | |
| 690 | 691 | .with(band) | |
| 691 | 692 | .with(Slot::new(LIST, RegionKind::Pane).with(list(state, view, open)?)) |
| @@ -232,6 +232,7 @@ | |||
| 232 | 232 | } | |
| 233 | 233 | ||
| 234 | 234 | Ok(Screen::list_detail("Events", false) | |
| 235 | + | .at_place(super::shell::EVENTS) | |
| 235 | 236 | .with(band) | |
| 236 | 237 | .with(pane) | |
| 237 | 238 | .with(Slot::new("events-detail", RegionKind::Pane).with(Node::empty("Nothing selected"))) |
| @@ -42,7 +42,7 @@ | |||
| 42 | 42 | //! exist and both are counted. The count starts falling at the flip. Progress is | |
| 43 | 43 | //! the first list, not the number. | |
| 44 | 44 | //! | |
| 45 | - | //! ## Described, and retires at the flip (35 files, 259 sites) | |
| 45 | + | //! ## Described, and retires at the flip (36 files, 259 sites) | |
| 46 | 46 | //! | |
| 47 | 47 | //! | Module | JS counterpart | Sites | | |
| 48 | 48 | //! |---|---|---| | |
| @@ -60,10 +60,19 @@ | |||
| 60 | 60 | //! | [`data`] | `import-external.js` 11, `import.js` 5, `export.js` 2 | 18 | | |
| 61 | 61 | //! | [`time_tracking`] | `time-tracking.js` 6, `time-summary.js` 3 | 9 | | |
| 62 | 62 | //! | [`events`] | `events.js` | 10 | | |
| 63 | + | //! | [`shell`] | `navigation.js` 0 | 0 | | |
| 63 | 64 | //! | |
| 64 | 65 | //! Two rows carry a second screen as a submodule: [`projects`] its dashboard, | |
| 65 | - | //! and [`settings`] its email accounts section. So the fourteen rows here are | |
| 66 | - | //! sixteen described screens. | |
| 66 | + | //! and [`settings`] its email accounts section. So the fifteen rows here are | |
| 67 | + | //! sixteen described screens and one that is no screen at all. | |
| 68 | + | //! | |
| 69 | + | //! [`shell`] is that last one, and it is the row that carries no `esc()` sites. | |
| 70 | + | //! It describes the app's furniture rather than a screen: the tabs, the places | |
| 71 | + | //! under them, and the running-timer band it asks [`time_tracking`] for. | |
| 72 | + | //! `navigation.js` is paired with it because that is the file the nav replaces, | |
| 73 | + | //! and pairing it is what puts a tab added to the JS and not to the description | |
| 74 | + | //! in front of the drift check. Its zero is honest: the file concatenates | |
| 75 | + | //! nothing, which is why the shell was invisible to this page until 2026-08-22. | |
| 67 | 76 | //! | |
| 68 | 77 | //! [`time_tracking`] is the one row here that is two things: the app's chrome, | |
| 69 | 78 | //! described because the floating widget belongs to no screen, and the Timer | |
| @@ -159,30 +168,39 @@ | |||
| 159 | 168 | //! Together with the 40 above, these 67 sites are what the done-condition | |
| 160 | 169 | //! actually has to answer for. Porting every candidate screen leaves them. | |
| 161 | 170 | //! | |
| 162 | - | //! ## The shell, which no row above reaches (38 files, 9,114 lines) | |
| 171 | + | //! ## The shell, which no row above reaches (37 files, 8,569 lines) | |
| 163 | 172 | //! | |
| 164 | 173 | //! Measured 2026-08-22, and it is the reason a table that looks complete is | |
| 165 | 174 | //! not. The four categories above account for every `esc()` call site in the | |
| 166 | - | //! tree and for 49 of the 87 files under `frontend/js/`. The other 38 are 9,114 | |
| 175 | + | //! tree and for 50 of the 87 files under `frontend/js/`. The other 37 are 8,569 | |
| 167 | 176 | //! lines and appear in no category, because the accounting here is `esc()`- | |
| 168 | 177 | //! centric and the shell concatenates almost nothing: it wires, routes, and | |
| 169 | 178 | //! switches panes. A file with no call site was invisible to every count on | |
| 170 | 179 | //! this page, so the remainder never read as missing. | |
| 171 | 180 | //! | |
| 181 | + | //! `navigation.js` left this list the same day, for [`shell`]'s row above. It | |
| 182 | + | //! is the only one of them a description replaces rather than makes redundant: | |
| 183 | + | //! the rest go when `index.html` goes. | |
| 184 | + | //! | |
| 172 | 185 | //! The largest of them, and what each is: `utils.js` 704, `components.js` 594 | |
| 173 | - | //! (context menu, action sheet), `navigation.js` 545 (view switching, the tab | |
| 174 | - | //! bar, window title), `api.js` 500, `app.js` 471 (bootstrap, the Tauri menu), | |
| 186 | + | //! (context menu, action sheet), `api.js` 500, `app.js` 471 (bootstrap, the | |
| 187 | + | //! Tauri menu), | |
| 175 | 188 | //! `virtual-scroller.js` 453, `mobile.js` 447 and `touch.js` 444, | |
| 176 | 189 | //! `keyboard.js` 279 (the shortcut table and the help overlay), `themes.js` | |
| 177 | 190 | //! 277, `router.js` 107, `dispatch.js` 109, `state.js` 158. | |
| 178 | 191 | //! | |
| 179 | - | //! Nothing on the described side draws any of it. [`quasi_router::Chrome`] | |
| 180 | - | //! carries key bindings and one panel, and goingson declares the panel as | |
| 181 | - | //! [`time_tracking::chrome`], the running-timer band. That is the whole of the | |
| 182 | - | //! app furniture that exists on this side. A tab bar is a second always-present | |
| 183 | - | //! thing and there is no second panel, which is a vocabulary question rather | |
| 184 | - | //! than a porting one: quasicoherent `71aa29b4`, with the goingson half on | |
| 185 | - | //! `24aa5833`. | |
| 192 | + | //! The navigation is described as of 2026-08-22 and the rest of that list is | |
| 193 | + | //! not. [`quasi_router::Chrome`] grew `nav` and roled panels under | |
| 194 | + | //! quasicoherent `71aa29b4`, ruled (e) by Max, and [`shell`] is this app's | |
| 195 | + | //! answer: the three tabs, the places under them, Settings, and the band. | |
| 196 | + | //! | |
| 197 | + | //! Two pieces of the shipped header are still unsaid, and neither is a gap in | |
| 198 | + | //! the nav. The sync indicator wants a `Role::Status` panel and has nothing | |
| 199 | + | //! describable to put in one, because its content is `settings-sync.js` and | |
| 200 | + | //! that stays JavaScript for the reason the by-decision list gives. The `?` | |
| 201 | + | //! shortcut overlay wants a container quasi-webview emits only alongside a | |
| 202 | + | //! chrome binding, which is quasicoherent `858be2a6`. [`shell`]'s header | |
| 203 | + | //! records both. | |
| 186 | 204 | //! | |
| 187 | 205 | //! Four files in the remainder are screen controllers rather than shell, and | |
| 188 | 206 | //! are candidates for a pairing row once somebody reads them against the | |
| @@ -274,6 +292,7 @@ | |||
| 274 | 292 | pub mod problems; | |
| 275 | 293 | pub mod projects; | |
| 276 | 294 | pub mod settings; | |
| 295 | + | pub mod shell; | |
| 277 | 296 | pub mod task_list; | |
| 278 | 297 | pub mod tasks; | |
| 279 | 298 | pub mod time_tracking; | |
| @@ -521,10 +540,9 @@ | |||
| 521 | 540 | // Not vendored, so asking for it would be one 404 per | |
| 522 | 541 | // document. Nothing described here uses it yet. | |
| 523 | 542 | .without_hyperscript() | |
| 524 | - | // The running-timer band, which belongs to the app rather | |
| 525 | - | // than to any screen it is drawn over. See | |
| 526 | - | // [`time_tracking`]. | |
| 527 | - | .with_chrome(time_tracking::chrome()), | |
| 543 | + | // The app's own furniture: where you can go, and the | |
| 544 | + | // running-timer band. See [`shell`]. | |
| 545 | + | .with_chrome(shell::chrome()), | |
| 528 | 546 | ), | |
| 529 | 547 | ), | |
| 530 | 548 | ); |
| @@ -504,6 +504,7 @@ | |||
| 504 | 504 | pane = pane.extend(reflection(&data, month)); | |
| 505 | 505 | ||
| 506 | 506 | Ok(Screen::sidebar_content("Monthly Review") | |
| 507 | + | .at_place(super::shell::MONTH) | |
| 507 | 508 | .with(band) | |
| 508 | 509 | .with(pane)) | |
| 509 | 510 | } |
| @@ -411,6 +411,7 @@ | |||
| 411 | 411 | } | |
| 412 | 412 | ||
| 413 | 413 | Ok(Screen::list_detail("Problems", false) | |
| 414 | + | .at_place(super::shell::PROBLEMS) | |
| 414 | 415 | .with(band) | |
| 415 | 416 | .with(Slot::new("problems-list", RegionKind::Pane).with(ranked(state, status, source)?)) | |
| 416 | 417 | .into()) |
| @@ -317,6 +317,7 @@ | |||
| 317 | 317 | } | |
| 318 | 318 | ||
| 319 | 319 | Ok(Screen::list_detail("Projects", false) | |
| 320 | + | .at_place(super::shell::PROJECTS) | |
| 320 | 321 | .with(band) | |
| 321 | 322 | .with(Slot::new("projects-grid", RegionKind::Pane).with(grid( | |
| 322 | 323 | state, |
| @@ -487,7 +487,10 @@ | |||
| 487 | 487 | _ => appearance(state, &config), | |
| 488 | 488 | }); | |
| 489 | 489 | ||
| 490 | - | Ok(Screen::sidebar_content("Settings").with(nav).with(pane)) | |
| 490 | + | Ok(Screen::sidebar_content("Settings") | |
| 491 | + | .at_place(super::shell::SETTINGS) | |
| 492 | + | .with(nav) | |
| 493 | + | .with(pane)) | |
| 491 | 494 | } | |
| 492 | 495 | ||
| 493 | 496 | /// Appearance, which is where the JS opens. |
| @@ -1095,6 +1095,7 @@ | |||
| 1095 | 1095 | .extend(filters(state, &view)?); | |
| 1096 | 1096 | ||
| 1097 | 1097 | Ok(Screen::list_detail("Tasks", false) | |
| 1098 | + | .at_place(super::shell::TASKS) | |
| 1098 | 1099 | .selecting(SELECTION) | |
| 1099 | 1100 | .with(band) | |
| 1100 | 1101 | .with(Slot::new("tasks-bulk", RegionKind::Band).extend(bulk(state, &view)?)) |