Files
webmin/unauthenticated/css/ui-lib.css
Ilia Ross 20a487ca5a
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
Add UI widgets and demo module
ⓘ Add composable widgets, shared styling and interactions, sortable-table options, and a read-only UI gallery.
2026-09-05 03:52:39 +02:00

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;
}
}