Workspaces left panel in homepage
This commit is contained in:
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="relative overflow-hidden h-dvh outline-none fixed inset-0">
|
||||
<PixelGrid />
|
||||
|
||||
<section className="relative h-dvh snap-start overflow-hidden">
|
||||
{/* Background layer */}
|
||||
<div
|
||||
className="absolute inset-0 z-0"
|
||||
style={{
|
||||
backgroundImage: 'var(--page-bg-image)',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center center',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content layer - above pixel grid */}
|
||||
<div className="absolute inset-0 z-[520] flex flex-col">
|
||||
<header
|
||||
className="shrink-0 border-b backdrop-blur-xl shadow-lg px-3 py-2 md:px-6 md:py-3 flex items-center justify-between"
|
||||
style={{ backgroundColor: 'var(--glass-bg)', borderColor: 'var(--glass-border)' }}
|
||||
>
|
||||
<Link to="/">
|
||||
<img
|
||||
src="/static/officer-logo.svg"
|
||||
alt="Officer"
|
||||
className="h-8 md:h-12 w-auto"
|
||||
style={{ transform: 'skew(-15deg, -2deg)' }}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-duck-yellow cursor-pointer">
|
||||
<Avatar className="h-9 w-9 rounded-full">
|
||||
<AvatarImage src={user?.avatar ?? undefined} />
|
||||
<AvatarFallback className="bg-duck-teal text-duck-yellow text-sm font-bold rounded-full">
|
||||
{user?.name?.charAt(0).toUpperCase() ?? '?'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="z-[600] w-48">
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link to="/settings/profile">
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
Profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link to="/settings/ai">
|
||||
<Bot className="mr-2 h-4 w-4" />
|
||||
AI Settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link to="/settings/server">
|
||||
<Server className="mr-2 h-4 w-4" />
|
||||
Server Settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link to="/settings/resources">
|
||||
<Package className="mr-2 h-4 w-4" />
|
||||
Resources
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link to="/auth/signout">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign Out
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</header>
|
||||
|
||||
<div className={`flex-1 min-h-0 ${mobileFull ? 'pb-0 md:pb-20' : 'pb-16 md:pb-20'}`}>
|
||||
{/* {isProduction && passkeyCount === 0 ? <PasskeyGate /> : children} */}
|
||||
{children}
|
||||
</div>
|
||||
<Dock items={dockItems} className={mobileFull ? 'hidden md:flex' : 'flex'} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
<section className="relative h-dvh snap-start overflow-hidden">
|
||||
<Background />
|
||||
<Dock items={dockItems} />
|
||||
<div className="absolute inset-0 z-2 pt-[52px] md:pt-[64px] pb-[80px] overflow-y-auto">
|
||||
<div className="absolute inset-0 z-2 pt-[52px] md:pt-[64px] pb-2 overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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<number | null>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const dockRef = useRef<HTMLDivElement | null>(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 (
|
||||
<div
|
||||
ref={dockRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={`fixed bottom-4 left-1/2 -translate-x-1/2 z-5 items-end gap-2 md:gap-6 px-2 py-1.5 md:px-3 md:py-2 rounded-2xl border backdrop-blur-xl shadow-lg ${className ?? 'flex'}`}
|
||||
style={{ backgroundColor: 'transparent', borderColor: 'transparent' }}
|
||||
className={`fixed left-1/2 z-5 items-end gap-2 md:gap-6 px-2 py-1.5 md:px-3 md:py-2 rounded-2xl border backdrop-blur-xl shadow-lg transition-transform duration-300 ease-in-out ${className ?? 'flex'}`}
|
||||
style={{
|
||||
backgroundColor: 'var(--dock-bg)',
|
||||
borderColor: 'var(--dock-border)',
|
||||
bottom: '16px',
|
||||
transform: `translateX(-50%) translateY(${visible ? '0' : 'calc(100% + 24px)'})`,
|
||||
}}
|
||||
>
|
||||
{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) => {
|
||||
<div
|
||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center transition-all"
|
||||
style={{
|
||||
background: active ? `${item.color}55` : `${item.color}30`,
|
||||
boxShadow: active ? `0 0 12px ${item.color}30` : 'none',
|
||||
background: item.color,
|
||||
boxShadow: active ? `0 0 12px ${item.color}40` : 'none',
|
||||
}}
|
||||
>
|
||||
<item.icon className="h-5 w-5 md:h-6 md:w-6" style={{ color: active ? item.color : `${item.color}cc` }} />
|
||||
<item.icon className="h-5 w-5 md:h-6 md:w-6 text-white" />
|
||||
</div>
|
||||
{active && (
|
||||
<div className="absolute -bottom-1.5 w-1.5 h-1.5 rounded-full" style={{ background: item.color }} />
|
||||
|
||||
@@ -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<WorkspaceDefinition[]>('workspaces', []);
|
||||
|
||||
return (
|
||||
<div className="px-4 pb-3 max-h-72 overflow-y-auto">
|
||||
{workspaces.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-2 py-6 text-duck-dark/40">
|
||||
<LayoutGrid className="h-6 w-6" />
|
||||
<p className="text-xs">No workspaces</p>
|
||||
<Link to="/workspaces" className="text-xs text-duck-teal hover:underline">
|
||||
Create one
|
||||
</Link>
|
||||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
<div className="p-3 pb-0">
|
||||
<div className="flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium bg-duck-teal/15 text-duck-teal">
|
||||
<LayoutGrid className="h-4 w-4" />
|
||||
Workspaces
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{workspaces.map((ws) => (
|
||||
<li key={ws.id} className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-duck-dark/5 group">
|
||||
<Link to={`/workspaces/${ws.id}`} className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<LayoutGrid className="h-4 w-4 shrink-0 text-duck-teal/60" />
|
||||
<span className="text-sm text-duck-dark truncate">{ws.name}</span>
|
||||
</Link>
|
||||
<Link
|
||||
to={`/workspaces/${ws.id}`}
|
||||
className="shrink-0 p-1 rounded text-duck-dark/20 md:opacity-0 md:group-hover:opacity-100 hover:text-duck-teal transition-opacity"
|
||||
>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 px-3 pt-3">
|
||||
{workspaces.map((ws) => (
|
||||
<Link
|
||||
key={ws.id}
|
||||
to={`/workspaces/${ws.id}`}
|
||||
className="flex items-center gap-2.5 py-2 px-3 rounded-lg text-sm font-medium text-duck-dark/60 hover:bg-duck-dark/5 hover:text-duck-dark transition-all"
|
||||
>
|
||||
<LayoutGrid className="h-4 w-4 shrink-0" />
|
||||
<span className="flex-1 text-left truncate">{ws.name}</span>
|
||||
</Link>
|
||||
))}
|
||||
{workspaces.length === 0 && (
|
||||
<p className="text-xs text-duck-dark/40 px-3 py-4 text-center">No workspaces yet</p>
|
||||
)}
|
||||
<Link
|
||||
to="/workspaces"
|
||||
className="flex items-center gap-2.5 py-2 px-3 rounded-lg text-sm font-medium text-duck-dark/40 hover:bg-duck-dark/5 hover:text-duck-dark transition-all"
|
||||
>
|
||||
<Plus className="h-4 w-4 shrink-0" />
|
||||
<span className="flex-1 text-left">New workspace</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user