diff --git a/patches/README.md b/patches/README.md index 4a67ac4e10367748a113e99fe086c322e06af8b0..db4332bda085b43ca15fe23dce244fef98d00f72 100644 --- a/patches/README.md +++ b/patches/README.md @@ -37,7 +37,9 @@ If `git apply --check` fails after a `sourcehut-refresh`, upstream has changed that file — re-derive the patch against the new context and update the copy in `~/data/home/phoebe-lab/srht/patches/` first, then re-import here. ### `core-nav-groups.patch` -**Repo:** `core.sr.ht`  ·  **Files:** `srht/app/nav.py` (new), `srht/app/flask.py`, `srht/templates/nav.html`, `scss/nav.scss`, `scss/dark.scss` +**Repo:** `core.sr.ht`  ·  **Files:** `srht/app/nav.py` (new), `srht/app/flask.py`, `srht/templates/nav.html` + +**Generated, not hand-written.** `make-core-nav-groups-patch.sh` builds this patch by running the real image-build script (`phoebe-lab/srht/scripts/patch-nav-groups.py`) against a scratch copy of the upstream clone and diffing the result. Regenerate it after touching that script or `phoebe-lab/srht/overlay/core/nav.py`, and commit the output. Two representations of one change is the drift hazard here, and generating one from the other is what removes it. **Problem.** The service switcher — the strip of service names between the brand and the login box — is a flat row listing every `*.sr.ht` section in the config. That works at eight services and stops working at fourteen: on a laptop the tail slides under the login box, and every service the instance gains makes it worse. Two further defects underneath the overflow: @@ -73,14 +75,16 @@ - `~/data/home/sourcehut/sr.ht-apkbuilds` is a read-only clone of **upstream's** repo (its only remote is `git.sr.ht/~sircmpwn/sr.ht-apkbuilds`), and its `core.sr.ht` APKBUILD still pins `0.83.0` while the instance runs **0.84.5**. Nothing here builds core.sr.ht from it. - The apk comes from `~bigbes/srht-upstream`, a channel that **mirrors upstream's prebuilt packages** (`mirror.sr.ht` → `apk-mirror` → Garage → the channel). No local build step exists to hook a patch into. - The mechanism actually in use for core.sr.ht is `phoebe-lab/srht/scripts/patch-*.py` — idempotent scripts that rewrite the *installed* files in `Dockerfile.base`. `nav.html` is already patched twice that way, by `patch-nav-brand.py` and `patch-nav-register.py`, and neither touches the `
{% if current_user %} ---- a/scss/nav.scss -+++ b/scss/nav.scss -@@ -22,6 +22,103 @@ - text-decoration: underline; - } - -+ // The overflow panel of the service switcher. It hangs off its own list -+ // item, so the navbar must not clip it: nothing here or above may introduce -+ // overflow: hidden, or the panel is cut off at the row it drops out of. -+ .nav-item.nav-more { -+ position: relative; -+ -+ // The default disclosure triangle is dropped in favour of a caret drawn -+ // beside the count, so the summary reads as one label rather than as a -+ // widget with a number next to it. list-style covers Firefox, the -+ // ::-webkit-details-marker rule covers Safari. -+ summary { -+ list-style: none; -+ cursor: pointer; -+ padding: 0 0.5rem; -+ color: $navbar-light-color; -+ white-space: nowrap; -+ -+ &::-webkit-details-marker { -+ display: none; -+ } -+ -+ &::after { -+ content: " \25be"; -+ } -+ -+ &:hover { -+ color: $navbar-light-active-color; -+ text-decoration: underline; -+ } -+ -+ // The summary is the only keyboard-reachable way into the panel, so its -+ // focus ring is not decoration. -+ &:focus, -+ &:focus-visible { -+ outline: 2px solid $link-color; -+ outline-offset: -2px; -+ color: $navbar-light-active-color; -+ } -+ } -+ -+ details[open] > summary::after { -+ content: " \25b4"; -+ } -+ -+ .nav-more-panel { -+ position: absolute; -+ z-index: 1030; // above page content, level with Bootstrap's fixed navbar -+ right: 0; -+ min-width: 12rem; -+ padding: 0.5rem 0; -+ background: $gray-100; -+ border: 1px solid $gray-300; -+ -+ // Two columns' worth of groups sit side by side on a wide viewport and -+ // stack on a narrow one, so the panel grows sideways rather than into a -+ // list long enough to need scrolling. -+ display: flex; -+ flex-wrap: wrap; -+ align-items: flex-start; -+ } -+ -+ .nav-more-group { -+ padding: 0 1rem; -+ min-width: 8rem; -+ -+ ul { -+ list-style: none; -+ margin: 0; -+ padding: 0; -+ } -+ -+ a { -+ display: block; -+ padding: 0.1rem 0; -+ white-space: nowrap; -+ } -+ } -+ -+ .nav-more-title { -+ color: $gray-600; -+ font-size: 0.85rem; -+ text-transform: uppercase; -+ padding-bottom: 0.1rem; -+ } -+ -+ // On a phone the row already wraps rather than pushing the page sideways; -+ // the panel follows it and spans the width instead of hanging off an edge -+ // that may be off-screen. -+ @include media-breakpoint-down(xs) { -+ position: static; -+ -+ .nav-more-panel { -+ width: 100%; -+ } -+ } -+ } -+ - .navbar-text { - color: $black; - ---- a/scss/dark.scss -+++ b/scss/dark.scss -@@ -177,6 +177,34 @@ - .active .nav-link { - color: $white; - } -+ -+ // The service switcher's overflow panel. Same two roles the rest of the -+ // dark theme uses for a raised surface and its edge, so the panel sits on -+ // the navbar the way .event sits on the page. -+ .nav-item.nav-more { -+ summary { -+ color: lighten($gray-600, 5); -+ -+ &:hover, -+ &:focus, -+ &:focus-visible { -+ color: $white; -+ } -+ } -+ -+ .nav-more-panel { -+ background: $darker; -+ border-color: $border; -+ } -+ -+ .nav-more-title { -+ color: $muted-dark; -+ } -+ -+ a { -+ color: $link-dark; -+ } -+ } - } - } - --- /dev/null +++ b/srht/app/nav.py -@@ -0,0 +1,147 @@ +@@ -0,0 +1,160 @@ +"""The service switcher: which services stay visible in the navbar, and how the +rest are grouped behind the counter. + ++COPYed into core.sr.ht's site-packages as srht/app/nav.py by Dockerfile.base, ++the way overlay/meta's two modules reach metasrht — a whole module dropped in ++beside upstream's, rather than one of the scripts/patch-*.py that edit files ++already there. patch-nav-groups.py is the other half: it teaches flask.py to ++import this and nav.html to render it. Nothing in this file knows it is an ++overlay. ++ +The strip between the brand and the login box is the instance's only catalogue, -+and on an installation with more than a handful of services it stopped fitting -+the row. It is now a pinned subset plus a grouped remainder, with both the -+subset and the grouping declared in config.ini rather than in code: ++and at fourteen services it stopped fitting the row. It is now a pinned subset ++plus a grouped remainder, with both the subset and the grouping declared in ++config.ini rather than in code: + + [sr.ht] + nav-groups=code work ci output account + nav-pinned=git builds artifacts + + [bench.sr.ht] -+ origin=https://bench.example ++ origin=https://bench.srht.bigb.es + nav-group=output + +An installation that sets none of these keys keeps the switcher it had, moved +behind the counter: every service falls into one unnamed group, which prints as +a plain alphabetical list with no headings. That is deliberate — it is what -+makes this safe to run against a config nobody has edited yet. ++makes this safe to run against a config nobody has edited yet, and it is why ++the whole change can be offered upstream one day. + -+This module exists as a module, rather than as more lines in flask.py, because -+the same three keys are read by the instance's non-Python services and the two -+implementations have to keep agreeing. A self-contained function is the part -+that can be compared against its counterpart, and the part a rebase onto a new -+upstream does not have to be re-threaded through. ++The same three keys are read by sr-ht-ecore's chrome package, which draws this ++same strip for the instance's own Go services. The two implementations have to ++keep agreeing, which is why the taxonomy lives in the shared config and why ++this is one self-contained function rather than lines spread through flask.py: ++a function is the part that can be compared against its counterpart. + +One rule is worth stating on its own: members sort alphabetically inside a +group, in every implementation. Not by config order — a config parser is free @@ -275,6 +209,11 @@ +# Sections that are never entries in the switcher. paste and pages have no +# top-level UI worth linking; hub is not a sibling service but the network's +# front page, and the brand already points at it; api is the federated GraphQL +# gateway and serves no pages at all. ++# ++# This is the only exclusion list that matters here. flask.py keeps its own for ++# the flat `network` list, but after patch-nav-brand.py and patch-nav-groups.py ++# nothing reads `network` any more — both of its uses were in nav.html, and both ++# were patched away. +_excluded = [ + "paste.sr.ht", + "pages.sr.ht", @@ -300,7 +239,7 @@ + that is not excluded and resolves to an origin. + + A section without an origin is skipped rather than listed with a dead link, + which is the one place this differs from the flat `network` list flask.py -+ still exposes for the brand. ++ still builds. + """ + items = [] + for section in config: diff --git a/patches/make-core-nav-groups-patch.sh b/patches/make-core-nav-groups-patch.sh new file mode 100755 index 0000000000000000000000000000000000000000..da22f758bdf947b41eeb886e457e7a4d216aa0a6 --- /dev/null +++ b/patches/make-core-nav-groups-patch.sh @@ -0,0 +1,60 @@ +#!/bin/sh +# Regenerate core-nav-groups.patch from the things that actually run. +# +# The patch is a derived artifact, not a source. What runs on this instance is +# ../scripts/patch-nav-groups.py (which rewrites the installed flask.py and +# nav.html) and ../overlay/core/nav.py (which is dropped in beside them); the +# patch exists so the same change can be read as a diff against upstream and +# offered there one day. Generating it from those two files is what stops the +# two representations from drifting — a hazard that is otherwise real, because +# nothing else compares them. +# +# Run it after touching either file, and commit the result. +# +# The scss half of the change is deliberately NOT here. core.sr.ht ships scss +# and this instance compiles none of it: the Python services arrive with their +# CSS prebuilt inside upstream's apks, and the Go ones build theirs in CI from +# an unpatched clone. The panel is styled from an inline