Files
linkvault/app/templates/base.html
Erik Thiele 4f99d51f9e Redesign header: theme toggle, user avatar, burger menu
The app was dark-only with the email shown as plain text and a bare
"Abmelden" button. Header now has:

- A light/dark theme toggle (sun/moon icon). Choice persists via
  localStorage and is applied before first paint (inline script in
  <head>) to avoid a flash of the wrong theme; falls back to the
  system preference on first visit. Added a light CSS variable set
  alongside the existing dark palette.
- A circular user icon (head-and-shoulders) showing the email as a
  tooltip, replacing the plain-text address.
- A burger-menu dropdown with the email, disabled placeholder items
  ("Tags verwalten", "Einstellungen" — marked "bald") to make room
  for future features, and the actual Abmelden action.

Dropdown opens/closes via a small vanilla-JS handler in base.html
(click outside or Escape closes it), consistent with the project's
no-build-step, mostly-HTMX frontend.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 23:01:22 +02:00

251 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}LinkVault{% endblock %}</title>
<script>
/* Theme so früh wie möglich setzen, damit die Seite nicht kurz hell aufblitzt. */
(function () {
try {
var stored = localStorage.getItem('linkvault-theme');
if (stored === 'light' || stored === 'dark') {
document.documentElement.setAttribute('data-theme', stored);
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
document.documentElement.setAttribute('data-theme', 'light');
}
} catch (e) {}
})();
</script>
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
<style>
:root {
color-scheme: dark;
--bg: #0f172a; --panel: #1e293b; --panel2: #273449;
--text: #e2e8f0; --muted: #94a3b8; --accent: #6366f1;
--accent-hover: #818cf8; --border: #334155; --danger: #ef4444;
}
:root[data-theme="light"] {
color-scheme: light;
--bg: #f8fafc; --panel: #ffffff; --panel2: #f1f5f9;
--text: #0f172a; --muted: #64748b; --accent: #6366f1;
--accent-hover: #4f46e5; --border: #e2e8f0; --danger: #dc2626;
}
body { transition: background-color .15s, color .15s; }
* { box-sizing: border-box; }
body {
margin: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
background: var(--bg); color: var(--text); line-height: 1.5;
}
a { color: var(--accent-hover); text-decoration: none; }
a:hover { text-decoration: underline; }
header.topbar {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 24px; background: var(--panel); border-bottom: 1px solid var(--border);
}
header.topbar .brand { font-weight: 700; font-size: 1.2rem; }
header.topbar .brand span { color: var(--accent-hover); }
.user-info { color: var(--muted); font-size: .9rem; display: flex; gap: 12px; align-items: center; }
button, input[type=submit] {
background: var(--accent); color: white; border: none; border-radius: 8px;
padding: 9px 16px; font-size: .95rem; cursor: pointer; font-weight: 500;
}
button:hover { background: var(--accent-hover); }
button.ghost { background: transparent; border: 1px solid var(--border); color: var(--muted); }
button.ghost:hover { background: var(--panel2); color: var(--text); }
button.danger { background: transparent; color: var(--muted); padding: 4px 8px; font-size: .8rem; }
button.danger:hover { color: var(--danger); background: transparent; }
input[type=text], input[type=email], input[type=password], input[type=url] {
background: var(--panel2); border: 1px solid var(--border); color: var(--text);
border-radius: 8px; padding: 10px 12px; font-size: .95rem; width: 100%;
}
input:focus, select:focus { outline: none; border-color: var(--accent); }
select {
background: var(--panel2); border: 1px solid var(--border); color: var(--text);
border-radius: 8px; padding: 6px 10px; font-size: .85rem; font-family: inherit;
cursor: pointer;
}
.layout { display: flex; gap: 24px; margin: 24px auto; padding: 0 24px; }
.sidebar { width: 240px; flex-shrink: 0; }
.main { flex: 1; min-width: 0; }
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; margin-bottom: 16px; }
/* Linkliste: bis zu drei Karten pro Zeile, je nach Fensterbreite.
Karten einer Zeile sind gleich hoch; die Zusammenfassung wird
dafür auf drei Zeilen gekürzt. */
#links { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
#links > .panel { margin-bottom: 0; }
#links > #empty-state { grid-column: 1 / -1; }
.link-card { display: flex; flex-direction: column; }
.link-card.needs-review { border-color: #ca8a04; }
.review-toggle.active { color: #fbbf24; border-color: #ca8a04; }
.card-head { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 10px; }
.card-avatar {
flex-shrink: 0; width: 34px; height: 34px; border-radius: 9px;
display: inline-flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 1rem;
background: hsl(var(--h) 45% 22%); color: hsl(var(--h) 80% 78%);
border: 1px solid hsl(var(--h) 40% 32%);
}
.card-badges {
display: flex; gap: 4px; flex: 1; min-width: 0;
overflow: hidden; max-height: 22px; align-items: flex-start;
}
.card-actions { display: flex; gap: 4px; flex-shrink: 0; }
.badge {
font-size: .7rem; padding: 2px 8px; border-radius: 99px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
.badge-cat { background: var(--accent); color: #fff; }
.badge-man { background: #0e7490; color: #cffafe; }
.badge-tag { background: var(--panel2); color: var(--muted); }
/* Feste Höhen für Titel und Zusammenfassung: so beginnt jeder Block auf
gleicher Höhe und das Kürzen mit "…" greift zuverlässig. */
.card-title {
font-weight: 600; font-size: 1rem; line-height: 1.35; margin-bottom: 6px;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; height: calc(2 * 1.35 * 1rem); flex: none;
}
.card-summary {
margin: 0 0 12px; font-size: .875rem; color: var(--muted); line-height: 1.5;
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
overflow: hidden; height: calc(3 * 1.5 * .875rem); flex: none;
}
.card-spacer { flex: 1 1 auto; }
/* Tags: nur eine Zeile, Rest wird abgeschnitten hält die Karten gleich hoch. */
.card-tags {
display: flex; gap: 4px;
overflow: hidden; max-height: 22px; flex-wrap: wrap;
}
.card-foot {
padding-top: 10px; border-top: 1px solid var(--border);
display: flex; justify-content: space-between; gap: 8px;
font-size: .75rem; color: var(--muted);
}
.card-domain { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-foot > span:last-child { flex-shrink: 0; }
@media (min-width: 1000px) { #links { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1500px) { #links { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* Auf schmalen Geräten die Seitenleiste über den Inhalt stellen. */
@media (max-width: 800px) {
.layout { flex-direction: column; gap: 16px; padding: 0 16px; }
.sidebar { width: auto; }
}
.facet-group h3 { font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); margin: 16px 0 8px; }
.facet { display: flex; justify-content: space-between; padding: 5px 8px; border-radius: 6px; font-size: .9rem; }
.facet:hover { background: var(--panel2); text-decoration: none; }
.facet.active { background: var(--accent); color: white; }
.facet .count { color: var(--muted); font-size: .8rem; }
.facet.active .count { color: #e0e7ff; }
.auth-wrap { max-width: 380px; margin: 8vh auto; }
.auth-wrap .panel { padding: 28px; }
.auth-wrap h1 { margin-top: 0; }
.field { margin-bottom: 14px; }
.field label { display: block; font-size: .85rem; color: var(--muted); margin-bottom: 5px; }
.error { background: #7f1d1d; color: #fecaca; padding: 10px 12px; border-radius: 8px; margin-bottom: 14px; font-size: .9rem; }
.muted { color: var(--muted); }
.htmx-indicator { opacity: 0; transition: opacity .2s; }
.htmx-request .htmx-indicator { opacity: 1; }
.htmx-request.htmx-indicator { opacity: 1; }
.icon { display: inline-block; vertical-align: -3px; flex-shrink: 0; }
.icon-spin { animation: icon-spin 0.8s linear infinite; }
@keyframes icon-spin { to { transform: rotate(360deg); } }
.icon-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 30px; height: 30px; padding: 0;
}
.theme-icon-dark, .theme-icon-light { display: inline-flex; }
.icon-warn { color: #eab308; }
.icon-bookmark-active { color: #eab308; }
.input-icon { position: relative; }
.input-icon > .icon {
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
color: var(--muted); pointer-events: none;
}
.input-icon > input { padding-left: 36px; }
footer.app-footer {
text-align: center; padding: 16px 24px; margin-top: 24px;
color: var(--muted); font-size: .8rem; border-top: 1px solid var(--border);
}
.user-menu { position: relative; display: flex; align-items: center; gap: 4px; }
.user-dropdown {
position: absolute; top: calc(100% + 8px); right: 0; min-width: 230px;
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
box-shadow: 0 12px 32px rgba(0,0,0,.35); padding: 6px; z-index: 50;
}
:root[data-theme="light"] .user-dropdown { box-shadow: 0 12px 32px rgba(15,23,42,.14); }
.dropdown-email {
padding: 8px 10px; font-size: .8rem; color: var(--muted);
word-break: break-all;
}
.dropdown-sep { height: 1px; background: var(--border); margin: 4px 0; }
.dropdown-item {
display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
background: transparent; color: var(--text); border: none; border-radius: 6px;
padding: 8px 10px; font-size: .875rem; font-weight: 400; cursor: pointer;
}
.dropdown-item:hover:not(:disabled) { background: var(--panel2); }
.dropdown-item:disabled { color: var(--muted); cursor: default; opacity: .65; }
.dropdown-item .soon {
margin-left: auto; font-size: .65rem; text-transform: uppercase; letter-spacing: .04em;
background: var(--panel2); color: var(--muted); padding: 2px 6px; border-radius: 99px;
}
</style>
</head>
<body>
{% block body %}{% endblock %}
<footer class="app-footer">
&copy; {{ app_author }} &middot; Version {{ app_version }} &middot; {{ app_host }}
</footer>
<script>
(function () {
var themeBtn = document.getElementById('theme-toggle');
if (themeBtn) {
var dark = themeBtn.querySelector('.theme-icon-dark');
var light = themeBtn.querySelector('.theme-icon-light');
var applyIcon = function (theme) {
dark.style.display = theme === 'light' ? 'none' : 'inline-flex';
light.style.display = theme === 'light' ? 'inline-flex' : 'none';
};
applyIcon(document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark');
themeBtn.addEventListener('click', function () {
var next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem('linkvault-theme', next); } catch (e) {}
applyIcon(next);
});
}
var menuBtn = document.getElementById('menu-toggle');
var dropdown = document.getElementById('user-dropdown');
if (menuBtn && dropdown) {
var close = function () {
dropdown.hidden = true;
menuBtn.setAttribute('aria-expanded', 'false');
};
var open = function () {
dropdown.hidden = false;
menuBtn.setAttribute('aria-expanded', 'true');
};
menuBtn.addEventListener('click', function (e) {
e.stopPropagation();
if (dropdown.hidden) open(); else close();
});
document.addEventListener('click', function (e) {
if (!dropdown.hidden && !dropdown.contains(e.target) && e.target !== menuBtn) close();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') close();
});
}
})();
</script>
</body>
</html>