diff --git a/src/apps/officer-web/Screens/Dashboard/Home/index.tsx b/src/apps/officer-web/Screens/Dashboard/Home/index.tsx
index 374ff1f2..9aa7741c 100644
--- a/src/apps/officer-web/Screens/Dashboard/Home/index.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Home/index.tsx
@@ -6,10 +6,21 @@ import { appRegistry } from '../Workspaces/app-registry';
const initialLayout: LayoutNode = {
type: 'group',
id: 'home-root',
- direction: 'vertical',
+ direction: 'horizontal',
children: [
- { node: { type: 'panel', id: 'homepage-widget-panel', appType: 'widget-panel' }, size: 60 },
- { node: { type: 'panel', id: 'home-bottom', appType: 'chat-launcher' }, size: 40 },
+ { node: { type: 'panel', id: 'home-left', appType: 'workspace-list' }, size: 20 },
+ {
+ node: {
+ type: 'group',
+ id: 'home-right',
+ direction: 'vertical',
+ children: [
+ { node: { type: 'panel', id: 'homepage-widget-panel', appType: 'widget-panel' }, size: 60 },
+ { node: { type: 'panel', id: 'home-chat', appType: 'chat-launcher' }, size: 40 },
+ ],
+ },
+ size: 80,
+ },
],
};
diff --git a/src/apps/officer-web/Screens/Dashboard/Layout.backup.tsx b/src/apps/officer-web/Screens/Dashboard/Layout.backup.tsx
deleted file mode 100644
index d81bfd89..00000000
--- a/src/apps/officer-web/Screens/Dashboard/Layout.backup.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import type { ReactNode } from 'react';
-import { Link } from 'react-router';
-import {
- User,
- LogOut,
- Terminal,
- TerminalSquare,
- FileText,
- FolderOpen,
- Server,
- Package,
- Bot,
- Sparkles,
- ClipboardList,
- ScrollText,
- Workflow,
-} from 'lucide-react';
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu';
-import type { UserWithToken } from 'hooks/useAuth';
-import { useAuth } from 'hooks/useAuth';
-import { PixelGrid } from '../LandingPage/components/PixelGrid';
-import { useIsProduction } from 'hooks/useIsProduction';
-import { useServerSettings } from '@/state/useServerSettings';
-import { PasskeyGate } from './PasskeyGate';
-import { Dock, type DockItem } from './Dock';
-
-type DashboardLayoutProps = {
- children?: ReactNode;
- mobileFull?: boolean;
-};
-
-export function DashboardLayout({ children, mobileFull }: DashboardLayoutProps) {
- const { user } = useAuth();
- const isProduction = useIsProduction();
- const { plugins } = useServerSettings();
- const passkeyCount = (user as UserWithToken & { passkeyCount?: number })?.passkeyCount ?? 0;
-
- const dockItems: DockItem[] = [
- { label: 'Chat', to: '/chat', icon: Terminal, color: '#60a5fa' },
- ...(plugins?.FileBrowser !== false
- ? [{ label: 'Files', to: '/files', icon: FolderOpen, color: '#fbbf24' } as DockItem]
- : []),
- ...(plugins?.Terminal !== false
- ? [{ label: 'Terminal', to: '/terminal', icon: TerminalSquare, color: '#34d399' } as DockItem]
- : []),
- { label: 'Plans', to: '/plans', icon: FileText, color: '#f472b6' },
- { label: 'Skills', to: '/skills', icon: Sparkles, color: '#c084fc' },
- { label: 'Tasks', to: '/tasks', icon: ClipboardList, color: '#fb923c' },
- { label: 'Processes', to: '/processes', icon: Workflow, color: '#2dd4bf' },
- { label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' },
- ];
-
- return (
-
-
-
-
- {/* Background layer */}
-
-
- {/* Content layer - above pixel grid */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Profile
-
-
-
-
-
- AI Settings
-
-
-
-
-
- Server Settings
-
-
-
-
-
- Resources
-
-
-
-
-
- Sign Out
-
-
-
-
-
-
-
- {/* {isProduction && passkeyCount === 0 ?
: children} */}
- {children}
-
-
-
-
-
- );
-}
diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx
index e70d6af4..dd5d063c 100644
--- a/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx
@@ -12,7 +12,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
-
+
{children}
diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx
index db2235b0..00be5f93 100644
--- a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { Link, useLocation } from 'react-router';
import type { LucideIcon } from 'lucide-react';
@@ -19,6 +19,7 @@ const ICON_GAP = 24;
const DOCK_PADDING = 12;
const MAX_SCALE = 1.5;
const MAX_DISTANCE = 150;
+const BOTTOM_THRESHOLD = 100;
const getScale = (mouseX: number | null, iconCenterX: number) => {
if (mouseX === null) return 1;
@@ -29,25 +30,39 @@ const getScale = (mouseX: number | null, iconCenterX: number) => {
export const Dock = ({ items, className }: DockProps) => {
const [mouseX, setMouseX] = useState
(null);
+ const [visible, setVisible] = useState(false);
const dockRef = useRef(null);
const location = useLocation();
const isActive = (to: string) => location.pathname.startsWith(to);
- const handleMouseMove = (ev: React.MouseEvent) => {
- const rect = dockRef.current?.getBoundingClientRect();
- if (rect) setMouseX(ev.clientX - rect.left);
- };
+ useEffect(() => {
+ const handleMouseMove = (ev: MouseEvent) => {
+ const distFromBottom = window.innerHeight - ev.clientY;
+ if (distFromBottom <= BOTTOM_THRESHOLD) {
+ setVisible(true);
+ const rect = dockRef.current?.getBoundingClientRect();
+ if (rect) setMouseX(ev.clientX - rect.left);
+ } else {
+ setVisible(false);
+ setMouseX(null);
+ }
+ };
- const handleMouseLeave = () => setMouseX(null);
+ document.addEventListener('mousemove', handleMouseMove);
+ return () => document.removeEventListener('mousemove', handleMouseMove);
+ }, []);
return (
{items.map((item, index) => {
const iconCenter = DOCK_PADDING + index * (ICON_SIZE + ICON_GAP) + ICON_SIZE / 2;
@@ -74,11 +89,11 @@ export const Dock = ({ items, className }: DockProps) => {
-
+
{active && (
diff --git a/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx b/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx
index 3b822d31..b4b0c906 100644
--- a/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspaceListApp.tsx
@@ -1,5 +1,5 @@
import { Link } from 'react-router';
-import { LayoutGrid, ArrowRight } from 'lucide-react';
+import { LayoutGrid, Plus } from 'lucide-react';
import { useUserState } from '@/state/useUserState';
import type { WorkspaceDefinition } from '@/components/Workspace';
@@ -7,33 +7,35 @@ export const WorkspaceListApp = () => {
const [workspaces] = useUserState
('workspaces', []);
return (
-
- {workspaces.length === 0 ? (
-
-
-
No workspaces
-
- Create one
-
+
+
+
+
+ Workspaces
- ) : (
-
- {workspaces.map((ws) => (
- -
-
-
- {ws.name}
-
-
-
-
-
- ))}
-
- )}
+
+
+ {workspaces.map((ws) => (
+
+
+
{ws.name}
+
+ ))}
+ {workspaces.length === 0 && (
+
No workspaces yet
+ )}
+
+
+
New workspace
+
+
);
};
diff --git a/src/workspaces/widgets/WidgetPanel/index.tsx b/src/workspaces/widgets/WidgetPanel/index.tsx
index 88fa4c44..2750e17f 100644
--- a/src/workspaces/widgets/WidgetPanel/index.tsx
+++ b/src/workspaces/widgets/WidgetPanel/index.tsx
@@ -7,7 +7,7 @@ import { Plus, X } from 'lucide-react';
import { widgetRegistry } from '../widget-registry';
import { WidgetPicker } from './WidgetPicker';
-type Position = { x: number; y: number };
+type Position = { x: number; y: number }; // percentages (0–100)
type WidgetInstance = {
id: string;
@@ -69,8 +69,8 @@ type DragState = {
startY: number;
originX: number;
originY: number;
- widgetW: number;
- widgetH: number;
+ widgetWPct: number;
+ widgetHPct: number;
panelW: number;
panelH: number;
};
@@ -91,8 +91,8 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg
startY: ev.clientY,
originX: instance.position.x,
originY: instance.position.y,
- widgetW: widgetRect.width,
- widgetH: widgetRect.height,
+ widgetWPct: (widgetRect.width / panelRect.width) * 100,
+ widgetHPct: (widgetRect.height / panelRect.height) * 100,
panelW: panelRect.width,
panelH: panelRect.height,
};
@@ -119,11 +119,11 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg
return;
}
const d = dragRef.current;
- const rawX = d.originX + (ev.clientX - d.startX);
- const rawY = d.originY + (ev.clientY - d.startY);
+ const deltaXPct = ((ev.clientX - d.startX) / d.panelW) * 100;
+ const deltaYPct = ((ev.clientY - d.startY) / d.panelH) * 100;
onMove({
- x: Math.max(0, Math.min(rawX, d.panelW - d.widgetW)),
- y: Math.max(0, Math.min(rawY, d.panelH - d.widgetH)),
+ x: Math.max(0, Math.min(d.originX + deltaXPct, 100 - d.widgetWPct)),
+ y: Math.max(0, Math.min(d.originY + deltaYPct, 100 - d.widgetHPct)),
});
},
[onMove, panelRef, endDrag],
@@ -144,7 +144,7 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg
{
const addWidget = useCallback((widgetType: string) => {
nextId.current++;
const id = `widget-${nextId.current}`;
- const offset = (nextId.current % 5) * 30;
- setInstances((prev) => [...prev, { id, widgetType, position: { x: 20 + offset, y: 20 + offset } }]);
+ const offset = (nextId.current % 5) * 5;
+ setInstances((prev) => [...prev, { id, widgetType, position: { x: 2 + offset, y: 2 + offset } }]);
setShowPicker(false);
}, []);
diff --git a/src/workspaces/widgets/Workspaces/index.tsx b/src/workspaces/widgets/Workspaces/index.tsx
deleted file mode 100644
index 7c11b822..00000000
--- a/src/workspaces/widgets/Workspaces/index.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Link } from 'react-router';
-import { LayoutGrid, ArrowRight } from 'lucide-react';
-import { useQuery } from '@tanstack/react-query';
-import { useClient } from 'hooks/useClient';
-import { useAuth } from 'hooks/useAuth';
-import type { WorkspaceDefinition } from '@/components/Workspace';
-import { Widget } from '../Widget';
-
-export const Workspaces = () => {
- const client = useClient();
- const { isAuthenticated } = useAuth();
-
- const { data: state = {} } = useQuery
>({
- queryKey: ['USER_STATE'],
- enabled: isAuthenticated,
- queryFn: () => client.get('/user/state'),
- staleTime: Infinity,
- });
-
- const workspaces = (state.workspaces ?? []) as WorkspaceDefinition[];
-
- return (
-
-
- {workspaces.length === 0 ? (
-
-
-
No workspaces
-
- Create one
-
-
- ) : (
-
- {workspaces.map((ws) => (
- -
-
-
- {ws.name}
-
-
-
-
-
- ))}
-
- )}
-
-
- );
-};
diff --git a/src/workspaces/widgets/widget-registry.tsx b/src/workspaces/widgets/widget-registry.tsx
index d91590dd..a9eeb042 100644
--- a/src/workspaces/widgets/widget-registry.tsx
+++ b/src/workspaces/widgets/widget-registry.tsx
@@ -1,9 +1,7 @@
-import { Clock as ClockIcon, LayoutGrid } from 'lucide-react';
+import { Clock as ClockIcon } from 'lucide-react';
import type { AppRegistryEntry } from '@/components/Workspace';
import { Clock } from './Clock/index';
-import { Workspaces } from './Workspaces/index';
export const widgetRegistry: Record = {
'clock': { name: 'Clock', icon: ClockIcon, component: () => },
- 'workspaces': { name: 'Workspaces', icon: LayoutGrid, component: () => },
};