v0.34.9_css_refactor

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-23 11:15:35 +02:00
1 parent 2b7c67d755
commit 66ff9dfaa2
28 files changed
+11518 -13023

No files matched your search

+5 -1
View File
@@ -79,7 +79,11 @@ static ASSETS: LazyLock<HashMap<&'static str, Asset>> = LazyLock::new(|| {
("/assets/streams.js", JS, include_str!("web/streams.js")),
("/assets/redirects.js", JS, include_str!("web/redirects.js")),
("/assets/host404.js", JS, include_str!("web/host404.js")),
("/assets/style.css", CSS, include_str!("web/style.css")),
(
"/assets/style.css",
CSS,
include_str!(concat!(env!("OUT_DIR"), "/panel-style.css")),
),
("/favicon.svg", SVG, include_str!("web/favicon.svg")),
];
entries
+63
View File
@@ -2966,6 +2966,69 @@ mod tests {
}
store.close().await;
}
#[tokio::test]
async fn modular_panel_css_is_one_embedded_public_asset_with_existing_compression() {
use std::io::Read;
let (router, store, _dir, secret, _) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let css = include_str!(concat!(env!("OUT_DIR"), "/panel-style.css"));
let path = format!("/assets/style_{}.css", super::asset_build_id());
for authenticated in [false, true] {
let mut req = request(&path);
if authenticated {
req = req.header("cookie", &cookie);
}
let response = router
.clone()
.oneshot(req.body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers()["content-type"], "text/css; charset=utf-8");
assert_eq!(response.headers()["vary"], "Accept-Encoding");
let bytes = axum::body::to_bytes(response.into_body(), css.len() + 1)
.await
.unwrap();
assert_eq!(bytes.as_ref(), css.as_bytes());
}
let response = router
.clone()
.oneshot(
request(&path)
.header("accept-encoding", "gzip")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers()["content-encoding"], "gzip");
let compressed = axum::body::to_bytes(response.into_body(), css.len() + 1024)
.await
.unwrap();
let mut decoded = Vec::new();
flate2::read::GzDecoder::new(compressed.as_ref())
.read_to_end(&mut decoded)
.unwrap();
assert_eq!(decoded.as_slice(), css.as_bytes());
for path in ["/assets/css/manifest.json", "/assets/css/base.css"] {
let response = router
.clone()
.oneshot(
request(path)
.header("cookie", &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND, "{path}");
}
store.close().await;
}
#[tokio::test]
async fn public_document_and_discovery_contain_only_login_information() {
let (router, store, _dir, _, _) = fixture().await;
+518
View File
@@ -0,0 +1,518 @@
/* Theme palettes and appearance tokens. Runtime preference variables remain writable by JavaScript.
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
*/
:root {
color-scheme: dark;
--bg: #131615;
--panel: #1b1f1d;
--panel2: #202622;
--line: #323a34;
--muted: #a0aaa0;
--text: #edf0e9;
--accent: #f69b74;
--green: #bdd293;
--red: #ffaaa2;
--sidebar-bg: #101310;
--brand-secondary: #cfe0ac;
--nav-active-bg: #242b23;
--nav-hover-bg: #20251f;
--text-subtle: #879082;
--separator: #596454;
--border-hover: #89917c;
--field-bg: #131813;
--field-line: #47523f;
--placeholder: #778070;
--row-line: #2b332c;
--outline-line: #717b67;
--chart-grid: #333c33;
--code-bg: #101810;
--code-alt-bg: #101922;
--code-alt-text: #d3e4ec;
--nav-selected-bg: #34412a;
--accent-ink: #221810;
--accent-hover: #ffb090;
--selected-ink: #10251e;
--notice-text: #c9b99c;
--notice-line: #554833;
--notice-bg: #29251c;
--pill-line: #6d5a45;
--pill-live-line: #576847;
--toast-bg: #d0dfb4;
--toast-text: #1c2818;
--toast-line: #ebf2de;
--toast-error-bg: #ffd2bd;
--toast-error-line: #ff9f7a;
--toast-error-text: #42241b;
--danger-line: #644a47;
--danger-line-strong: #79514e;
--warn-line: #786647;
--blue: #8ebcff;
--transform-chart: #d7adff;
--backdrop: rgba(0, 0, 0, .68);
--shadow-mid: #0006;
--shadow-strong: #0009;
--neutral-soft: hsla(0, 0%, 49.8%, .05);
--scrollbar-track: #151a16;
--scrollbar-thumb: #4f5d50;
--scrollbar-thumb-hover: #6b7b69;
--chart-accent: var(--accent);
--chart-secondary: var(--green);
--chart-tertiary: var(--blue);
--chart-quaternary: var(--transform-chart);
--chart-quinary: var(--red);
--chart-senary: var(--notice-text);
--chart-septenary: var(--border-hover);
--chart-octonary: var(--separator);
--chart-surface: var(--panel2);
--chart-surface-alt: var(--panel);
--chart-line-width: 2;
--chart-linecap: round;
--chart-linejoin: round;
--chart-grid-opacity: .28;
--chart-grid-dash: none;
--chart-svg-filter: none;
--rp-progress-fill: var(--chart-accent);
--rp-progress-track: var(--line);
--rp-progress-radius: 3px;
--rp-nav-active-color: var(--brand-secondary);
--rp-nav-active-bg: var(--nav-active-bg);
--rp-nav-hover-bg: var(--nav-hover-bg);
--traffic-h1: var(--chart-accent);
--traffic-h2: var(--chart-secondary);
--traffic-h3: var(--chart-tertiary);
--ui-scale: 1;
--ui-field-font-size: 12px;
--ui-field-pad-y: 6px;
--ui-field-pad-x: 8px;
--ui-field-min-height: 27px;
--ui-check-size: 15px;
--ui-range-height: 18px;
--mono: ui-monospace, SFMono-Regular, Consolas, monospace;
--sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
html[data-theme=light] {
color-scheme: light;
--bg: #f4f6f2;
--panel: #fff;
--panel2: #edf1eb;
--line: #cbd3c7;
--muted: #5e685f;
--text: #1d241f;
--accent: #b85b38;
--green: #5f783f;
--red: #b64a43;
--sidebar-bg: #eef2ec;
--brand-secondary: #6b8249;
--nav-active-bg: #e1e8dc;
--nav-hover-bg: #e8ede6;
--text-subtle: #687369;
--separator: #9ba69b;
--border-hover: #839083;
--field-bg: #fff;
--field-line: #b7c1b5;
--placeholder: #7a867b;
--row-line: #e0e5dd;
--outline-line: #98a497;
--chart-grid: #d4dbd1;
--code-bg: #edf3e9;
--code-alt-bg: #eef3f6;
--code-alt-text: #2d4049;
--nav-selected-bg: #e1e9db;
--accent-ink: #fff;
--accent-hover: #a84f30;
--selected-ink: #fff;
--notice-text: #685b45;
--notice-line: #d4c2a5;
--notice-bg: #fff9ef;
--pill-line: #cbb28f;
--pill-live-line: #a8ba8d;
--danger-line: #d7a29d;
--danger-line-strong: #c98f89;
--warn-line: #c6b27e;
--blue: #4f77a9;
--transform-chart: #7d4fa3;
--shadow-mid: rgba(31, 39, 33, .16);
--shadow-strong: rgba(31, 39, 33, .26);
--neutral-soft: rgba(70, 78, 72, .05);
--scrollbar-track: #e7ece4;
--scrollbar-thumb: #a9b5a6;
--scrollbar-thumb-hover: #879684;
}
html:is([data-theme=light], [data-theme=cream]) {
--rp-nav-active-color: color-mix(in srgb, var(--brand-secondary) 45%, var(--text) 55%);
}
html[data-theme=ultra_dark] {
color-scheme: dark;
--bg: #030404;
--panel: #0b0d0e;
--panel2: #121516;
--line: #292f30;
--muted: #a4afb0;
--text: #f1f4f2;
--accent: #93dfc8;
--logo-accent: #f69b74;
--logo-secondary: #cfe0ac;
--green: #b1dcb0;
--red: #ff9899;
--sidebar-bg: #000;
--brand-secondary: #a9cfb5;
--nav-active-bg: #172422;
--nav-hover-bg: #121819;
--text-subtle: #879695;
--separator: #526261;
--border-hover: #7c9f96;
--field-bg: #090b0c;
--field-line: #445352;
--placeholder: #879593;
--row-line: #202627;
--outline-line: #79968f;
--chart-grid: #303839;
--code-bg: #0e1313;
--code-alt-bg: #101a1d;
--code-alt-text: #d5eae9;
--nav-selected-bg: #213730;
--accent-ink: #071814;
--accent-hover: #b6f2dd;
--selected-ink: #071814;
--notice-text: #e6c99c;
--notice-line: #66533a;
--notice-bg: #201a11;
--pill-line: #625544;
--pill-live-line: #506c59;
--toast-bg: #bcf3da;
--toast-text: #10221a;
--toast-line: #d8ffe8;
--toast-error-bg: #ffd5d0;
--toast-error-line: #ff9c96;
--toast-error-text: #472625;
--danger-line: #754245;
--danger-line-strong: #965257;
--warn-line: #89754c;
--blue: #8bbdfb;
--transform-chart: #cfaeff;
--backdrop: rgba(0, 0, 0, .86);
--shadow-mid: rgba(0, 0, 0, .64);
--shadow-strong: rgba(0, 0, 0, .84);
--neutral-soft: rgba(132, 171, 158, .045);
--scrollbar-track: #060808;
--scrollbar-thumb: #364243;
--scrollbar-thumb-hover: #617373;
}
html[data-theme=cream] {
color-scheme: light;
--bg: #f2ede3;
--panel: #fcf9f2;
--panel2: #eae3d6;
--line: #d5cabb;
--muted: #6b6257;
--text: #302c27;
--accent: #8c5942;
--green: #5e7655;
--red: #a54645;
--sidebar-bg: #eae2d5;
--brand-secondary: #668367;
--nav-active-bg: #e0d3c0;
--nav-hover-bg: #e6dccc;
--text-subtle: #786e61;
--separator: #afa494;
--border-hover: #968776;
--field-bg: #fffcf7;
--field-line: #baad9a;
--placeholder: #807466;
--row-line: #e2d9cb;
--outline-line: #9c8d7c;
--chart-grid: #ddd3c4;
--code-bg: #f0e8da;
--code-alt-bg: #e7ebe9;
--code-alt-text: #364f4e;
--nav-selected-bg: #d9cbb7;
--accent-ink: #fff;
--accent-hover: #774632;
--selected-ink: #fff;
--notice-text: #765e40;
--notice-line: #d9c3a1;
--notice-bg: #f8efdf;
--pill-line: #c2a78d;
--pill-live-line: #b0bd9d;
--toast-bg: #d6ead2;
--toast-text: #263d29;
--toast-line: #b8d5b6;
--toast-error-bg: #f8ded7;
--toast-error-line: #dca99c;
--toast-error-text: #60352f;
--danger-line: #d2a29a;
--danger-line-strong: #b97870;
--warn-line: #b49c6b;
--blue: #4d7292;
--transform-chart: #83659a;
--backdrop: rgba(37, 27, 16, .44);
--shadow-mid: rgba(64, 49, 34, .13);
--shadow-strong: rgba(64, 49, 34, .24);
--neutral-soft: rgba(104, 91, 69, .045);
--scrollbar-track: #ece4d7;
--scrollbar-thumb: #b5a997;
--scrollbar-thumb-hover: #978a76;
}
body[data-density=dense] {
--ui-gap: 4px;
--ui-card: 7px;
--ui-font: 11.5px;
--ui-control: 25px;
}
body[data-density=comfortable] {
--ui-gap: 13px;
--ui-card: 17px;
--ui-font: 14px;
--ui-control: 36px;
}
html[data-button-style=compact] body {
--button-padding: 3px 7px;
--button-font-size: 10px;
--button-min-height: 22px;
}
.chart-style-choice[data-chart-preset=default] {
--sw1: var(--accent);
--sw2: var(--green);
--sw3: var(--blue);
--sw4: var(--transform-chart);
}
.chart-style-choice[data-chart-preset=soft] {
--sw1: #e8a998;
--sw2: #95c293;
--sw3: #8daef6;
--sw4: #b49de6;
}
.chart-style-choice[data-chart-preset=technical] {
--sw1: #79c9ff;
--sw2: #9adf90;
--sw3: #ffd56e;
--sw4: #c39cff;
}
.chart-style-choice[data-chart-preset=high_contrast] {
--sw1: #ffd54f;
--sw2: #8dff80;
--sw3: #6fd9ff;
--sw4: #ff78d7;
}
.chart-style-choice[data-chart-preset=neon] {
--sw1: #2cf6ff;
--sw2: #7eff85;
--sw3: #ff5cd0;
--sw4: #8b7cff;
}
.chart-style-choice[data-chart-preset=monochrome] {
--sw1: #f2f2f2;
--sw2: #b8b8b8;
--sw3: #858585;
--sw4: #595959;
}
.chart-style-choice[data-chart-preset=warm] {
--sw1: #ff9c5c;
--sw2: #f4c46b;
--sw3: #ff7b72;
--sw4: #f4a261;
}
.chart-style-choice[data-chart-preset=cool] {
--sw1: #5fb9ff;
--sw2: #45d3c3;
--sw3: #7d8fff;
--sw4: #70d7ff;
}
html[data-chart-style=default] {
--chart-accent: var(--accent);
--chart-secondary: var(--green);
--chart-tertiary: var(--blue);
--chart-quaternary: var(--transform-chart);
--chart-quinary: var(--red);
--chart-senary: var(--notice-text);
--chart-septenary: var(--border-hover);
--chart-octonary: var(--separator);
--chart-surface: var(--panel2);
--chart-surface-alt: var(--panel);
--chart-line-width: 2;
--chart-linecap: round;
--chart-linejoin: round;
--chart-grid-opacity: .28;
--chart-grid-dash: 2 5;
--chart-svg-filter: none;
}
html[data-chart-style=soft] {
--chart-accent: #e8a998;
--chart-secondary: #95c293;
--chart-tertiary: #8daef6;
--chart-quaternary: #b49de6;
--chart-quinary: #d99490;
--chart-senary: #d0b78e;
--chart-septenary: #99a790;
--chart-octonary: #728070;
--chart-surface: color-mix(in srgb, var(--panel2) 88%, #fff 12%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 92%, #fff 8%);
--chart-line-width: 2.8;
--chart-linecap: round;
--chart-linejoin: round;
--chart-grid-opacity: .42;
--chart-grid-dash: 1 0;
--chart-svg-filter: saturate(.88) brightness(1.04);
}
html[data-chart-style=technical] {
--chart-accent: #79c9ff;
--chart-secondary: #9adf90;
--chart-tertiary: #ffd56e;
--chart-quaternary: #c39cff;
--chart-quinary: #ff8c7b;
--chart-senary: #b8d0d7;
--chart-septenary: #7e9aac;
--chart-octonary: #6a7884;
--chart-surface: color-mix(in srgb, var(--panel2) 90%, #0b1a27 10%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 90%, #09151f 10%);
--chart-line-width: 1.35;
--chart-linecap: square;
--chart-linejoin: miter;
--chart-grid-opacity: .82;
--chart-grid-dash: 3 4;
--chart-svg-filter: saturate(.76) contrast(1.08);
}
html[data-chart-style=high_contrast] {
--chart-accent: #ffd54f;
--chart-secondary: #8dff80;
--chart-tertiary: #6fd9ff;
--chart-quaternary: #ff78d7;
--chart-quinary: #ff8b6f;
--chart-senary: #f2e48f;
--chart-septenary: #d7f0d9;
--chart-octonary: #cfcfcf;
--chart-surface: color-mix(in srgb, var(--panel2) 94%, #000 6%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 94%, #000 6%);
--chart-line-width: 3;
--chart-grid-opacity: .95;
--chart-grid-dash: 1 0;
--chart-svg-filter: saturate(1.6) contrast(1.22) brightness(1.08);
--chart-linecap: round;
--chart-linejoin: round;
}
html[data-chart-style=cool] {
--chart-linecap: round;
--chart-linejoin: round;
--chart-accent: #5fb9ff;
--chart-secondary: #45d3c3;
--chart-tertiary: #7d8fff;
--chart-quaternary: #70d7ff;
--chart-quinary: #54a0ff;
--chart-senary: #8bcbe0;
--chart-septenary: #6ea6bf;
--chart-octonary: #7d91a9;
--chart-surface: color-mix(in srgb, var(--panel2) 88%, #0d2740 12%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 90%, #0d2033 10%);
--chart-line-width: 2.2;
--chart-grid-opacity: .46;
--chart-grid-dash: 2 4;
--chart-svg-filter: saturate(1.08) brightness(1.03);
}
html[data-chart-style=monochrome] {
--chart-linecap: round;
--chart-linejoin: round;
--chart-accent: color-mix(in srgb, var(--text) 92%, #fff 8%);
--chart-secondary: color-mix(in srgb, var(--muted) 80%, var(--text) 20%);
--chart-tertiary: color-mix(in srgb, var(--separator) 72%, var(--text) 28%);
--chart-quaternary: color-mix(in srgb, var(--line) 45%, var(--text) 55%);
--chart-quinary: color-mix(in srgb, var(--muted) 55%, #fff 45%);
--chart-senary: color-mix(in srgb, var(--outline-line) 58%, var(--text) 42%);
--chart-septenary: color-mix(in srgb, var(--field-line) 70%, var(--text) 30%);
--chart-octonary: color-mix(in srgb, var(--scrollbar-thumb) 75%, var(--text) 25%);
--chart-surface: color-mix(in srgb, var(--panel2) 96%, var(--text) 4%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 96%, var(--text) 4%);
--chart-line-width: 2.1;
--chart-grid-opacity: .4;
--chart-grid-dash: 2 3;
--chart-svg-filter: saturate(0) contrast(1.05);
}
html[data-chart-style=neon] {
--chart-linecap: round;
--chart-linejoin: round;
--chart-accent: #2cf6ff;
--chart-secondary: #7eff85;
--chart-tertiary: #ff5cd0;
--chart-quaternary: #8b7cff;
--chart-quinary: #ffb454;
--chart-senary: #ffe66b;
--chart-septenary: #7edcff;
--chart-octonary: #d8b3ff;
--chart-surface: #0b1117;
--chart-surface-alt: #101720;
--chart-line-width: 2.45;
--chart-grid-opacity: .62;
--chart-grid-dash: 2 4;
--chart-svg-filter: saturate(1.25) brightness(1.12);
}
html[data-chart-style=warm] {
--chart-linecap: round;
--chart-linejoin: round;
--chart-accent: #ff9c5c;
--chart-secondary: #f4c46b;
--chart-tertiary: #ff7b72;
--chart-quaternary: #f4a261;
--chart-quinary: #e76f51;
--chart-senary: #d9a15e;
--chart-septenary: #c9895c;
--chart-octonary: #b37258;
--chart-surface: color-mix(in srgb, var(--panel2) 88%, #3d1f10 12%);
--chart-surface-alt: color-mix(in srgb, var(--panel) 90%, #32180d 10%);
--chart-line-width: 2.4;
--chart-grid-opacity: .36;
--chart-grid-dash: 1 0;
--chart-svg-filter: saturate(1.1) brightness(1.04);
}
html[data-chart-style] {
--traffic-h1: var(--chart-accent);
--traffic-h2: var(--chart-secondary);
--traffic-h3: var(--chart-tertiary);
}
html[data-corner-style=square] {
--rp-radius-surface: 0px;
--rp-radius-control: 0px;
--rp-progress-radius: 0px;
}
html[data-corner-style=subtle] {
--rp-radius-surface: 5px;
--rp-radius-control: 4px;
--rp-progress-radius: 2px;
}
html[data-corner-style=rounded] {
--rp-radius-surface: 12px;
--rp-radius-control: 8px;
--rp-progress-radius: 4px;
}
html[data-corner-style=extra] {
--rp-radius-surface: 18px;
--rp-radius-control: 12px;
--rp-progress-radius: 999px;
}
File diff suppressed because it is too large. Load diff
+773
View File
@@ -0,0 +1,773 @@
/* Application startup, authentication, navigation, page chrome and draft dock.
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
*/
html.app-loading .shell {
visibility: hidden;
pointer-events: none;
}
.app-startup {
position: fixed;
inset: 0;
z-index: 10000;
display: grid;
place-items: center;
padding: 24px;
box-sizing: border-box;
background: var(--bg, #10151b);
color: var(--text, #edf2f7);
text-align: center;
}
.page-source[hidden],
#toast[popover]::backdrop,
.operation-message:empty,
#certificate-api-cancel-edit[hidden],
#certificate-api-edit-note[hidden],
#certificate-api-token-field[hidden],
html:not(.app-loading) .app-startup,
html.app-startup-failed .app-startup-message,
html.app-startup-failed .app-startup-spinner,
.login-section #auth-message:empty,
.mobile-menu-toggle,
.draft-dock-list[hidden],
#settings-page-appearance .dashboard-widget-setting .overview-widget-type,
.dashboard-appearance-grid .dashboard-widget-list.is-expanded~.dashboard-widget-scroll-hint,
.vh-group[hidden],
.stream-socket-editor input[hidden],
.vh-cache-options[hidden],
.acl-policy-custom-controls[hidden],
.listener-protocol-settings-actions button[hidden],
.logs-dialog .logs-card>.card-heading>div:first-child {
display: none;
}
.app-startup-card {
width: min(100%, 360px);
}
.app-startup-brand {
font: 700 29px var(--mono, monospace);
letter-spacing: -2px;
margin-bottom: 24px;
}
.app-startup-brand span {
color: var(--logo-secondary, var(--accent, #65cfa2));
}
.app-startup-spinner {
box-sizing: border-box;
width: 40px;
height: 40px;
margin: 0 auto 22px;
border: 3px solid var(--line, #374352);
border-top-color: var(--accent, #65cfa2);
border-radius: 50%;
animation: app-startup-spin .8s linear infinite;
}
.app-startup h2 {
margin: 0;
font-size: 19px;
font-weight: 600;
line-height: 1.4;
}
.app-startup-message,
.app-startup-recovery p {
font-size: 13px;
line-height: 1.6;
color: var(--muted, #b5c0cc);
}
.app-startup-recovery {
visibility: hidden;
animation: app-startup-reveal 0s linear 12s forwards;
}
.app-startup-recovery .outline:not([hidden]) {
margin-top: 10px;
display: inline-flex;
}
html.app-startup-failed .app-startup-recovery {
visibility: visible;
animation: none;
}
.shell {
display: grid;
min-height: calc(100vh / var(--ui-scale, 1));
grid-template-columns: 182px minmax(0, 1fr);
}
.sidebar {
position: sticky;
top: 0;
height: calc(100vh / var(--ui-scale, 1));
background: var(--sidebar-bg);
border-right: 1px solid var(--line);
display: flex;
flex-direction: column;
padding: 20px 13px;
}
.brand {
display: flex;
align-items: center;
font-weight: 780;
line-height: 1.1;
letter-spacing: -.6px;
font-size: 17px;
gap: 8px;
}
.brand-mark {
font: 700 33px var(--mono);
border: 1px solid var(--logo-accent, var(--accent));
color: var(--logo-accent, var(--accent));
place-content: center;
position: relative;
box-sizing: border-box;
padding-right: 2px;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: -3px;
flex-shrink: 0;
width: 47px;
height: 47px;
font-size: 29px;
}
.brand-mark span {
font-size: 22px;
left: 29px;
top: 19px;
color: var(--logo-secondary, var(--brand-secondary));
position: static;
}
.edition {
font: 9px/1.6 var(--mono);
color: var(--muted);
letter-spacing: 1px;
margin: 17px 0 28px;
}
.edition span {
display: block;
color: var(--accent);
margin-top: 5px;
}
nav {
flex-direction: column;
display: flex;
gap: 5px;
}
.local-tag {
display: flex;
font: 9px var(--mono);
color: var(--green);
gap: 8px;
align-items: center;
}
.nav {
display: flex;
align-items: center;
gap: 13px;
text-align: left;
background: transparent;
color: var(--muted);
border-left: 2px solid transparent;
border-radius: var(--rp-radius-control, 0);
padding: 9px 10px;
}
.nav span {
font: 11px var(--mono);
opacity: .65;
}
.nav b {
font-weight: 530;
}
.nav.active {
background: var(--rp-nav-active-bg);
color: var(--rp-nav-active-color);
border-left-color: var(--rp-nav-active-color);
}
.nav:not(.active):hover {
background: var(--rp-nav-hover-bg);
}
.sidebar .main-navigation .nav:focus-visible {
outline: 2px solid var(--rp-nav-active-color);
outline-offset: 2px;
}
.sidebar-bottom {
margin-top: auto;
padding-top: 32px;
}
.sidebar-bottom p {
font-size: 11px;
line-height: 1.9;
color: var(--text-subtle);
margin: 13px 0 26px;
}
main {
min-width: 0;
width: 100%;
max-width: none;
padding: 0 16px;
margin: 0 auto;
}
.topbar {
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
height: 48px;
}
.breadcrumb {
font: 10px var(--mono);
color: var(--muted);
letter-spacing: 1px;
}
.breadcrumb span {
margin: 0 13px;
color: var(--separator);
}
.breadcrumb b {
font-weight: 400;
color: var(--text);
}
.top-actions {
display: flex;
align-items: center;
gap: 8px;
}
.system-card h2 small {
font: 10px var(--mono);
color: var(--muted);
}
.eyebrow {
font: 10px var(--mono);
letter-spacing: 2px;
color: var(--green);
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 16px;
}
.lead {
font-size: 13px;
line-height: 1.7;
color: var(--muted);
margin-top: 15px;
max-width: 690px;
}
.page-intro {
display: flex;
justify-content: space-between;
align-items: center;
padding: 13px 0 11px;
gap: 12px;
}
.login-card label {
display: block;
color: var(--muted);
font-size: 11px;
margin: 16px 0 7px;
}
.page-footer .footer-update {
color: var(--green);
text-decoration: underline;
text-underline-offset: 2px;
}
.login-section {
padding: 62px 0 40px;
max-width: 720px;
padding-top: 40px;
}
.login-section h1 {
font-size: calc(clamp(42px, 5vw, 68px));
letter-spacing: -3px;
max-width: 640px;
}
.login-section .lead {
font-size: 15px;
max-width: 570px;
}
.login-card {
background: var(--panel);
border: 1px solid var(--line);
padding: 27px;
max-width: 510px;
margin-top: 23px;
}
.login-card>p {
font-size: 12px;
color: var(--muted);
line-height: 1.7;
}
.login-card .primary {
width: 100%;
margin-top: 18px;
}
.login-card .fine {
font-size: 10px;
margin-top: 16px;
}
.login-footer {
font: 9px var(--mono);
letter-spacing: 1.7px;
color: var(--muted);
margin-top: 30px;
line-height: 2;
}
.login-footer span {
margin: 0 11px;
color: var(--accent);
}
.page-intro .eyebrow {
font-size: 9px;
margin-bottom: 4px;
}
[dir=rtl] .nav {
text-align: right;
border-left: 0;
border-right: 2px solid transparent;
}
[dir=rtl] .nav.active {
border-right-color: var(--rp-nav-active-color);
}
.app-confirm-message {
overflow-wrap: anywhere;
margin-top: 14px;
color: var(--text);
white-space: pre-line;
}
.sidebar nav {
margin-top: 22px;
}
.page-intro h1 {
font-size: 25px;
letter-spacing: -.7px;
}
.page-intro .lead {
font-size: 11px;
margin-top: 4px;
}
.app-confirm-dialog {
width: calc(min(520px, 100vw - 28px));
max-width: none;
padding: 0;
border: 1px solid var(--line);
border-radius: var(--rp-radius-surface, 4px);
background: var(--panel);
color: var(--text);
}
.app-confirm-dialog::backdrop {
background: var(--backdrop);
backdrop-filter: blur(2px);
}
.app-confirm-card {
margin: 0;
border: 0;
padding: 18px;
}
.app-confirm-actions {
justify-content: flex-end;
margin-top: 18px;
}
.app-confirm-danger {
border-color: var(--red);
color: var(--red);
}
.app-confirm-danger:hover {
border-color: var(--red);
background: rgba(255, 170, 162, .08);
}
.auth-page.auth-loading .auth-main,
.auth-page.auth-loading .auth-top {
visibility: hidden;
}
.auth-page {
min-height: 100vh;
}
.auth-top {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 24px;
border-bottom: 1px solid var(--line);
}
.auth-main {
max-width: 560px;
margin: auto;
padding: 28px 20px;
}
.auth-main .login-section {
padding: 0;
max-width: none;
}
.auth-main h1 {
font-size: 1.65rem;
margin-bottom: 18px;
}
.login-section #auth-message {
margin-top: 14px;
padding: 11px 12px;
border: 1px solid var(--line);
background: var(--panel2);
color: var(--text);
font-size: 12px;
line-height: 1.5;
overflow-wrap: anywhere;
}
.auth-advanced>summary,
.vh-backend-advanced>summary {
cursor: pointer;
color: var(--muted);
font-size: 11px;
font-weight: 600;
padding: 4px 0 8px;
}
.auth-subcard {
border: 1px solid var(--line);
border-radius: var(--rp-radius-surface, 6px);
padding: 10px;
display: grid;
gap: 8px;
}
.auth-subcard>h3 {
margin: 0;
font-size: 12px;
}
.auth-advanced {
border-top: 1px solid var(--line);
padding-top: 4px;
}
body[data-density=compact] .shell {
grid-template-columns: 164px minmax(0, 1fr);
}
body[data-density=compact] .sidebar {
padding: 14px 9px;
}
body[data-density=compact] .nav,
body[data-density=compact] .stat {
padding: 6px 7px;
}
body[data-density=compact] .topbar {
height: 42px;
}
body[data-density=compact] .page-intro {
padding: 7px 0 6px;
gap: 7px;
}
body[data-density=compact] .page-intro h1 {
font-size: 20px;
}
body[data-density=compact] .page-intro .lead {
font-size: 10px;
margin-top: 2px;
}
.top-log-menu>summary,
.backend-column-picker>summary,
.file-log-search-help>summary {
list-style: none;
cursor: pointer;
user-select: none;
}
.top-log-menu-items {
position: absolute;
z-index: 120;
top: calc(100% + 6px);
inset-inline-end: 0;
display: grid;
gap: 4px;
min-width: 170px;
padding: 6px;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: 0 12px 28px var(--shadow-strong);
}
.top-log-menu-items .quiet {
width: 100%;
text-align: start;
white-space: nowrap;
}
.draft-dock {
position: fixed;
right: 14px;
bottom: 14px;
z-index: 900;
width: calc(min(330px, 100vw - 28px));
padding: 0;
border: 1px solid var(--border-hover);
border-radius: var(--rp-radius-surface, 0);
background: var(--panel);
box-shadow: 0 12px 32px var(--shadow-strong);
}
.draft-dock-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 34px;
padding: 7px 9px;
border-bottom: 1px solid var(--line);
background: var(--panel2);
}
.draft-dock.is-collapsed .draft-dock-heading,
.draft-dock-item:last-child {
border-bottom: 0;
}
.draft-dock-heading>strong {
font: 700 10px var(--mono);
letter-spacing: .12em;
text-transform: uppercase;
}
.draft-dock-heading .pill {
min-width: 24px;
padding: 2px 6px;
text-align: center;
border-radius: var(--rp-radius-control, 0);
}
.draft-dock-heading-actions,
.traffic-protocol-legend span,
.stat-chart .stat-chart-legend span,
.function-live-chart-legend span,
.connection-legend span {
display: inline-flex;
align-items: center;
gap: 5px;
}
.draft-dock-toggle {
display: inline-grid;
place-items: center;
width: 26px;
min-width: 26px;
height: 24px;
padding: 0;
border: 1px solid var(--line);
border-radius: var(--rp-radius-control, 0);
background: var(--panel);
color: var(--text);
font: 700 14px var(--mono);
}
.draft-dock-toggle:hover,
.chart-style-choice:hover {
border-color: var(--border-hover);
background: var(--nav-hover-bg);
}
.draft-dock-list {
display: grid;
gap: 0;
max-height: calc(min(42vh, 360px));
overflow: auto;
}
.draft-dock-item {
display: grid;
grid-template-columns: minmax(0, 1fr) 32px;
gap: 0;
align-items: stretch;
border-bottom: 1px solid var(--line);
}
.draft-dock-open {
min-height: 42px;
border: 0;
border-radius: 0;
background: transparent;
min-width: 0;
padding: 6px 9px;
text-align: left;
border-right: 1px solid var(--line);
}
.draft-dock-remove {
min-height: 42px;
border: 0;
border-radius: 0;
background: transparent;
padding: 0;
font-size: 18px;
color: var(--muted);
}
.draft-dock-open:hover {
background: var(--nav-hover-bg);
color: var(--text);
box-shadow: inset 3px 0 0 var(--accent);
}
.draft-dock-remove:hover {
background: var(--nav-hover-bg);
color: var(--text);
}
.draft-dock-identity {
display: grid;
min-width: 0;
gap: 1px;
}
.draft-dock-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-width: 0;
}
.draft-dock-identity small {
color: var(--muted);
font: 9px var(--mono);
text-transform: uppercase;
letter-spacing: .08em;
}
.draft-dock-identity time {
flex: 0 0 auto;
color: var(--text-subtle);
font: 9px var(--mono);
}
.draft-dock-identity b {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
font-weight: 650;
}
.page-intro-copy,
.acl-picker,
.vh-acl-profile-picker,
.docker-health-grid input:not([type=checkbox]),
.docker-health-grid select,
.docker-route-grid input:not([type=checkbox]),
.docker-route-grid select {
min-width: 0;
width: 100%;
}
.page-intro-title-row {
min-width: 0;
width: 100%;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 10px;
}
.page-intro-title-row h1 {
min-width: 0;
flex: 1 1 auto;
margin-right: auto;
}
.page-intro .page-intro-meta-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 6px 16px;
min-width: 0;
margin-bottom: 4px;
}
.page-intro .page-intro-meta-row .eyebrow {
margin-bottom: 0;
flex: 0 0 auto;
}
@keyframes app-startup-spin {
to {
transform: rotate(1turn);
}
}
@keyframes app-startup-reveal {
to {
visibility: visible;
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+99
View File
@@ -0,0 +1,99 @@
# Administration stylesheet
This directory is the source of the shared dashboard and login stylesheet.
`manifest.json` defines its complete cascade order. A normal Cargo build runs
`scripts/build/panel_css.py`, compacts these sources conservatively and embeds
one `panel-style.css` from Cargo's `OUT_DIR` into the binary.
The browser continues to request `/assets/style_<build-id>.css` once. The
existing fingerprint, login access rules and cached gzip/Brotli/Zstandard
responses remain in use. Source modules are not separate HTTP assets. The
documentation Reader/API page keeps its independent `../docs.css` stylesheet.
## Module ownership and order
| Order | File | Responsibility |
|---|---|---|
| 1 | `00-tokens.css` | Theme palettes, appearance tokens and custom properties shared with JavaScript preferences |
| 2 | `10-base.css` | Reset, typography, buttons, form fields, tables, shared cards, dialogs and overlays |
| 3 | `20-shell.css` | Startup, login, navigation, page structure and editor draft dock |
| 4 | `30-overview.css` | Shared charts/legends and Overview metrics, density, widget layout controls and dashboard presets |
| 5 | `40-editors.css` | Vhosts and backends, Web Server, Streams, redirects, 404 hosts and ACL controls |
| 6 | `50-settings.css` | Settings, certificates, integrations and Docker service/agent management |
| 7 | `60-monitoring.css` | Traffic, cache, transformations, live/stored logs and configuration history |
| 8 | `70-responsive.css` | All viewport/container queries and reduced-motion adjustments, in their required cascade order |
## Editing rules
- Update the rule in its owning module. Keep one occurrence of a selector in
each conditional scope; group selectors when they deliberately share one
declaration block and the move preserves cascade behavior.
- Reuse the existing theme and appearance variables. JavaScript also writes
live values such as chart geometry, widget layout and preferences; retain
those variable and class names when reorganizing CSS.
- Place a responsive adjustment inside the existing matching condition in
`70-responsive.css`. A container name needs whitespace before its condition,
for example `@container overview-canvas (max-width: 700px)`.
- Preserve the manifest order and the order of overlapping conditions. The
query order is part of the cascade; do not sort breakpoints mechanically.
In particular, the `760px` condition follows `620px` to preserve the later
Transform and file-log adjustments. Container queries follow viewport rules.
- Explicit dashboard spans set `--overview-widget-placement` on the widget;
one common rule applies it to `grid-column`. Container queries change that
variable for supported span values. Keep the generic no-span defaults
separate so the legacy row-start and custom-layout contracts remain intact.
- Add a new module only when it has a distinct owner and register it once in
the manifest. Missing, repeated and unlisted CSS files fail the build.
`@import`, `@charset`, path traversal and symlinked modules are rejected.
- Group identical declaration bodies within each compatible selector family.
Moving them across intervening rules requires checking specificity and source
order, including combinations of state classes. Keep vendor pseudo-elements
in separate compatible lists; a WebKit/Gecko selector mix can invalidate the
whole rule in another engine.
The builder removes formatting whitespace beside braces/semicolons and normal
comments. It preserves selector whitespace, escaped tokens, strings, functions
and declaration order. It does not sort, merge or otherwise optimize rules at
build time. No Node.js/npm or runtime stylesheet compiler is required.
## Verification
From the repository root:
```bash
python3 scripts/build/panel_css.py --check
python3 tools/dev/test_panel_css.py
python3 scripts/preflight.py
python3 scripts/audit_css_usage.py --strict
bash dev.sh check
bash dev.sh test
```
The first three commands require only Python 3.10+. The optional CSS audit
requires `tinycss2` and `cssselect2`; install them in a development environment,
not on the deployed server. `--structure-json PATH` records repeated selectors,
conditional blocks, properties and identical declaration blocks. `--json PATH`
records selector references. The audit follows JavaScript embedded by Rust and
flags scripts outside those entry points for review. Missing literal class
names still need manual review because JavaScript can construct them.
Strict mode also rejects identical declaration bodies within the same compatible
selector family. Its only body-repetition exception is a single rule per vendor
family when merging would create an incompatible selector list.
Use browser comparisons for layout changes: cover login and every workspace,
open dialogs and expanded rows, themes, corner styles, dashboard density/width,
hidden/disabled states, viewport breakpoints, container widths and reduced
motion. A source audit alone does not establish visual or runtime equivalence.
Rebuild the Rust binary after every stylesheet change.
When replacing a previously built checkout with a source ZIP, include the root
`build.rs`. If Cargo reports that `OUT_DIR/panel-style.css` is missing, follow
the [generated-CSS recovery steps](../../../../scripts/README.md#missing-generated-css-during-rust-compilation).
Source preflight does not generate Cargo outputs. The build tracks the generated
bundle as well as its inputs, and rejects a missing or empty output.
The optional [dashboard browser regression runner](../../../../tools/dev/README-dashboard-css.md)
assembles the actual HTML and JavaScript against synthetic API fixtures and
compares a supplied baseline stylesheet with the built candidate. It includes
breakpoint/container boundaries and dynamic editor states. Its dependencies are
development-only; they are not part of the application build or deployment.
+12
View File
@@ -0,0 +1,12 @@
{
"files": [
"00-tokens.css",
"10-base.css",
"20-shell.css",
"30-overview.css",
"40-editors.css",
"50-settings.css",
"60-monitoring.css",
"70-responsive.css"
]
}
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -423,7 +423,7 @@ mod tests {
#[test]
fn css_minifier_preserves_dashboard_corner_selector() {
let input = include_str!("../admin/web/style.css");
let input = include_str!(concat!(env!("OUT_DIR"), "/panel-style.css"));
let result = css::minify(input.as_bytes()).expect("minify panel stylesheet");
let output = std::str::from_utf8(&result).expect("UTF-8 CSS");
assert!(output.contains("html:is([data-corner-style=square],[data-corner-style=subtle],[data-corner-style=rounded],[data-corner-style=extra]) :is("), "lost descendant pseudo-class combinator");