feat(web): migrate dashboard to WidgetGrid with layout persistence (#497)
All checks were successful
ci/woodpecker/push/web Pipeline was successful

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #497.
This commit is contained in:
2026-02-24 00:50:24 +00:00
committed by jason.woltje
parent f9cccd6965
commit cc56f2cbe1
8 changed files with 399 additions and 176 deletions

View File

@@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useRef, useState, useEffect } from "react";
import GridLayout from "react-grid-layout";
import type { Layout, LayoutItem } from "react-grid-layout";
import type { WidgetPlacement } from "@mosaic/shared";
@@ -33,6 +33,30 @@ export function WidgetGrid({
isEditing = false,
className,
}: WidgetGridProps): React.JSX.Element {
// Measure container width for responsive grid
const containerRef = useRef<HTMLDivElement>(null);
const [containerWidth, setContainerWidth] = useState(1200);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const observer = new ResizeObserver((entries): void => {
const entry = entries[0];
if (entry) {
setContainerWidth(entry.contentRect.width);
}
});
observer.observe(el);
// Set initial width
setContainerWidth(el.clientWidth);
return (): void => {
observer.disconnect();
};
}, []);
// Convert WidgetPlacement to react-grid-layout Layout format
const gridLayout: Layout = useMemo(
() =>
@@ -96,22 +120,34 @@ export function WidgetGrid({
// Empty state
if (layout.length === 0) {
return (
<div className="flex items-center justify-center h-full min-h-[400px] bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
<div
ref={containerRef}
className="flex items-center justify-center h-full min-h-[400px]"
style={{
background: "var(--surface-2)",
borderRadius: "var(--r-lg)",
border: "2px dashed var(--border)",
}}
>
<div className="text-center">
<p className="text-gray-500 text-lg font-medium">No widgets yet</p>
<p className="text-gray-400 text-sm mt-1">Add widgets to customize your dashboard</p>
<p className="text-lg font-medium" style={{ color: "var(--muted)" }}>
No widgets yet
</p>
<p className="text-sm mt-1" style={{ color: "var(--muted)", opacity: 0.7 }}>
Add widgets to customize your dashboard
</p>
</div>
</div>
);
}
return (
<div className={cn("widget-grid-container", className)}>
<div ref={containerRef} className={cn("widget-grid-container", className)}>
<GridLayout
className="layout"
layout={gridLayout}
onLayoutChange={handleLayoutChange}
width={1200}
width={containerWidth}
gridConfig={{
cols: 12,
rowHeight: 100,