feat(frontend): PWA mobile BottomNav + Quick-Add sheet (t-paliad-041)

Phone-first bottom navigation per pwa-baseline.md. Renders only at
<768px; tablets and desktop are unchanged.

Slots: Start / Projekte / [+] Anlegen / Agenda / Menü.

- Center [+] opens a slide-up <dialog> sheet with three rows: Frist,
  Termin, Projekt. Native showModal() + ::backdrop, ESC and backdrop-tap
  dismiss, transform-based slide-up transition.
- Right Menü slot reuses the existing Sidebar mobile drawer via a new
  exported toggleMobileSidebar() (DRY with the legacy hamburger handler).
- Agenda slot carries a red-dot badge: count = today + overdue pending
  deadlines (live via /api/deadlines/summary, refreshed every 60s). Pulse
  animation when overdue > 0 — m: "Due is the latest we can do, OVERDUE
  is a catastrophy."
- visualViewport resize watcher hides the bar when the on-screen keyboard
  opens (>100px height shrink) so it doesn't cover form fields.
- safe-area-inset-bottom padding on the bar; main padding-bottom adjusts
  on phones so the last row stays above the bar.

PWA shell groundwork (defers manifest/SW/install-prompt to follow-ups):
- viewport-fit=cover on every page (required for safe-area to register)
- theme-color #65a30d (lime), apple-mobile-web-app-capable, status-bar
  style — all 30 page heads updated in one sweep.

Backend: deadline_service.SummaryCounts gains a `today` bucket so the
Agenda badge can distinguish "due today" from "this week" without a new
endpoint.

Files added:
  frontend/src/components/BottomNav.tsx
  frontend/src/client/bottom-nav.ts

