feat(#41): implement Widget/HUD system

- BaseWidget wrapper with loading/error states
- WidgetRegistry for central widget management
- WidgetGrid with react-grid-layout integration
- TasksWidget, CalendarWidget, QuickCaptureWidget
- useLayouts hooks for layout persistence
- Comprehensive test suite (TDD approach)
This commit is contained in:
Jason Woltje
2026-01-29 17:54:46 -06:00
parent 95833fb4ea
commit 14a1e218a5
14 changed files with 2142 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
/**
* BaseWidget - Wrapper component for all widgets
* Provides consistent styling, controls, and error/loading states
*/
import type { ReactNode } from "react";
import { Settings, X } from "lucide-react";
import { cn } from "@mosaic/ui/lib/utils";
export interface BaseWidgetProps {
id: string;
title: string;
description?: string;
children: ReactNode;
onEdit?: () => void;
onRemove?: () => void;
className?: string;
isLoading?: boolean;
error?: string;
}
export function BaseWidget({
id,
title,
description,
children,
onEdit,
onRemove,
className,
isLoading = false,
error,
}: BaseWidgetProps) {
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
)}
>
{/* Widget Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-100 bg-gray-50">
<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>
)}
</div>
{/* Control buttons - only show if handlers provided */}
{(onEdit || onRemove) && (
<div className="flex items-center gap-1 ml-2">
{onEdit && (
<button
onClick={onEdit}
aria-label="Edit widget"
className="p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded transition-colors"
title="Edit widget"
>
<Settings className="w-4 h-4" />
</button>
)}
{onRemove && (
<button
onClick={onRemove}
aria-label="Remove widget"
className="p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
title="Remove widget"
>
<X className="w-4 h-4" />
</button>
)}
</div>
)}
</div>
{/* Widget Content */}
<div className="flex-1 p-4 overflow-auto">
{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>
</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>
</div>
) : (
children
)}
</div>
</div>
);
}