From 72bca6cd42b84169a84e4de352db0a90d69d88bd Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Wed, 18 Feb 2026 17:37:52 +0000 Subject: [PATCH] Workspaces left panel in homepage --- .../Screens/Dashboard/Home/index.tsx | 17 ++- .../Screens/Dashboard/Layout.backup.tsx | 144 ------------------ .../Dashboard/Layout/DashboardLayout.tsx | 2 +- .../Screens/Dashboard/Layout/Dock.tsx | 41 +++-- .../Dashboard/Workspaces/WorkspaceListApp.tsx | 56 +++---- src/workspaces/widgets/WidgetPanel/index.tsx | 24 +-- src/workspaces/widgets/Workspaces/index.tsx | 54 ------- src/workspaces/widgets/widget-registry.tsx | 4 +- 8 files changed, 85 insertions(+), 257 deletions(-) delete mode 100644 src/apps/officer-web/Screens/Dashboard/Layout.backup.tsx delete mode 100644 src/workspaces/widgets/Workspaces/index.tsx 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 */} -
-
- - Officer - - - - - - - - - - - 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: () => }, };