diff --git a/.gitignore b/.gitignore index ec99bcc..9b901ee 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ sessions/*.db-shm # macOS-Artefakte .DS_Store +sessions/*.txt diff --git a/26-4047 KfW SiKo_PlanKo_04_16.09.2026.pdf b/26-4047 KfW SiKo_PlanKo_04_16.09.2026.pdf deleted file mode 100644 index 7aeb3e1..0000000 Binary files a/26-4047 KfW SiKo_PlanKo_04_16.09.2026.pdf and /dev/null differ diff --git a/CLAUDE.md b/CLAUDE.md index 7248dbd..f9d81c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,13 +9,56 @@ Single-page app for tracking engineering projects across weekly sessions step, no bundler, no tests. Data is persisted in a SQLite database (`node:sqlite`, needs Node >= 22.13) at `sessions/projektuebersicht.db`. -Two HTML entrypoints, both served statically by Express: -- `Test.html` — the main app (tabs: Projektboard, Auslastung, Projekte, - Projektbeteiligte). Served at `/`. -- `project-plan.html` — per-project Gantt/TODO planning view, opened in a new - tab via `openProjectPlan()` in `js/gantt.js`. Loads the shared helper modules - (`globals`, `utils`, `ui`, `date-picker`, `server`, `gantt`) plus the - plan modules under `js/plan/` — NOT the full main module set. +**ONE** HTML entrypoint, served statically by Express: +- `App.html` — the whole app (sidebar nav items, in order: Meine Aufgaben, + **Projekte**, Wochenbericht, Auslastung, Projektbeteiligte, then + **Einstellungen** and — for admins only — **Admin**). Served at `/`. It + also contains the **Projektbereich** — the area of ONE project — as the + tab `plan` (`
`), with its own sub-navigation (`.plan-nav`) + over six sections: **Dashboard**, **Terminplan** (Gantt), **Aufgaben** + (task/milestone list), **Leistung** (erbrachter Leistungswert, see below), + **Dateien**, **Einstellungen** (project master data incl. honorar and the + beauftragte Leistungsphasen). Opened via `openProjectPlan()` in + `js/gantt.js`, which is now just `setPlanTarget()` + `showTab('plan')` — + the same kind of switch as Dashboard → Projekte, sidebar and topbar + stay put. + **There used to be a separate `Projekte` tab next to the Projektboard.** It + listed the very same projects a second time and offered a second editing + mask for the very same fields. Both are now ONE list — the tab `board`, + labelled **Projekte** (see "Eine Projektliste" below). The URL still reads `/Projektplan/?weekLabel=…&projNr=…` + and the selected section still lives in the URL fragment (`…#aufgaben`), + so both stay linkable. See "Projektbereich" below. +**`admin-users.html` no longer exists either.** It was a standalone +admin-only page with a hand-built header instead of the sidebar and its own +`checkAuthAndInit()` + role check. Its markup moved into `App.html` as the +`benutzer` section of the Admin area, its ` - - -
-
-
-

Projektplan

-
-
-
- - -
-
- -
-
-
- - - - - - - - - - - - -
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/styles.css b/public/styles.css index 832c101..4a2c19d 100644 --- a/public/styles.css +++ b/public/styles.css @@ -20,6 +20,38 @@ --green: #248a3d; --amber: #b25000; --red: #d70015; + --red-hover: #e8112d; + --red-active: #b8000f; + + /* Flächen des Anwesenheits-Kacheln im Zustand "anwesend" — eigene Tokens, + weil eine feste rgba-Grünfläche im Dark Mode nicht trägt. */ + --attend-bg: rgba(36,138,61,.10); + --attend-bg-hover: rgba(36,138,61,.16); + + /* Kopfband der Wochenbericht-Karten je Änderungsart. Das Band trägt weiße + Schrift, deshalb etwas dunkler als --green (#248a3d käme nur auf + Kontrast 4,4 : 1 und bliebe damit unter den 4,5 : 1 für normalen Text). */ + --band-neu: #0071e3; + --band-fortschritt: #22823a; + + /* Gelbe Markierung neu hinzugekommener Protokoll-Punkte — bewusst eigenes + Token statt --band-neu (dort blau, bedeutet "neu kommentiertes Projekt" + im Wochenbericht — eine andere Bedeutung, die hier kollidieren würde). */ + --protokoll-neu-bg: #fff6db; + --protokoll-neu-border: #e6b800; + + /* Die drei Zustände eines Protokoll-Punkts (protokoll_punkte.status) — + eigene Bezeichner statt direkter --amber/--green/--accent-Nutzung an + jeder Stelle, damit sich die Farbzuordnung an einer Stelle ändern + ließe, ohne jede Verwendung einzeln anzufassen. Referenzieren andere + Tokens per var(), lösen sich also auch im Dark Mode korrekt auf, ohne + dort erneut definiert zu werden. --status-info-bg ist die Fläche einer + kompletten Info-Zeile in der Protokoll-Tabelle (color-mix statt fester + rgba-Werte, damit die Deckkraft in beiden Themes gleich wirkt). */ + --status-offen: var(--amber); + --status-erledigt: var(--green); + --status-info: var(--accent); + --status-info-bg: color-mix(in srgb, var(--accent) 8%, transparent); --r-sm: 6px; --r-md: 8px; @@ -52,45 +84,75 @@ body { .container { padding: 20px 24px; } -/* === Kopfzeile & Navigation === */ -header { +/* === App-Shell: globales Menüband oben + linke Navigations-Sidebar + + Hauptbereich (OpenProject-Anordnung: ein durchgehendes globales Menüband + trägt Marke, aktuellen Kontext [Sitzung] und alle Aktionen/Konto rechts; + die Sidebar darunter ist reine Modul-Navigation, sonst nichts) === */ +.app-shell { display: flex; flex-direction: column; min-height: 100vh; } +.app-body { display: flex; flex: 1; min-height: 0; } +.app-main { flex: 1; min-width: 0; } + +/* Generisches Icon-System (js/icons.js): Icons skalieren wie ein Zeichen mit + der umgebenden Schriftgröße (1.1em), erben die Textfarbe über currentColor. */ +.icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } +.icon svg { width: 1.1em; height: 1.1em; display: block; } + +/* === Globales Menüband (immer sichtbar, alle Breiten) === */ +.app-topbar { + position: sticky; + top: 0; + z-index: 140; + display: flex; + align-items: center; + flex-wrap: wrap; + row-gap: 8px; + gap: 12px; + padding: 8px 20px; background: rgba(255,255,255,.82); -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px); border-bottom: 1px solid var(--line-soft); - color: var(--text); - padding: 10px 24px; } -.header-bar { - position: relative; - display: flex; - align-items: center; - gap: 16px; - min-height: 48px; -} +.mobile-nav-toggle { display: inline-flex; } +@media (min-width: 769px) { .app-topbar .mobile-nav-toggle { display: none; } } -header h1 { - font-size: 19px; +.topbar-brand { display: flex; align-items: center; gap: 8px; flex-shrink: 0; } +.topbar-logo { height: 28px; width: auto; object-fit: contain; } +.topbar-title { + font-size: 15px; font-weight: 600; margin: 0; - letter-spacing: -0.02em; + letter-spacing: -0.01em; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding-right: 140px; } -.header-logo { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - height: 44px; - width: auto; - max-width: 120px; - object-fit: contain; - pointer-events: none; + +.app-topbar-actions { + display: flex; + align-items: center; + gap: 6px; + margin-left: auto; + flex-wrap: wrap; +} + +.toolbar-group { display: flex; align-items: center; gap: 4px; } + +.toolbar-divider { + width: 1px; + height: 22px; + background: var(--line-soft); + flex-shrink: 0; +} + +@media (max-width: 768px) { + .app-topbar-actions { + order: 4; + flex: 1 1 100%; + margin-left: 0; + overflow-x: auto; + padding-bottom: 2px; + } } .header-toggle { @@ -103,20 +165,98 @@ header h1 { justify-content: center; } -.header-toggle-icon { font-size: 16px; } - -@media (max-width: 520px) { - .header-logo { height: 36px; max-width: 80px; } - header h1 { font-size: 16px; padding-right: 100px; } +.theme-toggle-circle { + width: 34px; + height: 34px; + border-radius: 50%; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--surface-sunk); + border: 1px solid var(--line-soft); + color: var(--text); + cursor: pointer; + flex-shrink: 0; + transition: background .2s var(--ease), color .2s var(--ease), transform .1s var(--ease); } -/* === Seiten-Menü (links ausfahrend) === */ +.theme-toggle-circle:hover { background: var(--line-soft); } +.theme-toggle-circle:active { transform: scale(.95); } + +.icon-btn-danger:hover { background: var(--red); color: #fff; } +.icon-btn-danger:active { background: var(--red-active); } + +/* Konto-Menü (Avatar-Knopf rechts im Menüband + Dropdown) */ +.topbar-user { position: relative; } + +.user-menu-dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 220px; + background: var(--surface); + border: 1px solid var(--line-soft); + border-radius: var(--r-lg); + box-shadow: var(--sh-3); + padding: 8px; + display: flex; + flex-direction: column; + gap: 2px; + z-index: 150; + opacity: 0; + visibility: hidden; + transform: translateY(-4px); + transition: opacity .15s var(--ease), transform .15s var(--ease), visibility .15s var(--ease); +} + +.user-menu-dropdown.open { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.user-menu-info { + padding: 8px 10px 10px; + margin-bottom: 2px; + border-bottom: 1px solid var(--line-soft); + font-size: 12px; + color: var(--text-muted); +} + +/* .menu-item: generischer Zeilen-Knopf/-Link (Text + optionales Icon) — + genutzt im Konto-Dropdown (dort über .user-menu-dropdown volle Breite) UND + als Basis für die Sidebar-Unterpunkte (siehe .sidebar-subitem). */ +a.menu-item, +button.menu-item { + display: inline-flex; + align-items: center; + gap: 8px; + text-align: left; + background: transparent; + color: var(--text); + padding: 8px 10px; + border: none; + border-radius: var(--r-md); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background .15s var(--ease); +} + +a.menu-item:hover, +button.menu-item:hover { background: var(--surface-sunk); } + +.user-menu-dropdown a.menu-item, +.user-menu-dropdown button.menu-item { display: flex; width: 100%; } + +/* === Seiten-Navigation (links, reine Modul-Navigation) === */ .side-menu { position: fixed; top: 0; left: 0; bottom: 0; - width: 280px; + width: 260px; max-width: 80vw; background: var(--surface); border-right: 1px solid var(--line-soft); @@ -132,251 +272,274 @@ header h1 { transform: translateX(0); } -.side-menu-header { +.side-menu-mobile-header { display: flex; + justify-content: flex-end; + padding: 8px 8px 0; +} +@media (min-width: 769px) { .side-menu-mobile-header { display: none; } } + +.sidebar-nav { + flex: 1; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 2px; + padding: 12px; +} + +.sidebar-collapse-bar { + flex-shrink: 0; + display: none; align-items: center; + justify-content: center; + width: 100%; + padding: 12px; + background: transparent; + border: none; + border-top: 1px solid var(--line-soft); + border-radius: 0; + color: var(--text-muted); + cursor: pointer; + transition: background .2s var(--ease), color .2s var(--ease); +} +.sidebar-collapse-bar:hover { background: var(--surface-sunk); color: var(--text); } +.sidebar-collapse-bar:active { transform: none; } + +#sidebarCollapseIcon { transition: transform .2s var(--ease); } + +/* === Wandtafel (js/wand/wand.js) === + Zweispaltiges Touch-Dashboard für ein dauerhaft laufendes Display, + gewerke- und nutzerübergreifend — ersetzt den früheren Show Mode, der nur + die gerade angemeldete Sitzung großflächig klonte (keine Aggregation über + Gewerke oder Nutzer hinweg, siehe CLAUDE.md). Seitenleiste und Kopfzeile + blendet showTab() über body.wandtafel-aktiv aus: Das Kiosk-Konto ist ein + gewöhnliches Mitgliedskonto mit echten Schreibrechten — die Kachelfläche + soll die einzige Berührfläche sein, damit eine zufällige Berührung nicht + in einen bearbeitbaren Bereich wie "Projekte" führt. + Verhält sich zusätzlich wie ein fester Bildschirmausschnitt statt einer + normal scrollenden Seite (height:100vh + overflow:hidden auf dem Weg + .app-main → .container → #wand) — ein unbeaufsichtigtes Display soll von + selbst nie scrollen müssen; die zwei Spalten strecken sich über die volle + restliche Höhe. + AUSNAHME, auf Eriks Wunsch (echter Touch-Monitor, "ich kann mich vor die + Tafel stellen und scrollen"): unterhalb der zwei Spalten folgt die + vollständige Projektliste (wandProjektlisteHtml(), js/wand/wand.js), und + NUR dieser Bereich (#wandContent, Klasse .wand-scrollable, von + renderWandboard() gesetzt) scrollt — eine offene Übersicht bleibt weiter + exakt einen Bildschirm hoch, siehe .wand-uebersicht unten. Ohne Berührung + kehrt die Ansicht nach WAND_IDLE_MS von selbst an den Anfang zurück + (wandAktivitaetErkannt()/wandBeiUntaetigkeitZuruecksetzen()). */ +body.wandtafel-aktiv .app-topbar, +body.wandtafel-aktiv .side-menu, +body.wandtafel-aktiv .side-menu-backdrop { display: none !important; } +body.wandtafel-aktiv .app-main { height: 100vh; overflow: hidden; } +body.wandtafel-aktiv .container { height: 100%; overflow: hidden; display: flex; flex-direction: column; padding: 14px 20px; } +body.wandtafel-aktiv #wand { display: flex !important; flex-direction: column; flex: 1; min-height: 0; } + +.wand-kopfzeile { + flex-shrink: 0; + display: flex; + align-items: baseline; justify-content: space-between; - padding: 14px 18px; - border-bottom: 1px solid var(--line-soft); - font-weight: 600; - font-size: 16px; + gap: 12px; + padding-bottom: 10px; +} +.wand-zurueck { + appearance: none; + border: none; + background: none; + color: var(--text-subtle); + font-size: 12px; + padding: 0; + cursor: pointer; +} +.wand-zurueck:hover { color: var(--text-muted); } + +/* wandIdleCountdown (js/wand/wand.js): die verbleibende Zeit bis zum + automatischen Rücksprung in den Normalzustand (WAND_IDLE_MS) — Erik wollte + sehen können, wann das passiert, statt davon überrascht zu werden. Sitzt + bewusst AUSSERHALB von #wandContent, damit sie von dessen Neu-Rendern + (jeder Datenabgleich, jedes Auf-/Zuklappen) unberührt bleibt. */ +.wand-idle-countdown { flex-shrink: 0; font-size: 11px; color: var(--text-subtle); font-variant-numeric: tabular-nums; } + +/* Normalzustand: #wandContent selbst bleibt genau ein Bildschirm hoch, kein + Scrollen (das ist der Zustand, den eine offene Übersicht ebenfalls nutzt — + siehe .wand-uebersicht). Erst .wand-scrollable (von renderWandboard() + gesetzt, wenn KEINE Übersicht offen ist) schaltet auf einen normalen + Block-Scrollcontainer um, dessen erstes Kind (.wand-ambient) weiterhin + genau einen Bildschirm hoch ist — die Projektliste danach verlängert den + Inhalt darüber hinaus und erzeugt dadurch den Scrollbalken. */ +.wand-grid-wrap { flex: 1; min-height: 0; display: flex; flex-direction: column; } +.wand-grid-wrap.wand-scrollable { display: block; overflow-y: auto; } +.wand-ambient { min-height: 100%; display: flex; flex-direction: column; } + +.wand-c-layout { flex: 1; min-height: 0; display: grid; grid-template-columns: 320px 1fr; gap: 16px; } +.wand-c-links { display: flex; flex-direction: column; gap: 16px; min-height: 0; } +.wand-c-links .wand-kachel { flex: 1; min-height: 0; } + +.wand-c-rechts { + background: var(--surface); + border: 1px solid var(--line-soft); + border-radius: var(--r-xl); + box-shadow: var(--sh-2); + padding: 18px 22px; + display: flex; + flex-direction: column; + min-height: 0; } -.side-menu-header-actions { +.wand-kachel { + background: var(--surface); + border: 1px solid var(--line-soft); + border-radius: var(--r-xl); + box-shadow: var(--sh-2); + padding: 18px 20px; + display: flex; + flex-direction: column; + min-height: 0; +} +.wand-kachel-titel { + margin: 0 0 12px; + font-size: 13px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--text-muted); + flex-shrink: 0; +} +/* Der Inhalt verteilt sich GLEICHMÄSSIG über die volle, gestreckte + Kachelhöhe (space-evenly) statt oben zu kleben und unten Leerraum zu + lassen — genau das ließ vorher dünn befüllte Kacheln neben vollen leer + wirken. */ +.wand-kachel-body { flex: 1; display: flex; flex-direction: column; justify-content: space-evenly; min-height: 0; overflow: hidden; } + +/* Meter-Zeilen: Auslastung je Gewerk, Wochenbericht-Fortschritt je Gewerk. + Nicht mehr die Kachel als Ganzes ist antippbar, sondern jede einzelne + Gewerk-Zeile für sich (wandZeigeUebersicht('auslastung'|'besprochen', g)) — + deshalb trägt .ist-klickbar hier die Zeile, nicht mehr .wand-kachel. */ +.wand-meter-zeile { display: flex; align-items: center; gap: 12px; } +.wand-meter-zeile.ist-klickbar { cursor: pointer; border-radius: var(--r-md); margin: 0 -6px; padding: 3px 6px; transition: background .15s var(--ease); } +.wand-meter-zeile.ist-klickbar:hover { background: var(--surface-sunk); } +.wand-meter-label { flex: 0 0 90px; font-size: 13px; font-weight: 600; color: var(--text); } +.wand-meter { + flex: 1; + height: 18px; + background: var(--surface-sunk); + border-radius: var(--r-pill); + overflow: hidden; display: flex; align-items: center; - gap: 8px; } +.wand-meter-fill { height: 100%; border-radius: var(--r-pill); transition: width .3s var(--ease); } +.wand-meter-leer { font-size: 12px; color: var(--text-subtle); padding-left: 4px; } +.wand-meter-wert { + flex: 0 0 76px; + text-align: right; + font-size: 15px; + font-weight: 600; + font-variant-numeric: tabular-nums; +} +.wand-einheit { font-size: 11px; font-weight: 400; color: var(--text-subtle); margin-left: 2px; } +.wand-leer-klein { font-size: 11.5px; color: var(--text-subtle); } -.theme-toggle-circle { - width: 34px; - height: 34px; +/* Kennzahlen: Kachel "Heute" (offene/überfällige Aufgaben, Abwesend) — jede + Kennzahl und jedes Kürzel ist für sich antippbar (.ist-klickbar), öffnet + die passende Vollbild-Übersicht. */ +.wand-stat-reihe { display: flex; gap: 28px; } +.wand-stat { display: flex; flex-direction: column; gap: 2px; } +.wand-stat b { font-size: 30px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; } +.wand-stat span { font-size: 12px; color: var(--text-subtle); } +.wand-stat.ist-rot b { color: var(--red); } +.wand-stat.ist-klickbar { cursor: pointer; } +.wand-stat.ist-klickbar:hover b { text-decoration: underline; } +.wand-namen { display: flex; flex-wrap: wrap; gap: 6px; } +.wand-avatar { + width: 28px; + height: 28px; border-radius: 50%; - padding: 0; display: inline-flex; align-items: center; justify-content: center; - background: var(--surface-sunk); - border: 1px solid var(--line-soft); - color: var(--text); - cursor: pointer; - transition: background .2s var(--ease), transform .1s var(--ease); + font-size: 10.5px; + font-weight: 700; + background: var(--accent-soft); + color: var(--accent); } +.wand-avatar.ist-klickbar { cursor: pointer; } +.wand-avatar.ist-klickbar:hover { box-shadow: 0 0 0 2px var(--accent); } +.wand-leer { font-size: 13px; color: var(--text-subtle); } -.theme-toggle-circle:hover { background: var(--line-soft); } -.theme-toggle-circle:active { transform: scale(.95); } - -.side-menu-content { - padding: 16px; - display: flex; - flex-direction: column; - gap: 18px; - overflow-y: auto; +/* Ereignis-Feed (rechte Spalte) — auf Eriks Wahl "Variante C" aus dem + Design-Vergleich: drei Spalten nach Art (Neu/Überfällig/Termine) statt + einer gemischten, chronologisch sortierten Liste. Zeigt auf einen Blick, + WIE VIELE Ereignisse welcher Art anstehen — kostet dafür die vorherige + "zeitlich am nächsten zuerst"-Durchmischung über die Arten hinweg; + innerhalb einer Spalte sortiert wandAktuelleEreignisse() unverändert nach + zeitlicher Nähe. Jeder Eintrag bleibt für sich antippbar und springt + direkt ins betroffene Projekt. */ +.wand-feed-wrap { flex: 1; min-height: 0; overflow: hidden; } +.wand-feed-gruppen { display: flex; gap: 16px; height: 100%; } +.wand-feed-gruppe { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; overflow: hidden; } +.wand-feed-gruppe-kopf { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; flex-shrink: 0; } +.wand-feed-gruppe-punkt { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; } +.wand-feed-gruppe-punkt.neu { background: var(--accent); } +.wand-feed-gruppe-punkt.ueberfaellig { background: var(--red); } +.wand-feed-gruppe-punkt.termin { background: var(--amber); } +.wand-feed-gruppe-titel { + font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); } - -.menu-section { - display: flex; - flex-direction: column; - gap: 8px; +.wand-feed-gruppe-anzahl { font-size: 10.5px; color: var(--text-subtle); margin-left: auto; } +.wand-feed-eintrag { + font-size: 12px; line-height: 1.4; padding: 8px 0; border-top: 1px solid var(--line-soft); + cursor: pointer; border-radius: var(--r-md); transition: background .15s var(--ease); } +.wand-feed-eintrag:first-of-type { border-top: none; padding-top: 0; } +.wand-feed-eintrag:hover { background: var(--surface-sunk); } +.wand-feed-eintrag b { display: block; font-weight: 600; color: var(--text); margin-bottom: 1px; } +.wand-feed-eintrag span { color: var(--text-muted); } +.wand-feed-zeit { display: block; font-size: 10.5px; color: var(--text-subtle); margin-top: 2px; } -.menu-label { - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: .04em; - color: var(--text-muted); - margin-bottom: 2px; +.wand-stand { text-align: center; font-size: 11px; color: var(--text-subtle); margin-top: 10px; flex-shrink: 0; } + +/* Vollbild-Übersichten (überfällige/offene Aufgaben, Auslastung/Besprochen + je Gewerk, Abwesenheiten) — ersetzen das zweispaltige Layout vorübergehend + vollständig (wandUebersicht). Die Liste ist die EINE bewusste Ausnahme vom + "kein Scrollen"-Grundsatz der Wandtafel: ein gezielter Einstieg mit + potenziell vielen Zeilen, kein passiver Dauerzustand. */ +.wand-uebersicht { + flex: 1; min-height: 0; display: flex; flex-direction: column; + background: var(--surface); border: 1px solid var(--line-soft); border-radius: var(--r-xl); + box-shadow: var(--sh-2); padding: 20px 24px; } - -.menu-section button { width: 100%; justify-content: center; } - -.menu-divider { - border: none; - border-top: 1px solid var(--line-soft); - margin: 0; +.wand-uebersicht-kopf { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; flex-shrink: 0; } +.wand-uebersicht-kopf h2 { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -.01em; } +.wand-uebersicht-liste { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; } +.wand-uebersicht-zeile { + display: flex; align-items: center; gap: 18px; padding: 13px 6px; border-top: 1px solid var(--line-soft); + transition: background .15s var(--ease); } +.wand-uebersicht-zeile:first-child { border-top: none; } +.wand-uebersicht-zeile.ist-klickbar { cursor: pointer; } +.wand-uebersicht-zeile.ist-klickbar:hover { background: var(--surface-sunk); } +.wand-uebersicht-projekt { flex: 0 0 280px; font-weight: 600; font-size: 14.5px; color: var(--text); } +.wand-uebersicht-titel { flex: 1; font-size: 14.5px; color: var(--text); } +/* Neutral, nicht mehr fest rot: eine Übersichts-Zeile kann auch eine + unauffällige Angabe zeigen (Stunden, "noch offen", Abwesenheitsende) — + .ist-rot markiert gezielt die wirklich überfälligen Zeilen. */ +.wand-uebersicht-seit { flex-shrink: 0; font-size: 13px; font-weight: 600; color: var(--text-muted); } +.wand-uebersicht-seit.ist-rot { color: var(--red); } -/* === Show Mode === */ -.show-mode-overlay { - position: fixed; - inset: 0; - background: var(--bg); - z-index: 1000; - display: flex; - flex-direction: column; -} - -.show-mode-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 24px; - background: var(--surface); - border-bottom: 1px solid var(--line-soft); - font-size: 18px; - font-weight: 600; -} - -.show-mode-content { - flex: 1; - overflow: auto; - padding: 24px; -} - -.show-mode-content .container { - padding: 0; - max-width: 1400px; - margin: 0 auto; -} - -.show-mode-content .section-title { - margin-top: 0; -} - -.show-mode-paused { - position: fixed; - bottom: 30px; - left: 50%; - transform: translateX(-50%); - background: var(--surface); - border: 1px solid var(--line-soft); - padding: 12px 24px; - border-radius: var(--r-pill); - box-shadow: var(--sh-3); - font-weight: 600; - z-index: 1001; - pointer-events: none; -} - -/* Kompakte Darstellung im Show Mode */ -.show-mode-content { - font-size: 12px; -} - -.show-mode-content .table-wrap { - max-height: none; - overflow: visible; -} - -.show-mode-content table { - font-size: 10px !important; -} - -.show-mode-content th, -.show-mode-content td { - padding: 3px 4px !important; -} - -.show-mode-content thead tr:nth-child(1) th, -.show-mode-content thead tr:nth-child(2) th { - position: static; -} - -.show-mode-content th.rot { - height: 60px !important; -} - -.show-mode-content .proj-name { - min-width: 120px !important; -} - -.show-mode-content .status { - min-width: 160px !important; -} - -.show-mode-content .status-edit { - min-height: 24px !important; - padding: 3px 5px !important; - font-size: 10px !important; -} - -.show-mode-content .person-cell { - width: 22px !important; -} - -.show-mode-content .lph-cell { - width: 38px !important; -} - -.show-mode-content .lph-cell input { - width: 30px !important; -} - -.show-mode-content .bar { - height: 3px !important; - margin-top: 2px !important; -} - -.show-mode-content .group-header td { - padding: 4px 6px !important; -} - -.show-mode-content #boardLegend { - display: none; -} - -.show-mode-content .load-grid { - gap: 12px; -} - -.show-mode-content .load-row { - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)) !important; - gap: 10px !important; -} - -.show-mode-content .load-card { - padding: 10px 12px !important; -} - -.show-mode-content .load-card h3 { - font-size: 12px !important; -} - -.show-mode-content .load-num { - font-size: 22px !important; -} - -.show-mode-content .load-detail { - font-size: 10px !important; - margin-top: 6px !important; -} - -.show-mode-loading { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - font-size: 24px; - color: var(--text-muted); -} - -/* Show Mode: Auslastungs-Steuerelemente und Überschrift ausblenden */ -.show-mode-overlay .show-mode-content #load .section-title, -.show-mode-overlay .show-mode-content #load .toolbar { - display: none !important; -} - -/* Show Mode: Bearbeitung deaktivieren */ -.show-mode-content .person-cell, -.show-mode-content .lph-cell input, -.show-mode-content .status-edit { - pointer-events: none !important; - cursor: default !important; -} - -.show-mode-content .del-proj { - pointer-events: none !important; -} - -.show-mode-content input[type=text].edit-name, -.show-mode-content input[type=checkbox] { - pointer-events: none !important; -} - -.show-mode-content button { - pointer-events: none !important; -} +/* Vollständige Projektliste unterhalb der zwei Spalten (wandProjektlisteHtml, + js/wand/wand.js) — nur sichtbar/erreichbar durch Herunterscrollen auf + einem echten Touch-Monitor. Auf Eriks Wunsch optisch identisch mit der + echten Projektliste: Gruppenüberschrift und Zeilen sind exakt + .section-title/.board-list/.board-row aus der Projektliste selbst (keine + eigene Zeilenform hier) — nur der äußere Abstand zu den Kacheln darüber + ist eine Wandtafel-eigene Ergänzung. */ +.wand-projektliste { margin-top: 22px; padding-bottom: 8px; } +.wand-projektliste .section-title:first-child { margin-top: 0; } .side-menu-backdrop { position: fixed; @@ -395,6 +558,66 @@ header h1 { visibility: visible; } +/* Ab 769px: Sidebar dauerhaft sichtbar (kein Off-Canvas mehr nötig) */ +@media (min-width: 769px) { + #headerMenu.side-menu { + position: sticky; + /* top muss der tatsächlichen Höhe von .app-topbar entsprechen (beide + sticky, gleicher Scroll-Container) — sonst zieht die Sidebar bei + Seiten, die länger als ein Bildschirm sind, beim Herunterscrollen mit + top:0 unter das Menüband statt sich direkt darunter einzureihen, und + am unteren Rand bleibt eine sidebarlose Lücke in Höhe des Menübands + übrig (height: calc(100vh - 52px) reichte dann nicht bis nach unten). */ + top: 52px; + height: calc(100vh - 52px); + transform: none; + box-shadow: none; + width: 260px; + flex: 0 0 260px; + border-right: 1px solid var(--line-soft); + } + #headerMenu.side-menu .header-toggle { display: none; } + .side-menu-backdrop { display: none; } + + /* Umschalt-Leiste am unteren Sidebar-Rand nur auf Desktop-Breite sichtbar + (auf Mobile bleibt es beim reinen Off-Canvas-Menü ohne Zwischenzustand) */ + .sidebar-collapse-bar { display: flex; } + + /* Eingeklappte Sidebar: nur noch eine schmale Icon-Rail mit den 5 + Navigationspunkten, alle Beschriftungen verschwinden */ + #headerMenu.side-menu.collapsed { + width: 64px; + flex: 0 0 64px; + } + + #headerMenu.side-menu.collapsed .sidebar-nav { + padding-left: 8px; + padding-right: 8px; + } + + #headerMenu.side-menu.collapsed .nav-item .nav-label { + display: none; + } + + #headerMenu.side-menu.collapsed .nav-item { + justify-content: center; + padding-left: 0; + padding-right: 0; + } + + #headerMenu.side-menu.collapsed #sidebarCollapseIcon { + transform: rotate(180deg); + } + + /* In der Icon-Rail ergeben aufgeklappte Unterlisten/Rubrik-Überschriften + ohne Textbeschriftung keinen Sinn — beides wird vollständig ausgeblendet. */ + #headerMenu.side-menu.collapsed .nav-chevron, + #headerMenu.side-menu.collapsed .sidebar-section-label, + #headerMenu.side-menu.collapsed .sidebar-subnav { + display: none; + } +} + .toolbar { display: flex; gap: 8px; @@ -408,36 +631,17 @@ button.active { border-color: var(--accent); } -.week-date { - color: var(--text-muted); - font-size: 13px; - margin-left: 4px; -} - -/* Abteilungs-Tabs: Segmented Control */ -.dept-tabs { - position: relative; - display: flex; - align-items: stretch; - justify-content: center; - min-height: 54px; - background: var(--bg); - padding: 0; -} - +/* Hamburger-Knopf im globalen Menüband (nur < 769px sichtbar, öffnet die + Sidebar als Off-Canvas-Menü, siehe .mobile-nav-toggle weiter oben). */ .menu-corner-btn { - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 54px; display: inline-flex; align-items: center; justify-content: center; - background: var(--bg); - border: none; - border-right: 1px solid var(--line-soft); - border-radius: 0; + width: 40px; + height: 40px; + background: var(--surface-sunk); + border: 1px solid var(--line-soft); + border-radius: var(--r-md); box-shadow: none; color: var(--text); cursor: pointer; @@ -445,72 +649,122 @@ button.active { transition: background .2s var(--ease); } -.menu-corner-btn:hover { background: var(--surface-sunk); } +.menu-corner-btn:hover { background: var(--line-soft); } .menu-corner-btn:active { transform: none; } -.dept-tabs-center { +/* Hauptnavigation (Sidebar) — ersetzt die alte horizontale .tabs-Leiste */ +.sidebar-nav { gap: 2px; } + +.nav-item { display: flex; align-items: center; - gap: 2px; - flex-wrap: wrap; - padding: 12px 24px 0 24px; -} - -.dept-tab { + gap: 8px; + width: 100%; + text-align: left; + text-decoration: none; background: transparent; color: var(--text-muted); - padding: 10px 22px; + padding: 9px 12px; cursor: pointer; border: none; border-radius: var(--r-md); - font-size: 15px; + font-size: 13px; font-weight: 500; transition: background .2s var(--ease), color .2s var(--ease); } -.dept-tab:hover { - background: rgba(0,0,0,.045); - color: var(--text); +.nav-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + flex-shrink: 0; + font-size: 15px; } -.dept-tab.active { - background: var(--surface); - color: var(--text); - font-weight: 600; - box-shadow: var(--sh-1); +.nav-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -/* Unter-Tabs */ -.tabs { - display: flex; - gap: 2px; - background: var(--bg); - padding: 6px 24px 0 24px; - flex-wrap: wrap; - border-bottom: 1px solid var(--line-soft); -} +/* .nav-item kann ein echter -Link sein statt eines Buttons — ohne + diese Regel würde ein bereits besuchtes Ziel (echte Browser-Historie) mit + der UA-Standardfarbe für besuchte Links abweichen. */ +.nav-item:visited { color: var(--text-muted); } +.nav-item:hover, +.nav-item:visited:hover { background: var(--surface-sunk); color: var(--text); } -.tab { - background: transparent; - color: var(--text-muted); - padding: 8px 14px; - cursor: pointer; - border: none; - border-radius: var(--r-sm) var(--r-sm) 0 0; - font-size: 13px; - font-weight: 500; - transition: color .2s var(--ease), box-shadow .2s var(--ease); -} - -.tab:hover { color: var(--text); } - -.tab.active { - background: transparent; +.nav-item.active { + background: var(--accent-soft); color: var(--accent); font-weight: 600; - box-shadow: inset 0 -2px 0 var(--accent); } +/* .sidebar-section-label: kleine Rubrik-Überschrift oberhalb einer Gruppe von + Sidebar-Einträgen (z. B. "Dashboard", "Navigation"). */ +.sidebar-section-label { + padding: 10px 12px 2px; + font-size: 11px; + font-weight: 700; + letter-spacing: .04em; + text-transform: uppercase; + color: var(--text-muted); +} + +/* .nav-group-toggle: Sidebar-Eintrag, der statt eines Tabs einen + auf-/zuklappbaren Unterbereich steuert (Einstellungen/Admin, siehe + toggleSidebarAccordion() in js/ui.js). */ +.nav-chevron { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + flex-shrink: 0; + margin-left: auto; + transition: transform .2s var(--ease); +} + +.nav-group-toggle.expanded .nav-chevron { transform: rotate(180deg); } + +/* .sidebar-subnav: Liste der Aktionen unter einem aufgeklappten + .nav-group-toggle, per max-height animiert ein-/ausgeblendet. */ +.sidebar-subnav { + display: flex; + flex-direction: column; + gap: 1px; + padding-left: 8px; + max-height: 0; + opacity: 0; + overflow: hidden; + transition: max-height .25s var(--ease), opacity .2s var(--ease); +} + +.sidebar-subnav.open { + /* Reicht für die längste Gruppe („Einstellungen", sieben Punkte); die + Animation läuft über max-height, deshalb steht hier eine feste Zahl. */ + max-height: 360px; + opacity: 1; +} + +.sidebar-subitem { + width: 100%; + padding-left: 34px; + font-size: 12.5px; + color: var(--text-muted); +} +.sidebar-subitem:hover { color: var(--text); } +/* .is-active: der gerade gezeigte Abschnitt der Gruppe „Einstellungen" + (showSettingsSection() in js/settings/settings.js). Die übrigen Gruppen + lösen Aktionen aus und kennen deshalb keinen dauerhaft aktiven Punkt. */ +.sidebar-subitem.is-active { + color: var(--accent); + font-weight: 600; + background: var(--accent-soft); +} +.sidebar-subitem.danger { color: var(--red); } +.sidebar-subitem.danger:hover { background: var(--red); color: #fff; } + /* === Buttons & Formulare === */ button { cursor: pointer; @@ -543,8 +797,8 @@ button.secondary:hover { background: rgba(0,0,0,.10); } button.secondary:active { background: rgba(0,0,0,.14); } button.danger { background: var(--red); } -button.danger:hover { background: #e8112d; } -button.danger:active { background: #b8000f; } +button.danger:hover { background: var(--red-hover); } +button.danger:active { background: var(--red-active); } button.small { padding: 3px 10px; @@ -553,6 +807,7 @@ button.small { select, input[type=text], +input[type=email], input[type=date] { padding: 7px 10px; border: 1px solid var(--line); @@ -566,6 +821,7 @@ input[type=date] { select:focus, input[type=text]:focus, +input[type=email]:focus, input[type=date]:focus { outline: none; border-color: var(--accent); @@ -627,46 +883,8 @@ thead tr:nth-child(1) th { z-index: 10; } -thead tr:nth-child(2) th { - background: var(--surface-alt); - color: var(--text); - font-weight: 500; - border: 1px solid var(--line); - position: sticky; - top: 0; /* Wird von adjustStickyOffsets() dynamisch überschrieben */ - z-index: 6; -} - -th.rot { - background: var(--surface); - writing-mode: vertical-rl; - transform: rotate(180deg); - height: 90px; - padding: 4px 2px; - font-weight: 500; -} - -td.left { - text-align: left; - min-width: 80px; -} - tr:nth-child(even) td { background: var(--surface-alt); } -.proj-name { - font-weight: 600; - min-width: 160px; - letter-spacing: -0.01em; -} - -.group-header td { - background: var(--surface-sunk) !important; - font-weight: 600; - text-align: left; - color: var(--text); - letter-spacing: -0.01em; -} - /* === Status-Felder in der Projektzeile === */ .status { min-width: 240px; @@ -704,6 +922,202 @@ tr:nth-child(even) td { background: var(--surface-alt); } .status-edit b, .status-edit strong { font-weight: 600; } +/* === Kommentar-Editor im Board und im Wochenbericht ====================== + Ersetzt das frühere contenteditable-Feld samt execCommand-Leiste: Textfeld + mit Verfasser-Kopfzeile, ausdrücklichem Speichern und dem eingeklappten, + scrollbaren Verlauf der früheren Sitzungen. Aufgebaut von + renderCommentEditor() in js/board/board.js. */ + +.comment-block { display: flex; flex-direction: column; gap: 10px; } + +.comment-editor { + border: 1px solid var(--line); + border-radius: var(--r-md); + background: var(--surface); + overflow: hidden; +} +.comment-editor-head, +.comment-head-line { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding: 6px 10px; + font-size: 11px; + color: var(--text-muted); + background: var(--surface-alt); + border-bottom: 1px solid var(--line-soft); +} +.comment-head-line { border: 1px solid var(--line-soft); border-radius: var(--r-md); } +.comment-who { display: flex; align-items: center; gap: 6px; color: var(--text); font-weight: 500; } +.comment-unknown { color: var(--text-subtle); font-weight: 400; font-style: italic; } +.comment-sep { color: var(--text-subtle); } +.comment-av { + width: 19px; height: 19px; border-radius: 50%; + background: var(--accent); color: #fff; + display: flex; align-items: center; justify-content: center; + font-size: 9px; font-weight: 600; flex-shrink: 0; +} +.comment-kbd { margin-left: auto; font-size: 10px; color: var(--text-subtle); white-space: nowrap; } + +/* .comment-field ist seit der Umstellung auf contenteditable (Eriks Wunsch + nach echter statt mit Sternchen markierter Fett-/Kursiv-/Farbschrift, + public/js/utils.js) ein
, kein