Workspaces left panel in homepage
This commit is contained in:
@@ -6,10 +6,21 @@ import { appRegistry } from '../Workspaces/app-registry';
|
|||||||
const initialLayout: LayoutNode = {
|
const initialLayout: LayoutNode = {
|
||||||
type: 'group',
|
type: 'group',
|
||||||
id: 'home-root',
|
id: 'home-root',
|
||||||
direction: 'vertical',
|
direction: 'horizontal',
|
||||||
children: [
|
children: [
|
||||||
{ node: { type: 'panel', id: 'homepage-widget-panel', appType: 'widget-panel' }, size: 60 },
|
{ node: { type: 'panel', id: 'home-left', appType: 'workspace-list' }, size: 20 },
|
||||||
{ node: { type: 'panel', id: 'home-bottom', appType: 'chat-launcher' }, size: 40 },
|
{
|
||||||
|
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">
|
<section className="relative h-dvh snap-start overflow-hidden">
|
||||||
<Background />
|
<Background />
|
||||||
<Dock items={dockItems} />
|
<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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router';
|
import { Link, useLocation } from 'react-router';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ const ICON_GAP = 24;
|
|||||||
const DOCK_PADDING = 12;
|
const DOCK_PADDING = 12;
|
||||||
const MAX_SCALE = 1.5;
|
const MAX_SCALE = 1.5;
|
||||||
const MAX_DISTANCE = 150;
|
const MAX_DISTANCE = 150;
|
||||||
|
const BOTTOM_THRESHOLD = 100;
|
||||||
|
|
||||||
const getScale = (mouseX: number | null, iconCenterX: number) => {
|
const getScale = (mouseX: number | null, iconCenterX: number) => {
|
||||||
if (mouseX === null) return 1;
|
if (mouseX === null) return 1;
|
||||||
@@ -29,25 +30,39 @@ const getScale = (mouseX: number | null, iconCenterX: number) => {
|
|||||||
|
|
||||||
export const Dock = ({ items, className }: DockProps) => {
|
export const Dock = ({ items, className }: DockProps) => {
|
||||||
const [mouseX, setMouseX] = useState<number | null>(null);
|
const [mouseX, setMouseX] = useState<number | null>(null);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
const dockRef = useRef<HTMLDivElement | null>(null);
|
const dockRef = useRef<HTMLDivElement | null>(null);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const isActive = (to: string) => location.pathname.startsWith(to);
|
const isActive = (to: string) => location.pathname.startsWith(to);
|
||||||
|
|
||||||
const handleMouseMove = (ev: React.MouseEvent) => {
|
useEffect(() => {
|
||||||
const rect = dockRef.current?.getBoundingClientRect();
|
const handleMouseMove = (ev: MouseEvent) => {
|
||||||
if (rect) setMouseX(ev.clientX - rect.left);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={dockRef}
|
ref={dockRef}
|
||||||
onMouseMove={handleMouseMove}
|
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'}`}
|
||||||
onMouseLeave={handleMouseLeave}
|
style={{
|
||||||
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'}`}
|
backgroundColor: 'var(--dock-bg)',
|
||||||
style={{ backgroundColor: 'transparent', borderColor: 'transparent' }}
|
borderColor: 'var(--dock-border)',
|
||||||
|
bottom: '16px',
|
||||||
|
transform: `translateX(-50%) translateY(${visible ? '0' : 'calc(100% + 24px)'})`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
const iconCenter = DOCK_PADDING + index * (ICON_SIZE + ICON_GAP) + ICON_SIZE / 2;
|
const iconCenter = DOCK_PADDING + index * (ICON_SIZE + ICON_GAP) + ICON_SIZE / 2;
|
||||||
@@ -74,11 +89,11 @@ export const Dock = ({ items, className }: DockProps) => {
|
|||||||
<div
|
<div
|
||||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center transition-all"
|
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center transition-all"
|
||||||
style={{
|
style={{
|
||||||
background: active ? `${item.color}55` : `${item.color}30`,
|
background: item.color,
|
||||||
boxShadow: active ? `0 0 12px ${item.color}30` : 'none',
|
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>
|
</div>
|
||||||
{active && (
|
{active && (
|
||||||
<div className="absolute -bottom-1.5 w-1.5 h-1.5 rounded-full" style={{ background: item.color }} />
|
<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 { Link } from 'react-router';
|
||||||
import { LayoutGrid, ArrowRight } from 'lucide-react';
|
import { LayoutGrid, Plus } from 'lucide-react';
|
||||||
import { useUserState } from '@/state/useUserState';
|
import { useUserState } from '@/state/useUserState';
|
||||||
import type { WorkspaceDefinition } from '@/components/Workspace';
|
import type { WorkspaceDefinition } from '@/components/Workspace';
|
||||||
|
|
||||||
@@ -7,33 +7,35 @@ export const WorkspaceListApp = () => {
|
|||||||
const [workspaces] = useUserState<WorkspaceDefinition[]>('workspaces', []);
|
const [workspaces] = useUserState<WorkspaceDefinition[]>('workspaces', []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 pb-3 max-h-72 overflow-y-auto">
|
<div className="flex flex-col h-full overflow-y-auto">
|
||||||
{workspaces.length === 0 ? (
|
<div className="p-3 pb-0">
|
||||||
<div className="flex flex-col items-center gap-2 py-6 text-duck-dark/40">
|
<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-6 w-6" />
|
<LayoutGrid className="h-4 w-4" />
|
||||||
<p className="text-xs">No workspaces</p>
|
Workspaces
|
||||||
<Link to="/workspaces" className="text-xs text-duck-teal hover:underline">
|
|
||||||
Create one
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<ul className="space-y-0.5">
|
<div className="flex flex-col gap-0.5 px-3 pt-3">
|
||||||
{workspaces.map((ws) => (
|
{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
|
||||||
<Link to={`/workspaces/${ws.id}`} className="flex items-center gap-2 flex-1 min-w-0">
|
key={ws.id}
|
||||||
<LayoutGrid className="h-4 w-4 shrink-0 text-duck-teal/60" />
|
to={`/workspaces/${ws.id}`}
|
||||||
<span className="text-sm text-duck-dark truncate">{ws.name}</span>
|
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"
|
||||||
</Link>
|
>
|
||||||
<Link
|
<LayoutGrid className="h-4 w-4 shrink-0" />
|
||||||
to={`/workspaces/${ws.id}`}
|
<span className="flex-1 text-left truncate">{ws.name}</span>
|
||||||
className="shrink-0 p-1 rounded text-duck-dark/20 md:opacity-0 md:group-hover:opacity-100 hover:text-duck-teal transition-opacity"
|
</Link>
|
||||||
>
|
))}
|
||||||
<ArrowRight className="h-3.5 w-3.5" />
|
{workspaces.length === 0 && (
|
||||||
</Link>
|
<p className="text-xs text-duck-dark/40 px-3 py-4 text-center">No workspaces yet</p>
|
||||||
</li>
|
)}
|
||||||
))}
|
<Link
|
||||||
</ul>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Plus, X } from 'lucide-react';
|
|||||||
import { widgetRegistry } from '../widget-registry';
|
import { widgetRegistry } from '../widget-registry';
|
||||||
import { WidgetPicker } from './WidgetPicker';
|
import { WidgetPicker } from './WidgetPicker';
|
||||||
|
|
||||||
type Position = { x: number; y: number };
|
type Position = { x: number; y: number }; // percentages (0–100)
|
||||||
|
|
||||||
type WidgetInstance = {
|
type WidgetInstance = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -69,8 +69,8 @@ type DragState = {
|
|||||||
startY: number;
|
startY: number;
|
||||||
originX: number;
|
originX: number;
|
||||||
originY: number;
|
originY: number;
|
||||||
widgetW: number;
|
widgetWPct: number;
|
||||||
widgetH: number;
|
widgetHPct: number;
|
||||||
panelW: number;
|
panelW: number;
|
||||||
panelH: number;
|
panelH: number;
|
||||||
};
|
};
|
||||||
@@ -91,8 +91,8 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg
|
|||||||
startY: ev.clientY,
|
startY: ev.clientY,
|
||||||
originX: instance.position.x,
|
originX: instance.position.x,
|
||||||
originY: instance.position.y,
|
originY: instance.position.y,
|
||||||
widgetW: widgetRect.width,
|
widgetWPct: (widgetRect.width / panelRect.width) * 100,
|
||||||
widgetH: widgetRect.height,
|
widgetHPct: (widgetRect.height / panelRect.height) * 100,
|
||||||
panelW: panelRect.width,
|
panelW: panelRect.width,
|
||||||
panelH: panelRect.height,
|
panelH: panelRect.height,
|
||||||
};
|
};
|
||||||
@@ -119,11 +119,11 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const d = dragRef.current;
|
const d = dragRef.current;
|
||||||
const rawX = d.originX + (ev.clientX - d.startX);
|
const deltaXPct = ((ev.clientX - d.startX) / d.panelW) * 100;
|
||||||
const rawY = d.originY + (ev.clientY - d.startY);
|
const deltaYPct = ((ev.clientY - d.startY) / d.panelH) * 100;
|
||||||
onMove({
|
onMove({
|
||||||
x: Math.max(0, Math.min(rawX, d.panelW - d.widgetW)),
|
x: Math.max(0, Math.min(d.originX + deltaXPct, 100 - d.widgetWPct)),
|
||||||
y: Math.max(0, Math.min(rawY, d.panelH - d.widgetH)),
|
y: Math.max(0, Math.min(d.originY + deltaYPct, 100 - d.widgetHPct)),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[onMove, panelRef, endDrag],
|
[onMove, panelRef, endDrag],
|
||||||
@@ -144,7 +144,7 @@ const DraggableWidget = ({ instance, panelRef, onMove, onRemove }: DraggableWidg
|
|||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="absolute group"
|
className="absolute group"
|
||||||
style={{ left: instance.position.x, top: instance.position.y }}
|
style={{ left: `${instance.position.x}%`, top: `${instance.position.y}%` }}
|
||||||
onPointerDown={onPointerDown}
|
onPointerDown={onPointerDown}
|
||||||
onPointerMove={onPointerMove}
|
onPointerMove={onPointerMove}
|
||||||
onPointerUp={onPointerUp}
|
onPointerUp={onPointerUp}
|
||||||
@@ -194,8 +194,8 @@ export const WidgetPanel = ({ panelId }: { panelId: string }) => {
|
|||||||
const addWidget = useCallback((widgetType: string) => {
|
const addWidget = useCallback((widgetType: string) => {
|
||||||
nextId.current++;
|
nextId.current++;
|
||||||
const id = `widget-${nextId.current}`;
|
const id = `widget-${nextId.current}`;
|
||||||
const offset = (nextId.current % 5) * 30;
|
const offset = (nextId.current % 5) * 5;
|
||||||
setInstances((prev) => [...prev, { id, widgetType, position: { x: 20 + offset, y: 20 + offset } }]);
|
setInstances((prev) => [...prev, { id, widgetType, position: { x: 2 + offset, y: 2 + offset } }]);
|
||||||
setShowPicker(false);
|
setShowPicker(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -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<Record<string, unknown>>({
|
|
||||||
queryKey: ['USER_STATE'],
|
|
||||||
enabled: isAuthenticated,
|
|
||||||
queryFn: () => client.get('/user/state'),
|
|
||||||
staleTime: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
const workspaces = (state.workspaces ?? []) as WorkspaceDefinition[];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Widget title="Workspaces">
|
|
||||||
<div className="px-2 pb-3 max-h-52 overflow-y-auto">
|
|
||||||
{workspaces.length === 0 ? (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-4 text-muted-foreground">
|
|
||||||
<LayoutGrid className="h-5 w-5" />
|
|
||||||
<p className="text-xs">No workspaces</p>
|
|
||||||
<Link to="/workspaces" className="text-xs text-primary hover:underline">
|
|
||||||
Create one
|
|
||||||
</Link>
|
|
||||||
</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-muted/50 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-muted-foreground" />
|
|
||||||
<span className="text-sm truncate">{ws.name}</span>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/workspaces/${ws.id}`}
|
|
||||||
className="shrink-0 p-1 rounded text-muted-foreground/40 md:opacity-0 md:group-hover:opacity-100 hover:text-primary transition-opacity"
|
|
||||||
>
|
|
||||||
<ArrowRight className="h-3.5 w-3.5" />
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Widget>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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 type { AppRegistryEntry } from '@/components/Workspace';
|
||||||
import { Clock } from './Clock/index';
|
import { Clock } from './Clock/index';
|
||||||
import { Workspaces } from './Workspaces/index';
|
|
||||||
|
|
||||||
export const widgetRegistry: Record<string, AppRegistryEntry> = {
|
export const widgetRegistry: Record<string, AppRegistryEntry> = {
|
||||||
'clock': { name: 'Clock', icon: ClockIcon, component: () => <Clock /> },
|
'clock': { name: 'Clock', icon: ClockIcon, component: () => <Clock /> },
|
||||||
'workspaces': { name: 'Workspaces', icon: LayoutGrid, component: () => <Workspaces /> },
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user