feat(ui): 24-Spalten-Dashboard-Raster, einklappbare Sidebar
CI / backend-tests (push) Failing after 2m7s
CI / frontend-build (push) Successful in 20s

Dashboard-Raster von 12 auf 24 Spalten - bei 12 waren Reihen sofort randvoll (3 Kacheln à w:4), kein Spielraum zum Breiter-Ziehen ohne Nachbarkachel zu verkleinern. Layout-localStorage-Key auf v3 gehoben (alte v2-Koordinaten passen nicht mehr zum neuen Raster). Sidebar links jetzt einklappbar (Toggle-Button bleibt sichtbar, Zustand in localStorage) - mehr Platz für breite Dashboard-Raster auf Desktop-Monitoren.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-07 14:42:19 +02:00
co-authored by Claude Sonnet 5
parent e6eda26eb2
commit e5ddfde52f
5 changed files with 145 additions and 46 deletions
+24
View File
@@ -6758,3 +6758,27 @@ Keine Commits in dieser Session.
- frontend/src/styles/tailwind.css | 61 +++++++++++++++++++++++++++++++++++++++++++++---------------- - frontend/src/styles/tailwind.css | 61 +++++++++++++++++++++++++++++++++++++++++++++----------------
--- ---
## 2026-09-07 14:31 14:36 (4m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
- e6eda26 fix(frontend): zirkuläre CSS-Variablen endgültig behoben - kein @theme-Namespace mehr
### Geänderte Dateien
- DEVLOG.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- frontend/src/components/status/ReadinessBadge.tsx | 8 ++++----
- frontend/src/components/ui/badge.tsx | 10 +++++-----
- frontend/src/components/ui/button.tsx | 11 ++++++-----
- frontend/src/components/ui/card.tsx | 7 +++++--
- frontend/src/pages/dashboard/DashboardPage.tsx | 10 +++++-----
- frontend/src/pages/dashboard/TileFrame.tsx | 6 +++---
- frontend/src/pages/dashboard/tiles/AblaufdatenTile.tsx | 8 ++++----
- frontend/src/pages/dashboard/tiles/EinsatzbereitschaftTile.tsx | 14 +++++++-------
- frontend/src/pages/dashboard/tiles/KennzahlenTile.tsx | 2 +-
- frontend/src/pages/dashboard/tiles/MaengelTile.tsx | 6 +++---
- frontend/src/pages/dashboard/tiles/PrueftermineTile.tsx | 8 ++++----
- frontend/src/pages/dashboard/tiles/QualifikationTile.tsx | 8 ++++----
- frontend/src/styles/tailwind.css | 58 +++++++++++-----------------------------------------------
---
+41 -2
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from "react"; import { useState, type ReactNode } from "react";
import { NavLink, useNavigate } from "react-router-dom"; import { NavLink, useNavigate } from "react-router-dom";
import { useAuth } from "../auth/AuthContext"; import { useAuth } from "../auth/AuthContext";
@@ -6,6 +6,24 @@ import { CommandPalette } from "./CommandPalette";
import { SyncStatus } from "./status/SyncStatus"; import { SyncStatus } from "./status/SyncStatus";
import { ThemeToggle } from "./ThemeToggle"; import { ThemeToggle } from "./ThemeToggle";
const SPEICHER_KEY = "mabea-sidebar-eingeklappt";
function eingeklapptLaden(): boolean {
try {
return localStorage.getItem(SPEICHER_KEY) === "1";
} catch {
return false;
}
}
function eingeklapptSpeichern(wert: boolean): void {
try {
localStorage.setItem(SPEICHER_KEY, wert ? "1" : "0");
} catch {
// Speichern optional
}
}
function paletteOeffnen() { function paletteOeffnen() {
window.dispatchEvent(new Event("mabea:open-command-palette")); window.dispatchEvent(new Event("mabea:open-command-palette"));
} }
@@ -14,6 +32,15 @@ function paletteOeffnen() {
export function AppShell({ children }: { children: ReactNode }) { export function AppShell({ children }: { children: ReactNode }) {
const { istAdmin, istVerantwortlich, logout } = useAuth(); const { istAdmin, istVerantwortlich, logout } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [eingeklappt, setEingeklappt] = useState(eingeklapptLaden);
function umschalten() {
setEingeklappt((bisherig) => {
const neu = !bisherig;
eingeklapptSpeichern(neu);
return neu;
});
}
function handleLogout() { function handleLogout() {
logout(); logout();
@@ -22,7 +49,17 @@ export function AppShell({ children }: { children: ReactNode }) {
return ( return (
<div className="app-shell"> <div className="app-shell">
<header className="topbar"> <header className={`topbar${eingeklappt ? " topbar-eingeklappt" : ""}`}>
<button
className="topbar-toggle"
onClick={umschalten}
title={eingeklappt ? "Navigation einblenden" : "Navigation ausblenden"}
aria-label={eingeklappt ? "Navigation einblenden" : "Navigation ausblenden"}
>
{eingeklappt ? "»" : "«"}
</button>
{!eingeklappt && (
<>
<NavLink to="/objekte" className="topbar-brand"> <NavLink to="/objekte" className="topbar-brand">
MABEA MABEA
</NavLink> </NavLink>
@@ -57,6 +94,8 @@ export function AppShell({ children }: { children: ReactNode }) {
Abmelden Abmelden
</button> </button>
</nav> </nav>
</>
)}
</header> </header>
<CommandPalette /> <CommandPalette />
{children} {children}
@@ -205,7 +205,7 @@ export function DashboardPage() {
const sichtbareIds = ALLE_TILE_IDS.filter((id) => bearbeitenModus || !layout.ausgeblendet.includes(id)); const sichtbareIds = ALLE_TILE_IDS.filter((id) => bearbeitenModus || !layout.ausgeblendet.includes(id));
const rasterLayout: Layout = useMemo( const rasterLayout: Layout = useMemo(
() => sichtbareIds.map((id) => ({ i: id, ...layout.raster[id], minW: 2, minH: 2 })), () => sichtbareIds.map((id) => ({ i: id, ...layout.raster[id], minW: 4, minH: 2 })),
[sichtbareIds, layout.raster] [sichtbareIds, layout.raster]
); );
+14 -10
View File
@@ -19,7 +19,11 @@ export const ALLE_TILE_IDS: DashboardTileId[] = [
"aktivitaet", "aktivitaet",
]; ];
export const GRID_SPALTEN = 12; // 24 statt 12 Spalten - feineres Raster, mehr Kontrolle beim Breiter/Schmaler
// ziehen auf großen Desktop-Monitoren (Nutzer-Feedback: bei 12 Spalten waren
// Reihen sofort randvoll, kein Spielraum zum Vergrößern ohne Nachbarkachel
// anzufassen).
export const GRID_SPALTEN = 24;
export const GRID_ZEILENHOEHE = 56; export const GRID_ZEILENHOEHE = 56;
export interface RasterPosition { export interface RasterPosition {
@@ -32,14 +36,14 @@ export interface RasterPosition {
// Freie Grid-Positionen als Startzustand - grob nach Wichtigkeit sortiert, // Freie Grid-Positionen als Startzustand - grob nach Wichtigkeit sortiert,
// Aufgaben-Kachel volle Breite oben. // Aufgaben-Kachel volle Breite oben.
export const STANDARD_RASTER: Record<DashboardTileId, RasterPosition> = { export const STANDARD_RASTER: Record<DashboardTileId, RasterPosition> = {
aufgaben: { x: 0, y: 0, w: 12, h: 3 }, aufgaben: { x: 0, y: 0, w: 24, h: 3 },
einsatzbereitschaft: { x: 0, y: 3, w: 4, h: 5 }, einsatzbereitschaft: { x: 0, y: 3, w: 8, h: 5 },
prueftermine: { x: 4, y: 3, w: 4, h: 5 }, prueftermine: { x: 8, y: 3, w: 8, h: 5 },
ablaufdaten: { x: 8, y: 3, w: 4, h: 5 }, ablaufdaten: { x: 16, y: 3, w: 8, h: 5 },
kennzahlen: { x: 0, y: 8, w: 4, h: 5 }, kennzahlen: { x: 0, y: 8, w: 8, h: 5 },
maengel: { x: 4, y: 8, w: 4, h: 5 }, maengel: { x: 8, y: 8, w: 8, h: 5 },
qualifikation: { x: 8, y: 8, w: 4, h: 4 }, qualifikation: { x: 16, y: 8, w: 8, h: 4 },
aktivitaet: { x: 0, y: 13, w: 8, h: 5 }, aktivitaet: { x: 0, y: 13, w: 16, h: 5 },
}; };
export interface DashboardLayout { export interface DashboardLayout {
@@ -47,7 +51,7 @@ export interface DashboardLayout {
ausgeblendet: DashboardTileId[]; ausgeblendet: DashboardTileId[];
} }
const SPEICHER_KEY = "mabea-dashboard-layout-v2"; const SPEICHER_KEY = "mabea-dashboard-layout-v3";
function istGueltigeId(wert: unknown): wert is DashboardTileId { function istGueltigeId(wert: unknown): wert is DashboardTileId {
return typeof wert === "string" && (ALLE_TILE_IDS as string[]).includes(wert); return typeof wert === "string" && (ALLE_TILE_IDS as string[]).includes(wert);
+32
View File
@@ -193,6 +193,38 @@ a {
top: 0; top: 0;
height: 100vh; height: 100vh;
overflow-y: auto; overflow-y: auto;
transition: width 0.18s ease, flex-basis 0.18s ease, padding 0.18s ease;
}
/* Sidebar einklappbar - Nutzer-Wunsch (mehr Platz für breite Dashboard-Raster
auf Desktop-Monitoren). Nur der Toggle-Button bleibt sichtbar. */
.topbar-eingeklappt {
width: 48px;
flex: 0 0 48px;
padding: 1.25rem 0.5rem;
align-items: center;
overflow: visible;
}
.topbar-toggle {
background: none;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.9rem;
line-height: 1;
padding: 0.4rem 0.55rem;
align-self: flex-end;
}
.topbar-eingeklappt .topbar-toggle {
align-self: center;
}
.topbar-toggle:hover {
background: var(--color-surface-sunken);
color: var(--color-text);
} }
.topbar-brand { .topbar-brand {