91e5902020
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>
156 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|