Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c86f583a15 | ||
|
|
fafd9320ab | ||
|
|
f18276ab01 | ||
|
|
c6cbc8fe70 | ||
|
|
2d1da2886d |
13 changed files with 582 additions and 93 deletions
155
.gitignore
vendored
Normal file
155
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/python
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
pytestdebug.log
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
doc/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
pythonenv*
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# profiling data
|
||||
.prof
|
||||
|
||||
# Db and static files
|
||||
*.sqlite3
|
||||
/media
|
||||
/static
|
||||
/postgres
|
||||
/EXAMPLE_*
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
docker-compose.override
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "yksa-web-kit"
|
||||
version = "0.1.0"
|
||||
version = "0.1.3"
|
||||
description = "Shared chrome, UI kit and vendored front-end assets for the YKSA services"
|
||||
requires-python = ">=3.13"
|
||||
license = { text = "Proprietary" }
|
||||
|
|
|
|||
|
|
@ -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("<nav") < html.index("<main") < html.index("<footer")
|
||||
|
|
@ -94,6 +94,24 @@ def test_page_title_links_its_parent():
|
|||
assert "W-1" in html
|
||||
|
||||
|
||||
def test_page_title_walks_the_whole_trail_when_given_one():
|
||||
"""A page two levels down names its grandparent, so the trail reaches the
|
||||
section root instead of starting halfway along it."""
|
||||
html = render_to_string("yksa/ui/_page_title.html", {
|
||||
"title": "Status", "parent": "W-1", "parent_url": "/widgets/w-1/",
|
||||
"grandparent": "Widgets", "grandparent_url": "/widgets/",
|
||||
})
|
||||
assert html.index("/widgets/\"") < html.index("/widgets/w-1/")
|
||||
assert html.count("<a ") == 2
|
||||
|
||||
|
||||
def test_page_title_without_a_grandparent_is_unchanged():
|
||||
html = render_to_string("yksa/ui/_page_title.html", {
|
||||
"title": "W-1", "parent": "Widgets", "parent_url": "/widgets/",
|
||||
})
|
||||
assert html.count("<a ") == 1
|
||||
|
||||
|
||||
# --- the estate-wide rules --------------------------------------------------
|
||||
|
||||
|
||||
|
|
|
|||
2
yksa_web/static/yksa/css/bootstrap.min.css
vendored
2
yksa_web/static/yksa/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,5 @@
|
|||
/* =============================================================================
|
||||
YKSA UI kit. Ships with yksa-web-kit; see ecosystem/ui-kit.md.
|
||||
YKSA UI kit.
|
||||
|
||||
Requires Bootstrap >= 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;
|
||||
|
|
@ -65,58 +78,85 @@
|
|||
}
|
||||
|
||||
/* Nav links fill the bar's height and butt against one another, so the active
|
||||
item reads as a selected tab rather than a highlighted word.
|
||||
|
||||
The height comes from the chain below, not from padding. It used to come from a
|
||||
hard-coded `padding-top: 12px` on top of a text line box, which meant any item
|
||||
whose content was not text -- the language flag -- came out short. */
|
||||
item reads as a selected tab rather than a highlighted word. */
|
||||
.nav-full-height {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* The top padding is deliberately larger than the bottom: it is what sits the link
|
||||
text on the same optical line as the wordmark in the brand block. Do not
|
||||
symmetrise it -- the alignment is the reason it is here. */
|
||||
.nav-full-height.nav-link {
|
||||
color: inherit;
|
||||
padding-left: 1rem !important;
|
||||
padding-right: 1rem !important;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-top: 12px;
|
||||
background-color: var(--bs-body-bg);
|
||||
margin-right: -1px;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
/* Give `height: 100%` above something definite to resolve against: the bar's
|
||||
height has to reach the <a> through the container, the collapse and the
|
||||
<li>, or it resolves to `auto` and the rule does nothing. */
|
||||
.custom-navbar > .container,
|
||||
.custom-navbar > .container-fluid,
|
||||
.custom-navbar .navbar-collapse,
|
||||
.custom-navbar .navbar-nav,
|
||||
.custom-navbar .nav-item {
|
||||
height: 100%;
|
||||
}
|
||||
/* The locale item's only content is a flag image. Because the link is a flex
|
||||
container, an image child gives it no text line box to take its height from, so
|
||||
the item came out ~9px shorter than every neighbour. Reserve the height one line
|
||||
of body text would have occupied and letterbox the flag inside it: same box as
|
||||
its neighbours, flag unscaled and undistorted.
|
||||
|
||||
.custom-navbar .navbar-nav {
|
||||
align-items: stretch;
|
||||
}
|
||||
Scoped to .nav-link so the brand block's wordmark is untouched. */
|
||||
.nav-full-height.nav-link > img {
|
||||
min-height: calc(1em * var(--bs-body-line-height));
|
||||
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;
|
||||
|
|
@ -124,7 +164,11 @@
|
|||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
/* health */
|
||||
.yksa-state > .bi,
|
||||
.yksa-chip > .bi {
|
||||
margin-right: .25rem;
|
||||
}
|
||||
|
||||
.yksa-state-ok,
|
||||
.yksa-state-complete {
|
||||
color: var(--bs-success-text-emphasis);
|
||||
|
|
@ -160,11 +204,10 @@
|
|||
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;
|
||||
|
|
@ -173,9 +216,39 @@
|
|||
border: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
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;
|
||||
|
|
@ -209,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 {
|
||||
|
|
@ -230,9 +333,92 @@
|
|||
font-size: .8em;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Layout
|
||||
-------------------------------------------------------------------------- */
|
||||
/* 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
|
||||
-- an `mb-0` on the last element of every card body -- which every element
|
||||
added later then has to remember. It is one rule and it belongs here.
|
||||
|
||||
Scoped to direct children: anything deeper is inside something else's box, and
|
||||
that box's spacing is its own business. */
|
||||
.card-body > :last-child,
|
||||
.alert > :last-child,
|
||||
.list-group-item > :last-child,
|
||||
.modal-body > :last-child,
|
||||
.offcanvas-body > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* 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
|
||||
these resolve to --bs-secondary-color, which is computed against the page. */
|
||||
.alert .text-body-secondary,
|
||||
.alert .yksa-label,
|
||||
.alert .yksa-hint {
|
||||
color: currentColor !important;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
/* 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.
|
||||
|
||||
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: 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-disabled-color: var(--bs-alert-color);
|
||||
--bs-btn-disabled-border-color: var(--bs-alert-color);
|
||||
--bs-btn-focus-shadow-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
|
|
@ -244,19 +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. */
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.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);
|
||||
background: var(--bs-success-bg-subtle);
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
.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
|
||||
|
|
@ -293,10 +567,6 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Cookie banner
|
||||
-------------------------------------------------------------------------- */
|
||||
|
||||
.cookie-banner {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
|
|
@ -311,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. */
|
||||
|
|
@ -324,46 +590,61 @@
|
|||
border: none !important;
|
||||
}
|
||||
|
||||
/* The bar grows here (the brand block keeps its height and the padding is
|
||||
added around it), and --navbar-height has to grow with it: it is what
|
||||
every in-page sticky bar offsets itself by, and a stale 44px slid ops's
|
||||
pass console header up under the navbar on tablet widths.
|
||||
|
||||
61px is the same arithmetic that produces 44px above -- 34px logo + the
|
||||
brand block's .3125rem padding either side -- plus .5rem of bar padding
|
||||
either side and the 1px bottom border. min-height, not height, so a taller
|
||||
brand overflows the offset rather than being clipped by it. */
|
||||
:root {
|
||||
--navbar-height: 61px;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
height: auto;
|
||||
min-height: var(--navbar-height);
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Stacked, the rows size to their own content again. */
|
||||
.custom-navbar .navbar-nav,
|
||||
.custom-navbar .nav-item {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
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;
|
||||
|
|
@ -389,6 +670,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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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:
|
|||
<link rel="stylesheet" href="{% static 'yksa/css/kit.css' %}">
|
||||
{% block extra_head %}{% endblock extra_head %}
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100 bg-light">
|
||||
{% 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 %}
|
||||
<body class="d-flex flex-column min-vh-100 bg-body-tertiary">
|
||||
<nav class="navbar navbar-expand-lg custom-navbar border-bottom sticky-top">
|
||||
<div class="{% block navbar_container_class %}container{% endblock navbar_container_class %}">
|
||||
<a class="navbar-brand nav-full-height" href="{% block brand_url %}/{% endblock brand_url %}">
|
||||
|
|
@ -163,7 +169,6 @@ Blocks a service normally fills:
|
|||
{% include "yksa/includes/_footer.html" %}
|
||||
<script src="{% static 'yksa/src/bootstrap.bundle.min.js' %}"></script>
|
||||
<script src="{% static 'yksa/src/cookie-banner.js' %}"></script>
|
||||
{% comment %}Shows the toasts yksa/includes/_messages.html renders.{% endcomment %}
|
||||
<script src="{% static 'yksa/src/toast.js' %}"></script>
|
||||
{% block extra_js %}{% endblock extra_js %}
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -17,10 +17,14 @@ Optional context:
|
|||
* empty_message: text shown when ``chips`` is empty (defaults to a generic
|
||||
placeholder via {{ empty_message|default:_("No options yet.") }}).
|
||||
|
||||
Each chip is a real <button>, so it is focusable and answers to Enter/Space; a
|
||||
<span> was reachable by mouse only.
|
||||
|
||||
The accompanying JS (static/src/chip-filters.js) toggles ``.filter-chip.active``
|
||||
on click and re-emits the comma-separated value into the hidden input keyed by
|
||||
``data-filter-csv``. Submitting the form sends the result; servers split on
|
||||
comma. No XHR -- keeps URLs bookmarkable.
|
||||
``data-filter-csv``, then fires `change` on it so a live table can redraw without
|
||||
a submit. Submitting the form sends the result; servers split on comma. No XHR by
|
||||
default -- keeps URLs bookmarkable.
|
||||
{% endcomment %}
|
||||
{% load i18n %}
|
||||
<div class="card mb-3">
|
||||
|
|
@ -29,8 +33,9 @@ comma. No XHR -- keeps URLs bookmarkable.
|
|||
{% if chips %}
|
||||
<div class="d-flex flex-wrap gap-1" data-filter-group="{{ name }}">
|
||||
{% for chip in chips %}
|
||||
<span class="badge bg-secondary filter-chip {% if chip.selected %}active{% endif %}"
|
||||
data-value="{{ chip.value }}">{{ chip.label }}</span>
|
||||
<button type="button" class="filter-chip {% if chip.selected %}active{% endif %}"
|
||||
aria-pressed="{% if chip.selected %}true{% else %}false{% endif %}"
|
||||
data-value="{{ chip.value }}">{{ chip.label }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,14 @@ KPI tile. One column of a .row-cols-* grid.
|
|||
A tile whose secondary line is itself markup (a timestamp through
|
||||
yksa/ui/_utc.html, say) builds the same three elements inline with the same
|
||||
classes, as with .yksa-empty.
|
||||
|
||||
It is an ordinary .card. It used to be `border-0 bg-white shadow-sm`, which was a
|
||||
raised white panel in a kit whose every other surface is a bordered flat one, and
|
||||
`bg-white` is a hard-coded light-theme colour besides -- wrong in a dark theme on
|
||||
every service. Depth is not a level of hierarchy this kit has.
|
||||
{% endcomment %}
|
||||
<div class="col">
|
||||
<div class="card h-100 border-0 bg-white shadow-sm">
|
||||
<div class="card h-100">
|
||||
<div class="card-body py-2 px-3{% if accent %} border-start border-{{ accent }} border-3{% endif %}">
|
||||
<div class="yksa-label text-truncate">{{ label }}</div>
|
||||
<div class="yksa-readout fs-4">{{ value }}</div>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,14 @@ fixed. Never pick a heading level for its size.
|
|||
{% trans "Widgets" as parent %}{% url 'public:widget-list' as parent_url %}
|
||||
{% include "yksa/ui/_page_title.html" with title=widget.name parent=parent parent_url=parent_url %}
|
||||
|
||||
A page one level deeper names its grandparent too, so the trail reaches the
|
||||
section root rather than starting halfway down it:
|
||||
|
||||
{% include "yksa/ui/_page_title.html" with title=_("Status") parent=widget.name parent_url=... grandparent=_("Widgets") grandparent_url=... %}
|
||||
|
||||
Put it inside the page header's flex row; the row owns the actions on the right.
|
||||
{% endcomment %}
|
||||
<h1 class="h3 mb-0">
|
||||
{% if parent %}<a class="text-decoration-none" href="{{ parent_url }}">{{ parent }}</a> / {% endif %}{{ title }}
|
||||
{% if grandparent %}<a class="text-decoration-none" href="{{ grandparent_url }}">{{ grandparent }}</a> / {% endif %}{% if parent %}<a class="text-decoration-none" href="{{ parent_url }}">{{ parent }}</a> / {% endif %}{{ title }}
|
||||
</h1>
|
||||
{% if subtitle %}<span class="text-body-secondary small">{{ subtitle }}</span>{% endif %}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ here. A conditional chain choosing a colour inside a template is the bug this
|
|||
partial exists to prevent: it is how the same run status ended up green on one
|
||||
page and grey on another.
|
||||
{% endcomment %}{% state state as s %}{% spaceless %}
|
||||
<span class="yksa-state yksa-state-{{ state|default:'unknown' }}"{% if title %} title="{{ title }}"{% endif %}>
|
||||
<span class="yksa-state yksa-state-{{ s.name }}"{% if title %} title="{{ title }}"{% endif %}>
|
||||
<i class="bi bi-{{ s.icon }}" aria-hidden="true"></i>
|
||||
<span>{{ label|default:s.label }}</span>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ register = template.Library()
|
|||
|
||||
@register.simple_tag
|
||||
def state(name):
|
||||
"""Resolve a state name to {variant, icon, label} for ui/_state.html."""
|
||||
variant, icon, label = STATES.get(name or "", STATES["unknown"])
|
||||
return {"variant": variant, "icon": icon, "label": label}
|
||||
"""Resolve a state name to {name, variant, icon, label} for ui/_state.html.
|
||||
|
||||
`name` is the *resolved* key, not what was asked for: the template builds its
|
||||
`.yksa-state-<name>` class from it, and building that from the raw input meant
|
||||
an unrecognised status got a class no stylesheet defines -- a blank pill, which
|
||||
is the one outcome the fallback exists to prevent."""
|
||||
key = name if name in STATES else "unknown"
|
||||
variant, icon, label = STATES[key]
|
||||
return {"name": key, "variant": variant, "icon": icon, "label": label}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue