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:
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
115
frontend/src/client/bottom-nav.ts
Normal file
115
frontend/src/client/bottom-nav.ts
Normal 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);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { initI18n } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
import { initBottomNav } from "./bottom-nav";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initI18n();
|
||||
initSidebar();
|
||||
initBottomNav();
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
80
frontend/src/components/BottomNav.tsx
Normal file
80
frontend/src/components/BottomNav.tsx
Normal 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 & 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 & 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>
|
||||
);
|
||||
}
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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ührentabellen — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — Patentwissen für Hogan Lovells</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — Paliad</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
|
||||
@@ -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 — Paliad</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
<body className="has-sidebar">
|
||||
<Sidebar currentPath="" />
|
||||
<BottomNav currentPath="" />
|
||||
|
||||
<main>
|
||||
<section className="tool-page">
|
||||
|
||||
@@ -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 — Paliad</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 — 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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user