mirror of
https://github.com/webmin/webmin.git
synced 2026-09-11 08:00:39 +01:00
ⓘ Add composable widgets, shared styling and interactions, sortable-table options, and a read-only UI gallery.
993 lines
27 KiB
CSS
993 lines
27 KiB
CSS
/*
|
|
* ui-lib.css
|
|
* Stylesheet for the widgets added to ui-lib.pl: page header, grids,
|
|
* cards, stat tiles, description lists, badges, chips, lists, activity
|
|
* feeds, empty states, progress bars, toggles, search boxes and the
|
|
* boxed choice, radio list and select switch widgets.
|
|
*
|
|
* Widgets use their own classes, with shared spacing and margin resets
|
|
* inside .ui_page. Existing tabs, forms, tables and buttons retain their
|
|
* theme styling. Widgets use the theme's --font-family and
|
|
* --font-family-mono when defined, otherwise text inherits the page font
|
|
* and code uses the monospace fallback below.
|
|
*
|
|
* The .ui_page wrapper emitted by ui_page_start supplies the widget tokens.
|
|
* The palette and shared spacing come from --ui-* custom properties,
|
|
* which a theme may redefine on .ui_page to restyle every
|
|
* widget at once. Setting data-ui-scheme="dark" (or "auto") on any
|
|
* ancestor switches to the built-in dark palette.
|
|
*/
|
|
|
|
.ui_page {
|
|
/* Palette tokens read the theme's own custom properties first - the
|
|
* names Authentic defines for its palettes and night mode - and
|
|
* fall back to the colors of the gray theme, which defines only the
|
|
* four --text-color-* state colors itself. */
|
|
|
|
/* Core palette */
|
|
--ui-canvas: var(--bg-color, #f2f3f5);
|
|
--ui-surface: var(--main-content, #ffffff);
|
|
--ui-surface-2: var(--bg-table-header, #f5f5f5);
|
|
--ui-border: var(--border-color, #d7dbe0);
|
|
--ui-border-soft: var(--border-color-light, #e8eaee);
|
|
--ui-border-strong: var(--border-color-darker, #b3bac2);
|
|
--ui-fg: var(--text-color, #212121);
|
|
--ui-fg-muted: var(--text-color-muted, #4b5563);
|
|
|
|
/* Accent */
|
|
--ui-accent: var(--link-color, #0b46ab);
|
|
--ui-accent-soft: var(--element-highlight-bg-color, #e8f1fb);
|
|
--ui-ring: var(--border-color-focused, rgba(11, 70, 171, 0.25));
|
|
|
|
/* States. The base color is used for icons, dots and bars; the
|
|
* -text variant is a darker shade for text sitting on the -soft
|
|
* background, as in badges. */
|
|
--ui-success: var(--text-color-success, #3c763d);
|
|
--ui-success-text: var(--text-color-success-stressed, #1e4f18);
|
|
--ui-success-soft: var(--bg-color-success, #edf7e7);
|
|
--ui-success-line: var(--border-color-success, #b9e0a4);
|
|
--ui-warning: var(--text-color-warning, #b58900);
|
|
--ui-warning-text: var(--text-color-warning-stressed, #795600);
|
|
--ui-warning-soft: var(--bg-color-warning, #fdf4d8);
|
|
--ui-warning-line: var(--border-color-warning, #eed483);
|
|
--ui-danger: var(--text-color-danger, #bc0303);
|
|
--ui-danger-text: var(--text-color-danger-stressed, #7d1007);
|
|
--ui-danger-soft: var(--bg-color-danger, #fbeae8);
|
|
--ui-danger-line: var(--border-color-danger, #f2bcb6);
|
|
--ui-info: var(--text-color-info, #108eda);
|
|
--ui-info-text: var(--text-color-info-stressed, #00477e);
|
|
--ui-info-soft: var(--bg-color-info, #e8f1fb);
|
|
--ui-info-line: var(--border-color-info, #b8d6f2);
|
|
--ui-neutral: var(--text-color-muted, #4b5563);
|
|
--ui-neutral-text: var(--text-color-neutral-stressed, #40474f);
|
|
--ui-neutral-soft: var(--bg-color-neutral, #eff1f3);
|
|
--ui-neutral-line: var(--border-color-neutral, #d7dbe0);
|
|
|
|
/* Controls */
|
|
--ui-toggle-bg: var(--toggle-bg-color, var(--ui-border-strong));
|
|
--ui-toggle-bg-checked: var(--toggle-bg-color-checked, var(--ui-accent));
|
|
--ui-toggle-thumb: var(--toggle-thumb-color, #ffffff);
|
|
|
|
/* Shape and rhythm. Boxed widgets have square corners; dots,
|
|
* activity markers, ring gauges and toggles use circles or rounded tracks. */
|
|
--ui-gap: 7px;
|
|
/* Shared inset for boxed choice widgets: padding, option spacing,
|
|
* vertical field gaps and gaps between labels and inputs */
|
|
--ui-inset: 5px;
|
|
/* Where the label text starts after a theme radio, so descriptions
|
|
* and fields line up under it */
|
|
--ui-radio-indent: 1.6em;
|
|
|
|
/* Fonts. A theme that defines --font-family and --font-family-mono
|
|
* (Authentic does) sets the widgets' text and code fonts with them;
|
|
* otherwise text inherits the page font and code uses the system
|
|
* monospace stack. */
|
|
--ui-font: var(--font-family, inherit);
|
|
--ui-font-mono: var(--font-family-mono, ui-monospace, SFMono-Regular,
|
|
"SF Mono", Menlo, Consolas, "Liberation Mono", monospace);
|
|
|
|
color-scheme: light;
|
|
color: var(--ui-fg);
|
|
font-family: var(--ui-font);
|
|
text-align: left;
|
|
}
|
|
|
|
/* Built-in dark palette, enabled with data-ui-scheme="dark" on any
|
|
* ancestor element (or on the .ui_page wrapper itself, via the scheme
|
|
* option of ui_page_start). With data-ui-scheme="auto" the dark
|
|
* palette follows the browser's prefers-color-scheme setting instead.
|
|
* The two blocks below must be kept identical. */
|
|
[data-ui-scheme="dark"] .ui_page,
|
|
.ui_page[data-ui-scheme="dark"] {
|
|
--ui-canvas: #15171b;
|
|
--ui-surface: #1e2126;
|
|
--ui-surface-2: #23262c;
|
|
--ui-border: #363b43;
|
|
--ui-border-soft: #2a2f36;
|
|
--ui-border-strong: #4d545e;
|
|
--ui-fg: #e6e8ea;
|
|
--ui-fg-muted: #99a3ae;
|
|
--ui-accent: #5aa2ec;
|
|
--ui-accent-soft: rgba(90, 162, 236, 0.14);
|
|
--ui-ring: rgba(90, 162, 236, 0.35);
|
|
--ui-success: #7ec563;
|
|
--ui-success-text: #8fd376;
|
|
--ui-success-soft: rgba(126, 197, 99, 0.13);
|
|
--ui-success-line: rgba(126, 197, 99, 0.4);
|
|
--ui-warning: #e0a63c;
|
|
--ui-warning-text: #eab54f;
|
|
--ui-warning-soft: rgba(224, 166, 60, 0.13);
|
|
--ui-warning-line: rgba(224, 166, 60, 0.4);
|
|
--ui-danger: #ef7b6f;
|
|
--ui-danger-text: #f28c82;
|
|
--ui-danger-soft: rgba(239, 123, 111, 0.13);
|
|
--ui-danger-line: rgba(239, 123, 111, 0.4);
|
|
--ui-info: #5aa2ec;
|
|
--ui-info-text: #78b3ef;
|
|
--ui-info-soft: rgba(90, 162, 236, 0.13);
|
|
--ui-info-line: rgba(90, 162, 236, 0.4);
|
|
--ui-neutral: #aeb6bf;
|
|
--ui-neutral-text: #b6bec7;
|
|
--ui-neutral-soft: #272b31;
|
|
--ui-neutral-line: #3c424b;
|
|
color-scheme: dark;
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
[data-ui-scheme="auto"] .ui_page,
|
|
.ui_page[data-ui-scheme="auto"] {
|
|
--ui-canvas: #15171b;
|
|
--ui-surface: #1e2126;
|
|
--ui-surface-2: #23262c;
|
|
--ui-border: #363b43;
|
|
--ui-border-soft: #2a2f36;
|
|
--ui-border-strong: #4d545e;
|
|
--ui-fg: #e6e8ea;
|
|
--ui-fg-muted: #99a3ae;
|
|
--ui-accent: #5aa2ec;
|
|
--ui-accent-soft: rgba(90, 162, 236, 0.14);
|
|
--ui-ring: rgba(90, 162, 236, 0.35);
|
|
--ui-success: #7ec563;
|
|
--ui-success-text: #8fd376;
|
|
--ui-success-soft: rgba(126, 197, 99, 0.13);
|
|
--ui-success-line: rgba(126, 197, 99, 0.4);
|
|
--ui-warning: #e0a63c;
|
|
--ui-warning-text: #eab54f;
|
|
--ui-warning-soft: rgba(224, 166, 60, 0.13);
|
|
--ui-warning-line: rgba(224, 166, 60, 0.4);
|
|
--ui-danger: #ef7b6f;
|
|
--ui-danger-text: #f28c82;
|
|
--ui-danger-soft: rgba(239, 123, 111, 0.13);
|
|
--ui-danger-line: rgba(239, 123, 111, 0.4);
|
|
--ui-info: #5aa2ec;
|
|
--ui-info-text: #78b3ef;
|
|
--ui-info-soft: rgba(90, 162, 236, 0.13);
|
|
--ui-info-line: rgba(90, 162, 236, 0.4);
|
|
--ui-neutral: #aeb6bf;
|
|
--ui-neutral-text: #b6bec7;
|
|
--ui-neutral-soft: #272b31;
|
|
--ui-neutral-line: #3c424b;
|
|
color-scheme: dark;
|
|
}
|
|
body[data-ui-scheme="auto"]:has(.ui_page),
|
|
[data-ui-scheme="auto"] body:has(.ui_page),
|
|
body:has(.ui_page[data-ui-scheme="auto"]) {
|
|
background: var(--ui-canvas, #15171b);
|
|
}
|
|
}
|
|
|
|
/* Paint the page canvas behind widget pages. Themes can override the
|
|
* color by defining --ui-canvas on :root or body. */
|
|
body:has(.ui_page) {
|
|
background: var(--ui-canvas, var(--bg-color, #f2f3f5));
|
|
}
|
|
body[data-ui-scheme="dark"]:has(.ui_page),
|
|
[data-ui-scheme="dark"] body:has(.ui_page),
|
|
body:has(.ui_page[data-ui-scheme="dark"]) {
|
|
background: var(--ui-canvas, #15171b);
|
|
}
|
|
|
|
/* ---- Spacing between blocks ----
|
|
* Top-level blocks are spaced by the page area. Inside any other wrapper
|
|
* (a tab panel, a table cell) a widget that follows a sibling, and
|
|
* whatever follows a widget, get the same gap, so rows of cards and the
|
|
* forms between them line up whatever the theme wraps them in.
|
|
* Children of grids, stacks, clusters and stat rows use their own gaps. */
|
|
|
|
.ui_page > * + * { margin-top: var(--ui-gap); }
|
|
.ui_page :not(.ui_grid, .ui_stack, .ui_cluster, .ui_stats) >
|
|
* + :is(.ui_grid, .ui_stack, .ui_card, .ui_empty),
|
|
.ui_page :not(.ui_grid, .ui_stack, .ui_cluster, .ui_stats) >
|
|
:is(.ui_grid, .ui_stack, .ui_card, .ui_empty) + * {
|
|
margin-top: var(--ui-gap);
|
|
}
|
|
.ui_page [hidden] { display: none !important; }
|
|
|
|
/* Widgets size their boxes including padding and borders */
|
|
.ui_page_head, .ui_grid, .ui_stack, .ui_cluster,
|
|
.ui_card, .ui_card *, .ui_stats, .ui_stat, .ui_stat *,
|
|
.ui_dl, .ui_dl *, .ui_badge, .ui_chip, .ui_list, .ui_list *,
|
|
.ui_feed, .ui_feed *, .ui_empty, .ui_empty *, .ui_progress,
|
|
.ui_progress *, .ui_toggle, .ui_toggle *,
|
|
.ui_search, .ui_search * {
|
|
box-sizing: border-box;
|
|
}
|
|
.ui_page h1, .ui_page h2, .ui_page p, .ui_page dl, .ui_page dd { margin: 0; }
|
|
.ui_page svg.ui_svg_icon { flex-shrink: 0; vertical-align: -0.18em; }
|
|
|
|
/* Plain links inside widget text are underlined so they still read as
|
|
* links next to chips and badges. Theme buttons made of ui_link are not. */
|
|
.ui_dl_row dd a:not(.ui_link), .ui_feed_text a:not(.ui_link),
|
|
.ui_list_desc a:not(.ui_link), .ui_empty_desc a:not(.ui_link),
|
|
.ui_card_desc a:not(.ui_link) {
|
|
text-decoration: underline;
|
|
text-underline-offset: 0.15em;
|
|
}
|
|
|
|
/* ---- Utilities ---- */
|
|
|
|
.ui_push { margin-left: auto; }
|
|
.ui_code {
|
|
font-family: var(--ui-font-mono);
|
|
font-size: 0.92em;
|
|
background: var(--ui-neutral-soft);
|
|
border: 1px solid var(--ui-border-soft);
|
|
padding: 1px 5px;
|
|
}
|
|
.ui_code_block {
|
|
font-family: var(--ui-font-mono);
|
|
font-size: 0.92em;
|
|
line-height: 1.5;
|
|
background: var(--ui-surface-2);
|
|
border: 1px solid var(--ui-border-soft);
|
|
padding: 10px 12px;
|
|
margin: 0;
|
|
overflow: auto;
|
|
white-space: pre;
|
|
color: var(--ui-fg);
|
|
}
|
|
.ui_fg_success { color: var(--ui-success); }
|
|
.ui_fg_warning { color: var(--ui-warning); }
|
|
.ui_fg_danger { color: var(--ui-danger); }
|
|
.ui_fg_info { color: var(--ui-info); }
|
|
.ui_fg_neutral { color: var(--ui-neutral); }
|
|
.ui_bg_success { background: var(--ui-success); }
|
|
.ui_bg_warning { background: var(--ui-warning); }
|
|
.ui_bg_danger { background: var(--ui-danger); }
|
|
.ui_bg_info { background: var(--ui-info); }
|
|
.ui_bg_neutral { background: var(--ui-neutral); }
|
|
|
|
/* ---- Page header ---- */
|
|
|
|
.ui_page_head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: 10px 24px;
|
|
flex-wrap: wrap;
|
|
margin: 4px 0 2px;
|
|
}
|
|
.ui_page_title {
|
|
font-size: 1.85em;
|
|
font-weight: 650;
|
|
letter-spacing: -0.015em;
|
|
line-height: 1.2;
|
|
}
|
|
.ui_page_titles {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
.ui_page_desc {
|
|
color: var(--ui-fg-muted);
|
|
margin-top: 4px;
|
|
}
|
|
.ui_page_actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding-top: 7px;
|
|
}
|
|
.ui_page_help {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* ---- Layout primitives ---- */
|
|
|
|
.ui_grid {
|
|
display: grid;
|
|
gap: var(--ui-gap);
|
|
grid-template-columns: repeat(auto-fit,
|
|
minmax(min(var(--ui-grid-min, 340px), 100%), 1fr));
|
|
align-items: stretch;
|
|
}
|
|
.ui_grid_fixed {
|
|
grid-template-columns: var(--ui-grid-template,
|
|
repeat(var(--ui-grid-cols, 2), minmax(0, 1fr)));
|
|
}
|
|
.ui_stack {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--ui-gap);
|
|
}
|
|
.ui_cluster {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: calc(var(--ui-gap) / 2) var(--ui-gap);
|
|
}
|
|
.ui_cluster { --ui-gap: 10px; }
|
|
|
|
/* Themes that regroup adjacent submit buttons into one box, as Authentic
|
|
* does with its btn-group, wrap them inside that box with no vertical
|
|
* space. Give those rows a gap while keeping the buttons joined. */
|
|
.ui_cluster > .btn-group {
|
|
display: inline-flex;
|
|
flex-wrap: wrap;
|
|
row-gap: 7px;
|
|
}
|
|
.ui_cluster_center { justify-content: center; }
|
|
.ui_cluster_end { justify-content: flex-end; }
|
|
.ui_cluster_between { justify-content: space-between; }
|
|
|
|
@media (max-width: 760px) {
|
|
.ui_grid_fixed { grid-template-columns: 1fr; }
|
|
}
|
|
|
|
/* ---- Cards ---- */
|
|
|
|
.ui_card {
|
|
background: var(--ui-surface);
|
|
border: 1px solid var(--ui-border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
}
|
|
.ui_card_success { border-left: 3px solid var(--ui-success); }
|
|
.ui_card_warning { border-left: 3px solid var(--ui-warning); }
|
|
.ui_card_danger { border-left: 3px solid var(--ui-danger); }
|
|
.ui_card_info { border-left: 3px solid var(--ui-info); }
|
|
.ui_card_head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: 4px 12px;
|
|
padding: 5px 6px 0 7px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.ui_card_title {
|
|
font-size: 1.07697em;
|
|
font-weight: 650;
|
|
line-height: 1.3;
|
|
}
|
|
.ui_card_desc {
|
|
color: var(--ui-fg-muted);
|
|
font-size: 0.93em;
|
|
margin-top: 3px;
|
|
}
|
|
.ui_card_actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
.ui_card_body {
|
|
padding: 3px 7px 7px;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.ui_card_body.ui_card_flush { padding: 1px 0 0; }
|
|
.ui_card_head + .ui_card_body.ui_card_flush { padding-top: 3px; }
|
|
.ui_card_foot {
|
|
border-top: 1px solid var(--ui-border-soft);
|
|
padding: 5px;
|
|
color: var(--ui-fg-muted);
|
|
font-size: 0.93em;
|
|
}
|
|
|
|
/* ---- Stat tiles ---- */
|
|
|
|
.ui_stats {
|
|
display: grid;
|
|
gap: 12px 24px;
|
|
grid-template-columns: repeat(auto-fit,
|
|
minmax(min(var(--ui-grid-min, 150px), 100%), 1fr));
|
|
}
|
|
.ui_stat {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1px;
|
|
min-width: 0;
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
.ui_stat_value {
|
|
font-size: 1.85em;
|
|
font-weight: 500;
|
|
line-height: 1.2;
|
|
letter-spacing: -0.01em;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.ui_stat_label { font-weight: 600; }
|
|
.ui_stat_desc { color: var(--ui-fg-muted); font-size: 0.9em; }
|
|
a.ui_stat, a.ui_stat:hover {
|
|
color: inherit !important;
|
|
text-decoration: none !important;
|
|
}
|
|
a.ui_stat:hover .ui_stat_value { color: var(--ui-accent); }
|
|
|
|
/* ---- Description lists ---- */
|
|
|
|
/* The list is one grid and every row contributes its dt and dd to it, so
|
|
* all labels share a single column sized to the longest label, up to the
|
|
* fraction given by --ui-dl-label. Labels only wrap past that limit. */
|
|
.ui_dl {
|
|
display: grid;
|
|
grid-template-columns: var(--ui-dl-label, fit-content(45%)) 1fr;
|
|
gap: 10px 20px;
|
|
align-items: baseline;
|
|
}
|
|
.ui_dl_cols {
|
|
grid-template-columns:
|
|
repeat(2, var(--ui-dl-label, fit-content(45%)) 1fr);
|
|
column-gap: 24px;
|
|
}
|
|
.ui_dl_row { display: contents; }
|
|
.ui_dl_row dt { font-weight: 600; }
|
|
.ui_dl_row dd {
|
|
min-width: 0;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
@media (max-width: 560px) {
|
|
.ui_dl, .ui_dl_cols { grid-template-columns: 1fr; }
|
|
.ui_dl { row-gap: 2px; }
|
|
.ui_dl_row dd { margin-bottom: 8px; }
|
|
}
|
|
|
|
/* ---- Badges and chips ---- */
|
|
|
|
.ui_badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 2px 10px;
|
|
border: 1px solid transparent;
|
|
font-size: 0.9em;
|
|
font-weight: 600;
|
|
line-height: 1.5;
|
|
vertical-align: middle;
|
|
white-space: nowrap;
|
|
}
|
|
.ui_badge_success {
|
|
background: var(--ui-success-soft);
|
|
border-color: var(--ui-success-line);
|
|
color: var(--ui-success-text);
|
|
}
|
|
.ui_badge_warning {
|
|
background: var(--ui-warning-soft);
|
|
border-color: var(--ui-warning-line);
|
|
color: var(--ui-warning-text);
|
|
}
|
|
.ui_badge_danger {
|
|
background: var(--ui-danger-soft);
|
|
border-color: var(--ui-danger-line);
|
|
color: var(--ui-danger-text);
|
|
}
|
|
.ui_badge_info {
|
|
background: var(--ui-info-soft);
|
|
border-color: var(--ui-info-line);
|
|
color: var(--ui-info-text);
|
|
}
|
|
.ui_badge_neutral {
|
|
background: var(--ui-neutral-soft);
|
|
border-color: var(--ui-neutral-line);
|
|
color: var(--ui-neutral-text);
|
|
}
|
|
.ui_chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 1px 8px;
|
|
background: var(--ui-neutral-soft);
|
|
border: 1px solid var(--ui-border);
|
|
color: var(--ui-neutral-text);
|
|
font-size: 0.86em;
|
|
line-height: 1.5;
|
|
vertical-align: middle;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ---- Lists ---- */
|
|
|
|
.ui_list { display: flex; flex-direction: column; }
|
|
.ui_list_item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding: 5px 0;
|
|
border-bottom: 1px solid var(--ui-border-soft);
|
|
}
|
|
.ui_list_item:last-child { border-bottom: 0; }
|
|
.ui_list_flush .ui_list_item { padding: 5px 6px 5px 7px; }
|
|
.ui_list_main { flex: 1; min-width: 0; }
|
|
.ui_list_title {
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.ui_list_desc {
|
|
color: var(--ui-fg-muted);
|
|
font-size: 0.93em;
|
|
margin-top: 2px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.ui_list_side {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-shrink: 0;
|
|
padding-top: 1px;
|
|
}
|
|
.ui_list_meta { color: var(--ui-fg-muted); font-size: 0.9em; }
|
|
/* Badges in list rows are chip-sized, so they do not outweigh the text */
|
|
.ui_list_item .ui_badge {
|
|
padding: 1px 8px;
|
|
gap: 4px;
|
|
font-size: 0.86em;
|
|
}
|
|
.ui_list_item .ui_badge .ui_svg_icon { width: 1em; height: 1em; }
|
|
/* Description text is 0.93em, so badges and chips inside it are scaled
|
|
* back by that much to match the ones next to the title */
|
|
.ui_list_desc :is(.ui_badge, .ui_chip) { font-size: calc(0.86em / 0.93); }
|
|
.ui_list_icon { padding-top: 2px; color: var(--ui-fg-muted); }
|
|
a.ui_list_link, a.ui_list_link:hover {
|
|
color: inherit !important;
|
|
text-decoration: none !important;
|
|
}
|
|
a.ui_list_link:hover { color: var(--ui-accent) !important; }
|
|
|
|
/* ---- Activity feed ---- */
|
|
|
|
.ui_feed { display: flex; flex-direction: column; }
|
|
.ui_feed_item {
|
|
position: relative;
|
|
padding: 0 0 16px 20px;
|
|
border-left: 2px solid var(--ui-border-soft);
|
|
margin-left: 5px;
|
|
}
|
|
.ui_feed_item:last-child { padding-bottom: 2px; }
|
|
.ui_feed_item::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: -6px;
|
|
top: 4px;
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 999px;
|
|
background: var(--ui-border-strong);
|
|
border: 2px solid var(--ui-surface);
|
|
}
|
|
.ui_feed_success::before { background: var(--ui-success); }
|
|
.ui_feed_warning::before { background: var(--ui-warning); }
|
|
.ui_feed_danger::before { background: var(--ui-danger); }
|
|
.ui_feed_info::before { background: var(--ui-info); }
|
|
.ui_feed_when { font-weight: 600; font-size: 1em; }
|
|
.ui_feed_text { color: var(--ui-fg-muted); font-size: 0.93em; margin-top: 1px; }
|
|
.ui_feed_flush { padding: 3px 19px 7px; }
|
|
|
|
/* ---- Empty states ---- */
|
|
|
|
.ui_empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 2px;
|
|
padding: 1px 7px 7px;
|
|
text-align: center;
|
|
}
|
|
.ui_empty_icon { color: var(--ui-border-strong); margin-bottom: 2px; }
|
|
.ui_empty_title { font-weight: 600; font-size: 1.04em; }
|
|
.ui_empty_desc {
|
|
color: var(--ui-fg-muted);
|
|
font-size: 0.93em;
|
|
max-width: 46ch;
|
|
}
|
|
.ui_empty_actions { margin-top: 6px; }
|
|
|
|
/* ---- Progress ---- */
|
|
|
|
.ui_progress_head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
font-size: 0.93em;
|
|
font-weight: 600;
|
|
margin-bottom: 6px;
|
|
}
|
|
.ui_progress_value { color: var(--ui-fg-muted); font-weight: 400; }
|
|
.ui_progress_track {
|
|
display: flex;
|
|
height: 4px;
|
|
background: var(--ui-neutral-soft);
|
|
overflow: hidden;
|
|
}
|
|
.ui_progress_bar {
|
|
height: 100%;
|
|
flex-shrink: 0;
|
|
transition: width 0.4s ease;
|
|
}
|
|
|
|
/* Inline layout : label, bar and value on one line */
|
|
.ui_progress_inline {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 0.93em;
|
|
}
|
|
.ui_progress_inline .ui_progress_label {
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
}
|
|
.ui_progress_inline .ui_progress_track { flex: 1 1 auto; }
|
|
.ui_progress_inline .ui_progress_value {
|
|
min-width: 3em;
|
|
text-align: right;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Value shown as a small marker riding on the end of the bar, which
|
|
* keeps the same height as every other bar */
|
|
.ui_progress_inside .ui_progress_track { overflow: visible; }
|
|
.ui_progress_inside .ui_progress_bar { position: relative; }
|
|
.ui_progress_inside_value {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 100%;
|
|
transform: translate(-50%, -50%);
|
|
padding: 1px 4px;
|
|
/* The bar's own color, lightened by a white veil, with dark text */
|
|
background: inherit;
|
|
background-image: linear-gradient(rgba(255, 255, 255, 0.6),
|
|
rgba(255, 255, 255, 0.6));
|
|
color: #1c1c1c;
|
|
font-size: 0.75em;
|
|
font-weight: 600;
|
|
line-height: 1.2;
|
|
white-space: nowrap;
|
|
}
|
|
.ui_progress_inside_start .ui_progress_inside_value {
|
|
transform: translate(0, -50%);
|
|
}
|
|
.ui_progress_inside_end .ui_progress_inside_value {
|
|
transform: translate(-100%, -50%);
|
|
}
|
|
|
|
/* Legend under a segmented bar */
|
|
.ui_progress_legend {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px 16px;
|
|
margin-top: 6px;
|
|
font-size: 0.9em;
|
|
}
|
|
.ui_progress_key {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.ui_progress_dot {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
/* Unknown progress : a bar sliding along the track */
|
|
.ui_progress_indeterminate .ui_progress_bar {
|
|
width: 35%;
|
|
animation: ui_progress_slide 1.4s ease-in-out infinite;
|
|
}
|
|
@keyframes ui_progress_slide {
|
|
from { transform: translateX(-100%); }
|
|
to { transform: translateX(300%); }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.ui_progress_indeterminate .ui_progress_bar {
|
|
width: 100%;
|
|
animation: none;
|
|
opacity: 0.6;
|
|
background-image: repeating-linear-gradient(45deg,
|
|
transparent 0 6px, rgba(255, 255, 255, 0.45) 6px 12px);
|
|
}
|
|
}
|
|
|
|
/* Ring gauge : the bar is a circle of circumference 100 whose dash
|
|
* length is the percentage */
|
|
.ui_progress_ring {
|
|
display: inline-flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.ui_progress_ring_box {
|
|
position: relative;
|
|
display: inline-block;
|
|
line-height: 0;
|
|
}
|
|
.ui_progress_ring_svg { transform: rotate(-90deg); }
|
|
/* The stroke width is set on the circles by ui_progress, scaled to the
|
|
* ring size so it is always 3 pixels */
|
|
.ui_progress_ring_track {
|
|
fill: none;
|
|
stroke: var(--ui-neutral-soft);
|
|
}
|
|
.ui_progress_ring_bar {
|
|
fill: none;
|
|
stroke: currentColor;
|
|
stroke-linecap: round;
|
|
transition: stroke-dasharray 0.4s ease;
|
|
}
|
|
.ui_progress_ring.ui_progress_indeterminate .ui_progress_ring_svg {
|
|
animation: ui_progress_spin 1s linear infinite;
|
|
}
|
|
@keyframes ui_progress_spin {
|
|
from { transform: rotate(-90deg); }
|
|
to { transform: rotate(270deg); }
|
|
}
|
|
.ui_progress_ring_box .ui_progress_value {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.8em;
|
|
font-weight: 600;
|
|
line-height: 1;
|
|
color: var(--ui-fg);
|
|
}
|
|
.ui_progress_ring .ui_progress_label {
|
|
font-size: 0.85em;
|
|
color: var(--ui-fg-muted);
|
|
}
|
|
|
|
/* ---- Choice lists ----
|
|
* One solid box holding a row per option : the theme's own radio button
|
|
* with its label, the inline inputs beside it, and further fields under
|
|
* them in as many columns as fit, indented under the label. Everything
|
|
* stays visible. */
|
|
.ui_choice {
|
|
display: grid;
|
|
gap: var(--ui-inset);
|
|
min-width: 0;
|
|
/* Half the inset above and below, the full inset at the sides */
|
|
padding: calc(var(--ui-inset) / 2) var(--ui-inset);
|
|
border: 1px solid var(--ui-border);
|
|
background: var(--ui-surface);
|
|
}
|
|
.ui_choice_item { min-width: 0; }
|
|
.ui_choice_head {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 4px var(--ui-inset);
|
|
min-width: 0;
|
|
}
|
|
/* Only the option's own label is bold, not labels of inputs in its content */
|
|
.ui_choice_head label:not(.ui_choice_content label) { font-weight: 600; }
|
|
.ui_choice_content {
|
|
display: inline-flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 4px var(--ui-inset);
|
|
min-width: 0;
|
|
}
|
|
.ui_choice_desc {
|
|
margin-left: var(--ui-radio-indent);
|
|
color: var(--ui-fg-muted);
|
|
font-size: 0.93em;
|
|
}
|
|
.ui_choice_fields {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
|
|
gap: var(--ui-inset) 20px;
|
|
margin: var(--ui-inset) 0 0 var(--ui-radio-indent);
|
|
}
|
|
.ui_choice_field {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 4px var(--ui-inset);
|
|
min-width: 0;
|
|
}
|
|
.ui_choice_field_label { color: var(--ui-fg-muted); }
|
|
.ui_choice_field_input { min-width: 0; }
|
|
.ui_choice_disabled { opacity: 0.6; }
|
|
|
|
/* ---- Radio lists ----
|
|
* The compact form : the theme's radio buttons at the left edge of a
|
|
* box, one option per line, an optional input after the label */
|
|
.ui_radio_list {
|
|
display: grid;
|
|
gap: var(--ui-inset);
|
|
min-width: 0;
|
|
padding: var(--ui-inset);
|
|
border: 1px solid var(--ui-border);
|
|
background: var(--ui-surface);
|
|
}
|
|
.ui_radio_list_item {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 4px var(--ui-inset);
|
|
min-width: 0;
|
|
}
|
|
.ui_radio_list_item label:not(.ui_radio_list_content label) {
|
|
font-weight: 600;
|
|
}
|
|
.ui_radio_list_content {
|
|
display: inline-flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 4px var(--ui-inset);
|
|
min-width: 0;
|
|
}
|
|
.ui_radio_list_disabled { opacity: 0.6; }
|
|
|
|
/* ---- Select switches ----
|
|
* A select, and under it the block of the chosen option only */
|
|
.ui_select_switch {
|
|
display: grid;
|
|
justify-items: start;
|
|
gap: var(--ui-inset);
|
|
min-width: 0;
|
|
}
|
|
.ui_select_switch_panel {
|
|
display: grid;
|
|
gap: var(--ui-inset);
|
|
width: 100%;
|
|
min-width: 0;
|
|
padding: var(--ui-inset);
|
|
border: 1px solid var(--ui-border);
|
|
background: var(--ui-surface);
|
|
}
|
|
.ui_select_switch_desc {
|
|
color: var(--ui-fg-muted);
|
|
font-size: 0.93em;
|
|
}
|
|
.ui_select_switch_content {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 4px var(--ui-inset);
|
|
min-width: 0;
|
|
}
|
|
.ui_select_switch_fields {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
|
|
gap: var(--ui-inset) 20px;
|
|
}
|
|
.ui_select_switch_field {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 4px var(--ui-inset);
|
|
min-width: 0;
|
|
}
|
|
.ui_select_switch_field_label { color: var(--ui-fg-muted); }
|
|
.ui_select_switch_field_input { min-width: 0; }
|
|
|
|
/* ---- Toggle switch ----
|
|
* Some themes wrap checkbox inputs in their own markup with JavaScript,
|
|
* so the real input - or whatever wrapper it ends up inside - is parked
|
|
* invisibly over the track, where clicks and keyboard focus still reach
|
|
* it, and the checked state is read with :has() which does not depend on
|
|
* the input being the track's sibling. */
|
|
|
|
.ui_toggle {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
cursor: pointer;
|
|
position: relative;
|
|
font-weight: normal;
|
|
}
|
|
.ui_toggle > :not(.ui_toggle_track):not(.ui_toggle_label),
|
|
.ui_toggle input {
|
|
position: absolute !important;
|
|
left: 0 !important;
|
|
top: 0 !important;
|
|
width: 28px !important;
|
|
height: 14px !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
opacity: 0 !important;
|
|
overflow: hidden;
|
|
z-index: 1;
|
|
cursor: pointer;
|
|
}
|
|
.ui_toggle_track {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
width: 28px;
|
|
height: 14px;
|
|
padding: 2px;
|
|
border-radius: 999px;
|
|
background: var(--ui-toggle-bg);
|
|
transition: background 0.15s ease;
|
|
flex-shrink: 0;
|
|
}
|
|
.ui_toggle_thumb {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 999px;
|
|
background: var(--ui-toggle-thumb);
|
|
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.25);
|
|
transition: transform 0.15s ease;
|
|
}
|
|
.ui_toggle input:checked ~ .ui_toggle_track,
|
|
.ui_toggle:has(input:checked) .ui_toggle_track {
|
|
background: var(--ui-toggle-bg-checked);
|
|
}
|
|
.ui_toggle input:checked ~ .ui_toggle_track .ui_toggle_thumb,
|
|
.ui_toggle:has(input:checked) .ui_toggle_thumb {
|
|
transform: translateX(14px);
|
|
}
|
|
.ui_toggle input:focus-visible ~ .ui_toggle_track,
|
|
.ui_toggle:has(input:focus-visible) .ui_toggle_track {
|
|
box-shadow: 0 0 0 3px var(--ui-ring);
|
|
}
|
|
.ui_toggle input:disabled ~ .ui_toggle_track,
|
|
.ui_toggle:has(input:disabled) .ui_toggle_track { opacity: 0.5; }
|
|
|
|
/* ---- Search box ---- */
|
|
|
|
.ui_search {
|
|
position: relative;
|
|
display: inline-flex;
|
|
max-width: 100%;
|
|
}
|
|
.ui_search .ui_search_icon {
|
|
position: absolute;
|
|
left: 10px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--ui-fg-muted);
|
|
pointer-events: none;
|
|
}
|
|
.ui_search .ui_search_input {
|
|
padding-left: 30px;
|
|
width: 100%;
|
|
min-width: 0;
|
|
font: inherit;
|
|
color: inherit;
|
|
background: var(--ui-surface);
|
|
border: 1px solid var(--ui-border-strong);
|
|
padding-top: 5px;
|
|
padding-bottom: 5px;
|
|
padding-right: 10px;
|
|
}
|
|
.ui_search .ui_search_input:focus {
|
|
outline: none;
|
|
border-color: var(--ui-accent);
|
|
box-shadow: 0 0 0 3px var(--ui-ring);
|
|
}
|
|
|
|
/* ---- Reduced motion ---- */
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.ui_progress_ring.ui_progress_indeterminate .ui_progress_ring_svg {
|
|
animation: none;
|
|
}
|
|
.ui_page *, .ui_page *::before, .ui_page *::after {
|
|
transition: none !important;
|
|
}
|
|
}
|