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:
99
apps/web/src/components/widgets/BaseWidget.tsx
Normal file
99
apps/web/src/components/widgets/BaseWidget.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user