yksa-web-kit/yksa_web/static/yksa/css/kit.css
2026-08-18 13:44:37 +08:00

394 lines
11 KiB
CSS

/* =============================================================================
YKSA UI kit. Ships with yksa-web-kit; see ecosystem/ui-kit.md.
Requires Bootstrap >= 5.3: the state pills below are built on the
--bs-*-bg-subtle / --bs-*-text-emphasis / --bs-*-border-subtle families, and
text-body-secondary is a 5.3 utility. On 5.2.3 every rule here still parses
and the page renders wrong -- muted text is not muted, tinted backgrounds are
transparent. The bundled bootstrap.min.css is the version this is written for;
do not point a service at its own older copy.
Two rules govern the whole kit:
1. Typography carries meaning through weight, colour and size only. No
small-caps, no thin/semibold, no letter-spacing, no .display-*. Weights
are 400 and 700.
2. Colour is never the only signal. Every state renders an icon and a word
alongside its hue.
Anything Bootstrap already provides is used as-is. The classes below exist
only where it has no equivalent.
============================================================================= */
:root {
--navbar-height: 44px;
/* One layer scale for the whole estate, because the alternative is each
surface picking a number and a sticky bar eventually landing on top of an
open menu -- which is exactly what ops's console header did to the navbar's
timezone dropdown. Bootstrap's own scale starts at modal-backdrop (1040),
so everything here stays below that.
1020 in-page sticky bars (ops's console header)
1030 page chrome: navbar, action bar, cookie banner
1035 anything transient the user just opened -- above all of it */
--yksa-z-sticky: 1020;
--yksa-z-chrome: 1030;
--yksa-z-floating: 1035;
}
/* Bootstrap declares --bs-dropdown-zindex on .dropdown-menu, not on :root, so it
has to be overridden here -- a value set on :root is shadowed and does nothing.
At Bootstrap's default of 1000 an open menu loses to every bar above. */
.dropdown-menu {
--bs-dropdown-zindex: var(--yksa-z-floating);
}
/* -----------------------------------------------------------------------------
Navbar and footer -- layout-identical across every service
-------------------------------------------------------------------------- */
.custom-navbar {
height: var(--navbar-height);
padding-top: 0;
padding-bottom: 0;
z-index: var(--yksa-z-chrome);
border: none;
background-color: var(--bs-body-bg) !important;
}
.navbar {
z-index: var(--yksa-z-chrome);
}
.navbar-brand {
margin-right: 1em;
}
/* 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. */
.nav-full-height {
display: flex;
align-items: center;
height: 100%;
}
.nav-full-height.nav-link {
color: inherit;
padding-left: 1rem !important;
padding-right: 1rem !important;
padding-top: 0;
padding-bottom: 0;
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%;
}
.custom-navbar .navbar-nav {
align-items: stretch;
}
}
.nav-full-height.nav-link:hover,
.nav-full-height.nav-link.active {
color: #fff !important;
background-color: var(--bs-primary);
}
/* -----------------------------------------------------------------------------
State -- the sanctioned rendering of status (ui/_state.html)
-------------------------------------------------------------------------- */
.yksa-state {
display: inline-flex;
align-items: center;
gap: .25rem;
font-size: .75rem;
font-weight: 700;
padding: .05rem .4rem;
white-space: nowrap;
border: 1px solid transparent;
}
/* health */
.yksa-state-ok,
.yksa-state-complete {
color: var(--bs-success-text-emphasis);
background: var(--bs-success-bg-subtle);
border-color: var(--bs-success-border-subtle);
}
.yksa-state-warning,
.yksa-state-forced {
color: var(--bs-warning-text-emphasis);
background: var(--bs-warning-bg-subtle);
border-color: var(--bs-warning-border-subtle);
}
.yksa-state-failed {
color: var(--bs-danger-text-emphasis);
background: var(--bs-danger-bg-subtle);
border-color: var(--bs-danger-border-subtle);
}
.yksa-state-running,
.yksa-state-in-progress {
color: var(--bs-primary-text-emphasis);
background: var(--bs-primary-bg-subtle);
border-color: var(--bs-primary-border-subtle);
}
.yksa-state-skipped,
.yksa-state-unknown,
.yksa-state-not-started {
color: var(--bs-secondary-color);
background: var(--bs-secondary-bg-subtle);
border-color: var(--bs-border-color);
}
/* Neutral metadata chip. Not a coloured badge, because it carries no status. */
.yksa-chip {
display: inline-flex;
align-items: center;
gap: .25rem;
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);
}
/* -----------------------------------------------------------------------------
Typography helpers
-------------------------------------------------------------------------- */
.yksa-label {
font-size: .75rem;
font-weight: 700;
color: var(--bs-secondary-color);
}
.yksa-hint {
font-size: .75rem;
font-weight: 400;
color: var(--bs-secondary-color);
}
/* Separator between items in a run of inline metadata: a hairline rule, not a
character. A middot at .75rem is hard to tell from a decimal point or a full
stop, it is announced by screen readers, and it lands in the clipboard when the
line is copied. This is the inline-text counterpart to Bootstrap's `.vr`, which
is built for flex rows and has no margins of its own. */
.yksa-sep {
display: inline-block;
width: 1px;
height: 0.9em;
margin-inline: 0.5rem;
vertical-align: -0.1em;
background-color: var(--bs-border-color);
}
/* Inside an already-tight run (a card header, a table cell), the default margin
is too generous. */
.yksa-sep-tight {
margin-inline: 0.3rem;
}
/* Tabular numerals in the body face. Monospace is reserved for verbatim machine
text -- hex dumps, TLE lines, command mnemonics. */
.yksa-num {
font-variant-numeric: tabular-nums;
}
.yksa-readout {
font-variant-numeric: tabular-nums;
font-weight: 700;
line-height: 1;
}
.yksa-utc {
font-variant-numeric: tabular-nums;
}
.yksa-utc .yksa-local {
color: var(--bs-secondary-color);
font-size: .8em;
}
/* -----------------------------------------------------------------------------
Layout
-------------------------------------------------------------------------- */
/* A record in a list is a row with a left status marker, not a card. */
.yksa-row {
display: flex;
align-items: center;
gap: .75rem;
padding: .5rem;
border-left: 4px solid var(--bs-border-color);
}
.yksa-row:hover {
background: var(--bs-tertiary-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); }
/* 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);
}
/* Empty state: a sentence plus, where there is one, the action that resolves
it -- beside the sentence, not under it. Never a blank card. */
.yksa-empty {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: .75rem;
padding: 1.25rem;
color: var(--bs-secondary-color);
}
/* Reading column for forms and reference pages that are worse for being wide. */
.yksa-narrow {
max-width: 920px;
}
/* Save bar pinned to the viewport bottom, plus a spacer so it never covers the
last row of the form it belongs to. */
.yksa-actionbar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: var(--yksa-z-chrome);
}
.yksa-actionbar-spacer {
height: 5rem;
}
.form-error-bar ul {
margin-bottom: 0;
}
/* -----------------------------------------------------------------------------
Cookie banner
-------------------------------------------------------------------------- */
.cookie-banner {
position: sticky;
bottom: 0;
left: 0;
right: 0;
z-index: var(--yksa-z-chrome);
background-color: #1f2937;
color: #f8fafc;
}
.cookie-banner a {
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. */
.nav-full-height.nav-link {
padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);
margin-right: 0;
border: none !important;
}
.custom-navbar {
height: auto;
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)
-------------------------------------------------------------------------- */
.filter-chip {
cursor: pointer;
user-select: none;
transition: background-color 0.12s ease;
}
.filter-chip:hover {
filter: brightness(110%);
}
.filter-chip.active {
background-color: var(--bs-primary) !important;
color: #fff;
}
/* -----------------------------------------------------------------------------
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.
-------------------------------------------------------------------------- */
.text-content {
overflow: auto;
hyphens: auto;
}
.text-content img {
max-width: 100%;
height: auto !important;
}
.text-content p:last-child,
.text-content ul:last-child,
.text-content ol:last-child,
td p:last-child,
td ul:last-child {
margin-bottom: 0 !important;
}
/* Django's messages framework emits `error`; Bootstrap only ships `danger`. */
.alert-error {
--bs-alert-color: #fff;
--bs-alert-bg: #c42526;
--bs-alert-border-color: #c42526;
}
.btn-outline-bordered {
border-color: rgba(0, 0, 0, 0.066);
}