feat: add domains, ideas, layouts, widgets API modules
- Add DomainsModule with full CRUD, search, and activity logging - Add IdeasModule with quick capture endpoint - Add LayoutsModule for user dashboard layouts - Add WidgetsModule for widget definitions (read-only) - Update ActivityService with domain/idea logging methods - Register all new modules in AppModule
This commit is contained in:
182
apps/web/src/components/hud/HUD.tsx
Normal file
182
apps/web/src/components/hud/HUD.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* HUD container - main dashboard interface
|
||||
*/
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Button } from "@mosaic/ui";
|
||||
import { RotateCcw } from "lucide-react";
|
||||
import { WidgetGrid } from "./WidgetGrid";
|
||||
import { WidgetRenderer } from "./WidgetRenderer";
|
||||
import { useLayout } from "@/lib/hooks/useLayout";
|
||||
import type { WidgetPlacement } from "@mosaic/shared";
|
||||
|
||||
export interface HUDProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of available widget components
|
||||
* This will be populated with actual widget components
|
||||
*/
|
||||
const WIDGET_REGISTRY = {
|
||||
TasksWidget: {
|
||||
name: "tasks",
|
||||
displayName: "Tasks",
|
||||
description: "View and manage your tasks",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 3,
|
||||
minWidth: 1,
|
||||
minHeight: 2,
|
||||
},
|
||||
CalendarWidget: {
|
||||
name: "calendar",
|
||||
displayName: "Calendar",
|
||||
description: "Upcoming events and schedule",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 2,
|
||||
},
|
||||
QuickCaptureWidget: {
|
||||
name: "quick-capture",
|
||||
displayName: "Quick Capture",
|
||||
description: "Capture ideas and notes",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 1,
|
||||
minWidth: 1,
|
||||
minHeight: 1,
|
||||
},
|
||||
AgentStatusWidget: {
|
||||
name: "agent-status",
|
||||
displayName: "Agent Status",
|
||||
description: "View running agent sessions",
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 1,
|
||||
},
|
||||
} as const;
|
||||
|
||||
type WidgetRegistryKey = keyof typeof WIDGET_REGISTRY;
|
||||
|
||||
export function HUD({ className = "" }: HUDProps) {
|
||||
const {
|
||||
currentLayout,
|
||||
updateLayout,
|
||||
addWidget,
|
||||
removeWidget,
|
||||
switchLayout,
|
||||
resetLayout,
|
||||
} = useLayout();
|
||||
|
||||
const isEditing = true; // For now, always in edit mode (can be toggled later)
|
||||
|
||||
const handleLayoutChange = (newLayout: readonly { i: string; x: number; y: number; w: number; h: number }[]) => {
|
||||
updateLayout([...newLayout] as WidgetPlacement[]);
|
||||
};
|
||||
|
||||
const handleAddWidget = (widgetType: WidgetRegistryKey) => {
|
||||
const widgetConfig = WIDGET_REGISTRY[widgetType];
|
||||
const widgetId = `${widgetType.toLowerCase()}-${Date.now()}`;
|
||||
|
||||
// Find the next available position
|
||||
const maxY = currentLayout?.layout.reduce((max, w) => Math.max(max, w.y + w.h), 0) || 0;
|
||||
|
||||
const newWidget = {
|
||||
i: widgetId,
|
||||
x: 0,
|
||||
y: maxY,
|
||||
w: widgetConfig.defaultWidth,
|
||||
h: widgetConfig.defaultHeight,
|
||||
minW: widgetConfig.minWidth,
|
||||
minH: widgetConfig.minHeight,
|
||||
isDraggable: true,
|
||||
isResizable: true,
|
||||
};
|
||||
|
||||
addWidget(newWidget);
|
||||
};
|
||||
|
||||
const handleResetLayout = () => {
|
||||
if (confirm("Are you sure you want to reset the layout? This will remove all widgets.")) {
|
||||
resetLayout();
|
||||
}
|
||||
};
|
||||
|
||||
const widgetComponents = useMemo(() => {
|
||||
if (!currentLayout?.layout) return [];
|
||||
|
||||
return currentLayout.layout.map((widget) => (
|
||||
<WidgetRenderer
|
||||
key={widget.i}
|
||||
widget={widget}
|
||||
isEditing={isEditing}
|
||||
onRemove={removeWidget}
|
||||
/>
|
||||
));
|
||||
}, [currentLayout?.layout, isEditing, removeWidget]);
|
||||
|
||||
return (
|
||||
<div className={`hud-container ${className}`}>
|
||||
{/* Toolbar */}
|
||||
<div className="bg-white border-b border-gray-200 px-6 py-4 mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={currentLayout?.id || ""}
|
||||
onChange={(e) => switchLayout(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Default Layout</option>
|
||||
{/* Add more layout options here */}
|
||||
</select>
|
||||
|
||||
<Button
|
||||
onClick={handleResetLayout}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
{/* Widget type selector */}
|
||||
<div className="relative">
|
||||
<select
|
||||
onChange={(e) => {
|
||||
const widgetType = e.target.value as WidgetRegistryKey;
|
||||
if (widgetType) {
|
||||
handleAddWidget(widgetType);
|
||||
e.target.value = "";
|
||||
}
|
||||
}}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer"
|
||||
defaultValue=""
|
||||
>
|
||||
<option value="" disabled>
|
||||
Add Widget
|
||||
</option>
|
||||
{Object.entries(WIDGET_REGISTRY).map(([key, config]) => (
|
||||
<option key={key} value={key}>
|
||||
{config.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Widget Grid */}
|
||||
<WidgetGrid
|
||||
layout={currentLayout?.layout || []}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
isEditing={isEditing}
|
||||
>
|
||||
{widgetComponents}
|
||||
</WidgetGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user