max / makenotwork
7 files changed,
+202 insertions,
-12 deletions
| @@ -10759,6 +10759,14 @@ | |||
| 10759 | 10759 | "pkg-config", | |
| 10760 | 10760 | ] | |
| 10761 | 10761 | ||
| 10762 | + | [[patch.unused]] | |
| 10763 | + | name = "synckit-client" | |
| 10764 | + | version = "0.10.0" | |
| 10765 | + | ||
| 10766 | + | [[patch.unused]] | |
| 10767 | + | name = "synckit-config" | |
| 10768 | + | version = "0.2.0" | |
| 10769 | + | ||
| 10762 | 10770 | [[patch.unused]] | |
| 10763 | 10771 | name = "quasi-immediate" | |
| 10764 | 10772 | version = "0.91.1" | |
| @@ -10786,11 +10794,3 @@ | |||
| 10786 | 10794 | [[patch.unused]] | |
| 10787 | 10795 | name = "painhours" | |
| 10788 | 10796 | version = "0.1.0" | |
| 10789 | - | ||
| 10790 | - | [[patch.unused]] | |
| 10791 | - | name = "synckit-client" | |
| 10792 | - | version = "0.10.0" | |
| 10793 | - | ||
| 10794 | - | [[patch.unused]] | |
| 10795 | - | name = "synckit-config" | |
| 10796 | - | version = "0.2.0" |
| @@ -46,7 +46,6 @@ | |||
| 46 | 46 | {% if crate::changelog::is_published() %}<a href="/changelog">Changelog</a>{% endif %} | |
| 47 | 47 | <a href="mailto:info@makenot.work">Contact</a> | |
| 48 | 48 | <a href="/health" title="Service status and uptime">Status</a> | |
| 49 | - | <a href="#" data-prevent data-action="toggleShortcutsHelp" title="Keyboard shortcuts (?)">Shortcuts</a> | |
| 50 | 49 | </div> | |
| 51 | 50 | <p>© 2026 Make Creative, LLC</p> | |
| 52 | 51 | </footer> |
| @@ -60,6 +60,7 @@ | |||
| 60 | 60 | pub mod rich_field; | |
| 61 | 61 | pub mod schedule_field; | |
| 62 | 62 | pub mod settings_tabs; | |
| 63 | + | pub mod shortcuts; | |
| 63 | 64 | pub mod ssh_keys; | |
| 64 | 65 | pub mod upload_field; | |
| 65 | 66 | pub mod user_analytics; | |
| @@ -361,7 +362,25 @@ | |||
| 361 | 362 | /// fixture, and both readings are about screens that hold a session. A public | |
| 362 | 363 | /// screen registers no mutating route, so the probe has nothing to skip. | |
| 363 | 364 | pub fn public_mounts(app: &AppState) -> Vec<(&'static str, axum::Router)> { | |
| 364 | - | vec![(pricing::PATH, pricing_mount(app))] | |
| 365 | + | vec![ | |
| 366 | + | (pricing::PATH, pricing_mount(app)), | |
| 367 | + | (shortcuts::PATH, shortcuts_mount()), | |
| 368 | + | ] | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | /// The keyboard-shortcuts overlay, on the same shell every described screen | |
| 372 | + | /// gets so the listing looks like the site it is drawn over. | |
| 373 | + | /// | |
| 374 | + | /// Its own mount rather than a route inside the pricing nest: an overlay | |
| 375 | + | /// reachable from every screen is not one screen's route, and the binding's | |
| 376 | + | /// address is absolute in the emitted markup (`e0c0d991`). | |
| 377 | + | fn shortcuts_mount() -> axum::Router { | |
| 378 | + | quasi_axum::Adapter::new( | |
| 379 | + | shortcuts::router(), | |
| 380 | + | std::sync::Arc::new(()), | |
| 381 | + | std::sync::Arc::new(pricing::renderer()), | |
| 382 | + | ) | |
| 383 | + | .into_router() | |
| 365 | 384 | } | |
| 366 | 385 | ||
| 367 | 386 | /// The fee calculator's own nest: the page and the recompute it answers. |
| @@ -671,7 +671,17 @@ | |||
| 671 | 671 | pub fn renderer() -> Webview { | |
| 672 | 672 | let shell = crate::shell::described() | |
| 673 | 673 | .with_body_first(crate::shell::skip_link(PAGE)) | |
| 674 | - | .with_body_last(crate::shell::body_last()); | |
| 674 | + | .with_body_last(crate::shell::body_last()) | |
| 675 | + | // What this site offers from everywhere, which today is one key | |
| 676 | + | // (`e0c0d991`). On the shell rather than on a screen because that is | |
| 677 | + | // the whole claim chrome makes: an affordance reachable from every | |
| 678 | + | // screen is not a fact about any one of them, and every screen | |
| 679 | + | // converted after this inherits it with no further work. | |
| 680 | + | // | |
| 681 | + | // It is also what gives `Outcome::Over` somewhere to land -- a renderer | |
| 682 | + | // emits the overlay container for an app that declares chrome, and an | |
| 683 | + | // app that declares none gets a swap that does nothing. | |
| 684 | + | .with_chrome(crate::quasi::shortcuts::chrome()); | |
| 675 | 685 | // The `Shell::head` append that stood here is gone as of quasi 0.80 | |
| 676 | 686 | // (quasicoherent `a0e16839`). It wrote the plain `<meta name="description">` | |
| 677 | 687 | // by hand because `Screen::summarised` reached `og:description` and |
| @@ -420,3 +420,47 @@ | |||
| 420 | 420 | "the pressed screens and the mountable ones have diverged" | |
| 421 | 421 | ); | |
| 422 | 422 | } | |
| 423 | + | ||
| 424 | + | /// `e0c0d991`. The shortcut is declared once on the shell, so every described | |
| 425 | + | /// screen offers it and none of them has to remember to. | |
| 426 | + | #[tokio::test] | |
| 427 | + | async fn a_described_screen_offers_the_shortcuts_key() { | |
| 428 | + | let mut h = viewing().await; | |
| 429 | + | let html = h.client.get("/pricing").await.text; | |
| 430 | + | ||
| 431 | + | // quasi's half: a hidden button per binding, firing on the key, aimed at | |
| 432 | + | // the overlay container. The renderer wires it; nothing here draws a list. | |
| 433 | + | assert!( | |
| 434 | + | html.contains("data-chrome"), | |
| 435 | + | "the chrome is emitted: {html:.400}" | |
| 436 | + | ); | |
| 437 | + | assert!( | |
| 438 | + | html.contains("Keyboard shortcuts"), | |
| 439 | + | "the binding is labelled: {html:.400}" | |
| 440 | + | ); | |
| 441 | + | assert!( | |
| 442 | + | html.contains("/shortcuts"), | |
| 443 | + | "and it reaches the listing: {html:.400}" | |
| 444 | + | ); | |
| 445 | + | ||
| 446 | + | // And the affordance it replaces is gone. | |
| 447 | + | assert!( | |
| 448 | + | !html.contains("toggleShortcutsHelp"), | |
| 449 | + | "the data-action link was retired: {html:.400}" | |
| 450 | + | ); | |
| 451 | + | } | |
| 452 | + | ||
| 453 | + | /// The listing is ours, and it answers as something drawn over the page rather | |
| 454 | + | /// than as a page of its own. | |
| 455 | + | #[tokio::test] | |
| 456 | + | async fn the_shortcuts_listing_names_every_key_this_site_binds() { | |
| 457 | + | let mut h = viewing().await; | |
| 458 | + | let html = h.client.htmx_get("/shortcuts").await.text; | |
| 459 | + | ||
| 460 | + | // The described binding, read off the Chrome rather than written twice. | |
| 461 | + | assert!(html.contains('?'), "the described key: {html:.400}"); | |
| 462 | + | // And the three the host still owns. | |
| 463 | + | for key in ["Cmd+K", "Esc", "Cmd+S"] { | |
| 464 | + | assert!(html.contains(key), "{key} is listed: {html:.400}"); | |
| 465 | + | } | |
| 466 | + | } |
| @@ -38,6 +38,23 @@ | |||
| 38 | 38 | document.body.appendChild(overlay); | |
| 39 | 39 | } | |
| 40 | 40 | ||
| 41 | + | /** | |
| 42 | + | * Whether this page declares its shortcuts rather than hard-coding them. | |
| 43 | + | * | |
| 44 | + | * `e0c0d991`. A described document carries `quasi-webview`'s chrome: one hidden | |
| 45 | + | * button per binding, marked `data-chrome`, and `?` is bound to the described | |
| 46 | + | * listing. This file loads on described and undescribed pages alike -- both are | |
| 47 | + | * served from one shell -- so without the check both handlers fire on `?` and | |
| 48 | + | * the reader gets two overlays. | |
| 49 | + | * | |
| 50 | + | * The described page wins, because its listing is derived from the bindings and | |
| 51 | + | * this one is a hand-written copy. When the last template converts, this | |
| 52 | + | * function and the overlay below it go with it. | |
| 53 | + | */ | |
| 54 | + | function describedChrome(): boolean { | |
| 55 | + | return document.querySelector('[data-chrome]') !== null; | |
| 56 | + | } | |
| 57 | + | ||
| 41 | 58 | /** Install global keyboard shortcuts + the nav toggle. Ported from mnw.js:504. */ | |
| 42 | 59 | export function initKeyboard(): void { | |
| 43 | 60 | document.addEventListener('keydown', (e) => { | |
| @@ -60,7 +77,7 @@ | |||
| 60 | 77 | searchInput.select(); | |
| 61 | 78 | } | |
| 62 | 79 | } | |
| 63 | - | if (e.key === '?' && !inInput && !e.metaKey && !e.ctrlKey) { | |
| 80 | + | if (e.key === '?' && !inInput && !e.metaKey && !e.ctrlKey && !describedChrome()) { | |
| 64 | 81 | e.preventDefault(); | |
| 65 | 82 | toggleShortcutsHelp(); | |
| 66 | 83 | } |
| @@ -1,0 +1,101 @@ | |||
| 1 | + | //! The keyboard shortcuts this site offers, and the overlay that lists them. | |
| 2 | + | //! | |
| 3 | + | //! `e0c0d991`, decided 2026-08-30. This server declared no `Chrome` at all -- | |
| 4 | + | //! the only one in the tree was `Chrome::new()`, the empty default, used twice | |
| 5 | + | //! in [`super::embeds`] for embeds that deliberately have no shell. | |
| 6 | + | //! | |
| 7 | + | //! Paid with the `/pricing` port rather than after it. Every screen converted | |
| 8 | + | //! from here on inherits the shortcut with no further work, which is the whole | |
| 9 | + | //! reason to do it now instead of accumulating a site-wide gap to close under | |
| 10 | + | //! launch pressure. | |
| 11 | + | //! | |
| 12 | + | //! # What quasi does, and what this owns | |
| 13 | + | //! | |
| 14 | + | //! quasi wires the key and does not draw the listing. | |
| 15 | + | //! `quasi-webview`'s `binding_html` emits a hidden button whose action targets | |
| 16 | + | //! the overlay container, and that is all of it; `Binding::group` names "the | |
| 17 | + | //! heading a listing shows it beneath", which says plainly that the listing is | |
| 18 | + | //! a screen the app describes. So the plumbing is quasi's and the screen is | |
| 19 | + | //! ours. There is no renderer-drawn help overlay to go looking for. | |
| 20 | + | //! | |
| 21 | + | //! # Its own mount, not a route inside the pricing nest | |
| 22 | + | //! | |
| 23 | + | //! An overlay reachable from every screen is not one screen's route. The | |
| 24 | + | //! binding's address is absolute in the emitted markup, so registering it under | |
| 25 | + | //! `/pricing` would give the site-wide shortcut a pricing-shaped address and | |
| 26 | + | //! move it the first time a second screen converted. | |
| 27 | + | ||
| 28 | + | use quasi_router::screen::{Cell, Cells, Column}; | |
| 29 | + | use quasi_router::{ | |
| 30 | + | Action, Chrome, Node, Outcome, RegionKind, Request, Response, RouteError, Router, Screen, Slot, | |
| 31 | + | }; | |
| 32 | + | ||
| 33 | + | /// Where the listing answers. | |
| 34 | + | pub const PATH: &str = "/shortcuts"; | |
| 35 | + | ||
| 36 | + | /// The key that opens it. | |
| 37 | + | const OPENS: &str = "?"; | |
| 38 | + | ||
| 39 | + | /// What this site binds. | |
| 40 | + | /// | |
| 41 | + | /// One binding, and the listing below says why the other three are not here: | |
| 42 | + | /// a `Binding` carries an [`Action`], and Escape, Cmd+S and Cmd+K perform no | |
| 43 | + | /// route. They are `static/dist/core/keyboard.js`'s, and they move into this | |
| 44 | + | /// function on the day the description layer can say what they do. | |
| 45 | + | /// | |
| 46 | + | /// Hung on the shell rather than built per screen, so every described screen | |
| 47 | + | /// this server serves offers it and none of them has to remember to. | |
| 48 | + | #[must_use] | |
| 49 | + | pub fn chrome() -> Chrome { | |
| 50 | + | Chrome::new().bind(OPENS, "Keyboard shortcuts", Action::get(PATH)) | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | /// One key and what it does, for the listing. | |
| 54 | + | /// | |
| 55 | + | /// The described binding is read off [`chrome`] rather than written again here, | |
| 56 | + | /// which is the point `Binding::label` exists to make: a help overlay that | |
| 57 | + | /// lists the bindings is otherwise a second, hand-written copy that drifts. | |
| 58 | + | /// The host keys have no `Binding` to read, so they are spelled -- once, beside | |
| 59 | + | /// the declaration, rather than in a template. | |
| 60 | + | const HOST_KEYS: &[(&str, &str)] = &[ | |
| 61 | + | ("Cmd+K", "Search"), | |
| 62 | + | ("Esc", "Close modal or overlay"), | |
| 63 | + | ("Cmd+S", "Save the current form"), | |
| 64 | + | ]; | |
| 65 | + | ||
| 66 | + | /// The listing, drawn over whatever the reader was looking at. | |
| 67 | + | /// | |
| 68 | + | /// A table because that is what it is: two columns, one row per key. The | |
| 69 | + | /// shipped overlay was a hand-written `<table>` inside a string of markup in | |
| 70 | + | /// `keyboard.js`, and this is the same thing said once. | |
| 71 | + | pub fn screen(_state: &(), _request: Request) -> Result<Response, RouteError> { | |
| 72 | + | let mut rows: Vec<Cells> = chrome() | |
| 73 | + | .bindings | |
| 74 | + | .iter() | |
| 75 | + | .map(|binding| Cells::new([Cell::new(&binding.key), Cell::new(&binding.label)])) | |
| 76 | + | .collect(); | |
| 77 | + | rows.extend( | |
| 78 | + | HOST_KEYS | |
| 79 | + | .iter() | |
| 80 | + | .map(|(key, what)| Cells::new([Cell::new(*key), Cell::new(*what)])), | |
| 81 | + | ); | |
| 82 | + | ||
| 83 | + | Ok(Outcome::Over( | |
| 84 | + | Screen::list_detail("Keyboard shortcuts", false).with( | |
| 85 | + | Slot::new("shortcuts", RegionKind::Modal) | |
| 86 | + | .label("Keyboard shortcuts") | |
| 87 | + | .with(Node::Table { | |
| 88 | + | columns: vec![Column::new("Key"), Column::new("Does")], | |
| 89 | + | rows, | |
| 90 | + | more: None, | |
| 91 | + | }), | |
| 92 | + | ), | |
| 93 | + | ) | |
| 94 | + | .into()) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | /// The listing's own nest. | |
| 98 | + | #[must_use] | |
| 99 | + | pub fn router() -> Router<()> { | |
| 100 | + | Router::<()>::new().get("/", screen) | |
| 101 | + | } |