Files
personal-dashboard/src/components/DashboardGrid.tsx
T
Claude 91e5902020 Add responsive grid layout for mobile/tablet support
Switch from fixed 48-column grid to responsive breakpoints:
- lg (>=1200px): 48 columns, free positioning (unchanged desktop behavior)
- md (>=900px): 24 columns, vertical compaction
- sm (>=600px): 12 columns, vertical compaction
- xs (<600px): 6 columns, vertical compaction

Widgets automatically reflow and stack on smaller screens instead of
being squished. Layout changes are only persisted from the desktop
breakpoint. Drag/resize editing is desktop-only.

Also adds mobile CSS refinements for topbar, tabs, and workspace padding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 10:32:59 +02:00

156 lines
4.3 KiB
TypeScript

"use client";
import type { ComponentType, ReactNode } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import { Responsive, WidthProvider } from "react-grid-layout/legacy";
import type { DashboardGridWidget, DashboardLayoutItem } from "@/lib/dashboard-layout";
export type DashboardGridProps = {
widgets: DashboardGridWidget[];
editMode: boolean;
activeMenuWidgetId?: string | null;
renderWidget: (widget: DashboardGridWidget) => ReactNode;
onLayoutChange: (layout: DashboardLayoutItem[]) => void;
};
const ResponsiveGridLayout = WidthProvider(Responsive) as ComponentType<any>;
const BREAKPOINTS = { lg: 1200, md: 900, sm: 600, xs: 0 };
const COLS = { lg: 48, md: 24, sm: 12, xs: 6 };
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
function getWidgetMinimumSize(widget: DashboardGridWidget): { minW: number; minH: number } {
if (widget.type === "search") {
return { minW: 8, minH: 5 };
}
if (widget.type === "clock") {
return { minW: 4, minH: 4 };
}
if (widget.type === "calendar") {
return { minW: 8, minH: 6 };
}
if (widget.type === "calculator") {
return { minW: 8, minH: 6 };
}
if (widget.type === "note") {
return { minW: 4, minH: 5 };
}
return { minW: 4, minH: 4 };
}
function widgetsToLayout(widgets: DashboardGridWidget[]): DashboardLayoutItem[] {
return widgets.map((widget) => {
const minimumSize = getWidgetMinimumSize(widget);
const width = clamp(widget.w, minimumSize.minW, 48);
const height = clamp(widget.h, minimumSize.minH, 180);
const x = clamp(widget.x, 0, 48 - width);
const y = clamp(widget.y, 0, 10000);
return {
i: widget.id,
x,
y,
w: width,
h: height,
minW: minimumSize.minW,
minH: minimumSize.minH,
maxW: 48,
maxH: 180
};
});
}
function normalizeLayout(layout: readonly DashboardLayoutItem[]): DashboardLayoutItem[] {
return layout.map((item) => ({
i: String(item.i),
x: Number.isFinite(item.x) ? item.x : 0,
y: Number.isFinite(item.y) ? item.y : 0,
w: Number.isFinite(item.w) ? item.w : 1,
h: Number.isFinite(item.h) ? item.h : 1,
minW: item.minW,
minH: item.minH,
maxW: item.maxW,
maxH: item.maxH
}));
}
export default function DashboardGrid({
widgets,
editMode,
activeMenuWidgetId = null,
renderWidget,
onLayoutChange
}: DashboardGridProps) {
const [currentBreakpoint, setCurrentBreakpoint] = useState("lg");
const breakpointRef = useRef("lg");
const isDesktop = currentBreakpoint === "lg";
const layouts = useMemo(() => ({
lg: widgetsToLayout(widgets)
}), [widgets]);
const handleBreakpointChange = useCallback((breakpoint: string) => {
breakpointRef.current = breakpoint;
setCurrentBreakpoint(breakpoint);
}, []);
const handleLayoutChange = useCallback((layout: DashboardLayoutItem[]) => {
if (breakpointRef.current === "lg") {
onLayoutChange(normalizeLayout(layout));
}
}, [onLayoutChange]);
return (
<div className="widgetGridShell">
{widgets.length === 0 ? (
<div className="emptyDashboard">
<h2>Keine Widgets aktiv</h2>
<p className="muted">Schalte den Bearbeitungsmodus ein und füge Widgets hinzu.</p>
</div>
) : null}
{widgets.length > 0 ? (
<ResponsiveGridLayout
className="widgetGrid"
layouts={layouts}
breakpoints={BREAKPOINTS}
cols={COLS}
rowHeight={8}
margin={[12, 12]}
containerPadding={[0, 0]}
compactType={isDesktop ? null : "vertical"}
preventCollision={isDesktop}
isBounded={false}
autoSize={true}
isDraggable={editMode && isDesktop}
isResizable={editMode && isDesktop}
draggableHandle=".widgetDragHandle"
draggableCancel=".widgetNoDrag"
resizeHandles={["se"]}
measureBeforeMount={true}
onBreakpointChange={handleBreakpointChange}
onLayoutChange={handleLayoutChange}
>
{widgets.map((widget) => (
<div
key={widget.id}
className={activeMenuWidgetId === widget.id ? "gridItemMenuOpen" : ""}
>
{renderWidget(widget)}
</div>
))}
</ResponsiveGridLayout>
) : null}
</div>
);
}