Files
klarbild/src/layouts/Base.astro
T
till cd9081823d feat: print page with crop editor and live sheet preview
New Druck tab: upload or pick from the library, choose a target size and copy
count per image, pan/zoom crop on a locked aspect ratio, and a live preview
that renders the same layout the PDF will contain. Sheet controls for paper,
margin, gap, bleed, crop marks, dpi and templates.
2026-08-18 06:20:46 +00:00

126 lines
6.3 KiB
Plaintext

---
import '@fontsource/space-grotesk/400.css';
import '@fontsource/space-grotesk/500.css';
import '@fontsource/space-grotesk/700.css';
import '@fontsource/space-mono/400.css';
import '@fontsource/space-mono/700.css';
import '../styles/tokens.css';
interface Props { title?: string }
const { title = 'Klarbild' } = Astro.props;
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: '/druck', label: 'Druck', icon: 'print' },
{ href: '/warteschlange', label: 'Warteschlange', icon: 'queue' },
{ href: '/anleitung', label: 'Hilfe', icon: 'help' },
...(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>`,
print: `<svg viewBox="0 0 24 24" ${S}><path d="M7 9V4.5h10V9"/><rect x="3.5" y="9" width="17" height="7" rx="2"/><path d="M7 14h10v5.5H7z"/></svg>`,
help: `<svg viewBox="0 0 24 24" ${S}><circle cx="12" cy="12" r="8.5"/><path d="M9.6 9.4a2.5 2.5 0 1 1 3.4 2.3c-.7.3-1 .8-1 1.6v.3"/><circle cx="12" cy="16.7" r="0.6" fill="currentColor" stroke="none"/></svg>`,
};
---
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#EAEAE3" />
<title>{title}</title>
</head>
<body>
{user && (
<header class="topbar">
<a href="/" class="brand"><span class="regmark"></span>klarbild</a>
<nav class="topnav">
{nav.map((n) => (
<a href={n.href} class:list={['tnav', { on: active(n.href) }]}>{n.label}</a>
))}
</nav>
<form method="post" action="/api/auth/logout" class="logout">
<span class="uname">{user.name}</span>
<button type="submit" aria-label="Abmelden">Abmelden</button>
</form>
</header>
)}
<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>
.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 {
display: flex; align-items: center; gap: 14px;
padding: 10px 16px; border-bottom: 1px solid var(--line);
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;
font-family: var(--font-display); font-weight: 700; font-size: 1.2rem;
color: var(--ink); text-decoration: none; letter-spacing: -0.02em; }
.brand .regmark { transform: translateY(1px); }
.topnav { display: none; }
@media (min-width: 768px) {
.topnav { display: flex; gap: 4px; margin-left: 8px; }
.tnav { padding: 7px 12px; border-radius: 999px; color: var(--ink-soft);
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;
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>
</body>
</html>