diff --git a/frontend/src/links.tsx b/frontend/src/links.tsx
index 2b68c5b..fcaf51b 100644
--- a/frontend/src/links.tsx
+++ b/frontend/src/links.tsx
@@ -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 {
-
+
+
+
+
Links — Paliad
+
diff --git a/frontend/src/login.tsx b/frontend/src/login.tsx
index 8ccb736..8d7ba45 100644
--- a/frontend/src/login.tsx
+++ b/frontend/src/login.tsx
@@ -7,7 +7,10 @@ export function renderLogin(loginJs: string): string {
-
+
+
+
+
Anmelden — Paliad
diff --git a/frontend/src/notfound.tsx b/frontend/src/notfound.tsx
index a71bc4f..dc9967a 100644
--- a/frontend/src/notfound.tsx
+++ b/frontend/src/notfound.tsx
@@ -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 {
-
+
+
+
+
Seite nicht gefunden — Paliad
+
diff --git a/frontend/src/onboarding.tsx b/frontend/src/onboarding.tsx
index cb6a215..762eac8 100644
--- a/frontend/src/onboarding.tsx
+++ b/frontend/src/onboarding.tsx
@@ -7,7 +7,10 @@ export function renderOnboarding(): string {
-
+
+
+
+
Willkommen — Paliad
diff --git a/frontend/src/projects-detail.tsx b/frontend/src/projects-detail.tsx
index 0a8e60a..cd57d57 100644
--- a/frontend/src/projects-detail.tsx
+++ b/frontend/src/projects-detail.tsx
@@ -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 {
-
+
+
+
+
Projekt — Paliad
+
diff --git a/frontend/src/projects-new.tsx b/frontend/src/projects-new.tsx
index 066e019..4a457b6 100644
--- a/frontend/src/projects-new.tsx
+++ b/frontend/src/projects-new.tsx
@@ -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 {
-
+
+
+
+
Neues Projekt — Paliad
+
diff --git a/frontend/src/projects.tsx b/frontend/src/projects.tsx
index efd6911..8a53298 100644
--- a/frontend/src/projects.tsx
+++ b/frontend/src/projects.tsx
@@ -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 {
-
+
+
+
+
Projekte — Paliad
+
diff --git a/frontend/src/settings.tsx b/frontend/src/settings.tsx
index 8ca79c0..c9d5acf 100644
--- a/frontend/src/settings.tsx
+++ b/frontend/src/settings.tsx
@@ -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 {
-
+
+
+
+
Einstellungen — Paliad
+
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index dbcb5fe..6047b8a 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -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));
+ }
+}
diff --git a/frontend/src/team.tsx b/frontend/src/team.tsx
index 7fc793e..a377bf3 100644
--- a/frontend/src/team.tsx
+++ b/frontend/src/team.tsx
@@ -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 {
-
+
+
+
+
Team — Paliad
+
diff --git a/internal/services/deadline_service.go b/internal/services/deadline_service.go
index 9c18cfd..303aaf8 100644
--- a/internal/services/deadline_service.go
+++ b/internal/services/deadline_service.go
@@ -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,