Bestenliste, Devlog-Filter, Galerie und leere Bereiche
Vier Stellen, die beim Durchsehen der Seiten aufgefallen sind.
Die Bestenliste zeigte nur Namen — und für Platz 1 bis 3 denselben Pokal
ohne Nummer, wodurch die Reihenfolge nur an der Position erkennbar war.
Jetzt: Rangnummer, Avatar und die ersten drei Plätze farblich abgesetzt.
Die Avatare kommen aus dem Member-Cache, der durch den GuildMembers-Intent
ohnehin gefüllt ist — das kostet keinen einzigen Discord-Aufruf. Wer nicht
im Cache liegt, bekommt seinen Anfangsbuchstaben statt eines leeren Kreises.
Die Projekt-Chips im Devlog-Archiv waren bloß Beschriftung. Jetzt filtern
Chips mit Anzahl nach Projekt (ECOGAME / D4RKBOT) — man will meist nur
eins von beiden lesen. Serverseitig über LIKE auf die erste Zeile, wo das
Projekt ohnehin steht; das spart eine Spalte und eine Migration. Eine
aktive Suche schlägt den Filter, sonst müsste die Volltextsuche beides
verbinden, ohne dass jemand danach fragt.
Galerie: Vorschaubilder von 240 auf 340 Pixel, und Name plus Datum stehen
dauerhaft am Bild statt nur beim Überfahren — am Handy gab es kein Hover
und damit nie eine Zuordnung.
Events, Changelog, Galerie und Level endeten bei wenig Inhalt mit einem
einzelnen Satz in einer sonst leeren Seite; das sah nach Fehler aus statt
nach „noch nichts da". Jetzt steht dort ein Block mit Symbol, Erklärung
was hier erscheinen wird, und Verweisen auf belebte Bereiche.
Dazu ein Sprung-nach-oben-Knopf im Devlog-Archiv und im Changelog, die
beide sehr lang werden.
Beim Testen gefunden: ESCAPE '\' in einem JS-String kommt als leeres
Zeichen in SQLite an ("ESCAPE expression must be a single character") —
der Backslash müsste doppelt entwertet werden. Jetzt ist das Escape-Zeichen
ein Ausrufezeichen, das die Falle gar nicht erst aufmacht.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
// Leerer Bereich mit Substanz: Symbol, Überschrift, ein Satz was hier erscheint
|
||||
// und wohin es stattdessen geht.
|
||||
//
|
||||
// Vorher stand auf Events, Changelog, Galerie und Level je ein einzelner Satz
|
||||
// in einer sonst leeren Seite — das sah nach Fehler aus statt nach „noch nichts da".
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* Icon: (props: object) => JSX.Element,
|
||||
* title: string,
|
||||
* text: string,
|
||||
* actions?: Array<{ to?: string, href?: string, label: string }>,
|
||||
* }} props
|
||||
*/
|
||||
export default function EmptyState({ Icon, title, text, actions = [] }) {
|
||||
return (
|
||||
<div className="empty-state">
|
||||
<span className="empty-icon"><Icon size={28} /></span>
|
||||
<h2 className="empty-title">{title}</h2>
|
||||
<p className="empty-text">{text}</p>
|
||||
{actions.length > 0 && (
|
||||
<div className="empty-actions">
|
||||
{actions.filter((a) => a.to || a.href).map((a) => (
|
||||
a.to
|
||||
? <Link className="btn" to={a.to} key={a.label}>{a.label}</Link>
|
||||
: <a className="btn" href={a.href} key={a.label} target="_blank" rel="noreferrer">{a.label}</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Sprung nach oben — erscheint erst, wenn man wirklich weit unten ist.
|
||||
// Das Devlog-Archiv wird sehr lang; ohne das scrollt man ewig zurück.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IconChevronUp } from '../icons.jsx';
|
||||
|
||||
export default function ToTop({ after = 900 }) {
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setShow(window.scrollY > after);
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, [after]);
|
||||
|
||||
if (!show) return null;
|
||||
return (
|
||||
<button
|
||||
className="to-top"
|
||||
aria-label="Nach oben"
|
||||
title="Nach oben"
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
>
|
||||
<IconChevronUp size={18} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
// Discord kennt keine SVGs — dort sind Emojis die richtige Wahl.
|
||||
import {
|
||||
Activity, AlertTriangle, Archive, ArrowLeft, ArrowRight, Award, Bell, Bot,
|
||||
Cake, Calendar, Camera, Check, ChevronDown, ChevronLeft, ChevronRight,
|
||||
Cake, Calendar, Camera, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp,
|
||||
Clock, Copy, Database, Download, Ticket, FileText, Gamepad2, Gauge, Gift,
|
||||
Globe, Hash, Heart, Image, Key, Layers, Link2, List, Lock, LogOut, Mail,
|
||||
MapPin, Megaphone, MessageSquare, Mic, Monitor, Moon, Package, Palette, UserCog,
|
||||
@@ -72,6 +72,7 @@ export const IconLock = make(Lock);
|
||||
export const IconArrowLeft = make(ArrowLeft);
|
||||
export const IconArrowRight = make(ArrowRight);
|
||||
export const IconChevronDown = make(ChevronDown);
|
||||
export const IconChevronUp = make(ChevronUp);
|
||||
export const IconChevronLeft = make(ChevronLeft);
|
||||
export const IconChevronRight = make(ChevronRight);
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet } from '../api.js';
|
||||
import { IconChevronLeft, IconChevronRight } from '../icons.jsx';
|
||||
import { IconChevronLeft, IconChevronRight, IconChangelog } from '../icons.jsx';
|
||||
import EmptyState from '../components/EmptyState.jsx';
|
||||
import { SkeletonCards } from '../components/Skeleton.jsx';
|
||||
import { Markdown } from '../markdown.jsx';
|
||||
import ToTop from '../components/ToTop.jsx';
|
||||
|
||||
const dateFmt = new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit', month: 'long', year: 'numeric',
|
||||
@@ -36,7 +38,14 @@ export default function Changelog() {
|
||||
<section className="content">
|
||||
{error && <p className="notice">Changelog konnte nicht geladen werden.</p>}
|
||||
{!error && !data && <SkeletonCards n={3} />}
|
||||
{data?.total === 0 && <p className="notice">Noch keine Releases veröffentlicht.</p>}
|
||||
{data?.total === 0 && (
|
||||
<EmptyState
|
||||
Icon={IconChangelog}
|
||||
title="Noch keine Releases"
|
||||
text="Sobald eine Version veröffentlicht wird, steht sie hier mit allen Änderungen. Bis dahin steht der Fortschritt im Devlog."
|
||||
actions={[{ to: '/devlogs', label: 'Devlogs lesen' }, { to: '/roadmap', label: 'Zur Roadmap' }]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data?.total > 0 &&
|
||||
data.items.map((r) => (
|
||||
@@ -73,6 +82,7 @@ export default function Changelog() {
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<ToTop />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { apiGet, apiDelete } from '../api.js';
|
||||
import { IconChevronLeft, IconChevronRight, IconSearch, IconLink, IconX } from '../icons.jsx';
|
||||
import { Markdown } from '../markdown.jsx';
|
||||
import ToTop from '../components/ToTop.jsx';
|
||||
import Lightbox from '../components/Lightbox.jsx';
|
||||
import { SkeletonCards } from '../components/Skeleton.jsx';
|
||||
|
||||
@@ -49,6 +50,8 @@ export default function Devlogs({ me }) {
|
||||
const [error, setError] = useState(false);
|
||||
const [qInput, setQInput] = useState('');
|
||||
const [q, setQ] = useState('');
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [project, setProject] = useState(''); // '' = alle
|
||||
const [lightbox, setLightbox] = useState(null); // { images, start }
|
||||
|
||||
// Sucheingabe entprellen (300 ms), bei neuer Suche zurück auf Seite 1
|
||||
@@ -63,8 +66,15 @@ export default function Devlogs({ me }) {
|
||||
const load = useCallback(() => {
|
||||
setError(false);
|
||||
const search = q ? `&q=${encodeURIComponent(q)}` : '';
|
||||
apiGet(`/api/devlogs?page=${page}${search}`).then(setData).catch(() => setError(true));
|
||||
}, [page, q]);
|
||||
// Bei aktiver Suche zählt der Filter nicht — der Server durchsucht immer alles
|
||||
const filter = !q && project ? `&project=${encodeURIComponent(project)}` : '';
|
||||
apiGet(`/api/devlogs?page=${page}${search}${filter}`).then(setData).catch(() => setError(true));
|
||||
}, [page, q, project]);
|
||||
|
||||
// Projektliste einmal laden — steuert die Filter-Chips
|
||||
useEffect(() => {
|
||||
apiGet('/api/devlog-projects').then((d) => setProjects(d.projects)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -102,6 +112,26 @@ export default function Devlogs({ me }) {
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
{projects.length > 1 && (
|
||||
<div className="dl-filters">
|
||||
<button
|
||||
className={`dl-filter${project === '' ? ' active' : ''}`}
|
||||
onClick={() => { setProject(''); setPage(1); }}
|
||||
>
|
||||
Alle <span className="dl-filter-n">{projects.reduce((s, p) => s + p.n, 0)}</span>
|
||||
</button>
|
||||
{projects.map((p) => (
|
||||
<button
|
||||
key={p.name}
|
||||
className={`dl-filter${project === p.name ? ' active' : ''}`}
|
||||
onClick={() => { setProject(p.name); setPage(1); }}
|
||||
>
|
||||
{p.name} <span className="dl-filter-n">{p.n}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="search">
|
||||
<span className="search-icon"><IconSearch size={16} /></span>
|
||||
<input
|
||||
@@ -116,7 +146,13 @@ export default function Devlogs({ me }) {
|
||||
{error && <p className="notice">Devlogs konnten nicht geladen werden.</p>}
|
||||
{!error && !data && <SkeletonCards n={3} />}
|
||||
{data?.total === 0 && (
|
||||
<p className="notice">{q ? `Keine Treffer für „${q}".` : 'Noch keine Devlogs archiviert.'}</p>
|
||||
<p className="notice">
|
||||
{q
|
||||
? `Keine Treffer für „${q}".`
|
||||
: project
|
||||
? `Noch keine Devlogs für ${project}.`
|
||||
: 'Noch keine Devlogs archiviert.'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{data?.total > 0 && (
|
||||
@@ -199,6 +235,7 @@ export default function Devlogs({ me }) {
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<ToTop />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet } from '../api.js';
|
||||
import { IconStar, IconChevronRight } from '../icons.jsx';
|
||||
import { IconStar, IconChevronRight, IconEvents } from '../icons.jsx';
|
||||
import EmptyState from '../components/EmptyState.jsx';
|
||||
import { Markdown } from '../markdown.jsx';
|
||||
|
||||
const dateFmt = new Intl.DateTimeFormat('de-DE', {
|
||||
@@ -31,7 +32,14 @@ export default function Events() {
|
||||
<section className="content">
|
||||
{error && <p className="notice">Events konnten nicht geladen werden.</p>}
|
||||
{!error && !data && <p className="notice">Lade …</p>}
|
||||
{data?.events.length === 0 && <p className="notice">Gerade keine geplanten Events — schau bald wieder rein!</p>}
|
||||
{data?.events.length === 0 && (
|
||||
<EmptyState
|
||||
Icon={IconEvents}
|
||||
title="Noch keine Events geplant"
|
||||
text="Playtests, Streams und Community-Runden kündigen wir hier an — und im Discord bekommst du eine Erinnerung."
|
||||
actions={[{ to: '/devlogs', label: 'Devlogs lesen' }, { to: '/roadmap', label: 'Zur Roadmap' }]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{data?.events.map((e) => (
|
||||
<article className="card event-card" key={e.url}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet } from '../api.js';
|
||||
import { IconChevronLeft, IconChevronRight } from '../icons.jsx';
|
||||
import { IconChevronLeft, IconChevronRight, IconGallery } from '../icons.jsx';
|
||||
import EmptyState from '../components/EmptyState.jsx';
|
||||
import Lightbox from '../components/Lightbox.jsx';
|
||||
import { SkeletonGrid } from '../components/Skeleton.jsx';
|
||||
|
||||
@@ -38,7 +39,14 @@ export default function Galerie() {
|
||||
<section className="content">
|
||||
{error && <p className="notice">Galerie konnte nicht geladen werden.</p>}
|
||||
{!error && !data && <SkeletonGrid n={9} />}
|
||||
{data?.total === 0 && <p className="notice">Noch keine Screenshots — poste welche im Discord!</p>}
|
||||
{data?.total === 0 && (
|
||||
<EmptyState
|
||||
Icon={IconGallery}
|
||||
title="Noch keine Screenshots"
|
||||
text="Alles, was im Screenshot-Kanal landet, erscheint automatisch hier — mitsamt Name und Datum."
|
||||
actions={[{ to: '/devlogs', label: 'Devlogs lesen' }]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="gallery-grid">
|
||||
{shots.map((s, i) => (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet } from '../api.js';
|
||||
import { SkeletonRows } from '../components/Skeleton.jsx';
|
||||
import { IconTrophy } from '../icons.jsx';
|
||||
import { IconTrophy, IconLevel } from '../icons.jsx';
|
||||
import EmptyState from '../components/EmptyState.jsx';
|
||||
|
||||
/** Muss der Formel in src/bot/levels.js entsprechen */
|
||||
function xpForLevel(level) {
|
||||
@@ -68,7 +69,14 @@ export default function Level() {
|
||||
<section className="content">
|
||||
{error && <p className="notice">Bestenliste konnte nicht geladen werden.</p>}
|
||||
{!error && !rows && <SkeletonRows n={10} />}
|
||||
{rows?.length === 0 && <p className="notice">Noch keine XP vergeben — schreibt was im Discord!</p>}
|
||||
{rows?.length === 0 && (
|
||||
<EmptyState
|
||||
Icon={IconLevel}
|
||||
title="Noch keine XP vergeben"
|
||||
text="XP gibt es fürs Mitreden im Discord. Sobald geschrieben wird, füllt sich die Bestenliste von selbst."
|
||||
actions={[{ to: '/devlogs', label: 'Devlogs lesen' }]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{rows?.length > 0 && (
|
||||
<div className="lvl-list">
|
||||
@@ -76,14 +84,18 @@ export default function Level() {
|
||||
const base = xpForLevel(r.level);
|
||||
const next = xpForLevel(r.level + 1);
|
||||
const pct = Math.floor(((r.xp - base) / (next - base)) * 100);
|
||||
const name = r.display_name || r.username || r.user_id;
|
||||
return (
|
||||
<div className="lvl-row" key={r.user_id}>
|
||||
<div className={`lvl-row${i < 3 ? ` top top-${i + 1}` : ''}`} key={r.user_id}>
|
||||
<span className="lvl-rank">
|
||||
{i < 3
|
||||
? <IconTrophy size={18} style={{ color: MEDAL_COLORS[i] }} />
|
||||
: `#${i + 1}`}
|
||||
{i < 3 && <IconTrophy size={15} style={{ color: MEDAL_COLORS[i] }} />}
|
||||
<span className="lvl-rank-n">{i + 1}</span>
|
||||
</span>
|
||||
<span className="lvl-name">{r.username ?? r.user_id}</span>
|
||||
{/* Ohne Bild bleibt der Anfangsbuchstabe — kein leeres Loch */}
|
||||
{r.avatar
|
||||
? <img className="lvl-avatar" src={r.avatar} alt="" loading="lazy" />
|
||||
: <span className="lvl-avatar lvl-avatar-empty">{name.slice(0, 1).toUpperCase()}</span>}
|
||||
<span className="lvl-name">{name}</span>
|
||||
<div className="lvl-progress">
|
||||
<div className="progress"><div className="progress-bar" style={{ width: `${pct}%` }} /></div>
|
||||
</div>
|
||||
|
||||
+93
-11
@@ -472,8 +472,8 @@ body::after {
|
||||
/* ── GALERIE ───────────────────────────────────────── */
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: .9rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.shot {
|
||||
position: relative; display: block; overflow: hidden;
|
||||
@@ -501,16 +501,18 @@ body::after {
|
||||
filter: brightness(.92);
|
||||
}
|
||||
.shot:hover img { transform: scale(1.06); filter: brightness(1.05); }
|
||||
/* Wer das Bild gemacht hat, steht dauerhaft dran — beim Überfahren nur
|
||||
deutlicher. Vorher war es nur im Hover sichtbar und damit am Handy nie. */
|
||||
.shot-meta {
|
||||
position: absolute; inset: auto 0 0 0;
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
padding: .55rem .7rem .45rem;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, .9), transparent);
|
||||
font-family: var(--mono); font-size: .62rem; letter-spacing: .1em;
|
||||
opacity: 0; transform: translateY(4px);
|
||||
transition: opacity .25s, transform .25s;
|
||||
padding: 1.4rem .7rem .5rem;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, .92), transparent);
|
||||
font-family: var(--mono); font-size: .64rem; letter-spacing: .1em;
|
||||
opacity: .8;
|
||||
transition: opacity .25s;
|
||||
}
|
||||
.shot:hover .shot-meta { opacity: 1; transform: translateY(0); }
|
||||
.shot:hover .shot-meta { opacity: 1; }
|
||||
.shot-author { color: var(--neon); }
|
||||
.shot-date { color: var(--muted); }
|
||||
|
||||
@@ -1079,8 +1081,29 @@ label.toggle {
|
||||
}
|
||||
.lvl-row:last-child { border-bottom: none; }
|
||||
.lvl-row:first-child { border-radius: 12px 12px 0 0; }
|
||||
.lvl-rank { font-family: var(--mono); font-size: .85rem; color: var(--muted); min-width: 2.6rem; }
|
||||
.lvl-name { font-weight: 600; min-width: 10rem; }
|
||||
.lvl-rank {
|
||||
display: inline-flex; align-items: center; gap: .35rem;
|
||||
font-family: var(--mono); font-size: .85rem; color: var(--muted);
|
||||
min-width: 3.4rem;
|
||||
}
|
||||
.lvl-rank-n { min-width: 1.2rem; text-align: right; }
|
||||
/* Die ersten drei Plätze dürfen sich abheben — sonst sieht die Liste flach aus */
|
||||
.lvl-row.top .lvl-rank-n { color: var(--text); font-size: .95rem; }
|
||||
.lvl-row.top-1 { background: linear-gradient(90deg, rgba(245, 197, 24, .07), transparent 55%); }
|
||||
.lvl-row.top-2 { background: linear-gradient(90deg, rgba(192, 196, 204, .05), transparent 55%); }
|
||||
.lvl-row.top-3 { background: linear-gradient(90deg, rgba(192, 122, 62, .05), transparent 55%); }
|
||||
|
||||
.lvl-avatar {
|
||||
flex: none; width: 2.1rem; height: 2.1rem; border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background: var(--bg3); border: 1px solid var(--border);
|
||||
}
|
||||
.lvl-row.top-1 .lvl-avatar { border-color: rgba(245, 197, 24, .55); }
|
||||
.lvl-avatar-empty {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-family: var(--display); font-size: 1rem; color: var(--muted);
|
||||
}
|
||||
.lvl-name { font-weight: 600; min-width: 9rem; }
|
||||
.lvl-progress { flex: 1; }
|
||||
.lvl-level {
|
||||
font-family: var(--mono); font-size: .72rem; letter-spacing: .1em;
|
||||
@@ -1670,7 +1693,6 @@ button.with-icon, a.with-icon { justify-content: center; }
|
||||
.srv-map, .srv-ping { display: inline-flex; align-items: center; gap: .3rem; }
|
||||
|
||||
/* Bestenliste: Pokal statt Medaillen-Emoji */
|
||||
.lvl-rank { display: inline-flex; align-items: center; justify-content: center; }
|
||||
|
||||
/* Bot-Avatar-Platzhalter */
|
||||
.bot-avatar-empty { display: flex; align-items: center; justify-content: center; color: var(--muted); }
|
||||
@@ -1926,6 +1948,66 @@ button.with-icon, a.with-icon { justify-content: center; }
|
||||
.tpl-peek { display: none; }
|
||||
}
|
||||
|
||||
/* ── DEVLOG-FILTER ─────────────────────────────────── */
|
||||
/* Gleiche Form wie die Modul-Filter — beides sind Chips zum Einschränken */
|
||||
.dl-filters { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .9rem; }
|
||||
.dl-filter {
|
||||
display: inline-flex; align-items: center; gap: .45rem;
|
||||
font-family: var(--mono); font-size: .62rem;
|
||||
letter-spacing: .12em; text-transform: uppercase;
|
||||
color: var(--muted); background: var(--bg2);
|
||||
border: 1px solid var(--border); border-radius: 99px;
|
||||
padding: .35rem .8rem; cursor: pointer;
|
||||
transition: color .15s, border-color .15s, background .15s;
|
||||
}
|
||||
.dl-filter:hover { color: var(--text); border-color: var(--neon); }
|
||||
.dl-filter.active { color: #0a0a0a; background: var(--neon); border-color: var(--neon); }
|
||||
.dl-filter-n {
|
||||
font-size: .58rem; opacity: .7;
|
||||
padding-left: .35rem; border-left: 1px solid currentColor;
|
||||
}
|
||||
|
||||
/* ── LEERE BEREICHE ────────────────────────────────── */
|
||||
/* Vorher stand hier ein einzelner Satz in einer sonst leeren Seite —
|
||||
das sah nach Fehler aus statt nach „noch nichts da". */
|
||||
.empty-state {
|
||||
display: flex; flex-direction: column; align-items: center; text-align: center;
|
||||
gap: .6rem;
|
||||
padding: 3.5rem 1.5rem;
|
||||
border: 1px dashed var(--border); border-radius: 14px;
|
||||
background: rgba(255, 255, 255, .012);
|
||||
}
|
||||
.empty-icon {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 3.6rem; height: 3.6rem; border-radius: 50%;
|
||||
background: var(--bg3); border: 1px solid var(--border);
|
||||
color: var(--muted2);
|
||||
margin-bottom: .3rem;
|
||||
}
|
||||
.empty-title {
|
||||
font-family: var(--display); font-size: 1.5rem;
|
||||
letter-spacing: .04em; color: var(--text); margin: 0;
|
||||
}
|
||||
.empty-text {
|
||||
color: var(--muted); font-weight: 300;
|
||||
line-height: 1.6; max-width: 34rem; margin: 0;
|
||||
}
|
||||
.empty-actions { display: flex; flex-wrap: wrap; gap: .6rem; justify-content: center; margin-top: .8rem; }
|
||||
|
||||
/* ── NACH OBEN ─────────────────────────────────────── */
|
||||
.to-top {
|
||||
position: fixed; right: 1.5rem; bottom: 1.5rem; z-index: 400;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 2.8rem; height: 2.8rem; border-radius: 50%;
|
||||
background: var(--bg2); border: 1px solid var(--border);
|
||||
color: var(--muted); cursor: pointer;
|
||||
transition: color .2s, border-color .2s, opacity .25s, transform .25s;
|
||||
}
|
||||
.to-top:hover { color: var(--neon); border-color: var(--neon); }
|
||||
@media (max-width: 640px) {
|
||||
.to-top { right: 1rem; bottom: 1rem; }
|
||||
}
|
||||
|
||||
/* ── STELLWERTE ────────────────────────────────────── */
|
||||
.tune-list {
|
||||
display: flex; flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user