feat: mobile-first redesign (design-taste-frontend)

- responsive nav: compact sticky topbar + bottom tab bar on mobile (SVG icons, safe-area insets), inline top nav on desktop; no more horizontal overflow
- previews show full aspect ratio (object-fit contain) on subtle checker bg for transparency (library, studio, queue)
- 16px inputs on mobile (no iOS zoom), tactile :active feedback, larger touch targets
- keeps Klarbild identity (Space Grotesk/Mono, blue, paper, regmark), no glows/gradients
This commit is contained in:
2026-07-23 18:43:47 +00:00
parent dba5620ff6
commit b047f55687
5 changed files with 105 additions and 28 deletions
+7 -4
View File
@@ -112,11 +112,14 @@ function LibStyles() {
.hidden{display:none;} .hidden{display:none;}
.leer{padding:60px 24px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:10px;} .leer{padding:60px 24px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:10px;}
.leer h3{margin:0;font-size:17px;}.leer p{margin:0;color:var(--soft);max-width:38ch;} .leer h3{margin:0;font-size:17px;}.leer p{margin:0;color:var(--soft);max-width:38ch;}
.galerie{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:14px;} .galerie{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;align-items:start;}
.kachel{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:9px;} @media(min-width:768px){.galerie{grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px;}}
.kachel{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:8px;}
.kachel.gewaehlt{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent);} .kachel.gewaehlt{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent);}
.kachel-bild{position:relative;aspect-ratio:1;background:var(--paper);border-radius:2px;overflow:hidden;cursor:pointer;} .kachel-bild{position:relative;border-radius:3px;overflow:hidden;cursor:pointer;
.kachel-bild img{width:100%;height:100%;object-fit:cover;display:block;} background:repeating-conic-gradient(oklch(90% 0.006 95) 0% 25%, oklch(96% 0.004 95) 0% 50%) 50%/16px 16px;}
.kachel-bild img{width:100%;height:auto;display:block;transition:transform .12s;}
.kachel-bild:active img{transform:scale(.98);}
.haken{position:absolute;top:6px;left:6px;width:21px;height:21px;border-radius:50%;border:1.5px solid #fff;background:rgba(22,21,15,.35);color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px;} .haken{position:absolute;top:6px;left:6px;width:21px;height:21px;border-radius:50%;border:1.5px solid #fff;background:rgba(22,21,15,.35);color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px;}
.kachel.gewaehlt .haken{background:var(--accent);} .kachel.gewaehlt .haken{background:var(--accent);}
.name{margin-top:8px;width:100%;font-size:12.5px;padding:6px 8px;font-family:var(--font-mono);border:1px solid var(--line);border-radius:3px;background:#fff;} .name{margin-top:8px;width:100%;font-size:12.5px;padding:6px 8px;font-family:var(--font-mono);border:1px solid var(--line);border-radius:3px;background:#fff;}
+3 -2
View File
@@ -110,8 +110,9 @@ function QStyles() {
.fein.err{color:var(--err);} .fein.err{color:var(--err);}
.items{display:flex;flex-direction:column;} .items{display:flex;flex-direction:column;}
.item{display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:1px solid var(--line);} .item{display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:1px solid var(--line);}
.thumb{width:44px;height:44px;flex:none;background:var(--paper);border:1px solid var(--line);border-radius:3px;overflow:hidden;display:flex;align-items:center;justify-content:center;} .thumb{width:46px;height:46px;flex:none;border:1px solid var(--line);border-radius:3px;overflow:hidden;display:flex;align-items:center;justify-content:center;
.thumb img{width:100%;height:100%;object-fit:cover;} background:repeating-conic-gradient(oklch(90% 0.006 95) 0% 25%, oklch(96% 0.004 95) 0% 50%) 50%/12px 12px;}
.thumb img{width:100%;height:100%;object-fit:contain;}
.itxt{flex:1;min-width:0;} .itxt{flex:1;min-width:0;}
.itxt b{font-size:13.5px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .itxt b{font-size:13.5px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.iact{flex:none;} .iact{flex:none;}
+4 -3
View File
@@ -273,10 +273,11 @@ function StudioStyles() {
.drop.drag{border-color:var(--accent);background:var(--accent-bg);} .drop.drag{border-color:var(--accent);background:var(--accent-bg);}
.drop p{margin:0;color:var(--soft);font-size:14px;line-height:1.55;} .drop p{margin:0;color:var(--soft);font-size:14px;line-height:1.55;}
.kbd{font-family:var(--font-mono);font-size:12px;background:var(--paper);border:1px solid var(--line);border-radius:3px;padding:1px 6px;} .kbd{font-family:var(--font-mono);font-size:12px;background:var(--paper);border:1px solid var(--line);border-radius:3px;padding:1px 6px;}
.mini-raster{display:grid;grid-template-columns:repeat(auto-fill,minmax(78px,1fr));gap:8px;width:100%;} .mini-raster{display:grid;grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:8px;width:100%;}
.mini-bild{position:relative;aspect-ratio:3/4;background:var(--paper);border:1px solid var(--line);border-radius:3px;overflow:hidden;} .mini-bild{position:relative;aspect-ratio:1;border:1px solid var(--line);border-radius:3px;overflow:hidden;
background:repeating-conic-gradient(oklch(90% 0.006 95) 0% 25%, oklch(96% 0.004 95) 0% 50%) 50%/14px 14px;}
.mini-bild.err{border-color:var(--err);} .mini-bild.err{border-color:var(--err);}
.mini-bild img{width:100%;height:100%;object-fit:cover;display:block;} .mini-bild img{width:100%;height:100%;object-fit:contain;display:block;}
.mini-bild>button{position:absolute;top:3px;right:3px;width:20px;height:20px;border:none;border-radius:50%;background:rgba(22,21,15,.75);color:#fff;cursor:pointer;font-size:14px;line-height:1;} .mini-bild>button{position:absolute;top:3px;right:3px;width:20px;height:20px;border:none;border-radius:50%;background:rgba(22,21,15,.75);color:#fff;cursor:pointer;font-size:14px;line-height:1;}
.mini-bild .badge{position:absolute;bottom:3px;left:3px;background:var(--err);color:#fff;width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;} .mini-bild .badge{position:absolute;bottom:3px;left:3px;background:var(--err);color:#fff;width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;}
.lade{position:absolute;inset:0;background:var(--paper);opacity:.6;} .lade{position:absolute;inset:0;background:var(--paper);opacity:.6;}
+83 -19
View File
@@ -8,50 +8,114 @@ import '../styles/tokens.css';
interface Props { title?: string } interface Props { title?: string }
const { title = 'Klarbild' } = Astro.props; const { title = 'Klarbild' } = Astro.props;
const user = Astro.locals.user; const user = Astro.locals.user;
const path = Astro.url.pathname;
const active = (href: string) => href === '/' ? path === '/' : path.startsWith(href);
const nav = [
{ href: '/', label: 'Studio', icon: 'studio' },
{ href: '/bibliothek', label: 'Bibliothek', icon: 'library' },
{ href: '/warteschlange', label: 'Warteschlange', icon: 'queue' },
...(user?.role === 'admin' ? [{ href: '/admin', label: 'Admin', icon: 'admin' }] : []),
];
const S = 'fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"';
const ICONS: Record<string, string> = {
studio: `<svg viewBox="0 0 24 24" ${S}><rect x="3.5" y="3.5" width="17" height="17" rx="4"/><path d="M12 8.5v7M8.5 12h7"/></svg>`,
library: `<svg viewBox="0 0 24 24" ${S}><rect x="3.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.5"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.5"/></svg>`,
queue: `<svg viewBox="0 0 24 24" ${S}><path d="M9 6h11M9 12h11M9 18h7"/><circle cx="4.5" cy="6" r="1.15" fill="currentColor" stroke="none"/><circle cx="4.5" cy="12" r="1.15" fill="currentColor" stroke="none"/><circle cx="4.5" cy="18" r="1.15" fill="currentColor" stroke="none"/></svg>`,
admin: `<svg viewBox="0 0 24 24" ${S}><path d="M4 8h9M17 8h3M4 16h3M11 16h9"/><circle cx="15" cy="8" r="2.2"/><circle cx="9" cy="16" r="2.2"/></svg>`,
};
--- ---
<!doctype html> <!doctype html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0e5f6e" /> <meta name="theme-color" content="#EAEAE3" />
<title>{title}</title> <title>{title}</title>
</head> </head>
<body> <body>
{user && ( {user && (
<header class="topbar"> <header class="topbar">
<a href="/" class="brand"><span class="regmark"></span>klarbild</a> <a href="/" class="brand"><span class="regmark"></span>klarbild</a>
<nav> <nav class="topnav">
<a href="/">Studio</a> {nav.map((n) => (
<a href="/bibliothek">Bibliothek</a> <a href={n.href} class:list={['tnav', { on: active(n.href) }]}>{n.label}</a>
<a href="/warteschlange">Warteschlange</a> ))}
{user.role === 'admin' && <a href="/admin">Admin</a>}
</nav> </nav>
<form method="post" action="/api/auth/logout" class="logout"> <form method="post" action="/api/auth/logout" class="logout">
<span>{user.name}</span> <span class="uname">{user.name}</span>
<button type="submit">Abmelden</button> <button type="submit" aria-label="Abmelden">Abmelden</button>
</form> </form>
</header> </header>
)} )}
<main class="wrap"><slot /></main> <main class="wrap"><slot /></main>
{user && (
<nav class="tabbar" aria-label="Hauptnavigation">
{nav.map((n) => (
<a href={n.href} class:list={['tab', { on: active(n.href) }]}>
<span class="ticon" set:html={ICONS[n.icon]} />
<span class="tlabel">{n.label}</span>
</a>
))}
</nav>
)}
<style is:global> <style is:global>
.wrap { max-width: var(--maxw); margin: 0 auto; padding: 24px 20px 80px; } .wrap { max-width: var(--maxw); margin: 0 auto; padding: 20px 16px calc(88px + env(safe-area-inset-bottom)); }
@media (min-width: 768px) { .wrap { padding: 26px 22px 80px; } }
/* Top bar */
.topbar { .topbar {
display: flex; align-items: center; gap: 20px; display: flex; align-items: center; gap: 14px;
padding: 12px 20px; border-bottom: 1px solid var(--line); padding: 10px 16px; border-bottom: 1px solid var(--line);
background: var(--surface); position: sticky; top: 0; z-index: 10; background: color-mix(in oklab, var(--card) 88%, transparent);
backdrop-filter: saturate(1.2) blur(8px);
position: sticky; top: 0; z-index: 20;
padding-top: calc(10px + env(safe-area-inset-top));
} }
.brand { display: inline-flex; align-items: center; gap: 8px; .brand { display: inline-flex; align-items: center; gap: 8px;
font-family: var(--font-display); font-weight: 700; font-size: 1.3rem; font-family: var(--font-display); font-weight: 700; font-size: 1.2rem;
color: var(--ink); text-decoration: none; letter-spacing: -0.02em; } color: var(--ink); text-decoration: none; letter-spacing: -0.02em; }
.brand .regmark { transform: translateY(1px); } .brand .regmark { transform: translateY(1px); }
.topbar nav { display: flex; gap: 16px; margin-left: 8px; } .topnav { display: none; }
.topbar nav a { color: var(--ink-soft); text-decoration: none; font-size: 0.95rem; } @media (min-width: 768px) {
.topbar nav a:hover { color: var(--ink); } .topnav { display: flex; gap: 4px; margin-left: 8px; }
.logout { margin-left: auto; display: flex; align-items: center; gap: 10px; .tnav { padding: 7px 12px; border-radius: 999px; color: var(--ink-soft);
color: var(--ink-mute); font-size: 0.9rem; } text-decoration: none; font-size: 0.92rem; transition: background .15s, color .15s; }
.tnav:hover { color: var(--ink); background: var(--surface-2, var(--paper)); }
.tnav.on { color: var(--accent); background: var(--accent-bg); font-weight: 600; }
}
.logout { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.uname { color: var(--ink-mute, var(--soft)); font-size: 0.9rem; display: none; }
@media (min-width: 480px) { .uname { display: inline; } }
.logout button { background: none; border: 1px solid var(--line); border-radius: 999px; .logout button { background: none; border: 1px solid var(--line); border-radius: 999px;
padding: 5px 12px; color: var(--ink-soft); cursor: pointer; } padding: 7px 14px; color: var(--ink-soft); cursor: pointer; font-family: inherit; font-size: 0.85rem;
transition: transform .06s ease, background .15s; min-height: 36px; }
.logout button:active { transform: scale(0.96); }
/* Bottom tab bar (mobile-first) */
.tabbar {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
background: color-mix(in oklab, var(--card) 92%, transparent);
backdrop-filter: saturate(1.3) blur(12px);
border-top: 1px solid var(--line);
padding-bottom: env(safe-area-inset-bottom);
}
.tab { display: flex; flex-direction: column; align-items: center; gap: 3px;
padding: 9px 4px 8px; text-decoration: none; color: var(--ink-mute, var(--soft));
font-size: 10.5px; letter-spacing: .01em; transition: color .15s, transform .06s; }
.tab:active { transform: scale(0.94); }
.tab.on { color: var(--accent); }
.tab .ticon { width: 22px; height: 22px; display: grid; place-items: center; }
.tab .ticon svg { width: 22px; height: 22px; }
.tlabel { line-height: 1; }
@media (min-width: 768px) { .tabbar { display: none; } }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
</style> </style>
</body> </body>
</html> </html>
+8
View File
@@ -56,6 +56,14 @@ a { color: var(--accent); }
.regmark::before { left: 50%; top: 0; width: 1.5px; height: 100%; transform: translateX(-50%); } .regmark::before { left: 50%; top: 0; width: 1.5px; height: 100%; transform: translateX(-50%); }
.regmark::after { top: 50%; left: 0; height: 1.5px; width: 100%; transform: translateY(-50%); } .regmark::after { top: 50%; left: 0; height: 1.5px; width: 100%; transform: translateY(-50%); }
/* Mobile-First: 16px-Felder gegen iOS-Zoom beim Fokus */
@media (max-width: 767px) {
input, select, textarea { font-size: 16px !important; }
}
/* Taktiles Feedback auf allen Buttons */
button { transition: transform 0.06s ease; }
button:active { transform: scale(0.97); }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; } * { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
} }