Files
weircon-front/templates/base.html
T

254 lines
15 KiB
HTML

<!doctype html>
<html lang="da" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0b0f17" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#f6f8fb" media="(prefers-color-scheme: light)" />
<link rel="icon" href="/static/icons/icon.svg" type="image/svg+xml" />
<title>{% block title %}Weircon{% endblock %}</title>
<script>
(function () {
var t = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
document.documentElement.dataset.theme = t;
document.addEventListener('click', function (e) {
var b = e.target.closest('[data-theme-toggle]');
if (!b) return;
var next = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);
});
})();
</script>
<style>
@font-face {
font-family: "Inter Variable"; font-style: normal; font-weight: 100 900;
font-display: swap; src: url("/static/fonts/inter.woff2") format("woff2");
}
:root, html[data-theme="dark"] {
--bg:#0b0f17; --surface:#121a27; --surface2:#182232; --border:#243044;
--text:#e6e9ef; --muted:#97a3b6; --accent:#38bdf8; --accent2:#818cf8;
}
html[data-theme="light"] {
--bg:#f6f8fb; --surface:#ffffff; --surface2:#eef2f8; --border:#dde4ee;
--text:#0f1729; --muted:#54617a; --accent:#0284c7; --accent2:#4f46e5;
}
* { box-sizing:border-box; }
html { scroll-behavior:smooth; color-scheme:dark; }
html[data-theme="light"] { color-scheme:light; }
body {
margin:0; min-height:100vh; display:flex; flex-direction:column;
background:var(--bg); color:var(--text);
font:16px/1.7 "Inter Variable", ui-sans-serif, system-ui, -apple-system, sans-serif;
-webkit-font-smoothing:antialiased;
}
body, a, span, div, p, h1, h2, .card, input, button, nav { transition: background-color .35s ease, border-color .35s ease, color .35s ease; }
body::before {
content:""; position:fixed; inset:0; z-index:-1; pointer-events:none;
background:
radial-gradient(60rem 30rem at 70% -10%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 70%),
radial-gradient(50rem 30rem at 10% 0%, color-mix(in srgb, var(--accent2) 10%, transparent), transparent 70%);
}
::selection { background: color-mix(in srgb, var(--accent) 35%, transparent); }
a { color:inherit; }
.wrap { width:100%; max-width:960px; margin:0 auto; padding:2.5rem 1.25rem 4rem; flex:1; }
/* nav */
nav { display:flex; align-items:center; gap:1.1rem; width:100%; max-width:960px; margin:0 auto; padding:1.1rem 1.25rem; }
nav .brand { font-weight:800; font-size:1.15rem; letter-spacing:-.02em; }
nav .brand .dot { color:var(--accent); }
nav .links { display:flex; gap:1.1rem; }
nav a { text-decoration:none; color:var(--muted); font-size:.95rem; font-weight:500; }
nav a:hover { color:var(--text); }
nav .spacer { flex:1; }
nav .who { color:var(--muted); font-size:.9rem; }
/* typografi */
h1 { font-size:2.2rem; letter-spacing:-.025em; margin:.6rem 0 .35rem; font-weight:800; }
h2 { font-size:.8rem; text-transform:uppercase; letter-spacing:.14em; color:var(--muted); margin:2.6rem 0 1rem; font-weight:600; }
.lead { color:var(--muted); font-size:1.05rem; max-width:46rem; }
/* filter */
.filter { margin:1.4rem 0 .2rem; position:relative; max-width:22rem; }
.filter input { padding-left:2.2rem; }
.filter .mag { position:absolute; left:.7rem; top:50%; transform:translateY(-50%); color:var(--muted); }
/* card-grid med ENS størrelser */
.grid { display:grid; gap:.9rem; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); margin-top:1rem; }
.card {
display:flex; flex-direction:column; min-height:7.25rem; height:100%;
background:color-mix(in srgb, var(--surface) 70%, transparent);
border:1px solid var(--border); border-radius:1rem; padding:1.1rem 1.15rem;
text-decoration:none; color:inherit;
transition:border-color .18s ease, transform .18s ease, background .18s ease;
}
a.card:hover { border-color:var(--accent); transform:translateY(-3px);
background:color-mix(in srgb, var(--surface) 92%, transparent); }
.card.hidden { display:none; }
.card .head { display:flex; align-items:center; gap:.65rem; }
.card .ico {
flex:0 0 auto; width:2.1rem; height:2.1rem; border-radius:.6rem;
display:grid; place-items:center; font-weight:700; font-size:1rem;
background:color-mix(in srgb, var(--accent) 16%, transparent);
color:var(--accent); overflow:hidden;
}
.card .ico img { width:100%; height:100%; object-fit:contain; }
.card .name { font-weight:600; letter-spacing:-.01em; line-height:1.25; }
.card .sub { color:var(--muted); font-size:.86rem; margin-top:.6rem; flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.card .go { color:var(--accent); font-size:.8rem; margin-top:.7rem; opacity:.85; }
/* knapper / forms */
.btn { display:inline-block; background:var(--accent); color:#06101f; font-weight:600;
text-decoration:none; padding:.6rem 1.15rem; border-radius:.7rem; border:0; cursor:pointer; font:inherit; font-size:.95rem; }
.btn:hover { filter:brightness(1.08); }
.btn.secondary { background:var(--surface2); color:var(--text); border:1px solid var(--border); }
table { width:100%; border-collapse:collapse; }
th, td { border:1px solid var(--border); padding:.45rem .55rem; text-align:left; }
th { color:var(--muted); font-weight:600; font-size:.8rem; text-transform:uppercase; letter-spacing:.06em; }
input, textarea { width:100%; background:var(--surface2); border:1px solid var(--border); color:var(--text);
border-radius:.55rem; padding:.5rem .65rem; font:inherit; }
input:focus, textarea:focus { outline:none; border-color:var(--accent); }
textarea { min-height:5rem; resize:vertical; }
.flash { background:color-mix(in srgb, var(--accent) 12%, transparent); border:1px solid var(--accent);
color:var(--accent); border-radius:.7rem; padding:.65rem .95rem; margin:1rem 0; font-size:.9rem; }
.err { background:color-mix(in srgb, #f5a623 14%, transparent); border:1px solid #f5a623;
color:#f5a623; border-radius:.7rem; padding:.65rem .95rem; margin:1rem 0; font-size:.9rem; }
.muted { color:var(--muted); }
code { background:var(--surface2); padding:.12em .42em; border-radius:.35rem; font-size:.85em; }
.stat { background:color-mix(in srgb, var(--surface) 70%, transparent); border:1px solid var(--border); border-radius:1rem; padding:1.3rem; }
.stat .n { font-size:2.1rem; font-weight:800; letter-spacing:-.02em; }
/* footer */
.foot { width:100%; max-width:960px; margin:0 auto; padding:1.6rem 1.25rem 2.5rem; color:var(--muted); font-size:.85rem; border-top:1px solid var(--border); }
.foot a { color:var(--accent); text-decoration:none; }
/* tema-toggle (sol/måne) — ført videre fra asger.weirsøe.dk */
.theme-toggle { display:inline-grid; place-items:center; width:2.25rem; height:2.25rem; border-radius:.75rem;
color:var(--muted); background:transparent; cursor:pointer; border:1px solid transparent; }
.theme-toggle:hover { color:var(--text); background:var(--surface2); }
.theme-toggle:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.sun-and-moon > :is(.moon, .sun, .sun-beams) { transform-origin:center; }
.sun-and-moon > :is(.moon, .sun) { fill:currentColor; }
.sun-and-moon > .sun-beams { stroke:currentColor; stroke-width:2px; }
.sun-and-moon > .sun { transition:transform .5s cubic-bezier(.5,1.25,.75,1.25); }
.sun-and-moon > .sun-beams { transition:transform .5s cubic-bezier(.5,1.5,.75,1.25), opacity .5s cubic-bezier(.25,0,.3,1); }
.sun-and-moon .moon > circle { transition:transform .25s cubic-bezier(0,0,0,1); }
@supports (cx:1) { .sun-and-moon .moon > circle { transition:cx .25s cubic-bezier(0,0,0,1); } }
[data-theme="dark"] .sun-and-moon > .sun { transform:scale(1.75); transition-timing-function:cubic-bezier(.25,0,.3,1); transition-duration:.25s; }
[data-theme="dark"] .sun-and-moon > .sun-beams { opacity:0; transform:rotateZ(-25deg); transition-duration:.15s; }
[data-theme="dark"] .sun-and-moon > .moon > circle { transform:translateX(-7px); transition-delay:.25s; transition-duration:.5s; }
@supports (cx:1) { [data-theme="dark"] .sun-and-moon > .moon > circle { cx:17; transform:translateX(0); } }
@media (prefers-reduced-motion: reduce) { .sun-and-moon *, body, .card { transition:none !important; } }
</style>
</head>
<body>
<nav>
<span class="brand">weircon<span class="dot">.</span></span>
<div class="links">
<a href="/">Forside</a>
{% if is_admin %}<a href="/admin">Admin</a>{% endif %}
</div>
<span class="spacer"></span>
{% if user %}<span class="who">{{ user.name }}</span> <a href="/auth/logout">Log ud</a>{% endif %}
<button type="button" data-theme-toggle class="theme-toggle" title="Skift mellem lyst og mørkt tema" aria-label="Skift tema">
<svg class="sun-and-moon" aria-hidden="true" width="22" height="22" viewBox="0 0 24 24">
<mask class="moon" id="moon-mask"><rect x="0" y="0" width="100%" height="100%" fill="white" /><circle cx="24" cy="10" r="6" fill="black" /></mask>
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
<g class="sun-beams" stroke="currentColor">
<line x1="12" y1="1" x2="12" y2="3" /><line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" /><line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" /><line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" /><line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</g>
</svg>
</button>
</nav>
<div class="wrap">
{% block body %}{% endblock %}
</div>
<footer class="foot">
Driftet af Asger Weirsøe · self-hosted på Proxmox · <a href="mailto:asger@weirsoe.dk">asger@weirsoe.dk</a>
</footer>
<!-- ⌘K / Ctrl+K kommandopalette — ført videre fra asger.weirsøe.dk, tilpasset portalens apps -->
<div id="palette" class="palette-overlay" hidden>
<div class="palette-box" role="dialog" aria-modal="true" aria-label="Kommandopalette">
<input id="palette-input" class="palette-input" type="text" placeholder="Søg tjenester, sider eller kommandoer…" autocomplete="off" spellcheck="false" />
<ul id="palette-list" class="palette-list" role="listbox"></ul>
<div class="palette-hint">↑↓ navigér · ↵ vælg · esc luk · ⌘/Ctrl K åbn</div>
</div>
</div>
<style>
.palette-overlay { position:fixed; inset:0; z-index:100;
background:color-mix(in srgb, var(--bg) 60%, transparent); backdrop-filter:blur(4px);
display:flex; align-items:flex-start; justify-content:center; padding-top:15vh; }
.palette-overlay[hidden] { display:none; }
.palette-box { width:min(36rem, calc(100vw - 2rem)); border:1px solid var(--border); border-radius:1rem;
background:var(--surface); box-shadow:0 24px 64px rgb(0 0 0 / .45); overflow:hidden; }
.palette-input { width:100%; padding:.9rem 1.1rem; font-size:1rem; background:transparent; color:var(--text);
border:0; outline:none; border-bottom:1px solid var(--border); }
.palette-list { max-height:50vh; overflow-y:auto; padding:.35rem; margin:0; list-style:none; }
.palette-item { display:flex; justify-content:space-between; align-items:center; gap:1rem;
padding:.55rem .8rem; border-radius:.6rem; cursor:pointer; font-size:.925rem; }
.palette-item.is-selected, .palette-item:hover:not(.palette-empty) { background:var(--surface2); }
.palette-empty { color:var(--muted); cursor:default; }
.palette-badge { font-size:.7rem; color:var(--muted); border:1px solid var(--border); border-radius:999px; padding:.05rem .5rem; }
.palette-hint { padding:.5rem 1.1rem; font-size:.72rem; color:var(--muted); border-top:1px solid var(--border); }
</style>
<script>
(function () {
if (window.__palette) return; window.__palette = true;
var index = null, items = [], sel = 0;
var staticActions = [
{ type:'action', title:'Tema: mørkt', run:function(){ setTheme('dark'); } },
{ type:'action', title:'Tema: lyst', run:function(){ setTheme('light'); } },
{ type:'action', title:'Log ud', url:'/auth/logout' },
];
function setTheme(t){ document.documentElement.dataset.theme = t; localStorage.setItem('theme', t); }
function el(id){ return document.getElementById(id); }
function ensureIndex(){
if (index) return Promise.resolve();
return fetch('/api/search-index').then(function(r){ return r.json(); })
.then(function(j){ index = j; }).catch(function(){ index = []; });
}
function score(q, it){
var hay = (it.title + ' ' + (it.extra || '')).toLowerCase();
if (!q) return (it.type === 'page' || it.type === 'action') ? 1 : 0.5;
if (hay.indexOf(q) === 0) return 3;
if (hay.indexOf(q) > -1) return 2;
var i = 0; for (var k = 0; k < hay.length; k++) if (hay[k] === q[i]) i++;
return i === q.length ? 1 : 0;
}
function refresh(){
var q = el('palette-input').value.trim().toLowerCase();
items = (index || []).concat(staticActions)
.map(function(it){ return { it: it, s: score(q, it) }; })
.filter(function(x){ return x.s > 0; })
.sort(function(a, b){ return b.s - a.s; })
.slice(0, 9).map(function(x){ return x.it; });
sel = 0;
el('palette-list').innerHTML = items.map(function(it, i){
var badge = it.type === 'app' ? 'app' : it.type === 'action' ? 'kommando' : it.ext ? 'link' : 'side';
return '<li class="palette-item' + (i === sel ? ' is-selected' : '') + '" data-i="' + i + '" role="option">' +
'<span>' + String(it.title).replace(/</g, '&lt;') + '</span><span class="palette-badge">' + badge + '</span></li>';
}).join('') || '<li class="palette-item palette-empty">Ingen resultater</li>';
}
function paint(){ var ls = el('palette-list').querySelectorAll('.palette-item');
for (var i = 0; i < ls.length; i++) ls[i].classList.toggle('is-selected', i === sel); }
function choose(i){ var it = items[i]; if (!it) return; close();
if (it.run) it.run(); else if (it.ext) window.open(it.url, '_blank', 'noopener'); else window.location.href = it.url; }
function open(){ ensureIndex().then(function(){ el('palette').hidden = false; el('palette-input').value = ''; refresh(); el('palette-input').focus(); }); }
function close(){ var p = el('palette'); if (p) p.hidden = true; }
document.addEventListener('keydown', function(e){
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k'){ e.preventDefault(); el('palette').hidden ? open() : close(); return; }
var p = el('palette'); if (!p || p.hidden) return;
if (e.key === 'Escape') close();
else if (e.key === 'ArrowDown'){ e.preventDefault(); sel = Math.min(sel + 1, items.length - 1); paint(); }
else if (e.key === 'ArrowUp'){ e.preventDefault(); sel = Math.max(sel - 1, 0); paint(); }
else if (e.key === 'Enter'){ e.preventDefault(); choose(sel); }
});
document.addEventListener('input', function(e){ if (e.target && e.target.id === 'palette-input') refresh(); });
document.addEventListener('click', function(e){
if (e.target === el('palette')) close();
var li = e.target.closest && e.target.closest('.palette-item[data-i]');
if (li) choose(+li.dataset.i);
});
})();
</script>
</body>
</html>