#sidebar-dashboard-slot > nav) > a {
- background-color: transparent;
- font-weight: 500;
-}
-li:has(> #sidebar-dashboard-slot > nav) > a:hover {
- background-color: var(--sidebar-hover);
-}
-li:has(> #sidebar-dashboard-slot > nav) > a [data-testid="nav-selection-pill"] {
- display: none;
-}
-
/* Runde 3 — mehr Leben in den Kacheln, ohne bunt zu werden.
Symbol-Chip: Flaeche und Symbol aus der persoenlichen Akzentfarbe. */
.widget-icon-chip {
diff --git a/apps/web/src/components/dashboard/dashboard-greeting.tsx b/apps/web/src/components/dashboard/dashboard-greeting.tsx
index 6332516..176a3c2 100644
--- a/apps/web/src/components/dashboard/dashboard-greeting.tsx
+++ b/apps/web/src/components/dashboard/dashboard-greeting.tsx
@@ -19,8 +19,8 @@ export function greetingName(displayName: string | null | undefined, username: s
}
/**
- * Ruhige Begruessung ueber dem Raster (Design „Mosaik“, Runde 2/3): „Guten
- * Morgen, Max“ plus Wochentag und Datum in einer Zeile. Keine Kennzahlen. Die Uhrzeit wird
+ * Ruhige Begruessung (Design „Mosaik“): „Guten Morgen, Max“ plus Wochentag
+ * und Datum. Seit Runde 3 unten in der Seitenleiste ueber „Einklappen“. Keine Kennzahlen. Die Uhrzeit wird
* erst im Browser gelesen (kein Unterschied zwischen Server und Browser) und
* jede Minute aufgefrischt.
*/
@@ -36,7 +36,7 @@ export function DashboardGreeting() {
return () => window.clearInterval(id);
}, []);
- if (!now) return ;
+ if (!now) return ;
const name = greetingName(user?.displayName, user?.username);
const period = greetingPeriod(now);
@@ -46,14 +46,14 @@ export function DashboardGreeting() {
month: 'long',
}).format(now);
- // Runde 3: eine Zeile auf Hoehe der Befehlsleiste statt zweizeiliger
- // Ueberschrift — die Begruessung bleibt, kostet aber kaum noch Platz.
+ // Runde 3: unten in der Seitenleiste statt ueber den Kacheln — zwei
+ // ruhige Zeilen, nimmt dem Dashboard keinen Platz mehr weg.
return (
-
-
+
+
{name ? t(`${period}Named`, { name }) : t(period)}
-
-
{dateLine}
+
+
{dateLine}
);
}
diff --git a/apps/web/src/components/dashboard/dashboard-tabs.tsx b/apps/web/src/components/dashboard/dashboard-tabs.tsx
index 6f88be8..8a09a6d 100644
--- a/apps/web/src/components/dashboard/dashboard-tabs.tsx
+++ b/apps/web/src/components/dashboard/dashboard-tabs.tsx
@@ -12,8 +12,7 @@ import {
useState,
} from 'react';
import { createPortal } from 'react-dom';
-import { HEADER_CENTER_SLOT_ID, SIDEBAR_DASHBOARD_SLOT_ID } from '@/components/layout/header-slot';
-import { useSidebarStore } from '@/lib/stores/sidebar-store';
+import { HEADER_CENTER_SLOT_ID } from '@/components/layout/header-slot';
import type { DashboardTab } from '@/lib/dashboard-api';
interface DashboardTabsProps {
@@ -47,30 +46,6 @@ interface DragState {
id: string;
pointerId: number;
startX: number;
- startY: number;
-}
-
-/**
- * Wo die Reiter stehen (Design „Mosaik“, Runde 3): in der aufgeklappten
- * Seitenleiste am Desktop als Liste unter „Dashboard“ — dort kosten sie
- * keinen Platz ueber den Kacheln, und die Kopfzeile zeigt nur den Namen des
- * gewaehlten Dashboards. Eingeklappt oder auf dem Handy gibt es diese Liste
- * nicht, dann wie bisher als Umschalter in der Kopfzeile.
- */
-type Placement = 'sidebar' | 'header';
-
-function useTabsPlacement(): Placement {
- const isCollapsed = useSidebarStore((st) => st.isCollapsed);
- const [isDesktop, setIsDesktop] = useState(false);
- useEffect(() => {
- if (typeof window.matchMedia !== 'function') return;
- const mq = window.matchMedia('(min-width: 768px)');
- const update = () => setIsDesktop(mq.matches);
- update();
- mq.addEventListener('change', update);
- return () => mq.removeEventListener('change', update);
- }, []);
- return isDesktop && !isCollapsed ? 'sidebar' : 'header';
}
/**
@@ -82,16 +57,16 @@ function useTabsPlacement(): Placement {
function computeReorderedIds(
order: string[],
draggedId: string,
- rects: Map,
- pointer: number,
+ rects: Map,
+ clientX: number,
): string[] {
const others = order.filter((id) => id !== draggedId);
let insertAt = others.length;
for (let i = 0; i < others.length; i++) {
const rect = rects.get(others[i]);
if (!rect) continue;
- const midpoint = rect.start + rect.size / 2;
- if (pointer < midpoint) {
+ const midpoint = rect.left + rect.width / 2;
+ if (clientX < midpoint) {
insertAt = i;
break;
}
@@ -156,20 +131,13 @@ export function DashboardTabs({
const scrollRef = useRef(null);
const hintId = useId();
const [slot, setSlot] = useState(null);
- const [headerSlot, setHeaderSlot] = useState(null);
const [overflow, setOverflow] = useState({ left: false, right: false });
- const placement = useTabsPlacement();
- const vertical = placement === 'sidebar';
- // Einhaengepunkte suchen. Kopfzeile und Seitenleiste stehen im selben
- // Seitenrahmen (AppShell) und sind nach dem ersten Zeichnen im DOM; der
- // Punkt der Seitenleiste entsteht bzw. verschwindet mit dem Ein-/Ausklappen
- // im selben Durchgang, der `placement` aendert.
+ // Einhaengepunkt der Kopfzeile suchen. Die Kopfzeile steht im selben
+ // Seitenrahmen (AppShell) und ist nach dem ersten Zeichnen im DOM.
useEffect(() => {
- const header = document.getElementById(HEADER_CENTER_SLOT_ID);
- setHeaderSlot(header);
- setSlot(vertical ? (document.getElementById(SIDEBAR_DASHBOARD_SLOT_ID) ?? header) : header);
- }, [vertical]);
+ setSlot(document.getElementById(HEADER_CENTER_SLOT_ID));
+ }, []);
/** Misst, ob die Spur links/rechts ueberlaeuft — steuert die weiche Ausblendung. */
const measureOverflow = useCallback(() => {
@@ -232,7 +200,7 @@ export function DashboardTabs({
function handlePointerDown(e: PointerEvent, tabId: string) {
if (e.button !== 0 || renamingId) return;
- dragRef.current = { id: tabId, pointerId: e.pointerId, startX: e.clientX, startY: e.clientY };
+ dragRef.current = { id: tabId, pointerId: e.pointerId, startX: e.clientX };
}
function handlePointerMove(e: PointerEvent) {
@@ -241,8 +209,7 @@ export function DashboardTabs({
const wasNotDragging = previewOrder === null;
if (wasNotDragging) {
- const moved = vertical ? e.clientY - drag.startY : e.clientX - drag.startX;
- if (Math.abs(moved) < DRAG_THRESHOLD_PX) return;
+ if (Math.abs(e.clientX - drag.startX) < DRAG_THRESHOLD_PX) return;
hasDraggedRef.current = true;
capturePointer(e.currentTarget, drag.pointerId);
}
@@ -254,18 +221,15 @@ export function DashboardTabs({
// wird) keine Verschiebung zeigen, weil der ERSTE Move-Aufruf nur in den
// Ziehzustand wechselt, ohne die Position auszuwerten.
const baseOrder = previewOrder ?? dashboards.map((d) => d.id);
- const rects = new Map();
+ const rects = new Map();
for (const id of baseOrder) {
const el = tabRefs.current.get(id);
if (el) {
const rect = el.getBoundingClientRect();
- rects.set(
- id,
- vertical ? { start: rect.top, size: rect.height } : { start: rect.left, size: rect.width },
- );
+ rects.set(id, { left: rect.left, width: rect.width });
}
}
- const next = computeReorderedIds(baseOrder, drag.id, rects, vertical ? e.clientY : e.clientX);
+ const next = computeReorderedIds(baseOrder, drag.id, rects, e.clientX);
if (wasNotDragging || next.join('\u0000') !== baseOrder.join('\u0000')) {
setPreviewOrder(next);
}
@@ -323,10 +287,8 @@ export function DashboardTabs({
function handleTabKeyDown(e: KeyboardEvent, index: number) {
const count = orderedTabs.length;
let next: number;
- const forward = vertical ? 'ArrowDown' : 'ArrowRight';
- const backward = vertical ? 'ArrowUp' : 'ArrowLeft';
- if (e.key === forward) next = (index + 1) % count;
- else if (e.key === backward) next = (index - 1 + count) % count;
+ if (e.key === 'ArrowRight') next = (index + 1) % count;
+ else if (e.key === 'ArrowLeft') next = (index - 1 + count) % count;
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = count - 1;
else return;
@@ -530,153 +492,6 @@ export function DashboardTabs({
);
- // Senkrechte Liste unter „Dashboard“ in der Seitenleiste (Runde 3).
- // Gleiche Bedienung wie die Leiste: Klick waehlt, Ziehen sortiert (hier
- // senkrecht), Pfeiltasten hoch/runter wandern, Umbenennen/Loeschen/Neu nur
- // im Bearbeitungsmodus. Mit nur einem Dashboard und ausserhalb des
- // Bearbeitungsmodus bleibt die Liste weg — dann gibt es nichts zu waehlen.
- const pencilIcon = (
-
- );
- const closeIcon = (
-
- );
-
- const showList = dashboards.length > 1 || isEditMode;
- const list = showList ? (
-
- ) : null;
-
- // Kopfzeile, solange die Reiter in der Seitenleiste stehen: nur der Name
- // des gewaehlten Dashboards, wie die anderen Seiten ihren Titel zeigen.
- const activeName = dashboards.find((d) => d.id === activeDashboardId)?.name ?? '';
- const headerTitle = (
- {activeName}
- );
- const inSidebar = vertical && slot !== null && slot !== headerSlot;
-
// Der Loeschdialog haengt am Dokumentkoerper, nicht in der Kopfzeile —
// sonst saesse er in deren Stapelkontext.
const dialog = pendingDeleteTab
@@ -727,14 +542,7 @@ export function DashboardTabs({
return (
<>
- {inSidebar ? (
- <>
- {list && createPortal(list, slot)}
- {headerSlot && createPortal(headerTitle, headerSlot)}
- >
- ) : (
- createPortal(bar, slot)
- )}
+ {createPortal(bar, slot)}
{dialog}
>
);
diff --git a/apps/web/src/components/layout/header-slot.ts b/apps/web/src/components/layout/header-slot.ts
index 0493d71..fa33574 100644
--- a/apps/web/src/components/layout/header-slot.ts
+++ b/apps/web/src/components/layout/header-slot.ts
@@ -4,10 +4,3 @@
* 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';
diff --git a/apps/web/src/components/layout/sidebar.test.tsx b/apps/web/src/components/layout/sidebar.test.tsx
index 87d47bc..6de459d 100644
--- a/apps/web/src/components/layout/sidebar.test.tsx
+++ b/apps/web/src/components/layout/sidebar.test.tsx
@@ -66,6 +66,10 @@ vi.mock('@/components/layout/sidebar-footer', () => ({
// Das Abzeichen ruft `loadApiVersion()` und damit `fetch` — ohne Modul-Mock
// bekaeme es das Modul-Array aus `stubFetch()` (quick-260914-ku1).
+vi.mock('@/components/dashboard/dashboard-greeting', () => ({
+ DashboardGreeting: () => ,
+}));
+
vi.mock('@/components/layout/app-version-badge', () => ({
AppVersionBadge: () => ,
}));
diff --git a/apps/web/src/components/layout/sidebar.tsx b/apps/web/src/components/layout/sidebar.tsx
index 28eab26..d66092a 100644
--- a/apps/web/src/components/layout/sidebar.tsx
+++ b/apps/web/src/components/layout/sidebar.tsx
@@ -12,7 +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';
+import { DashboardGreeting } from '@/components/dashboard/dashboard-greeting';
/** Fluent-Auswahlmarke: 3 x 16 px Pille am linken Rand des gewaehlten Eintrags. */
function SelectionPill() {
@@ -129,10 +129,7 @@ export function Sidebar() {
);
- // `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) => (
+ const sidebarContent = (
+ {/* Begruessung (Design „Mosaik“, Runde 3): im sonst leeren unteren
+ Bereich der Leiste statt ueber den Kacheln; eingeklappt entfaellt sie. */}
+ {!isCollapsed && (
+