wrolpi/app/src/components/ui/ui.css
Roland 851485ef1b ui: Common.js and thirteen more files off Semantic
Phase 2, part 3.  Baseline 63 -> 50.

Common.js is the one that mattered: 51 files import it, so its Semantic usage
was propping up the rest of the migration.  Everything in it now comes from the
library -- Paginator, the two search inputs, DirectorySearch, TabLinks, the
toggles, the messages, APIButton, Breadcrumbs, SimpleAccordion, IframeViewer,
CardPoster, FileIcon, LoadStatistic, the theme menu, and the hotspot icons.

Also migrated: Placeholder, Apps, ArchivePreview, CbzViewer, AddToPlaylist,
TaggedDeleteConfirmModal, the collection edit/tag forms, and four inventory
components.

New in the library, all in the gallery and tested:

  SearchBox     Semantic's `<Search category>` with its grouped results shape
                kept, because four call sites already build it.  A real
                combobox rather than a div that listens for clicks.
  Pagination    Semantic's prop names; onPageChange gets the page number.
  TabBar        the strip only -- routing stays with the caller.
  IconStack     Semantic's IconGroup, named once for assistive technology.
  Table.HeaderCell sorted/onSort -- a real button carrying aria-sort.
  Button/IconButton translate Semantic's size names.

That last one is defensive: unmigrated callers still pass size='tiny', and
Mantine drops a size it does not recognise without a word, so the button
quietly renders at the wrong size.  Mapping here keeps the mixed state honest.

Three real bugs found and fixed on the way, none of them cosmetic:

  * `fileSuffixIconName` returned fourteen icon names the map did not have, so
    opening a folder with a PDF, a Word document or an archive in it logged an
    error and drew a fallback glyph.  All fourteen mapped, with a test that
    walks the mapper's own return values, and another asserting every mapped
    name resolves to a Tabler component that exists.
  * `Statistic` relied on Semantic's `Statistic.Group` for its spacing, so on
    the dashboard the load averages ran together into "1 MIN. LOAD5 MIN.
    LOAD15 MIN. LOAD".  It carries its own spacing now.
  * Semantic's inverted stylesheet colors every descendant of an inverted
    segment, so Toggle labels inherited #dddddd -- white pixels in night mode,
    on the Settings page, which is exactly where someone adjusts their WROLPi
    in the dark.  Restated from tokens.

The baseline ratchet gained a third assertion: a migrated file may still lean
on Semantic's CSS classes without importing the library, which the import check
cannot see and which would collapse silently when Phase 4 deletes the
stylesheet.  There is one such bridge today and it is now declared.
2026-07-30 11:55:39 -06:00

790 lines
19 KiB
CSS

