agent-ecosystem/src/renderer/components/dashboard/DashboardView.tsx

152 lines
5.4 KiB
TypeScript

/**
* DashboardView - Main dashboard shell.
* Keeps only screen composition and delegates recent-projects logic to the feature slice.
*/
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { RecentProjectsSection } from '@features/recent-projects/renderer';
import { useStore } from '@renderer/store';
import { formatShortcut } from '@renderer/utils/stringUtils';
import { Command, Search, Users } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { CliStatusBanner } from './CliStatusBanner';
import { DashboardUpdateBanner } from './DashboardUpdateBanner';
import { TmuxStatusBanner } from './TmuxStatusBanner';
import { WebPreviewBanner } from './WebPreviewBanner';
interface CommandSearchProps {
value: string;
onChange: (value: string) => void;
}
const CommandSearch = ({ value, onChange }: Readonly<CommandSearchProps>): React.JSX.Element => {
const [isFocused, setIsFocused] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const { openCommandPalette, selectedProjectId } = useStore(
useShallow((state) => ({
openCommandPalette: state.openCommandPalette,
selectedProjectId: state.selectedProjectId,
}))
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent): void => {
if ((event.metaKey || event.ctrlKey) && event.code === 'KeyK') {
event.preventDefault();
openCommandPalette();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [openCommandPalette]);
useLayoutEffect(() => {
const input = inputRef.current;
if (!input) {
return;
}
input.focus({ preventScroll: true });
const timeoutId = window.setTimeout(() => {
if (document.activeElement !== input) {
input.focus({ preventScroll: true });
}
}, 50);
return () => window.clearTimeout(timeoutId);
}, []);
return (
<div className="relative w-full">
<div
className={`relative flex items-center gap-3 rounded-sm border bg-surface-raised px-4 py-3 transition-all duration-200 ${
isFocused
? 'border-zinc-500 shadow-[0_0_20px_rgba(255,255,255,0.04)] ring-1 ring-zinc-600/30'
: 'border-border hover:border-zinc-600'
} `}
>
<Search className="size-4 shrink-0 text-text-muted" />
<input
ref={inputRef}
type="text"
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder="Search projects..."
className="flex-1 bg-transparent text-sm text-text outline-none placeholder:text-text-muted"
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
<button
onClick={() => openCommandPalette()}
className="flex shrink-0 items-center gap-1 transition-opacity hover:opacity-80"
title={
selectedProjectId
? `Search in sessions (${formatShortcut('K')})`
: `Search projects (${formatShortcut('K')})`
}
>
<kbd className="flex h-5 items-center justify-center rounded border border-border bg-surface-overlay px-1.5 text-[10px] font-medium text-text-muted">
<Command className="size-2.5" />
</kbd>
<kbd className="flex size-5 items-center justify-center rounded border border-border bg-surface-overlay text-[10px] font-medium text-text-muted">
K
</kbd>
</button>
</div>
</div>
);
};
export const DashboardView = (): React.JSX.Element => {
const [searchQuery, setSearchQuery] = useState('');
const openTeamsTab = useStore((state) => state.openTeamsTab);
return (
<div className="relative flex-1 overflow-auto bg-surface">
<div
className="pointer-events-none absolute inset-x-0 top-0 h-[600px] bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(99,102,241,0.08),transparent)]"
aria-hidden="true"
/>
<div className="relative mx-auto max-w-5xl px-8 py-12">
<WebPreviewBanner />
<DashboardUpdateBanner />
<CliStatusBanner />
<TmuxStatusBanner />
<div className="mb-12 flex items-center justify-center gap-3">
<button
onClick={openTeamsTab}
className="flex shrink-0 items-center gap-2 rounded-sm border border-border bg-surface-raised px-4 py-3 text-sm text-text-secondary transition-all duration-200 hover:border-zinc-500 hover:text-text"
>
<Users className="size-4" />
Select Team
</button>
<span className="shrink-0 text-xs text-text-muted">or</span>
<div className="flex-1">
<CommandSearch value={searchQuery} onChange={setSearchQuery} />
</div>
</div>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xs font-medium uppercase tracking-wider text-text-muted">
{searchQuery.trim() ? 'Search Results' : 'Recent Projects'}
</h2>
{searchQuery.trim() && (
<button
onClick={() => setSearchQuery('')}
className="text-xs text-text-muted transition-colors hover:text-text-secondary"
>
Clear search
</button>
)}
</div>
<RecentProjectsSection searchQuery={searchQuery} />
</div>
</div>
);
};