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>
This commit is contained in:
Claude
2026-06-18 10:32:59 +02:00
parent a4051ae132
commit 91e5902020
5 changed files with 1890 additions and 39 deletions
+42 -37
View File
@@ -1,8 +1,8 @@
"use client";
import type { ComponentType, ReactNode } from "react";
import { useMemo } from "react";
import ReactGridLayoutBase, { WidthProvider } from "react-grid-layout/legacy";
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 = {
@@ -13,7 +13,10 @@ export type DashboardGridProps = {
onLayoutChange: (layout: DashboardLayoutItem[]) => void;
};
const WidthAwareGridLayout = WidthProvider(ReactGridLayoutBase) as ComponentType<any>;
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));
@@ -21,44 +24,26 @@ function clamp(value: number, min: number, max: number): number {
function getWidgetMinimumSize(widget: DashboardGridWidget): { minW: number; minH: number } {
if (widget.type === "search") {
return {
minW: 8,
minH: 5
};
return { minW: 8, minH: 5 };
}
if (widget.type === "clock") {
return {
minW: 4,
minH: 4
};
return { minW: 4, minH: 4 };
}
if (widget.type === "calendar") {
return {
minW: 8,
minH: 6
};
return { minW: 8, minH: 6 };
}
if (widget.type === "calculator") {
return {
minW: 8,
minH: 6
};
return { minW: 8, minH: 6 };
}
if (widget.type === "note") {
return {
minW: 4,
minH: 5
};
return { minW: 4, minH: 5 };
}
return {
minW: 4,
minH: 4
};
return { minW: 4, minH: 4 };
}
function widgetsToLayout(widgets: DashboardGridWidget[]): DashboardLayoutItem[] {
@@ -104,7 +89,25 @@ export default function DashboardGrid({
renderWidget,
onLayoutChange
}: DashboardGridProps) {
const layout = useMemo(() => widgetsToLayout(widgets), [widgets]);
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">
@@ -116,24 +119,26 @@ export default function DashboardGrid({
) : null}
{widgets.length > 0 ? (
<WidthAwareGridLayout
<ResponsiveGridLayout
className="widgetGrid"
layout={layout}
cols={48}
layouts={layouts}
breakpoints={BREAKPOINTS}
cols={COLS}
rowHeight={8}
margin={[12, 12]}
containerPadding={[0, 0]}
compactType={null}
preventCollision={true}
compactType={isDesktop ? null : "vertical"}
preventCollision={isDesktop}
isBounded={false}
autoSize={true}
isDraggable={editMode}
isResizable={editMode}
isDraggable={editMode && isDesktop}
isResizable={editMode && isDesktop}
draggableHandle=".widgetDragHandle"
draggableCancel=".widgetNoDrag"
resizeHandles={["se"]}
measureBeforeMount={true}
onLayoutChange={(nextLayout: DashboardLayoutItem[]) => onLayoutChange(normalizeLayout(nextLayout))}
onBreakpointChange={handleBreakpointChange}
onLayoutChange={handleLayoutChange}
>
{widgets.map((widget) => (
<div
@@ -143,7 +148,7 @@ export default function DashboardGrid({
{renderWidget(widget)}
</div>
))}
</WidthAwareGridLayout>
</ResponsiveGridLayout>
) : null}
</div>
);