Verified visually via headless chromium at 375x812, 800x600, 1280x800:
phone shows BottomNav (5 slots, lime [+] elevated), tablet shows the
existing hamburger only, desktop sidebar untouched. go build/vet/test
and bun run build all clean.
This commit is contained in:
m
2026-04-26 10:32:00 +02:00
parent 263a4605e3
commit 3f0c26fd3a
37 changed files with 708 additions and 42 deletions

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
// The /*__PALIAD_AGENDA_DATA__*/ token is replaced at request time by the Go
@@ -12,13 +13,17 @@ export function renderAgenda(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="agenda.title">Agenda &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
<script dangerouslySetInnerHTML={{ __html: HYDRATION_SCRIPT }} />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/agenda" />
<BottomNav currentPath="/agenda" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderAppointmentsCalendar(): string {
@@ -7,12 +8,16 @@ export function renderAppointmentsCalendar(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="termine.kalender.title">Terminkalender &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/appointments" />
<BottomNav currentPath="/appointments" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderAppointmentsDetail(): string {
@@ -7,12 +8,16 @@ export function renderAppointmentsDetail(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="termine.detail.title">Termin &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/appointments" />
<BottomNav currentPath="/appointments" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderAppointmentsNew(): string {
@@ -7,12 +8,16 @@ export function renderAppointmentsNew(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="termine.neu.title">Neuer Termin &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/appointments/new" />
<BottomNav currentPath="/appointments/new" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderAppointments(): string {
@@ -7,12 +8,16 @@ export function renderAppointments(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="termine.list.title">Termine &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/appointments" />
<BottomNav currentPath="/appointments" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderChangelog(): string {
@@ -7,12 +8,16 @@ export function renderChangelog(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="changelog.title">Neuigkeiten &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/changelog" />
<BottomNav currentPath="/changelog" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
// Template detail page. Shows template metadata + list of existing
@@ -11,12 +12,16 @@ export function renderChecklistsDetail(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="checklisten.title">Checkliste &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/checklists" />
<BottomNav currentPath="/checklists" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
// Interactive instance page. Loads template + instance JSON, renders
@@ -10,12 +11,16 @@ export function renderChecklistsInstance(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="checklisten.instance.title">Checklisten-Instanz &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/checklists" />
<BottomNav currentPath="/checklists" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderChecklists(): string {
@@ -7,12 +8,16 @@ export function renderChecklists(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="checklisten.title">Checklisten &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/checklists" />
<BottomNav currentPath="/checklists" />
<main>
<section className="tool-page">

View File

@@ -0,0 +1,115 @@
import { toggleMobileSidebar } from "./sidebar";
const KEYBOARD_THRESHOLD_PX = 100;
const BADGE_REFRESH_MS = 60_000;
export function initBottomNav(): void {
const nav = document.getElementById("bottom-nav");
if (!nav) return;
initMenuSlot();
initQuickAddSheet();
initKeyboardWatcher();
initAgendaBadge();
}
function initMenuSlot(): void {
const btn = document.getElementById("bottom-nav-menu");
btn?.addEventListener("click", (e) => {
e.preventDefault();
toggleMobileSidebar();
});
}
function initQuickAddSheet(): void {
const trigger = document.getElementById("bottom-nav-add") as HTMLButtonElement | null;
const dialog = document.getElementById("quick-add-sheet") as HTMLDialogElement | null;
const cancel = document.getElementById("quick-add-cancel") as HTMLButtonElement | null;
if (!trigger || !dialog) return;
trigger.addEventListener("click", (e) => {
e.preventDefault();
if (typeof dialog.showModal === "function") {
dialog.showModal();
} else {
dialog.setAttribute("open", "");
}
dialog.classList.add("is-open");
});
function close(): void {
dialog!.classList.remove("is-open");
if (typeof dialog!.close === "function") {
dialog!.close();
} else {
dialog!.removeAttribute("open");
}
}
cancel?.addEventListener("click", close);
dialog.addEventListener("click", (e) => {
if (e.target === dialog) close();
});
dialog.addEventListener("close", () => {
dialog.classList.remove("is-open");
});
dialog.querySelectorAll<HTMLAnchorElement>(".quick-add-row").forEach((row) => {
row.addEventListener("click", () => {
// Native <a> navigation handles routing; close sheet first so it
// does not flash on next page paint via bfcache.
close();
});
});
}
function initKeyboardWatcher(): void {
const vv = window.visualViewport;
if (!vv) return;
let baseHeight = window.innerHeight;
window.addEventListener("orientationchange", () => {
setTimeout(() => {
baseHeight = window.innerHeight;
document.body.classList.remove("keyboard-open");
}, 250);
});
const handler = () => {
const delta = baseHeight - vv.height;
document.body.classList.toggle("keyboard-open", delta > KEYBOARD_THRESHOLD_PX);
};
vv.addEventListener("resize", handler);
}
function initAgendaBadge(): void {
const badge = document.getElementById("bottom-nav-agenda-badge");
if (!badge) return;
function refresh(): void {
fetch("/api/deadlines/summary", { credentials: "same-origin" })
.then((r) => (r.ok ? r.json() : null))
.then((data: { overdue?: number; today?: number } | null) => {
if (!data) return;
const overdue = typeof data.overdue === "number" ? data.overdue : 0;
const today = typeof data.today === "number" ? data.today : 0;
const total = overdue + today;
if (total <= 0) {
badge!.style.display = "none";
badge!.classList.remove("bottom-nav-badge-overdue");
return;
}
badge!.textContent = total > 9 ? "9+" : String(total);
badge!.style.display = "";
badge!.classList.toggle("bottom-nav-badge-overdue", overdue > 0);
})
.catch(() => {
// Badge is decorative; never break the page.
});
}
refresh();
setInterval(refresh, BADGE_REFRESH_MS);
}

View File

@@ -37,6 +37,18 @@ const translations: Record<Lang, Record<string, string>> = {
"nav.neuigkeiten": "Neuigkeiten",
"nav.soon.tooltip": "Bald verf\u00fcgbar",
// BottomNav (mobile)
"bottomnav.add": "Anlegen",
"bottomnav.menu": "Menü",
"bottomnav.add.title": "Schnell anlegen",
"bottomnav.add.deadline": "Frist anlegen",
"bottomnav.add.deadline.sub": "Neue Frist mit Datum & Projekt",
"bottomnav.add.appointment": "Termin anlegen",
"bottomnav.add.appointment.sub": "Neuer Termin mit Uhrzeit & Ort",
"bottomnav.add.project": "Projekt anlegen",
"bottomnav.add.project.sub": "Neues Mandat / Verfahren / Patent",
"bottomnav.add.cancel": "Abbrechen",
// Changelog (What's New) — t-paliad-027
"changelog.title": "Neuigkeiten — Paliad",
"changelog.heading": "Neuigkeiten",
@@ -1157,6 +1169,18 @@ const translations: Record<Lang, Record<string, string>> = {
"nav.neuigkeiten": "What's New",
"nav.soon.tooltip": "Coming soon",
// BottomNav (mobile)
"bottomnav.add": "New",
"bottomnav.menu": "Menu",
"bottomnav.add.title": "Quick add",
"bottomnav.add.deadline": "New deadline",
"bottomnav.add.deadline.sub": "Deadline with date & project",
"bottomnav.add.appointment": "New appointment",
"bottomnav.add.appointment.sub": "Appointment with time & place",
"bottomnav.add.project": "New project",
"bottomnav.add.project.sub": "New matter / case / patent",
"bottomnav.add.cancel": "Cancel",
// Changelog (What's New) — t-paliad-027
"changelog.title": "What's New — Paliad",
"changelog.heading": "What's New",

View File

@@ -1,7 +1,9 @@
import { initI18n } from "./i18n";
import { initSidebar } from "./sidebar";
import { initBottomNav } from "./bottom-nav";
document.addEventListener("DOMContentLoaded", () => {
initI18n();
initSidebar();
initBottomNav();
});

View File

@@ -6,6 +6,25 @@ import { getChangelogSeen } from "./changelog-seen";
const PIN_KEY = "paliad-sidebar-pinned";
const LEGACY_PIN_KEY = "patholo-sidebar-pinned";
// toggleMobileSidebar opens or closes the slide-out drawer. Exposed so the
// BottomNav menu slot can call it without duplicating the open/close
// machinery (overlay, body-scroll lock, etc.).
export function toggleMobileSidebar(): void {
const sidebar = document.querySelector<HTMLElement>(".sidebar");
const overlay = document.querySelector<HTMLElement>(".sidebar-overlay");
if (!sidebar) return;
const isOpen = sidebar.classList.contains("mobile-open");
if (isOpen) {
sidebar.classList.remove("mobile-open");
overlay?.classList.remove("visible");
document.body.classList.remove("no-scroll");
} else {
sidebar.classList.add("mobile-open");
overlay?.classList.add("visible");
document.body.classList.add("no-scroll");
}
}
// migrateLegacyPinKey copies the pre-rebrand pin state into the new key on
// first load and removes the stale entry. Drop this fallback once the rename
// grace period is over.
@@ -85,14 +104,7 @@ export function initSidebar() {
if (hamburger) {
hamburger.addEventListener("click", () => {
const isOpen = sidebar.classList.contains("mobile-open");
if (isOpen) {
closeMobile();
} else {
sidebar.classList.add("mobile-open");
overlay?.classList.add("visible");
document.body.classList.add("no-scroll");
}
toggleMobileSidebar();
});
}
@@ -100,6 +112,12 @@ export function initSidebar() {
overlay.addEventListener("click", closeMobile);
}
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && sidebar.classList.contains("mobile-open")) {
closeMobile();
}
});
// Close mobile sidebar on nav click
sidebar.querySelectorAll<HTMLAnchorElement>("a[href]").forEach((link) => {
link.addEventListener("click", () => {

View File

@@ -0,0 +1,80 @@
import { h, Fragment } from "../jsx";
const ICON_GAUGE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 14l3.5-3.5"/><path d="M3 12a9 9 0 0 1 18 0"/><path d="M12 3v2"/><path d="M3 12H5"/><path d="M19 12h2"/><path d="M5.6 5.6l1.4 1.4"/><path d="M17 7l1.4-1.4"/></svg>';
const ICON_FOLDER = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
const ICON_AGENDA = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="17" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="7" y1="13" x2="13" y2="13"/><line x1="7" y1="17" x2="17" y2="17"/></svg>';
const ICON_MENU = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg>';
const ICON_PLUS = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
const ICON_DEADLINE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
const ICON_APPOINTMENT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>';
interface BottomNavProps {
currentPath: string;
}
function isActive(href: string, currentPath: string): boolean {
return href === currentPath || (href !== "/" && currentPath.startsWith(href + "/"));
}
function slot(href: string, icon: string, i18nKey: string, label: string, currentPath: string, badgeId?: string): string {
const active = isActive(href, currentPath);
return (
<a href={href} className={`bottom-nav-slot${active ? " active" : ""}`} data-bn-slot={href}>
<span className="bottom-nav-icon" dangerouslySetInnerHTML={{ __html: icon }} />
<span className="bottom-nav-label" data-i18n={i18nKey}>{label}</span>
{badgeId ? <span className="bottom-nav-badge" id={badgeId} style="display:none" aria-hidden="true" /> : ""}
</a>
);
}
export function BottomNav({ currentPath }: BottomNavProps): string {
return (
<Fragment>
<nav className="bottom-nav" id="bottom-nav" aria-label="Mobile navigation">
{slot("/dashboard", ICON_GAUGE, "nav.home", "Start", currentPath)}
{slot("/projects", ICON_FOLDER, "nav.projekte", "Projekte", currentPath)}
<button type="button" className="bottom-nav-slot bottom-nav-add" id="bottom-nav-add" aria-label="Anlegen">
<span className="bottom-nav-add-circle" dangerouslySetInnerHTML={{ __html: ICON_PLUS }} />
<span className="bottom-nav-label" data-i18n="bottomnav.add">Anlegen</span>
</button>
{slot("/agenda", ICON_AGENDA, "nav.agenda", "Agenda", currentPath, "bottom-nav-agenda-badge")}
<button type="button" className="bottom-nav-slot" id="bottom-nav-menu" aria-label="Menü">
<span className="bottom-nav-icon" dangerouslySetInnerHTML={{ __html: ICON_MENU }} />
<span className="bottom-nav-label" data-i18n="bottomnav.menu">Menü</span>
</button>
</nav>
<dialog className="quick-add-sheet" id="quick-add-sheet" aria-label="Schnell anlegen">
<div className="quick-add-card">
<div className="quick-add-handle" aria-hidden="true" />
<h2 className="quick-add-title" data-i18n="bottomnav.add.title">Schnell anlegen</h2>
<a href="/deadlines/new" className="quick-add-row" data-bn-add="deadline">
<span className="quick-add-icon" dangerouslySetInnerHTML={{ __html: ICON_DEADLINE }} />
<span className="quick-add-row-label">
<span className="quick-add-row-title" data-i18n="bottomnav.add.deadline">Frist anlegen</span>
<span className="quick-add-row-sub" data-i18n="bottomnav.add.deadline.sub">Neue Frist mit Datum &amp; Projekt</span>
</span>
</a>
<a href="/appointments/new" className="quick-add-row" data-bn-add="appointment">
<span className="quick-add-icon" dangerouslySetInnerHTML={{ __html: ICON_APPOINTMENT }} />
<span className="quick-add-row-label">
<span className="quick-add-row-title" data-i18n="bottomnav.add.appointment">Termin anlegen</span>
<span className="quick-add-row-sub" data-i18n="bottomnav.add.appointment.sub">Neuer Termin mit Uhrzeit &amp; Ort</span>
</span>
</a>
<a href="/projects/new" className="quick-add-row" data-bn-add="project">
<span className="quick-add-icon" dangerouslySetInnerHTML={{ __html: ICON_FOLDER }} />
<span className="quick-add-row-label">
<span className="quick-add-row-title" data-i18n="bottomnav.add.project">Projekt anlegen</span>
<span className="quick-add-row-sub" data-i18n="bottomnav.add.project.sub">Neues Mandat / Verfahren / Patent</span>
</span>
</a>
<button type="button" className="quick-add-cancel" id="quick-add-cancel" data-i18n="bottomnav.add.cancel">Abbrechen</button>
</div>
</dialog>
</Fragment>
);
}

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderCourts(): string {
@@ -7,12 +8,16 @@ export function renderCourts(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="gerichte.title">Gerichtsverzeichnis &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/courts" />
<BottomNav currentPath="/courts" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
// The /* __PALIAD_DASHBOARD_DATA__ */ token below is replaced at request time
@@ -14,13 +15,17 @@ export function renderDashboard(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="dashboard.title">Dashboard &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
<script dangerouslySetInnerHTML={{ __html: HYDRATION_SCRIPT }} />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/dashboard" />
<BottomNav currentPath="/dashboard" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderDeadlinesCalendar(): string {
@@ -7,12 +8,16 @@ export function renderDeadlinesCalendar(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="fristen.kalender.title">Fristenkalender &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/deadlines" />
<BottomNav currentPath="/deadlines" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderDeadlinesDetail(): string {
@@ -7,12 +8,16 @@ export function renderDeadlinesDetail(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="fristen.detail.title">Frist &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/deadlines" />
<BottomNav currentPath="/deadlines" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderDeadlinesNew(): string {
@@ -7,12 +8,16 @@ export function renderDeadlinesNew(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="fristen.neu.title">Neue Frist &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/deadlines/new" />
<BottomNav currentPath="/deadlines/new" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderDeadlines(): string {
@@ -7,12 +8,16 @@ export function renderDeadlines(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="fristen.list.title">Fristen &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/deadlines" />
<BottomNav currentPath="/deadlines" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
const ICON_WORD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><path d="M8 13l1.5 5 1.5-4 1.5 4 1.5-5"/></svg>';
@@ -29,12 +30,16 @@ export function renderDownloads(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="downloads.title">Downloads &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/downloads" />
<BottomNav currentPath="/downloads" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
interface ProceedingDef {
@@ -41,12 +42,16 @@ export function renderFristenrechner(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="fristen.title">Fristenrechner &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/tools/fristenrechner" />
<BottomNav currentPath="/tools/fristenrechner" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderGebuehrentabellen(): string {
@@ -7,12 +8,16 @@ export function renderGebuehrentabellen(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="gebuehren.title">Geb&uuml;hrentabellen &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/tools/gebuehrentabellen" />
<BottomNav currentPath="/tools/gebuehrentabellen" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderGlossary(): string {
@@ -7,12 +8,16 @@ export function renderGlossary(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="glossar.title">Patentglossar &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/glossary" />
<BottomNav currentPath="/glossary" />
<main>
<section className="tool-page">

View File

@@ -18,7 +18,10 @@ export function renderIndex(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="index.title">Paliad &mdash; Patentwissen f&uuml;r Hogan Lovells</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
const ICON_CALC = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="8" y2="10.01"/><line x1="12" y1="10" x2="12" y2="10.01"/><line x1="16" y1="10" x2="16" y2="10.01"/><line x1="8" y1="14" x2="8" y2="14.01"/><line x1="12" y1="14" x2="12" y2="14.01"/><line x1="16" y1="14" x2="16" y2="14.01"/><line x1="8" y1="18" x2="16" y2="18"/></svg>';
@@ -92,12 +93,16 @@ export function renderKostenrechner(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="kosten.title">Prozesskostenrechner &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/tools/kostenrechner" />
<BottomNav currentPath="/tools/kostenrechner" />
<div className="print-header" id="print-header">
<div className="print-header-brand">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderLinks(): string {
@@ -7,12 +8,16 @@ export function renderLinks(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="links.title">Links &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/links" />
<BottomNav currentPath="/links" />
<main>
<section className="tool-page">

View File

@@ -7,7 +7,10 @@ export function renderLogin(loginJs: string): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="login.title">Anmelden &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
// renderNotFound is the chromed 404 page served for any unknown
@@ -11,12 +12,16 @@ export function renderNotFound(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="notfound.title">Seite nicht gefunden &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="" />
<BottomNav currentPath="" />
<main>
<section className="tool-page">

View File

@@ -7,7 +7,10 @@ export function renderOnboarding(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="onboarding.title">Willkommen &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
// Project detail shell (v2). DOM IDs use the English `project-*` /
@@ -11,12 +12,16 @@ export function renderProjectsDetail(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="projekte.detail.title">Projekt &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/projects" />
<BottomNav currentPath="/projects" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
// "Neues Projekt" form (v2). Rendered at /projekte/neu. Supports five types;
@@ -9,12 +10,16 @@ export function renderProjectsNew(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="projekte.neu.title">Neues Projekt &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/projects/new" />
<BottomNav currentPath="/projects/new" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
// Renders the /projekte list page. File + export name stays `Akten` for build
@@ -9,12 +10,16 @@ export function renderProjects(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="projekte.title">Projekte &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/projects" />
<BottomNav currentPath="/projects" />
<main>
<section className="tool-page">

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
// Unified settings page. Three tabs today (Profil / Benachrichtigungen / CalDAV)
@@ -11,12 +12,16 @@ export function renderSettings(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="einstellungen.title">Einstellungen &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/settings" />
<BottomNav currentPath="/settings" />
<main>
<section className="tool-page">

View File

@@ -18,6 +18,7 @@
--max-width: 1080px;
--sidebar-collapsed: 64px;
--sidebar-expanded: 240px;
--bottom-nav-height: 56px;
}
*, *::before, *::after {
@@ -6304,3 +6305,281 @@ input[type="range"]::-moz-range-thumb {
text-align: center;
}
}
/* --- BottomNav (mobile, <768px) --- */
.bottom-nav {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 30;
background: var(--color-surface);
border-top: 1px solid var(--color-border);
box-shadow: 0 -1px 3px rgba(0, 0, 0, 0.04);
padding-bottom: env(safe-area-inset-bottom);
transition: transform 200ms ease-out;
}
.bottom-nav-slot {
flex: 1 1 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.15rem;
height: var(--bottom-nav-height);
color: var(--color-text-muted);
text-decoration: none;
background: none;
border: none;
font-family: var(--font-sans);
font-size: 0.65rem;
font-weight: 500;
cursor: pointer;
padding: 0.25rem 0.1rem 0.35rem;
position: relative;
-webkit-tap-highlight-color: transparent;
}
.bottom-nav-slot.active {
color: var(--color-accent);
}
.bottom-nav-slot.active::before {
content: "";
position: absolute;
top: 0;
left: 25%;
right: 25%;
height: 3px;
background: var(--color-accent);
border-radius: 0 0 2px 2px;
}
.bottom-nav-icon {
display: flex;
align-items: center;
justify-content: center;
}
.bottom-nav-icon svg {
width: 22px;
height: 22px;
}
.bottom-nav-label {
line-height: 1;
letter-spacing: 0.01em;
}
.bottom-nav-badge {
position: absolute;
top: 4px;
right: calc(50% - 18px);
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: var(--color-accent);
color: #fff;
font-size: 0.65rem;
font-weight: 700;
line-height: 16px;
text-align: center;
box-shadow: 0 0 0 2px var(--color-surface);
}
.bottom-nav-badge-overdue {
background: #dc2626;
animation: bn-pulse 1800ms ease-in-out infinite;
}
@keyframes bn-pulse {
0%, 100% { box-shadow: 0 0 0 2px var(--color-surface); }
50% { box-shadow: 0 0 0 2px var(--color-surface), 0 0 0 5px rgba(220, 38, 38, 0.25); }
}
/* Center [+] slot — visually elevated lime circle */
.bottom-nav-add {
color: var(--color-text-muted);
}
.bottom-nav-add-circle {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 50%;
background: var(--color-accent);
color: #fff;
margin-top: -10px;
box-shadow: var(--shadow-md);
transition: background 150ms ease, transform 100ms ease;
}
.bottom-nav-add:active .bottom-nav-add-circle {
background: var(--color-accent-light);
transform: scale(0.95);
}
.bottom-nav-add-circle svg {
width: 22px;
height: 22px;
}
/* Hide BottomNav when keyboard is open (visualViewport watcher) */
body.keyboard-open .bottom-nav {
transform: translateY(120%);
}
/* --- Quick-Add slide-up sheet --- */
dialog.quick-add-sheet {
border: none;
padding: 0;
margin: 0;
background: transparent;
color: var(--color-text);
max-width: none;
max-height: none;
width: 100%;
height: 100%;
}
dialog.quick-add-sheet[open] {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
background: transparent;
display: flex;
align-items: flex-end;
justify-content: center;
overflow: hidden;
}
dialog.quick-add-sheet::backdrop {
background: rgba(0, 0, 0, 0.5);
}
.quick-add-card {
width: 100%;
max-width: 480px;
background: var(--color-surface);
border-radius: 16px 16px 0 0;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.12);
padding: 0.5rem 1rem calc(1rem + env(safe-area-inset-bottom));
display: flex;
flex-direction: column;
gap: 0.25rem;
transform: translateY(100%);
transition: transform 220ms ease-out;
}
.quick-add-sheet.is-open .quick-add-card {
transform: translateY(0);
}
.quick-add-handle {
width: 36px;
height: 4px;
border-radius: 2px;
background: var(--color-border);
margin: 0.5rem auto 0.75rem;
}
.quick-add-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
margin: 0 0.25rem 0.25rem;
}
.quick-add-row {
display: flex;
align-items: center;
gap: 0.85rem;
padding: 0.85rem 0.5rem;
border-radius: var(--radius);
color: var(--color-text);
text-decoration: none;
cursor: pointer;
transition: background 100ms ease;
}
.quick-add-row:hover,
.quick-add-row:active {
background: rgba(0, 0, 0, 0.04);
}
.quick-add-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(101, 163, 13, 0.1);
color: var(--color-accent);
flex-shrink: 0;
}
.quick-add-icon svg {
width: 20px;
height: 20px;
}
.quick-add-row-label {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
}
.quick-add-row-title {
font-weight: 600;
font-size: 0.95rem;
}
.quick-add-row-sub {
font-size: 0.78rem;
color: var(--color-text-muted);
}
.quick-add-cancel {
margin-top: 0.5rem;
padding: 0.85rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
font-family: var(--font-sans);
font-size: 0.95rem;
font-weight: 600;
color: var(--color-text);
cursor: pointer;
}
.quick-add-cancel:hover {
background: rgba(0, 0, 0, 0.03);
}
/* --- Phone breakpoint (<768px): show BottomNav, hide legacy hamburger --- */
@media (max-width: 767px) {
.bottom-nav {
display: flex;
}
.sidebar-hamburger {
display: none !important;
}
body.has-sidebar main {
padding-bottom: calc(var(--bottom-nav-height) + 1rem + env(safe-area-inset-bottom));
}
}

View File

@@ -1,5 +1,6 @@
import { h } from "./jsx";
import { Sidebar } from "./components/Sidebar";
import { BottomNav } from "./components/BottomNav";
import { Footer } from "./components/Footer";
export function renderTeam(): string {
@@ -7,12 +8,16 @@ export function renderTeam(): string {
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#65a30d" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<title data-i18n="team.title">Team &mdash; Paliad</title>
<link rel="stylesheet" href="/assets/global.css" />
</head>
<body className="has-sidebar">
<Sidebar currentPath="/team" />
<BottomNav currentPath="/team" />
<main>
<section className="tool-page">

View File

@@ -385,6 +385,7 @@ func (s *DeadlineService) Delete(ctx context.Context, userID, fristID uuid.UUID)
// SummaryCounts returns traffic-light counts across the user's visible Deadlines.
type SummaryCounts struct {
Overdue int `json:"overdue" db:"overdue"`
Today int `json:"today" db:"today"`
ThisWeek int `json:"this_week" db:"this_week"`
Upcoming int `json:"upcoming" db:"upcoming"`
Completed int `json:"completed" db:"completed"`
@@ -403,14 +404,16 @@ func (s *DeadlineService) SummaryCounts(ctx context.Context, userID uuid.UUID, p
}
now := time.Now().UTC()
today := now.Truncate(24 * time.Hour)
tomorrow := today.AddDate(0, 0, 1)
endWeek := today.AddDate(0, 0, 7)
conds := []string{visibilityPredicate("p")}
args := map[string]any{
"user_id": userID,
"role": user.Role,
"today": today,
"endweek": endWeek,
"user_id": userID,
"role": user.Role,
"today": today,
"tomorrow": tomorrow,
"endweek": endWeek,
}
if projektID != nil {
conds = append(conds, `f.project_id = :project_id`)
@@ -420,6 +423,7 @@ func (s *DeadlineService) SummaryCounts(ctx context.Context, userID uuid.UUID, p
query := `
SELECT
COUNT(*) FILTER (WHERE f.status = 'pending' AND f.due_date < :today) AS overdue,
COUNT(*) FILTER (WHERE f.status = 'pending' AND f.due_date >= :today AND f.due_date < :tomorrow) AS today,
COUNT(*) FILTER (WHERE f.status = 'pending' AND f.due_date >= :today AND f.due_date < :endweek) AS this_week,
COUNT(*) FILTER (WHERE f.status = 'pending' AND f.due_date >= :endweek) AS upcoming,
COUNT(*) FILTER (WHERE f.status = 'completed') AS completed,