aboutsummaryrefslogtreecommitdiffstats
path: root/templates
diff options
context:
space:
mode:
authorGravatar Tom van der Lee <tom@vanderlee.io>2026-07-30 16:03:32 +0200
committerGravatar Tom van der Lee <tom@vanderlee.io>2026-07-30 16:03:32 +0200
commit2d22158dd95f76399f5716d556be60c77da04a46 (patch)
tree15fca3adae0f44674fe06bc141879804659c3664 /templates
parentaa56bc05285981d57c5adb2cee3c4e898fb2a702 (diff)
downloadserver-2d22158dd95f76399f5716d556be60c77da04a46.tar.gz
server-2d22158dd95f76399f5716d556be60c77da04a46.tar.bz2
server-2d22158dd95f76399f5716d556be60c77da04a46.zip
Admin ui
Diffstat (limited to 'templates')
-rw-r--r--templates/_styles.html539
-rw-r--r--templates/global.html19
-rw-r--r--templates/macros.html49
3 files changed, 607 insertions, 0 deletions
diff --git a/templates/_styles.html b/templates/_styles.html
new file mode 100644
index 0000000..f68b9c0
--- /dev/null
+++ b/templates/_styles.html
@@ -0,0 +1,539 @@
1:root,
2:root[data-theme="dark"] {
3 --bg: #12161c;
4 --surface: #1a2029;
5 --surface-raised: #212836;
6 --border: #2a323d;
7 --text: #e7ebf1;
8 --text-muted: #8891a0;
9 --accent: #f5a623;
10 --accent-strong: #ffbb47;
11 --accent-ink: #1a1305;
12 --status-good: #3ddc84;
13 --status-bad: #e5484d;
14 color-scheme: dark;
15}
16
17:root[data-theme="light"] {
18 --bg: #eef1f6;
19 --surface: #ffffff;
20 --surface-raised: #f7f9fc;
21 --border: #d7dce3;
22 --text: #12161c;
23 --text-muted: #5b6472;
24 --accent: #b9750f;
25 --accent-strong: #8f5c0c;
26 --accent-ink: #fff8ec;
27 --status-good: #1e9a5a;
28 --status-bad: #c53328;
29 color-scheme: light;
30}
31
32*,
33*::before,
34*::after {
35 box-sizing: border-box;
36}
37
38html {
39 -webkit-text-size-adjust: 100%;
40}
41
42body {
43 background: var(--bg);
44 color: var(--text);
45 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
46 font-size: 15px;
47 line-height: 1.6;
48 min-height: 100vh;
49}
50
51body,
52.pulse-line,
53.theme-toggle,
54.field__input,
55.btn,
56.module-card {
57 transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
58}
59
60.mono {
61 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
62}
63
64.eyebrow {
65 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
66 font-size: 11px;
67 letter-spacing: 0.08em;
68 text-transform: uppercase;
69 color: var(--text-muted);
70}
71
72.muted {
73 color: var(--text-muted);
74}
75
76.small {
77 font-size: 13px;
78}
79
80a {
81 color: var(--accent);
82 text-decoration: none;
83}
84
85a:hover {
86 text-decoration: underline;
87}
88
89h1 {
90 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
91 font-size: 26px;
92 font-weight: 600;
93 letter-spacing: -0.01em;
94}
95
96h2 {
97 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
98 font-size: 18px;
99 font-weight: 600;
100}
101
102/* signature element: one-time handshake pulse on load */
103.pulse-line {
104 position: fixed;
105 top: 0;
106 left: 0;
107 right: 0;
108 height: 2px;
109 z-index: 40;
110 background: linear-gradient(90deg, transparent, var(--accent), transparent);
111 background-size: 200% 100%;
112 animation: pulse-sweep 1.4s ease-out 1;
113}
114
115@keyframes pulse-sweep {
116 from { background-position: 160% 0; }
117 to { background-position: -60% 0; }
118}
119
120/* theme toggle */
121.theme-toggle {
122 position: fixed;
123 top: 16px;
124 right: 16px;
125 z-index: 50;
126 display: flex;
127 align-items: center;
128 justify-content: center;
129 width: 36px;
130 height: 36px;
131 border-radius: 8px;
132 border: 1px solid var(--border);
133 background: var(--surface);
134 color: var(--text-muted);
135 cursor: pointer;
136}
137
138.theme-toggle:hover {
139 color: var(--accent);
140 border-color: var(--accent);
141}
142
143.theme-toggle:focus-visible {
144 outline: 2px solid var(--accent);
145 outline-offset: 2px;
146}
147
148.theme-toggle .icon-sun { display: none; }
149.theme-toggle .icon-moon { display: block; }
150:root[data-theme="light"] .theme-toggle .icon-sun { display: block; }
151:root[data-theme="light"] .theme-toggle .icon-moon { display: none; }
152
153/* focus ring, used everywhere */
154a:focus-visible,
155button:focus-visible,
156input:focus-visible {
157 outline: 2px solid var(--accent);
158 outline-offset: 2px;
159}
160
161/* buttons */
162.btn {
163 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
164 font-size: 14px;
165 font-weight: 600;
166 padding: 10px 18px;
167 border-radius: 8px;
168 border: 1px solid transparent;
169 cursor: pointer;
170 display: inline-flex;
171 align-items: center;
172 gap: 8px;
173}
174
175.btn--primary {
176 background: var(--accent);
177 color: var(--accent-ink);
178}
179
180.btn--primary:hover {
181 background: var(--accent-strong);
182}
183
184.btn--full {
185 width: 100%;
186 justify-content: center;
187}
188
189/* form fields */
190.field {
191 margin-bottom: 18px;
192}
193
194.field__label {
195 display: block;
196 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
197 font-size: 11px;
198 letter-spacing: 0.08em;
199 text-transform: uppercase;
200 color: var(--text-muted);
201 margin-bottom: 6px;
202}
203
204.field__input {
205 width: 100%;
206 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
207 font-size: 14px;
208 padding: 10px 12px;
209 background: var(--bg);
210 color: var(--text);
211 border: 1px solid var(--border);
212 border-radius: 8px;
213}
214
215.field__input:focus {
216 outline: 2px solid var(--accent);
217 outline-offset: 1px;
218 border-color: var(--accent);
219}
220
221.field__input--error {
222 border-color: var(--status-bad);
223}
224
225.field__error {
226 margin-top: 6px;
227 font-size: 13px;
228 color: var(--status-bad);
229}
230
231/* auth split-panel shell */
232.auth-page {
233 min-height: 100vh;
234 display: flex;
235 align-items: center;
236 justify-content: center;
237 padding: 32px 16px;
238}
239
240.auth-shell {
241 display: grid;
242 grid-template-columns: 1fr 1fr;
243 max-width: 880px;
244 width: 100%;
245 border: 1px solid var(--border);
246 border-radius: 16px;
247 overflow: hidden;
248}
249
250.auth-hero {
251 background: var(--surface-raised);
252 padding: 40px 32px;
253 display: flex;
254 flex-direction: column;
255 justify-content: space-between;
256 gap: 32px;
257 border-right: 1px solid var(--border);
258}
259
260.auth-hero__brand {
261 display: flex;
262 align-items: center;
263 gap: 10px;
264 color: var(--accent);
265 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
266 font-size: 18px;
267 font-weight: 600;
268}
269
270.auth-hero__diagram {
271 display: flex;
272 align-items: center;
273 gap: 10px;
274 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
275 font-size: 13px;
276 color: var(--text);
277}
278
279.auth-hero__diagram .connector {
280 color: var(--accent);
281 flex: 1;
282}
283
284.auth-hero__diagram span.mono {
285 background: var(--surface);
286 border: 1px solid var(--border);
287 padding: 4px 8px;
288 border-radius: 6px;
289 white-space: nowrap;
290}
291
292.auth-hero__tagline {
293 font-size: 14px;
294 color: var(--text-muted);
295 line-height: 1.5;
296}
297
298.auth-form {
299 background: var(--surface);
300 padding: 40px 32px;
301 display: flex;
302 flex-direction: column;
303 justify-content: center;
304}
305
306.auth-form h1 {
307 margin-bottom: 4px;
308}
309
310.auth-form__intro {
311 color: var(--text-muted);
312 font-size: 14px;
313 margin-bottom: 24px;
314}
315
316.auth-switch {
317 margin-top: 16px;
318 font-size: 13px;
319 color: var(--text-muted);
320}
321
322/* management top bar */
323.nav {
324 display: flex;
325 align-items: center;
326 justify-content: space-between;
327 gap: 16px;
328 padding: 14px 24px;
329 background: var(--surface-raised);
330 border-bottom: 1px solid var(--border);
331 flex-wrap: wrap;
332}
333
334.nav__brand {
335 display: flex;
336 align-items: center;
337 gap: 8px;
338 color: var(--accent);
339 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
340 font-weight: 600;
341 font-size: 16px;
342}
343
344.nav__links {
345 display: flex;
346 align-items: center;
347 gap: 20px;
348}
349
350.nav__link {
351 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
352 font-size: 12px;
353 letter-spacing: 0.05em;
354 text-transform: uppercase;
355 color: var(--text-muted);
356 padding-bottom: 2px;
357 border-bottom: 2px solid transparent;
358}
359
360.nav__link:hover {
361 color: var(--text);
362 text-decoration: none;
363}
364
365.nav__link--active {
366 color: var(--accent);
367 border-bottom-color: var(--accent);
368}
369
370.nav__actions {
371 display: flex;
372 align-items: center;
373 gap: 20px;
374 margin-left: auto;
375}
376
377.nav__status {
378 display: flex;
379 align-items: center;
380 gap: 8px;
381}
382
383.status-dot {
384 width: 8px;
385 height: 8px;
386 border-radius: 50%;
387 display: inline-block;
388}
389
390.status-dot--good {
391 background: var(--status-good);
392 box-shadow: 0 0 0 3px color-mix(in srgb, var(--status-good) 25%, transparent);
393}
394
395.status-dot--bad {
396 background: var(--status-bad);
397 box-shadow: 0 0 0 3px color-mix(in srgb, var(--status-bad) 25%, transparent);
398}
399
400/* content area */
401.content {
402 max-width: 960px;
403 margin: 0 auto;
404 padding: 40px 24px;
405}
406
407.content__header {
408 margin-bottom: 28px;
409}
410
411.content__header h1 {
412 margin-top: 4px;
413}
414
415/* dashboard module grid */
416.module-grid {
417 display: grid;
418 grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
419 gap: 16px;
420}
421
422.module-card {
423 display: block;
424 background: var(--surface);
425 border: 1px solid var(--border);
426 border-radius: 12px;
427 padding: 20px;
428}
429
430.module-card:hover {
431 border-color: var(--accent);
432 background: var(--surface-raised);
433 text-decoration: none;
434}
435
436.module-card__title {
437 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
438 font-size: 15px;
439 font-weight: 600;
440 color: var(--text);
441 margin-bottom: 6px;
442}
443
444.module-card__desc {
445 font-size: 13px;
446 color: var(--text-muted);
447 line-height: 1.5;
448}
449
450.module-card__meta {
451 margin-top: 12px;
452 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
453 font-size: 12px;
454 color: var(--accent);
455}
456
457/* data table */
458.table-wrap {
459 overflow-x: auto;
460 border: 1px solid var(--border);
461 border-radius: 12px;
462}
463
464table {
465 width: 100%;
466 border-collapse: collapse;
467}
468
469thead th {
470 text-align: left;
471 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
472 font-size: 11px;
473 letter-spacing: 0.08em;
474 text-transform: uppercase;
475 color: var(--text-muted);
476 padding: 12px 16px;
477 border-bottom: 1px solid var(--border);
478 background: var(--surface-raised);
479}
480
481tbody td {
482 padding: 12px 16px;
483 border-bottom: 1px solid var(--border);
484 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
485 font-size: 13px;
486}
487
488tbody tr:last-child td {
489 border-bottom: none;
490}
491
492tbody tr:hover {
493 background: var(--surface-raised);
494}
495
496@media (max-width: 720px) {
497 .auth-shell {
498 grid-template-columns: 1fr;
499 }
500
501 .auth-hero {
502 border-right: none;
503 border-bottom: 1px solid var(--border);
504 padding: 24px;
505 }
506
507 .auth-hero__diagram {
508 display: none;
509 }
510
511 .auth-form {
512 padding: 24px;
513 }
514
515 .nav {
516 gap: 12px;
517 }
518
519 .nav__actions {
520 margin-left: 0;
521 }
522}
523
524@media (prefers-reduced-motion: reduce) {
525 .pulse-line {
526 animation: none;
527 background: var(--accent);
528 opacity: 0.6;
529 }
530
531 body,
532 .pulse-line,
533 .theme-toggle,
534 .field__input,
535 .btn,
536 .module-card {
537 transition: none;
538 }
539}
diff --git a/templates/global.html b/templates/global.html
index d9bd29d..97212df 100644
--- a/templates/global.html
+++ b/templates/global.html
@@ -1,8 +1,17 @@
1{% import 'macros.html' as ui %}
1<!DOCTYPE html> 2<!DOCTYPE html>
2<html lang="en"> 3<html lang="en">
3<head> 4<head>
4 <meta charset="UTF-8"> 5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1">
5 <title>TTUN</title> 7 <title>TTUN</title>
8 <script>
9 (function () {
10 var stored = localStorage.getItem('ttun-theme');
11 var theme = stored || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
12 document.documentElement.setAttribute('data-theme', theme);
13 })();
14 </script>
6 <style> 15 <style>
7/* http://meyerweb.com/eric/tools/css/reset/ 16/* http://meyerweb.com/eric/tools/css/reset/
8 v2.0 | 20110126 17 v2.0 | 20110126
@@ -52,9 +61,19 @@ table {
52 border-collapse: collapse; 61 border-collapse: collapse;
53 border-spacing: 0; 62 border-spacing: 0;
54} 63}
64{% include '_styles.html' %}
55 </style> 65 </style>
56</head> 66</head>
57<body> 67<body>
68 <div class="pulse-line" aria-hidden="true"></div>
69 <button class="theme-toggle" type="button" aria-label="Toggle color theme" onclick="
70 var next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
71 document.documentElement.setAttribute('data-theme', next);
72 localStorage.setItem('ttun-theme', next);
73 ">
74 {{ ui.sun_icon() }}
75 {{ ui.moon_icon() }}
76 </button>
58 {% block template %}{% endblock %} 77 {% block template %}{% endblock %}
59</body> 78</body>
60</html> 79</html>
diff --git a/templates/macros.html b/templates/macros.html
new file mode 100644
index 0000000..7c35f5e
--- /dev/null
+++ b/templates/macros.html
@@ -0,0 +1,49 @@
1{% macro connector_icon(size=16) %}
2<svg width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24" fill="none" aria-hidden="true">
3 <circle cx="4" cy="12" r="3" fill="currentColor" />
4 <line x1="7" y1="12" x2="17" y2="12" stroke="currentColor" stroke-width="2" stroke-dasharray="1 4" stroke-linecap="round" />
5 <circle cx="20" cy="12" r="3" fill="currentColor" />
6</svg>
7{% endmacro %}
8
9{% macro sun_icon(size=16) %}
10<svg class="icon-sun" width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24" fill="none" aria-hidden="true">
11 <circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="2" />
12 <path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
13</svg>
14{% endmacro %}
15
16{% macro moon_icon(size=16) %}
17<svg class="icon-moon" width="{{ size }}" height="{{ size }}" viewBox="0 0 24 24" fill="none" aria-hidden="true">
18 <path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
19</svg>
20{% endmacro %}
21
22{% macro field(name, type='text', label='', value='', error='', placeholder='') %}
23<div class="field">
24 <label class="field__label" for="field-{{ name }}">{{ label }}</label>
25 <input
26 class="field__input{% if error %} field__input--error{% endif %}"
27 type="{{ type }}"
28 id="field-{{ name }}"
29 name="{{ name }}"
30 value="{{ value or '' }}"
31 placeholder="{{ placeholder }}"
32 {% if error %}aria-invalid="true" aria-describedby="field-{{ name }}-error"{% endif %}
33 />
34 {% if error %}
35 <p class="field__error" id="field-{{ name }}-error">{{ error }}</p>
36 {% endif %}
37</div>
38{% endmacro %}
39
40{% macro button(text, variant='primary', type='submit') %}
41<button class="btn btn--{{ variant }} btn--full" type="{{ type }}">{{ text }}</button>
42{% endmacro %}
43
44{% macro status_badge(label, variant='good') %}
45<span class="nav__status">
46 <span class="status-dot status-dot--{{ variant }}" aria-hidden="true"></span>
47 <span class="mono small muted">{{ label }}</span>
48</span>
49{% endmacro %}