/*
* Styles for the token-driven component library.
*
* Rules here encode design decisions Mantine's variables cannot express on their
* own — chiefly the two that change *form* rather than color between themes:
* the dashed danger treatment and status-by-brightness. See ui-design.md.
*
* Nothing in this file hardcodes a color; every value comes from a token.
*/
/* ------------------------------------------------------------------ Icons */
.wrolpi-icon-spin {
animation: wrolpi-icon-spin 0.9s linear infinite;
}
@keyframes wrolpi-icon-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.wrolpi-icon-spin {
animation: none;
}
}
.wrolpi-icon-stack {
position: relative;
display: inline-flex;
line-height: 0;
}
/*
* The corner glyph sits on the main one, so it carries the surface color behind
* it — without that the two sets of strokes cross and neither reads.
*/
.wrolpi-icon-stack-corner {
position: absolute;
right: -3px;
bottom: -3px;
display: flex;
background: var(--bg);
border-radius: 50%;
padding: 1px;
}
/* ---------------------------------------------------------------- Buttons */
/*
* Dashed borders mean "destructive or failed", and nothing else may use them.
* In night mode a destructive button drops its fill entirely: 1.5px keeps the
* dashes legible at low brightness, where a 1px hairline dash disappears.
*/
/*
* In light, dark, and amber the button stays filled, which `color="red"` already
* does. Night is the exception, and it cannot be expressed by overriding
* Mantine's `--button-*` variables: Mantine writes those as inline styles on the
* element, which no stylesheet rule can outrank. Setting the real properties
* works instead — this selector (0,2,0 plus an element) outranks the Mantine
* class rule that reads the variables.
*/
html[data-theme="night"] .wrolpi-button-danger {
background: transparent;
color: var(--red);
border: 1.5px dashed var(--red);
}
html[data-theme="night"] .wrolpi-button-danger:hover:not(:disabled) {
background: var(--panel);
}
/* The icon inside inherits currentColor, so it follows without another rule. */
/* ----------------------------------------------------------- Theme picker */
.wrolpi-theme-picker {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 10px;
}
.wrolpi-theme-option {
display: flex;
flex-direction: column;
gap: 4px;
align-items: flex-start;
text-align: left;
padding: 10px 12px;
background: var(--panel);
color: var(--text);
border: 1px solid var(--border);
font: inherit;
cursor: pointer;
}
.wrolpi-theme-option:hover {
background: var(--head);
}
/* The selected theme is marked by a heavier border as well as color, so the
selection survives in monochrome themes. */
.wrolpi-theme-option-active {
border: 2px solid var(--blue);
padding: 9px 11px;
}
html[data-theme="night"] .wrolpi-theme-option-active,
html[data-theme="amber"] .wrolpi-theme-option-active {
border-color: var(--text);
}
.wrolpi-theme-option:focus-visible {
outline: 2px solid var(--blue);
outline-offset: 1px;
}
.wrolpi-theme-option-name {
display: inline-flex;
align-items: center;
gap: 7px;
font-weight: 600;
font-size: 13px;
}
.wrolpi-theme-option-description {
font-size: 12px;
color: var(--muted);
line-height: 1.4;
}
/* Separated from the theme grid: it configures the chosen theme, it is not a choice. */
.wrolpi-media-filter-setting {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
/* ------------------------------------------------------------ Pagination */
.wrolpi-pagination button {
background: var(--panel);
color: var(--text);
border: 1px solid var(--border);
}
.wrolpi-pagination button[data-active] {
background: var(--blue);
color: var(--btn-text);
border-color: var(--blue);
}
/* Night and amber have no second hue to spend, so the current page is marked by a
heavier border and the panel surface instead of a filled block of color. */
html[data-theme="night"] .wrolpi-pagination button[data-active],
html[data-theme="amber"] .wrolpi-pagination button[data-active] {
background: var(--panel);
color: var(--text);
border: 2px solid var(--text);
}
.wrolpi-pagination button:disabled {
color: var(--muted);
background: var(--bg);
}
/* ---------------------------------------------------------------- Tab bar */
.wrolpi-tabs {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
border-bottom: 1px solid var(--border);
margin-bottom: 14px;
}
.wrolpi-tabs-links {
display: flex;
flex-wrap: wrap;
}
.wrolpi-tabs-right {
padding-bottom: 6px;
}
.wrolpi-tab {
display: inline-block;
padding: 8px 14px;
color: var(--muted);
text-decoration: none;
font-size: 13px;
border: 1px solid transparent;
/* Sits on the bar's own rule, so the active tab can erase it and read as
connected to the panel below. */
margin-bottom: -1px;
}
.wrolpi-tab:hover {
color: var(--text);
}
.wrolpi-tab-active {
color: var(--text);
background: var(--panel);
border-color: var(--border);
border-bottom-color: var(--panel);
font-weight: 600;
}
.wrolpi-tab:focus-visible {
outline: 2px solid var(--blue);
outline-offset: -2px;
}
/* ------------------------------------------------------------- Search box */
.wrolpi-searchbox {
position: relative;
width: 100%;
}
.wrolpi-searchbox-control {
display: flex;
align-items: center;
gap: 8px;
background: var(--panel);
border: 1px solid var(--border);
padding: 0 8px;
}
.wrolpi-searchbox-control:focus-within {
border-color: var(--blue);
}
html[data-theme="night"] .wrolpi-searchbox-control:focus-within,
html[data-theme="amber"] .wrolpi-searchbox-control:focus-within {
border-color: var(--text);
}
.wrolpi-searchbox-icon {
display: flex;
color: var(--muted);
}
.wrolpi-searchbox-input {
flex: 1;
min-width: 0;
padding: 8px 0;
background: none;
border: none;
outline: none;
color: var(--text);
font: inherit;
font-size: 13px;
}
.wrolpi-searchbox-input::placeholder {
color: var(--muted);
}
.wrolpi-searchbox-loading,
.wrolpi-searchbox-clear {
flex: none;
}
/*
* The suggestion list overlays the page rather than pushing it down — a search
* that reflows the results underneath it is unusable while typing.
*/
.wrolpi-searchbox-results {
position: absolute;
z-index: 20;
top: 100%;
left: 0;
right: 0;
max-height: 320px;
overflow-y: auto;
background: var(--panel);
border: 1px solid var(--border);
border-top: none;
}
.wrolpi-searchbox-group-name {
padding: 6px 10px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
background: var(--head);
}
.wrolpi-searchbox-result {
padding: 7px 10px;
cursor: pointer;
border-top: 1px solid var(--border);
}
.wrolpi-searchbox-result-active {
background: var(--head);
}
.wrolpi-searchbox-result-title {
font-size: 13px;
color: var(--text);
overflow-wrap: anywhere;
}
.wrolpi-searchbox-result-description {
font-size: 11px;
color: var(--muted);
}
.wrolpi-searchbox-empty {
padding: 10px;
font-size: 12px;
color: var(--muted);
}
/* ---------------------------------------------------------------- Modals */
/*
* Actions sit below the body, separated by a hairline rather than a raised bar.
* Right-aligned, so the confirming action lands where the eye finishes reading.
*/
.wrolpi-modal-actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 10px;
/* Bleed back out through the modal body's own padding so the rule spans the
full width; -1 * whatever Mantine padded the body with. */
margin: 18px calc(var(--mantine-spacing-md) * -1) calc(var(--mantine-spacing-md) * -1);
padding: 12px var(--mantine-spacing-md);
border-top: 1px solid var(--border);
}
/* ---------------------------------------------------------- Action inputs */
/*
* Input and button share one outline: the input gives up its right border and
* the button's left border becomes the seam between them.
*/
.wrolpi-action-input {
display: flex;
align-items: stretch;
width: 100%;
}
.wrolpi-action-input > .mantine-TextInput-root {
flex: 1;
min-width: 0;
}
.wrolpi-action-input .mantine-TextInput-input {
border-right-width: 0;
}
.wrolpi-action-input > :not(.mantine-TextInput-root) {
flex: none;
}
/* --------------------------------------------------------------- Switches */
/*
* Mantine's unchecked track is a gray from its own palette, which would put a
* non-red pixel on screen in night mode. Scoped to `:not(:checked)` so the
* checked track keeps coming from the `color` prop.
*/
/*
* Semantic's inverted stylesheet colors *every* descendant of `.ui.inverted.segment`
* (rgba(255,255,255,.9), and #dddddd on labels). Our components are mounted inside
* unmigrated pages that still use those wrappers, so an inherited color puts white
* pixels on screen in night mode. Restating from tokens is the fix; the rule can go
* when the last Semantic wrapper does.
*/
.wrolpi-switch .mantine-Switch-label {
color: var(--text);
}
.wrolpi-switch .mantine-Switch-description {
color: var(--muted);
}
.wrolpi-switch input:disabled + * .mantine-Switch-label,
.wrolpi-switch input:disabled ~ * .mantine-Switch-label {
color: var(--muted);
}
.wrolpi-switch input:not(:checked) + .mantine-Switch-track {
--switch-bg: var(--border);
}
/*
* The disabled thumb has the same problem: Mantine sets it from its gray palette,
* on the thumb element itself, which is a near-white pixel in night mode.
*/
.wrolpi-switch input:disabled + * > .mantine-Switch-thumb,
.wrolpi-switch input[data-disabled] + * > .mantine-Switch-thumb {
--switch-thumb-bg-disabled: var(--muted);
}
/* ----------------------------------------------------------------- Labels */
.wrolpi-label {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 9px;
font-size: 11.5px;
font-weight: 500;
line-height: 1.3;
background: var(--label-color);
color: var(--btn-text);
border: 1px solid transparent;
}
/* White and black labels would vanish into the surface they sit on. */
.wrolpi-label-white {
color: var(--black);
border-color: var(--border);
}
.wrolpi-label-black {
color: var(--white);
border-color: var(--border);
}
/*
* Night mode: a filled label is a bright patch, and a wall of tags would be a
* wall of light. Every label becomes an outline instead.
*/
html[data-theme="night"] .wrolpi-label {
background: transparent;
color: var(--label-color);
border-color: var(--label-color);
}
/* ---------------------------------------------------------------- Headers */
.wrolpi-header {
margin: 0 0 10px;
}
.wrolpi-header-dividing {
border-bottom: 1px solid var(--border);
padding-bottom: 6px;
}
.wrolpi-header-text {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
/*
* Stated explicitly because semantic.min.css sets `h1,h2,h3,h4,h5 { font-family:
* Lato }` and loads after Mantine's element rule, so headings would ignore the
* theme's body face — visible in amber, where the body is mono and headings were
* not. A class outranks the element selector. Can go once Semantic is gone.
*/
font-family: var(--font-body);
font-weight: 600;
line-height: 1.25;
color: var(--text);
text-wrap: balance;
}
/* The scale lives here so no call site invents a size of its own. */
.wrolpi-header-h1 {
font-size: 26px;
}
.wrolpi-header-h2 {
font-size: 21px;
}
.wrolpi-header-h3 {
font-size: 17px;
}
.wrolpi-header-h4 {
font-size: 14px;
}
.wrolpi-header-h5 {
font-size: 13px;
letter-spacing: 0.02em;
}
.wrolpi-header-sub {
font-size: 12px;
color: var(--muted);
margin-top: 3px;
}
/* --------------------------------------------------------------- Messages */
.wrolpi-message {
display: flex;
align-items: flex-start;
gap: 10px;
border: 1px solid var(--border);
border-left: 3px solid var(--message-color, var(--border));
background: var(--panel);
padding: 10px 14px;
font-size: 13px;
}
.wrolpi-message-content {
flex: 1;
min-width: 0;
}
/* The icon repeats what the border color already says, so it takes that color. */
.wrolpi-message-icon {
color: var(--message-color, var(--text));
display: flex;
padding-top: 1px;
}
.wrolpi-message-dismiss {
background: none;
border: none;
padding: 0;
color: var(--muted);
cursor: pointer;
display: flex;
/* Widen the tap target past the glyph without moving it. */
margin: -6px -6px -6px 0;
padding: 6px;
}
.wrolpi-message-dismiss:hover {
color: var(--text);
}
.wrolpi-message-dismiss:focus-visible {
outline: 2px solid var(--blue);
outline-offset: -2px;
}
.wrolpi-message-title {
font-weight: 600;
margin-bottom: 2px;
}
.wrolpi-message-body {
color: var(--muted);
}
/* Errors take the danger treatment in every theme, so "dashed" stays one rule. */
.wrolpi-message-error {
border: 1.5px dashed var(--red);
}
/* --------------------------------------------------------------- Progress */
.wrolpi-progress {
position: relative;
height: 16px;
min-width: 120px;
background: var(--bg);
border: 1px solid var(--border);
}
.wrolpi-progress-fill {
height: 100%;
background: var(--progress-color, var(--blue));
transition: width 0.2s linear;
}
@media (prefers-reduced-motion: reduce) {
.wrolpi-progress-fill {
transition: none;
}
}
.wrolpi-progress-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
color: var(--text);
font-variant-numeric: tabular-nums;
}
/*
* The percent text sits on both the filled and empty halves of the bar, so the
* fill has to stay light enough in light mode for dark text to read over it.
*/
html[data-theme="light"] .wrolpi-progress {
background: var(--panel);
}
html[data-theme="light"] .wrolpi-progress-fill {
filter: brightness(1.75) saturate(0.55);
}
/* Night: a mostly-full bar must not become a large bright surface. */
html[data-theme="night"] .wrolpi-progress-fill,
html[data-theme="amber"] .wrolpi-progress-fill {
background: var(--muted);
}
/* ----------------------------------------------------------------- Panels */
.wrolpi-panel {
background: var(--panel);
border: 1px solid var(--border);
padding: 14px 16px;
}
/* Danger zones are dashed in every theme; it doubles as a colorblind-safe cue. */
.wrolpi-panel-danger {
border: 1.5px dashed var(--red);
}
/* ----------------------------------------------------------- Card posters */
/*
* The icon shown when a file has no poster, and the corner mark for a tagged
* file. Both live here rather than in Common.js so the card surface and the
* poster area agree in every theme.
*/
.wrolpi-card-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 120px;
background: var(--head);
}
.wrolpi-card-tag {
position: absolute;
top: 0;
left: 0;
display: flex;
padding: 3px 4px;
background: var(--green);
color: var(--btn-text);
}
/* Night and amber have no green to spend; the mark reads as an outline instead. */
html[data-theme="night"] .wrolpi-card-tag,
html[data-theme="amber"] .wrolpi-card-tag {
background: var(--panel);
color: var(--text);
border-right: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
/* ------------------------------------------------------------- Statistics */
/*
* A statistic carries its own spacing rather than relying on a parent to supply
* it: unmigrated pages still drop these inside Semantic's `Statistic.Group`,
* whose CSS only spaces elements carrying Semantic's own classes, and without
* this the labels run together into one unreadable line.
*/
.wrolpi-statistic {
display: inline-block;
vertical-align: top;
margin: 0 26px 10px 0;
}
/* Inside our own group the grid does the spacing, so the intrinsic margin goes. */
.wrolpi-statistic-cell .wrolpi-statistic {
display: block;
margin: 0;
}
.wrolpi-statistic-value {
font-size: 24px;
font-weight: 600;
font-variant-numeric: tabular-nums;
line-height: 1.15;
}
.wrolpi-statistic-label {
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
margin-top: 2px;
}
/* ---------------------------------------------------------------- Status */
/*
* Status is encoded by hue in light/dark. In night mode hue is unavailable, so
* brightness carries it instead: dim = done, mid = active, brightest + bold =
* failed. Callers use these classes rather than naming a color.
*/
.wrolpi-status {
font-size: 12.5px;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 5px;
}
.wrolpi-status-complete {
color: var(--green);
}
.wrolpi-status-active {
color: var(--blue);
}
.wrolpi-status-pending {
color: var(--muted);
}
.wrolpi-status-failed {
color: var(--red);
}
html[data-theme="night"] .wrolpi-status-complete {
color: var(--muted);
}
html[data-theme="night"] .wrolpi-status-active {
color: var(--text);
}
html[data-theme="night"] .wrolpi-status-failed {
color: var(--red);
font-weight: 500;
}
/* ----------------------------------------------------------------- Tables */
/* A sortable header: a real button, so the column is reachable by keyboard. */
.wrolpi-th-sort {
display: inline-flex;
align-items: center;
gap: 5px;
background: none;
border: none;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
cursor: pointer;
white-space: nowrap;
}
/* The unsorted columns show a faint arrow: the affordance is visible, but it does
not compete with the column actually doing the sorting. */
.wrolpi-th-sort-idle {
opacity: 0.35;
}
.wrolpi-th-sort:hover .wrolpi-th-sort-idle {
opacity: 0.7;
}
.wrolpi-th-sort:focus-visible {
outline: 2px solid var(--blue);
outline-offset: 2px;
}
html[data-theme="night"] .wrolpi-th-sort:focus-visible,
html[data-theme="amber"] .wrolpi-th-sort:focus-visible {
outline-color: var(--text);
}
/* A failed row carries the danger border, matching failed messages. */
.wrolpi-row-failed > td {
border-top: 1.5px dashed var(--red);
border-bottom: 1.5px dashed var(--red);
}
.wrolpi-row-failed > td:first-child {
border-left: 1.5px dashed var(--red);
}
.wrolpi-row-failed > td:last-child {
border-right: 1.5px dashed var(--red);
}
/* Wide tables scroll inside their own container; the page never scrolls sideways. */
.wrolpi-table-scroll {
overflow-x: auto;
}