feat(l4d2-web): add neutral shell and theme tokens
This commit is contained in:
parent
98872727a7
commit
881b6635f9
7 changed files with 158 additions and 62 deletions
|
|
@ -1,10 +1,10 @@
|
||||||
|
.panel,
|
||||||
.card {
|
.card {
|
||||||
background: var(--color-card);
|
background: var(--color-surface);
|
||||||
border: 1px solid var(--color-border);
|
border: var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius-m);
|
||||||
padding: var(--space-4);
|
padding: var(--space-l);
|
||||||
margin-bottom: var(--space-4);
|
margin-bottom: var(--space-l);
|
||||||
box-shadow: 0 8px 20px #0F766E12;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
|
|
@ -15,8 +15,8 @@
|
||||||
.table th,
|
.table th,
|
||||||
.table td {
|
.table td {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-s) var(--space-m);
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted {
|
.muted {
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
.stack {
|
.stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-3);
|
gap: var(--space-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
|
|
@ -36,10 +36,27 @@ textarea {
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background: var(--color-link);
|
background: var(--color-primary);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-s);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: var(--space-2) var(--space-4);
|
padding: var(--space-s) var(--space-l);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.danger {
|
||||||
|
background: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-link);
|
||||||
|
padding: 0;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-form {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,35 +4,42 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Segoe UI", "Helvetica Neue", sans-serif;
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
background: radial-gradient(circle at top right, #DDEFEA, #F3F7F6 45%);
|
background: var(--color-bg);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-header {
|
.site-header {
|
||||||
background: #FFFFFFD9;
|
background: var(--color-surface);
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: var(--line);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-header-inner {
|
.site-header-inner {
|
||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: var(--space-4);
|
padding: var(--space-l);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primary-nav,
|
||||||
|
.account-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
margin-right: var(--space-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: var(--space-6) var(--space-4) var(--space-6);
|
padding: var(--space-2xl) var(--space-l);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
.log-stream {
|
.log-stream {
|
||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
max-height: 360px;
|
max-height: 480px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: #0A1412;
|
background: var(--color-log-bg);
|
||||||
color: #CCE9E1;
|
color: var(--color-log-text);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-s);
|
||||||
padding: var(--space-3);
|
padding: var(--space-m);
|
||||||
font-family: "SFMono-Regular", Menlo, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,49 @@
|
||||||
:root {
|
:root {
|
||||||
--color-link: #0F766E;
|
--color-bg: #f4f4f5;
|
||||||
--color-bg: #F3F7F6;
|
--color-surface: #ffffff;
|
||||||
--color-text: #11201D;
|
--color-surface-muted: #f8fafc;
|
||||||
--color-card: #FFFFFF;
|
--color-text: #18181b;
|
||||||
--color-border: #D4E4DF;
|
--color-muted: #60646c;
|
||||||
--color-muted: #4A6A63;
|
--color-border: #d4d4d8;
|
||||||
--radius: 10px;
|
--color-link: #1d4ed8;
|
||||||
--space-2: 0.5rem;
|
--color-primary: #1d4ed8;
|
||||||
--space-3: 0.75rem;
|
--color-danger: #b42318;
|
||||||
--space-4: 1rem;
|
--color-warning: #a15c07;
|
||||||
--space-6: 1.5rem;
|
--color-success: #067647;
|
||||||
|
--color-focus: #2563eb;
|
||||||
|
--color-log-bg: #111827;
|
||||||
|
--color-log-text: #e5e7eb;
|
||||||
|
|
||||||
|
--space-base: 0.25rem;
|
||||||
|
--space-xs: var(--space-base);
|
||||||
|
--space-s: calc(var(--space-base) * 2);
|
||||||
|
--space-m: calc(var(--space-base) * 3);
|
||||||
|
--space-l: calc(var(--space-base) * 4);
|
||||||
|
--space-xl: calc(var(--space-base) * 6);
|
||||||
|
--space-2xl: calc(var(--space-base) * 8);
|
||||||
|
|
||||||
|
--radius-base: 0.25rem;
|
||||||
|
--radius-s: var(--radius-base);
|
||||||
|
--radius-m: calc(var(--radius-base) * 2);
|
||||||
|
|
||||||
|
--line: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-bg: #18181b;
|
||||||
|
--color-surface: #27272a;
|
||||||
|
--color-surface-muted: #1f1f23;
|
||||||
|
--color-text: #f4f4f5;
|
||||||
|
--color-muted: #a1a1aa;
|
||||||
|
--color-border: #3f3f46;
|
||||||
|
--color-link: #93c5fd;
|
||||||
|
--color-primary: #93c5fd;
|
||||||
|
--color-danger: #fca5a5;
|
||||||
|
--color-warning: #fcd34d;
|
||||||
|
--color-success: #86efac;
|
||||||
|
--color-focus: #bfdbfe;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,22 @@
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<div class="site-header-inner">
|
<div class="site-header-inner">
|
||||||
|
<nav class="primary-nav" aria-label="Main navigation">
|
||||||
<a class="brand" href="/dashboard">left4me</a>
|
<a class="brand" href="/dashboard">left4me</a>
|
||||||
<nav>
|
<a href="/servers">servers</a>
|
||||||
<a href="/dashboard">Dashboard</a>
|
<a href="/blueprints">blueprints</a>
|
||||||
<a href="/admin/overlays">Overlays</a>
|
<a href="/overlays">overlays</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% if g.user %}
|
||||||
|
<nav class="account-nav" aria-label="Account navigation">
|
||||||
|
{% if g.user.admin %}<a href="/admin">admin</a>{% endif %}
|
||||||
|
<span class="muted">{{ g.user.username }}</span>
|
||||||
|
<form method="post" action="/logout" class="inline-form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
|
||||||
|
<button class="link-button" type="submit">logout</button>
|
||||||
|
</form>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="container">
|
<main class="container">
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,8 @@
|
||||||
{% block title %}Dashboard | left4me{% endblock %}
|
{% block title %}Dashboard | left4me{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="card">
|
<section class="panel">
|
||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
<p class="muted">Status refresh every {{ refresh_seconds }}s.</p>
|
<p class="muted">Use the navigation to manage servers, blueprints, and overlays.</p>
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr><th>Name</th><th>Port</th><th>Desired</th><th>Actual</th><th></th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for server in servers %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ server.name }}</td>
|
|
||||||
<td>{{ server.port }}</td>
|
|
||||||
<td>{{ server.desired_state }}</td>
|
|
||||||
<td>{{ server.actual_state }}</td>
|
|
||||||
<td><a href="/servers/{{ server.id }}">View</a></td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
<tr><td colspan="5" class="muted">No servers yet.</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from l4d2web.app import create_app
|
from l4d2web.app import create_app
|
||||||
from l4d2web.auth import hash_password
|
from l4d2web.auth import hash_password
|
||||||
|
|
@ -59,11 +60,52 @@ def user_client_and_other_blueprint(tmp_path, monkeypatch):
|
||||||
return client, blueprint_id
|
return client, blueprint_id
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_renders_server_and_status(auth_client_with_server) -> None:
|
def test_dashboard_is_simple_landing_page(auth_client_with_server) -> None:
|
||||||
response = auth_client_with_server.get("/dashboard")
|
response = auth_client_with_server.get("/dashboard")
|
||||||
text = response.get_data(as_text=True)
|
text = response.get_data(as_text=True)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "alpha" in text
|
assert "Dashboard" in text
|
||||||
|
assert "Use the navigation to manage servers, blueprints, and overlays." in text
|
||||||
|
assert "alpha" not in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_shell_nav_uses_main_sections(auth_client_with_server) -> None:
|
||||||
|
response = auth_client_with_server.get("/dashboard")
|
||||||
|
text = response.get_data(as_text=True)
|
||||||
|
|
||||||
|
assert 'href="/dashboard"' in text
|
||||||
|
assert 'href="/servers"' in text
|
||||||
|
assert 'href="/blueprints"' in text
|
||||||
|
assert 'href="/overlays"' in text
|
||||||
|
assert 'action="/logout"' in text
|
||||||
|
assert "csrf_token" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_tokens_define_neutral_light_and_dark_theme() -> None:
|
||||||
|
css = Path("l4d2web/static/css/tokens.css").read_text()
|
||||||
|
|
||||||
|
for token in [
|
||||||
|
"--color-bg",
|
||||||
|
"--color-surface",
|
||||||
|
"--color-text",
|
||||||
|
"--color-muted",
|
||||||
|
"--color-border",
|
||||||
|
"--color-link",
|
||||||
|
"--space-base",
|
||||||
|
"--space-xs",
|
||||||
|
"--space-s",
|
||||||
|
"--space-m",
|
||||||
|
"--space-l",
|
||||||
|
"--space-xl",
|
||||||
|
"--space-2xl",
|
||||||
|
"--radius-s",
|
||||||
|
"--radius-m",
|
||||||
|
"--line",
|
||||||
|
]:
|
||||||
|
assert token in css
|
||||||
|
assert "prefers-color-scheme: dark" in css
|
||||||
|
assert "radial-gradient" not in Path("l4d2web/static/css/layout.css").read_text()
|
||||||
|
|
||||||
|
|
||||||
def test_blueprint_page_private_visibility(user_client_and_other_blueprint) -> None:
|
def test_blueprint_page_private_visibility(user_client_and_other_blueprint) -> None:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue