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

@@ -37,16 +37,31 @@ export function BaseWidget({
return (
<div
data-widget-id={id}
className={cn(
"flex flex-col h-full bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden",
className
)}
className={cn("flex flex-col h-full overflow-hidden", className)}
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
borderRadius: "var(--r-lg)",
boxShadow: "var(--shadow-sm)",
}}
>
{/* Widget Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-100 bg-gray-50">
<div
className="flex items-center justify-between px-4 py-3"
style={{
borderBottom: "1px solid var(--border)",
background: "var(--surface-2)",
}}
>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-gray-900 truncate">{title}</h3>
{description && <p className="text-xs text-gray-500 truncate mt-0.5">{description}</p>}
<h3 className="text-sm font-semibold truncate" style={{ color: "var(--text)" }}>
{title}
</h3>
{description && (
<p className="text-xs truncate mt-0.5" style={{ color: "var(--muted)" }}>
{description}
</p>
)}
</div>
{/* Control buttons - only show if handlers provided */}
@@ -56,7 +71,8 @@ export function BaseWidget({
<button
onClick={onEdit}
aria-label="Edit widget"
className="p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded transition-colors"
className="p-1 rounded transition-colors"
style={{ color: "var(--muted)" }}
title="Edit widget"
>
<Settings className="w-4 h-4" />
@@ -66,7 +82,8 @@ export function BaseWidget({
<button
onClick={onRemove}
aria-label="Remove widget"
className="p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
className="p-1 rounded transition-colors"
style={{ color: "var(--muted)" }}
title="Remove widget"
>
<X className="w-4 h-4" />
@@ -81,15 +98,24 @@ export function BaseWidget({
{isLoading ? (
<div className="flex items-center justify-center h-full">
<div className="flex flex-col items-center gap-2">
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
<span className="text-sm text-gray-500">Loading...</span>
<div
className="w-8 h-8 border-2 border-t-transparent rounded-full animate-spin"
style={{ borderColor: "var(--primary)", borderTopColor: "transparent" }}
/>
<span className="text-sm" style={{ color: "var(--muted)" }}>
Loading...
</span>
</div>
</div>
) : error ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="text-red-500 text-sm font-medium mb-1">Error</div>
<div className="text-xs text-gray-600">{error}</div>
<div className="text-sm font-medium mb-1" style={{ color: "var(--danger)" }}>
Error
</div>
<div className="text-xs" style={{ color: "var(--muted)" }}>
{error}
</div>
</div>
</div>
) : (