From f18276ab011a45b196194d7ebf6210a7f567ffe7 Mon Sep 17 00:00:00 2001 From: ThePetrovich Date: Fri, 21 Aug 2026 22:09:17 +0800 Subject: [PATCH 1/3] Comment cleanup --- README.md | 3 +- tests/test_ui_kit.py | 20 +- yksa_web/static/yksa/css/kit.css | 375 ++++++++++++++---- yksa_web/static/yksa/src/chip-filters.js | 4 + yksa_web/templates/yksa/base.html | 11 +- .../templates/yksa/includes/chip_filters.html | 13 +- yksa_web/templates/yksa/ui/_page_title.html | 7 +- 7 files changed, 336 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index dbb3838..261a5e8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # yksa-web-kit -Common UI modules and packages (navbar, chrome, vendored libs). Should be used across all new projects in YKSA TM/TC family. +Common UI: navbar, page chrome, and vendored front-end libs, shared across the +YKSA TM/TC services. ## Install diff --git a/tests/test_ui_kit.py b/tests/test_ui_kit.py index de5d6ff..139f5a7 100644 --- a/tests/test_ui_kit.py +++ b/tests/test_ui_kit.py @@ -42,7 +42,7 @@ def test_the_navbar_and_footer_are_on_every_page(): @pytest.mark.django_db def test_the_service_cannot_reorder_the_chrome(): - """navbar, then main, then footer — a service that wants otherwise has to + """navbar, then main, then footer: a service that wants otherwise has to stop extending the kit, which is the point.""" html = Client().get("/privacy/").content.decode() assert html.index("= 5.3: the state pills below are built on the --bs-*-bg-subtle / --bs-*-text-emphasis / --bs-*-border-subtle families, and @@ -34,6 +34,23 @@ --yksa-z-sticky: 1020; --yksa-z-chrome: 1030; --yksa-z-floating: 1035; + + /* One hover treatment for the whole estate: the surface's ground goes one + step darker and nothing else moves. Every hoverable thing -- table rows, + list rows, step rows, catalog cards, disclosures -- uses this token, so a + page cannot end up with three ways of saying "you are pointing at this". + Borders, shadows and colour shifts are all reserved for *state*, which is + a property of the record; hover is a property of the pointer. */ + --yksa-hover-bg: var(--bs-tertiary-bg); +} + +/* Bootstrap's own hoverable components, pointed at the same token. */ +.table { + --bs-table-hover-bg: var(--yksa-hover-bg); +} + +.list-group { + --bs-list-group-action-hover-bg: var(--yksa-hover-bg); } /* Bootstrap declares --bs-dropdown-zindex on .dropdown-menu, not on :root, so it @@ -43,10 +60,6 @@ --bs-dropdown-zindex: var(--yksa-z-floating); } -/* ----------------------------------------------------------------------------- - Navbar and footer -- layout-identical across every service - -------------------------------------------------------------------------- */ - .custom-navbar { height: var(--navbar-height); padding-top: 0; @@ -96,32 +109,66 @@ object-fit: contain; } +/* An open dropdown is as much "the item you are on" as an active one. Without + this the toggle dropped back to the resting colours the moment its own menu + appeared, which reads as the click having missed. */ .nav-full-height.nav-link:hover, -.nav-full-height.nav-link.active { +.nav-full-height.nav-link.active, +.nav-full-height.nav-link.show { color: #fff !important; background-color: var(--bs-primary); } -/* ----------------------------------------------------------------------------- - State -- the sanctioned rendering of status (ui/_state.html) - -------------------------------------------------------------------------- */ +/* Muted text inside a highlighted nav item takes the item's colour. + Bootstrap's text utilities set `color` with `!important`, so a muted span -- + a countdown, a hint -- kept its resting grey when the item turned solid primary + underneath it: grey on blue, the one combination the theme has no contrast for. + `inherit` needs `!important` here for the same reason the utility does. + + Deliberately only the muted utility, not every descendant. A badge and a state + pill are separate surfaces carrying their own background, and flattening them + to the parent's colour would erase what they are for. */ +.nav-full-height.nav-link:hover .text-body-secondary, +.nav-full-height.nav-link.active .text-body-secondary, +.nav-full-height.nav-link.show .text-body-secondary, +/* Same problem, same fix, one row down: a dropdown item with a muted hint or + sub-line under its label -- the outcome menu on the pass console, the duty + menu in the header -- highlights on hover, focus and keyboard selection, and + the muted line stayed grey against it. Any menu that explains its options has + this shape, so the rule belongs on `.dropdown-item` rather than on each one. */ +.dropdown-item:hover .text-body-secondary, +.dropdown-item:focus .text-body-secondary, +.dropdown-item:active .text-body-secondary, +.dropdown-item.active .text-body-secondary { + color: inherit !important; +} + +/* `inline-block`, not `inline-flex`, and this is the whole of the table-alignment + fix. An inline-flex box's baseline is its last flex item's baseline, and its + padding and border hang *below* that -- so in a table cell the pill sits low, + and no amount of `vertical-align` corrects it without also fighting the line + height it inherits. An inline-block shares the row's baseline with the text + beside it and straddles it symmetrically, which is why Bootstrap's own `.badge` + has always looked right in a table. The icon is centred by bootstrap-icons' + `vertical-align: -.125em`, the same way it is in running text. + + Nothing here sets `line-height`: the pill takes the surface's, so it matches + the row it sits in rather than being a second type size. */ .yksa-state { - display: inline-flex; - align-items: center; - gap: .25rem; + display: inline-block; font-size: .75rem; font-weight: 700; padding: .05rem .4rem; white-space: nowrap; border: 1px solid transparent; - /* An inline-flex box baseline-aligns on its own last line box, which in a - table cell or a run of text leaves it sitting a few pixels low. It is a - self-contained box; align it on the middle instead. */ - vertical-align: middle; } -/* health */ +.yksa-state > .bi, +.yksa-chip > .bi { + margin-right: .25rem; +} + .yksa-state-ok, .yksa-state-complete { color: var(--bs-success-text-emphasis); @@ -157,23 +204,51 @@ border-color: var(--bs-border-color); } -/* Neutral metadata chip. Not a coloured badge, because it carries no status. */ +/* Neutral metadata chip. Not a coloured badge, because it carries no status. + Same box model as .yksa-state, for the same reason. */ .yksa-chip { - display: inline-flex; - align-items: center; - gap: .25rem; + display: inline-block; font-size: .75rem; padding: 0 .35rem; white-space: nowrap; color: var(--bs-secondary-color); background: var(--bs-tertiary-bg); border: 1px solid var(--bs-border-color); - vertical-align: middle; } -/* ----------------------------------------------------------------------------- - Typography helpers - -------------------------------------------------------------------------- */ +/* A borderless icon button for dense rows: a table row's remove control, an + overflow `...` toggle, a field's clear. Bordered buttons in those places draw a + box around every row and the boxes become the pattern the eye follows. + + This exists because the alternative was being spelled out by hand, differently, + in five templates -- `btn btn-sm btn-link p-0 text-body-secondary`, sometimes + with `border-0`, sometimes with `px-1`, once with an `fs-5` on the glyph. It is + a button, not a link: `.btn-link` is reserved for navigation, and a control + styled as a link but wired to a POST invites a middle-click that silently + does nothing. */ +.yksa-btn-icon { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.75rem; + min-height: 1.75rem; + padding: 0 .25rem; + color: var(--bs-secondary-color); + background: none; + border: none; + line-height: 1; +} + +.yksa-btn-icon:hover, +.yksa-btn-icon:focus-visible { + color: var(--bs-body-color); + background: var(--yksa-hover-bg); +} + +.yksa-btn-icon.is-danger:hover, +.yksa-btn-icon.is-danger:focus-visible { + color: var(--bs-danger); +} .yksa-label { font-size: .75rem; @@ -207,6 +282,36 @@ margin-inline: 0.3rem; } +/* A run of metadata separated by rules. Use this rather than dropping bare + `.yksa-sep`s between text nodes. + + The margins above are symmetric, but the *source* around them is not: a + template that puts the separator at the start of a line contributes a collapsed + space on its left and none on its right, so the rule sits visibly off-centre + between the two items it divides. Nobody sees that while writing the template + and everybody sees it on the page. + + A flex container fixes it at the root: whitespace-only text nodes never become + flex items, so indentation stops existing, and the gap is the only spacing. + Each item goes in its own element -- an unwrapped text node *would* become an + anonymous flex item, and then the whole run is one item and the gap does + nothing. That is the reason for the spans. */ +.yksa-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0 .5rem; +} + +.yksa-meta-tight { + gap: 0 .3rem; +} + +/* Inside the run the gap does the spacing; the rule's own margins would double it. */ +.yksa-meta > .yksa-sep { + margin-inline: 0; +} + /* Tabular numerals in the body face. Monospace is reserved for verbatim machine text -- hex dumps, TLE lines, command mnemonics. */ .yksa-num { @@ -228,10 +333,6 @@ font-size: .8em; } -/* ----------------------------------------------------------------------------- - Containers - -------------------------------------------------------------------------- */ - /* A container that supplies its own padding must not also inherit the trailing margin of whatever it ends with. Bootstrap does this for `p` in a few places and not at all for `dl`, `ul` or `table`, so the fix was being applied by hand @@ -248,16 +349,20 @@ margin-bottom: 0; } -/* ----------------------------------------------------------------------------- - Alerts +/* A definition list ends with a `dd`, which carries a bottom margin of its own. + `mb-0` on the `dl` does not reach it, so the gap survives the very utility + applied to remove it -- which is how several fact lists ended up looking + bottom-heavy inside an otherwise even card. */ +dl:last-child > dd:last-child { + margin-bottom: 0; +} - The estate theme paints alerts in solid colour, not the tinted panel Bootstrap - ships by default. That changes what belongs inside one: a utility text colour - is computed against the page ground and is unreadable on a saturated one, and - an outline button drawn in `--bs-secondary` all but vanishes. - -------------------------------------------------------------------------- */ +/* Alerts here paint in solid colour, not Bootstrap's default tinted panel: a + utility text colour computed against the page ground is unreadable on a + saturated one, and an outline button drawn in `--bs-secondary` all but + vanishes -- hence the overrides below. -/* Muted text inside an alert is the alert's own colour, softened. All three of + Muted text inside an alert is the alert's own colour, softened. All three of these resolve to --bs-secondary-color, which is computed against the page. */ .alert .text-body-secondary, .alert .yksa-label, @@ -269,26 +374,51 @@ /* Outline buttons take their colour from the alert rather than from the palette, and invert to the alert's ground on hover. The semantic distinction between `-secondary` and `-danger` is lost here, which is correct: the alert already - says which of those it is, and a red outline on a red ground says nothing. */ + says which of those it is, and a red outline on a red ground says nothing. + + Every value comes from `--bs-alert-color` / `--bs-alert-bg`, which the alert + variant declares and the button inherits, so one rule covers all six variants + in both text directions. Not `currentColor`: inside `:hover` that resolves to + the *hover* colour, so a hover background of `currentColor` paints the button + the colour it is about to be and the hover state disappears. */ .alert .btn-outline-primary, .alert .btn-outline-secondary, .alert .btn-outline-success, .alert .btn-outline-danger, .alert .btn-outline-warning { - --bs-btn-color: inherit; - --bs-btn-border-color: currentColor; + --bs-btn-color: var(--bs-alert-color); + --bs-btn-border-color: var(--bs-alert-color); --bs-btn-hover-color: var(--bs-alert-bg); - --bs-btn-hover-bg: currentColor; - --bs-btn-hover-border-color: currentColor; + --bs-btn-hover-bg: var(--bs-alert-color); + --bs-btn-hover-border-color: var(--bs-alert-color); --bs-btn-active-color: var(--bs-alert-bg); - --bs-btn-active-bg: currentColor; - --bs-btn-active-border-color: currentColor; + --bs-btn-active-bg: var(--bs-alert-color); + --bs-btn-active-border-color: var(--bs-alert-color); + --bs-btn-disabled-color: var(--bs-alert-color); + --bs-btn-disabled-border-color: var(--bs-alert-color); --bs-btn-focus-shadow-rgb: 255, 255, 255; } -/* ----------------------------------------------------------------------------- - Layout - -------------------------------------------------------------------------- */ +/* A filled button on a solid alert is the alert's own colour inverted -- the same + pair, the other way round. `btn-light` was doing this by hand and only happened + to be right on the four dark variants. */ +.alert .btn-contrast { + --bs-btn-color: var(--bs-alert-bg); + --bs-btn-bg: var(--bs-alert-color); + --bs-btn-border-color: var(--bs-alert-color); + --bs-btn-hover-color: var(--bs-alert-bg); + --bs-btn-hover-bg: var(--bs-alert-color); + --bs-btn-hover-border-color: var(--bs-alert-color); + --bs-btn-active-color: var(--bs-alert-bg); + --bs-btn-active-bg: var(--bs-alert-color); + --bs-btn-active-border-color: var(--bs-alert-color); + --bs-btn-focus-shadow-rgb: 255, 255, 255; + opacity: 1; +} + +.alert .btn-contrast:hover { + opacity: .9; +} /* A record in a list is a row with a left status marker, not a card. */ .yksa-row { @@ -300,39 +430,107 @@ } .yksa-row:hover { - background: var(--bs-tertiary-bg); + background: var(--yksa-hover-bg); } .yksa-row.is-in-progress { border-left-color: var(--bs-primary); } .yksa-row.is-complete { border-left-color: var(--bs-success); } .yksa-row.is-pending { border-left-color: var(--bs-warning); } .yksa-row.is-failed { border-left-color: var(--bs-danger); } +/* Withdrawn/skipped: still a record, but nothing here is work. The border + stays the resting grey and the row's text steps back, which is the same + thing `skipped` says in the state vocabulary. */ +.yksa-row.is-skipped { color: var(--bs-secondary-color); } -/* The one saturated element allowed on a surface: the active shift, the pass in - contact, the source currently polling. Two on a page means one is wrong. */ -.yksa-row.is-active { - border-left-color: var(--bs-success); - background: var(--bs-success-bg-subtle); +/* A table row that *is* a record: the row is the link to it. Sanctioned where a + card's equivalent is not -- a row is one target, while a card is a container + of several. `.stretched-link` goes on the cell that names the record; this + class is only here to give that anchor something to stretch against. + + Sibling links in the row are lifted above it, the same lift `.card-footer` + needed and for the same reason: the stretched anchor covers the whole row, so + without this a press on the station's own link silently opens the record + instead. A row with no sibling links needs no lift and pays nothing for it. */ +.yksa-row-link { + position: relative; } -/* Catalog card: a card summarising one record, where the whole card is the link - to it. The hover treatment is defined once, here, because three services had a - grid of these and none of them said what hovering meant -- the cursor changed - over the title only, and the padding around it was dead. The card carries this - class and the title anchor carries .stretched-link. */ +.yksa-row-link a:not(.stretched-link) { + position: relative; + z-index: 2; +} + +/* The one emphasised element allowed on a surface: the active shift, the pass in + contact, the source currently polling. Two on a page means one is wrong. + + It is a heavier border in the accent colour, not a tint. A row already carries + a coloured left border and usually a state pill; adding a filled ground makes + three ways of saying one thing, and the strongest of the three drowns the row's + own content. Emphasis is the *weight* of the existing signal, not a new one. */ +.yksa-row.is-active { + border-left-color: var(--bs-success); + border-left-width: 6px; + padding-left: calc(.5rem - 2px); +} + +/* Catalog card: a card summarising one record. The card is a container, not a + control: its title is the link and nothing else in it moves under the pointer. + Two earlier versions made the whole card hot -- one recoloured the border and + title, one painted the hover ground -- and both put a card-sized reaction + behind a card-sized target that also held its own footer buttons, so the page + read as a grid of giant buttons and a press near a footer control was a coin + toss. Hover is reserved for rows, which are one target each. + + `position: relative` stays so a card can still host absolutely-positioned + corner chrome; it no longer backs a .stretched-link. */ .yksa-card-link { position: relative; } -.yksa-card-link:hover { - border-color: var(--bs-primary); - background: var(--bs-tertiary-bg); +/* The title is a link and says so in the link colour, the way odms has always + drawn it. `.text-decoration-none` on the anchor suppresses the underline at + rest so a grid of forty titles is not forty underlines; hover brings it back, + which is this card's one hover affordance. Both rules have to out-specify that + utility, hence the element-qualified selectors. */ +.yksa-card-link .card-title a, +.yksa-card-link h2 a, +.yksa-card-link h3 a { + color: var(--bs-link-color); } -/* The title is already the link; underlining it on hover would be the only - moving part in a grid of forty. The border carries the affordance instead. */ -.yksa-card-link:hover .stretched-link { - color: var(--bs-primary); +.yksa-card-link .card-title a:hover, +.yksa-card-link h2 a:hover, +.yksa-card-link h3 a:hover, +.yksa-card-link .card-title a:focus-visible, +.yksa-card-link h2 a:focus-visible, +.yksa-card-link h3 a:focus-visible { + color: var(--bs-link-hover-color); + text-decoration: underline; +} + +/* Row chrome that appears under the pointer: the retract on one entry of a long + append-only stream, where forty always-visible trash icons would be the loudest + thing on the page. It keeps its box at rest, so revealing it never reflows the + row. Focus counts as pointing -- without that clause the control exists only + for mouse users. */ +.yksa-reveal { + opacity: 0; +} + +.yksa-reveal-host:hover .yksa-reveal, +.yksa-reveal:focus-visible, +.yksa-reveal-host:focus-within .yksa-reveal { + opacity: 1; +} + +/* The fact grid inside a catalog card: two columns of `.yksa-label` + figure. + It was `.meta-col` in odms.css, defined against `.sat-row-card`, which is why + ops built its own with a `dl.row` instead and got a different rhythm and a + stray bottom margin. One card, one grid. */ +.yksa-card-meta { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: .25rem .75rem; } /* Empty state: a sentence plus, where there is one, the action that resolves @@ -369,10 +567,6 @@ margin-bottom: 0; } -/* ----------------------------------------------------------------------------- - Cookie banner - -------------------------------------------------------------------------- */ - .cookie-banner { position: sticky; bottom: 0; @@ -387,10 +581,6 @@ color: #bfdbfe; } -/* ----------------------------------------------------------------------------- - Responsive - -------------------------------------------------------------------------- */ - @media (max-width: 991px) { /* Collapsed, the nav is a stacked list; the full-height tab treatment would put a border around every row. */ @@ -407,33 +597,40 @@ } } -/* ----------------------------------------------------------------------------- - Chip filters (yksa/includes/chip_filters.html + src/chip-filters.js) - -------------------------------------------------------------------------- */ - +/* A filter chip is a toggle, so it looks like one at both ends of its state: an + outlined chip at rest, a filled one when it is on. It used to be `badge + bg-secondary` at rest, which made an unselected filter a solid grey block -- + as loud as the selected one and easy to read as already applied. The class is + self-contained now; callers need no `badge bg-*` alongside it. */ .filter-chip { + display: inline-block; + font-size: .75rem; + line-height: 1.5; + padding: .05rem .5rem; + color: var(--bs-body-color); + background-color: var(--bs-body-bg); + border: 1px solid var(--bs-border-color); cursor: pointer; user-select: none; transition: background-color 0.12s ease; } .filter-chip:hover { - filter: brightness(110%); + background-color: var(--yksa-hover-bg); } .filter-chip.active { - background-color: var(--bs-primary) !important; color: #fff; + background-color: var(--bs-primary); + border-color: var(--bs-primary); } -/* ----------------------------------------------------------------------------- - Prose and tables - - Long-form text rendered from the database (policies, descriptions) and the - table cells that hold it. Everything else that used to live in main.css was - qsl-only and did not come across. - -------------------------------------------------------------------------- */ +.filter-chip.active:hover { + background-color: var(--bs-primary); +} +/* Long-form text rendered from the database (policies, descriptions) and the + table cells that hold it. */ .text-content { overflow: auto; hyphens: auto; @@ -459,6 +656,10 @@ td ul:last-child { --bs-alert-border-color: #c42526; } +/* A quiet border for grouped buttons that sit inside another bordered surface -- + a catalog card's footer -- where the button group's own outline would read as a + second box around the first. `--bs-border-color` rather than a black alpha: the + alpha approximated exactly this colour on white and disappeared on anything else. */ .btn-outline-bordered { - border-color: rgba(0, 0, 0, 0.066); + border-color: var(--bs-border-color); } diff --git a/yksa_web/static/yksa/src/chip-filters.js b/yksa_web/static/yksa/src/chip-filters.js index db8eead..7e3357d 100644 --- a/yksa_web/static/yksa/src/chip-filters.js +++ b/yksa_web/static/yksa/src/chip-filters.js @@ -42,6 +42,9 @@ active.push(chip.dataset.value); }); hidden.value = active.join(","); + // Live surfaces (a server-side table, a chart) listen here rather than + // waiting for a submit. A hidden input never fires `change` on its own. + hidden.dispatchEvent(new Event("change", { bubbles: true })); } document.querySelectorAll("[data-filter-group]").forEach(function (group) { @@ -49,6 +52,7 @@ var chip = event.target.closest(".filter-chip"); if (!chip || !group.contains(chip)) return; chip.classList.toggle("active"); + chip.setAttribute("aria-pressed", chip.classList.contains("active") ? "true" : "false"); syncHidden(group); }); }); diff --git a/yksa_web/templates/yksa/base.html b/yksa_web/templates/yksa/base.html index 6bc0c4d..e06589e 100644 --- a/yksa_web/templates/yksa/base.html +++ b/yksa_web/templates/yksa/base.html @@ -3,7 +3,7 @@ The estate skeleton. Every service's own base.html extends this and fills the blocks; nothing here is meant to be copied into a service. The navbar is written out here rather than {% include %}d because blocks inside -an included template cannot be overridden — and the navbar's layout has to be +an included template cannot be overridden, and the navbar's layout has to be identical everywhere while its items differ per service. Same for the footer's position. Fill `navbar_items` and `navbar_tools`; change nothing else. @@ -38,7 +38,13 @@ Blocks a service normally fills: {% block extra_head %}{% endblock extra_head %} - + {% comment %} + `bg-body-tertiary`, not `bg-light`. `bg-light` is a fixed near-white that does + not follow `data-bs-theme`, so it was the one element standing between the + estate and a working dark theme: every card and control below it recoloured + and the page they sat on did not. + {% endcomment %} +