feat(web): Design Mosaik Runde 3 - Reiter in der Seitenleiste, lebendigere Kacheln

- Dashboard-Reiter stehen in der aufgeklappten Seitenleiste als Liste
  unter "Dashboard" (Ziehen sortiert senkrecht, Umbenennen/Loeschen/Neu
  im Bearbeitungsmodus); eingeklappt und auf dem Handy wie bisher in der
  Kopfzeile. Die Kopfzeile zeigt sonst den Namen des Dashboards.
- Begruessung einzeilig auf Hoehe der Befehlsleiste.
- Kachel-Groesse: kollidiert die gezogene Groesse mit einem Nachbarn, wird
  nur die freie Achse uebernommen - vorher verwarf react-grid-layout mit
  preventCollision die ganze Aenderung, sobald die Maus eine Zeile nach
  unten wackelte (Favoriten liessen sich deshalb oft nicht schmaler ziehen).
- Ansicht: belegte Spalten mittig, Begruessung/Befehlsleiste ruecken mit.
- Kalender: einzeilige Terminliste mit "Heute"/"Morgen" in Akzentfarbe,
  Ort im Tooltip, Standardhoehe 16 statt 12.
- Hintergrund "Bluete" im Dunkelmodus weggelassen (Farbstufen), Ersatz Nebel.
- Kacheln: Symbol-Chip in der Akzentfarbe, leichtes Anheben beim Zeigen,
  gestaffeltes Einblenden beim Laden (ohne bei reduzierter Bewegung).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 15:58:01 +02:00
parent b06a2d1066
commit 8532b63097
20 changed files with 552 additions and 73 deletions
@@ -4,3 +4,10 @@
* zeichnen per `createPortal` hinein (heute: die Dashboard-Reiter).
*/
export const HEADER_CENTER_SLOT_ID = 'header-center-slot';
/**
* Einhaengepunkt unter „Dashboard“ in der Seitenleiste (Design „Mosaik“,
* Runde 3). Nur in der aufgeklappten Seitenleiste am Desktop vorhanden; dort
* zeichnen die Dashboard-Reiter hinein, sonst in {@link HEADER_CENTER_SLOT_ID}.
*/
export const SIDEBAR_DASHBOARD_SLOT_ID = 'sidebar-dashboard-slot';
+10 -3
View File
@@ -12,6 +12,7 @@ import { AppVersionBadge } from '@/components/layout/app-version-badge';
import { ModuleTile } from '@/components/modules/module-tile';
import { useCategoryLabel } from '@/lib/use-category-label';
import { useNavStore } from '@/lib/stores/nav-store';
import { SIDEBAR_DASHBOARD_SLOT_ID } from '@/components/layout/header-slot';
/** Fluent-Auswahlmarke: 3 x 16 px Pille am linken Rand des gewaehlten Eintrags. */
function SelectionPill() {
@@ -128,7 +129,10 @@ export function Sidebar() {
</svg>
);
const sidebarContent = (
// `inDrawer`: die mobile Schublade rendert denselben Inhalt ein zweites
// Mal — der Einhaengepunkt der Dashboard-Reiter darf nur einmal im DOM
// stehen und gehoert zur Desktop-Leiste.
const renderContent = (inDrawer: boolean) => (
<div className="flex h-full flex-col bg-sidebar">
<nav className={`flex-1 overflow-y-auto py-3 ${isCollapsed ? 'px-2' : 'px-3'}`}>
<ul className="flex flex-col gap-0.5">
@@ -145,6 +149,9 @@ export function Sidebar() {
)}
{!isCollapsed && <span className="truncate">{t('dashboard')}</span>}
</Link>
{/* Dashboard-Reiter (Runde 3): `DashboardTabs` zeichnet per Portal
hierher, der Punkt bleibt im React-Baum der Leiste leer. */}
{!inDrawer && !isCollapsed && <div id={SIDEBAR_DASHBOARD_SLOT_ID} />}
</li>
<li>
<Link href="/marketplace" className={linkClass('/marketplace')} aria-label={t('marketplace')} title={isCollapsed ? t('marketplace') : undefined}>
@@ -275,7 +282,7 @@ export function Sidebar() {
: 'var(--sidebar-width)',
}}
>
{sidebarContent}
{renderContent(false)}
</aside>
{isMobileOpen && (
@@ -307,7 +314,7 @@ export function Sidebar() {
</svg>
</button>
</div>
<div className="min-h-0 flex-1">{sidebarContent}</div>
<div className="min-h-0 flex-1">{renderContent(true)}</div>
</aside>
</div>
)}