The "Hinzufügen" buttons reserved layout space for their loading spinner even when idle, since .htmx-indicator only toggled opacity (not display), leaving a visible gap before the centered text. Switch to display:none/inline-flex so the spinner takes no space until an htmx request is in flight, and drop the static plus icon next to "Hinzufügen" so the idle button just shows centered text with the spinner appearing in its place while loading. Also adds the favicon as a small logo image in front of "LinkVault" in the shared topbar. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
366 lines
17 KiB
HTML
366 lines
17 KiB
HTML
<!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>
|
||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||
<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; color: var(--text); }
|
||
header.topbar .brand span { color: var(--accent-hover); }
|
||
.topbar-right { display: flex; align-items: center; gap: 16px; }
|
||
.user-info { color: var(--muted); font-size: .9rem; display: flex; gap: 12px; align-items: center; }
|
||
.mode-switch { display: flex; gap: 2px; background: var(--panel2); border-radius: 8px; padding: 2px; }
|
||
.mode-tab {
|
||
display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px;
|
||
border-radius: 6px; font-size: .85rem; color: var(--muted); text-decoration: none;
|
||
}
|
||
.mode-tab:hover { color: var(--text); text-decoration: none; }
|
||
.mode-tab.active { background: var(--accent); color: #fff; }
|
||
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%;
|
||
}
|
||
.file-input {
|
||
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
||
overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
|
||
}
|
||
.file-label {
|
||
display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
|
||
white-space: nowrap;
|
||
}
|
||
.file-label:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||
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, #prompts { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
|
||
#links > .panel, #prompts > .panel { margin-bottom: 0; }
|
||
#links > #empty-state, #prompts > #empty-state { grid-column: 1 / -1; }
|
||
/* Beim Bearbeiten mehr Platz geben: Karte spannt über alle Spalten. */
|
||
.card-edit-wide { 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;
|
||
}
|
||
/* Markdown-Vorschau (Prompts): line-clamp funktioniert nicht zuverlässig
|
||
über mehrere Block-Elemente (Überschriften, Listen, Codeblöcke)
|
||
hinweg, daher stattdessen feste Höhe + Fade-Ausblendung. */
|
||
.card-summary.md-preview {
|
||
display: block; -webkit-line-clamp: unset; position: relative;
|
||
height: calc(4 * 1.5 * .875rem);
|
||
}
|
||
.md-preview::after {
|
||
content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1.6em;
|
||
background: linear-gradient(to bottom, transparent, var(--panel));
|
||
}
|
||
.md-preview > :first-child { margin-top: 0; }
|
||
.md-preview > :last-child { margin-bottom: 0; }
|
||
.md-preview h1, .md-preview h2, .md-preview h3,
|
||
.md-preview h4, .md-preview h5, .md-preview h6 {
|
||
font-size: .95rem; margin: 0 0 4px; color: var(--text);
|
||
}
|
||
.md-preview p { margin: 0 0 6px; }
|
||
.md-preview ul, .md-preview ol { margin: 0 0 6px; padding-left: 1.2em; }
|
||
.md-preview li { margin-bottom: 2px; }
|
||
.md-preview code {
|
||
background: var(--panel2); padding: 1px 5px; border-radius: 4px; font-size: .85em;
|
||
}
|
||
.md-preview pre {
|
||
background: var(--panel2); padding: 8px 10px; border-radius: 8px;
|
||
overflow-x: auto; margin: 0 0 6px;
|
||
}
|
||
.md-preview pre code { background: none; padding: 0; }
|
||
.md-preview blockquote {
|
||
border-left: 3px solid var(--border); margin: 0 0 6px; padding-left: 10px;
|
||
}
|
||
.md-preview a { text-decoration: underline; }
|
||
.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, #prompts { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||
@media (min-width: 1500px) { #links, #prompts { 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); }
|
||
.md-help { margin-top: 6px; }
|
||
.md-help summary {
|
||
cursor: pointer; display: inline-flex; align-items: center; gap: 5px;
|
||
color: var(--accent-hover); font-size: .8rem; user-select: none;
|
||
}
|
||
.md-help summary::-webkit-details-marker { display: none; }
|
||
.md-help table {
|
||
margin-top: 8px; border-collapse: collapse; font-size: .8rem;
|
||
}
|
||
.md-help td {
|
||
padding: 3px 12px 3px 0; vertical-align: top; color: var(--muted);
|
||
}
|
||
.md-help td:first-child { white-space: nowrap; }
|
||
.md-help code {
|
||
background: var(--panel2); padding: 1px 5px; border-radius: 4px; color: var(--text);
|
||
}
|
||
/* display:none statt opacity:0, damit der (unsichtbare) Indikator im
|
||
Ruhezustand keinen Platz im Button reserviert. */
|
||
.htmx-indicator { display: none; }
|
||
.htmx-request .htmx-indicator { display: inline-flex; }
|
||
.htmx-request.htmx-indicator { display: inline-flex; }
|
||
|
||
.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; }
|
||
.icon-copied { color: #22c55e; border-color: #16a34a; }
|
||
|
||
.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">
|
||
© {{ app_author }} · Version {{ app_version }} · {{ 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>
|
||
<script>
|
||
var COPY_ICON_CHECK =
|
||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" ' +
|
||
'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
|
||
'stroke-linejoin="round" class="icon"><polyline points="20 6 9 17 4 12"/></svg>';
|
||
|
||
function copyPromptContent(btn) {
|
||
var text = btn.getAttribute('data-content') || '';
|
||
if (!btn.dataset.originalIcon) btn.dataset.originalIcon = btn.innerHTML;
|
||
|
||
var showResult = function (ok) {
|
||
btn.innerHTML = ok ? COPY_ICON_CHECK : btn.dataset.originalIcon;
|
||
btn.title = ok ? 'In die Zwischenablage kopiert' : 'Kopieren fehlgeschlagen';
|
||
btn.classList.toggle('icon-copied', ok);
|
||
setTimeout(function () {
|
||
btn.innerHTML = btn.dataset.originalIcon;
|
||
btn.title = 'In die Zwischenablage kopieren';
|
||
btn.classList.remove('icon-copied');
|
||
}, 1500);
|
||
};
|
||
|
||
// Fallback für http (kein sicherer Kontext) oder ältere Browser, in
|
||
// denen die asynchrone Clipboard-API nicht verfügbar ist/abgelehnt wird.
|
||
var legacyCopy = function () {
|
||
var ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
ta.style.position = 'fixed';
|
||
ta.style.opacity = '0';
|
||
document.body.appendChild(ta);
|
||
ta.focus();
|
||
ta.select();
|
||
var ok = false;
|
||
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
||
document.body.removeChild(ta);
|
||
showResult(ok);
|
||
};
|
||
|
||
if (window.isSecureContext && navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(function () { showResult(true); }, legacyCopy);
|
||
} else {
|
||
legacyCopy();
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|