mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: add global immersive/fullscreen mode support across pages
- Add isImmersiveMode state to appStore for global fullscreen management - Update AppShell to hide Header and Sidebar when immersive mode is active - Add fullscreen toggle button to workflow and knowledge pages: - TerminalDashboardPage, IssueHubPage, SessionsPage, LiteTasksPage - HistoryPage, TeamPage, MemoryPage, SkillsManagerPage - CommandsManagerPage, RulesManagerPage, CliViewerPage - OrchestratorPage (via FlowToolbar) - Preserve padding in fullscreen mode for better visual appearance
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
// AppShell Component
|
// AppShell Component
|
||||||
// ========================================
|
// ========================================
|
||||||
// Root layout component combining Header, Sidebar, and MainContent
|
// Root layout component combining Header, Sidebar, and MainContent
|
||||||
|
// Supports immersive mode to hide chrome for fullscreen experiences
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@@ -16,6 +17,7 @@ import { AskQuestionDialog, A2UIPopupCard } from '@/components/a2ui';
|
|||||||
import { BackgroundImage } from '@/components/shared/BackgroundImage';
|
import { BackgroundImage } from '@/components/shared/BackgroundImage';
|
||||||
import { useNotificationStore, selectCurrentQuestion, selectCurrentPopupCard } from '@/stores';
|
import { useNotificationStore, selectCurrentQuestion, selectCurrentPopupCard } from '@/stores';
|
||||||
import { useWorkflowStore } from '@/stores/workflowStore';
|
import { useWorkflowStore } from '@/stores/workflowStore';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
import { useWebSocketNotifications, useWebSocket } from '@/hooks';
|
import { useWebSocketNotifications, useWebSocket } from '@/hooks';
|
||||||
|
|
||||||
export interface AppShellProps {
|
export interface AppShellProps {
|
||||||
@@ -40,6 +42,9 @@ export function AppShell({
|
|||||||
const projectPath = useWorkflowStore((state) => state.projectPath);
|
const projectPath = useWorkflowStore((state) => state.projectPath);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Immersive mode (fullscreen) - hide chrome
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
|
||||||
// Workspace initialization logic (URL > localStorage)
|
// Workspace initialization logic (URL > localStorage)
|
||||||
const [isWorkspaceInitialized, setWorkspaceInitialized] = useState(false);
|
const [isWorkspaceInitialized, setWorkspaceInitialized] = useState(false);
|
||||||
|
|
||||||
@@ -157,32 +162,36 @@ export function AppShell({
|
|||||||
}, [setCurrentPopupCard]);
|
}, [setCurrentPopupCard]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-background">
|
<div className={cn("flex flex-col min-h-screen bg-background", isImmersiveMode && "h-screen overflow-hidden")}>
|
||||||
{/* Background image layer (z-index: -3 to -2) */}
|
{/* Background image layer (z-index: -3 to -2) */}
|
||||||
<BackgroundImage />
|
<BackgroundImage />
|
||||||
|
|
||||||
{/* Header - fixed at top */}
|
{/* Header - fixed at top (hidden in immersive mode) */}
|
||||||
<Header
|
{!isImmersiveMode && (
|
||||||
onRefresh={onRefresh}
|
<Header
|
||||||
isRefreshing={isRefreshing}
|
onRefresh={onRefresh}
|
||||||
onCliMonitorClick={handleCliMonitorClick}
|
isRefreshing={isRefreshing}
|
||||||
/>
|
onCliMonitorClick={handleCliMonitorClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main layout - sidebar + content */}
|
{/* Main layout - sidebar + content */}
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className={cn("flex flex-1 overflow-hidden", isImmersiveMode && "h-full")}>
|
||||||
{/* Sidebar - collapsed by default */}
|
{/* Sidebar - collapsed by default (hidden in immersive mode) */}
|
||||||
<Sidebar
|
{!isImmersiveMode && (
|
||||||
collapsed={sidebarCollapsed}
|
<Sidebar
|
||||||
onCollapsedChange={handleCollapsedChange}
|
collapsed={sidebarCollapsed}
|
||||||
mobileOpen={mobileOpen}
|
onCollapsedChange={handleCollapsedChange}
|
||||||
onMobileClose={handleMobileClose}
|
mobileOpen={mobileOpen}
|
||||||
/>
|
onMobileClose={handleMobileClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<MainContent
|
<MainContent
|
||||||
className={cn(
|
className={cn(
|
||||||
'app-shell-content transition-all duration-300',
|
'app-shell-content transition-all duration-300',
|
||||||
sidebarCollapsed ? 'md:ml-16' : 'md:ml-64'
|
isImmersiveMode ? 'ml-0' : sidebarCollapsed ? 'md:ml-16' : 'md:ml-64'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ export function CliViewerPage() {
|
|||||||
const CurrentLayoutIcon = currentLayoutOption.icon;
|
const CurrentLayoutIcon = currentLayoutOption.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col -m-4 md:-m-6">
|
<div className="h-full flex flex-col">
|
||||||
{/* ======================================== */}
|
{/* ======================================== */}
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
{/* ======================================== */}
|
{/* ======================================== */}
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ import {
|
|||||||
Folder,
|
Folder,
|
||||||
User,
|
User,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
@@ -40,6 +43,10 @@ export function CommandsManagerPage() {
|
|||||||
// Search state
|
// Search state
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// Immersive mode state
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
commands,
|
commands,
|
||||||
groupedCommands,
|
groupedCommands,
|
||||||
@@ -96,7 +103,7 @@ export function CommandsManagerPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
@@ -109,10 +116,24 @@ export function CommandsManagerPage() {
|
|||||||
{formatMessage({ id: 'commands.description' })}
|
{formatMessage({ id: 'commands.description' })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
|
<div className="flex items-center gap-2">
|
||||||
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
|
<button
|
||||||
{formatMessage({ id: 'common.actions.refresh' })}
|
onClick={toggleImmersiveMode}
|
||||||
</Button>
|
className={cn(
|
||||||
|
'p-2 rounded-md transition-colors',
|
||||||
|
isImmersiveMode
|
||||||
|
? 'bg-primary/10 text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
|
||||||
|
>
|
||||||
|
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
|
||||||
|
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
|
||||||
|
{formatMessage({ id: 'common.actions.refresh' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error alert */}
|
{/* Error alert */}
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Search,
|
Search,
|
||||||
X,
|
X,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useHistory } from '@/hooks/useHistory';
|
import { useHistory } from '@/hooks/useHistory';
|
||||||
import { ConversationCard } from '@/components/shared/ConversationCard';
|
import { ConversationCard } from '@/components/shared/ConversationCard';
|
||||||
@@ -53,6 +56,8 @@ export function HistoryPage() {
|
|||||||
const [isPanelOpen, setIsPanelOpen] = React.useState(false);
|
const [isPanelOpen, setIsPanelOpen] = React.useState(false);
|
||||||
const [nativeExecutionId, setNativeExecutionId] = React.useState<string | null>(null);
|
const [nativeExecutionId, setNativeExecutionId] = React.useState<string | null>(null);
|
||||||
const [isNativePanelOpen, setIsNativePanelOpen] = React.useState(false);
|
const [isNativePanelOpen, setIsNativePanelOpen] = React.useState(false);
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
executions,
|
executions,
|
||||||
@@ -134,7 +139,7 @@ export function HistoryPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -146,6 +151,18 @@ export function HistoryPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={toggleImmersiveMode}
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-md transition-colors',
|
||||||
|
isImmersiveMode
|
||||||
|
? 'bg-primary/10 text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
|
||||||
|
>
|
||||||
|
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
Github,
|
Github,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { IssueHubHeader } from '@/components/issue/hub/IssueHubHeader';
|
import { IssueHubHeader } from '@/components/issue/hub/IssueHubHeader';
|
||||||
import { IssueHubTabs, type IssueTab } from '@/components/issue/hub/IssueHubTabs';
|
import { IssueHubTabs, type IssueTab } from '@/components/issue/hub/IssueHubTabs';
|
||||||
@@ -29,6 +31,7 @@ import { useIssues, useIssueMutations, useIssueQueue } from '@/hooks';
|
|||||||
import { pullIssuesFromGitHub, uploadAttachments } from '@/lib/api';
|
import { pullIssuesFromGitHub, uploadAttachments } from '@/lib/api';
|
||||||
import type { Issue } from '@/lib/api';
|
import type { Issue } from '@/lib/api';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
|
|
||||||
// Issue types
|
// Issue types
|
||||||
type IssueType = 'bug' | 'feature' | 'improvement' | 'other';
|
type IssueType = 'bug' | 'feature' | 'improvement' | 'other';
|
||||||
@@ -286,6 +289,10 @@ export function IssueHubPage() {
|
|||||||
const [isNewIssueOpen, setIsNewIssueOpen] = useState(false);
|
const [isNewIssueOpen, setIsNewIssueOpen] = useState(false);
|
||||||
const [isGithubSyncing, setIsGithubSyncing] = useState(false);
|
const [isGithubSyncing, setIsGithubSyncing] = useState(false);
|
||||||
|
|
||||||
|
// Immersive mode (fullscreen) - hide app chrome
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||||
|
|
||||||
// Issues data
|
// Issues data
|
||||||
const { refetch: refetchIssues, isFetching: isFetchingIssues } = useIssues();
|
const { refetch: refetchIssues, isFetching: isFetchingIssues } = useIssues();
|
||||||
// Queue data
|
// Queue data
|
||||||
@@ -392,17 +399,36 @@ export function IssueHubPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
|
||||||
{/* Header and action buttons on same row */}
|
{/* Header and action buttons on same row */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<IssueHubHeader currentTab={currentTab} />
|
<IssueHubHeader currentTab={currentTab} />
|
||||||
|
|
||||||
{/* Action buttons - dynamic based on current tab */}
|
<div className="flex items-center gap-2">
|
||||||
{renderActionButtons() && (
|
{/* Action buttons - dynamic based on current tab */}
|
||||||
<div className="flex gap-2">
|
{renderActionButtons()}
|
||||||
{renderActionButtons()}
|
|
||||||
</div>
|
{/* Fullscreen toggle */}
|
||||||
)}
|
<button
|
||||||
|
onClick={toggleImmersiveMode}
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-md transition-colors',
|
||||||
|
isImmersiveMode
|
||||||
|
? 'bg-primary/10 text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
title={isImmersiveMode
|
||||||
|
? formatMessage({ id: 'issueHub.exitFullscreen', defaultMessage: 'Exit Fullscreen' })
|
||||||
|
: formatMessage({ id: 'issueHub.fullscreen', defaultMessage: 'Fullscreen' })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isImmersiveMode ? (
|
||||||
|
<Minimize2 className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Maximize2 className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IssueHubTabs currentTab={currentTab} onTabChange={setCurrentTab} />
|
<IssueHubTabs currentTab={currentTab} onTabChange={setCurrentTab} />
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ import {
|
|||||||
Link2,
|
Link2,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Settings2,
|
Settings2,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
import { useLiteTasks } from '@/hooks/useLiteTasks';
|
import { useLiteTasks } from '@/hooks/useLiteTasks';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
@@ -55,6 +58,7 @@ import { TaskDrawer } from '@/components/shared/TaskDrawer';
|
|||||||
import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext, type RoundSynthesis, type MultiCliContextPackage } from '@/lib/api';
|
import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext, type RoundSynthesis, type MultiCliContextPackage } from '@/lib/api';
|
||||||
import { LiteContextContent } from '@/components/lite-tasks/LiteContextContent';
|
import { LiteContextContent } from '@/components/lite-tasks/LiteContextContent';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
type LiteTaskTab = 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
|
type LiteTaskTab = 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
|
||||||
type SortField = 'date' | 'name' | 'tasks';
|
type SortField = 'date' | 'name' | 'tasks';
|
||||||
@@ -1209,6 +1213,8 @@ export function LiteTasksPage() {
|
|||||||
const [searchQuery, setSearchQuery] = React.useState('');
|
const [searchQuery, setSearchQuery] = React.useState('');
|
||||||
const [sortField, setSortField] = React.useState<SortField>('date');
|
const [sortField, setSortField] = React.useState<SortField>('date');
|
||||||
const [sortOrder, setSortOrder] = React.useState<SortOrder>('desc');
|
const [sortOrder, setSortOrder] = React.useState<SortOrder>('desc');
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||||
|
|
||||||
// Filter and sort sessions
|
// Filter and sort sessions
|
||||||
const filterAndSort = React.useCallback((sessions: LiteTaskSession[]) => {
|
const filterAndSort = React.useCallback((sessions: LiteTaskSession[]) => {
|
||||||
@@ -1525,7 +1531,7 @@ export function LiteTasksPage() {
|
|||||||
const totalSessions = litePlan.length + liteFix.length + multiCliPlan.length;
|
const totalSessions = litePlan.length + liteFix.length + multiCliPlan.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -1542,6 +1548,18 @@ export function LiteTasksPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={toggleImmersiveMode}
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-md transition-colors',
|
||||||
|
isImmersiveMode
|
||||||
|
? 'bg-primary/10 text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
|
||||||
|
>
|
||||||
|
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ import {
|
|||||||
Terminal,
|
Terminal,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Hash,
|
Hash,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
@@ -624,6 +627,8 @@ export function MemoryPage() {
|
|||||||
const [currentTab, setCurrentTab] = useState<'memories' | 'favorites' | 'archived' | 'unifiedSearch'>('memories');
|
const [currentTab, setCurrentTab] = useState<'memories' | 'favorites' | 'archived' | 'unifiedSearch'>('memories');
|
||||||
const [unifiedQuery, setUnifiedQuery] = useState('');
|
const [unifiedQuery, setUnifiedQuery] = useState('');
|
||||||
const [selectedCategory, setSelectedCategory] = useState('');
|
const [selectedCategory, setSelectedCategory] = useState('');
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||||
|
|
||||||
const isUnifiedTab = currentTab === 'unifiedSearch';
|
const isUnifiedTab = currentTab === 'unifiedSearch';
|
||||||
|
|
||||||
@@ -784,7 +789,7 @@ export function MemoryPage() {
|
|||||||
const activeError = isUnifiedTab ? unifiedError : error;
|
const activeError = isUnifiedTab ? unifiedError : error;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -797,6 +802,18 @@ export function MemoryPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={toggleImmersiveMode}
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-md transition-colors',
|
||||||
|
isImmersiveMode
|
||||||
|
? 'bg-primary/10 text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
|
||||||
|
>
|
||||||
|
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
{isUnifiedTab && (
|
{isUnifiedTab && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ import {
|
|||||||
Folder,
|
Folder,
|
||||||
User,
|
User,
|
||||||
Globe,
|
Globe,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
import {
|
import {
|
||||||
useRules,
|
useRules,
|
||||||
useCreateRule,
|
useCreateRule,
|
||||||
@@ -64,6 +67,10 @@ export function RulesManagerPage() {
|
|||||||
const [searchQuery, setSearchQuery] = React.useState('');
|
const [searchQuery, setSearchQuery] = React.useState('');
|
||||||
const [categoryFilter, setCategoryFilter] = React.useState<string[]>([]);
|
const [categoryFilter, setCategoryFilter] = React.useState<string[]>([]);
|
||||||
|
|
||||||
|
// Immersive mode state
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||||
|
|
||||||
// Dialog state
|
// Dialog state
|
||||||
const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
|
const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
|
||||||
const [editDialogOpen, setEditDialogOpen] = React.useState(false);
|
const [editDialogOpen, setEditDialogOpen] = React.useState(false);
|
||||||
@@ -193,7 +200,7 @@ export function RulesManagerPage() {
|
|||||||
categoryFilter.length > 0 || searchQuery.length > 0;
|
categoryFilter.length > 0 || searchQuery.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -203,6 +210,18 @@ export function RulesManagerPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={toggleImmersiveMode}
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-md transition-colors',
|
||||||
|
isImmersiveMode
|
||||||
|
? 'bg-primary/10 text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
|
||||||
|
>
|
||||||
|
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
X,
|
X,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
useSessions,
|
useSessions,
|
||||||
@@ -43,6 +45,7 @@ import {
|
|||||||
import { TabsNavigation } from '@/components/ui/TabsNavigation';
|
import { TabsNavigation } from '@/components/ui/TabsNavigation';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { SessionMetadata } from '@/types/store';
|
import type { SessionMetadata } from '@/types/store';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
|
|
||||||
type LocationFilter = 'all' | 'active' | 'archived';
|
type LocationFilter = 'all' | 'active' | 'archived';
|
||||||
|
|
||||||
@@ -71,6 +74,10 @@ export function SessionsPage() {
|
|||||||
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
|
||||||
const [sessionToDelete, setSessionToDelete] = React.useState<string | null>(null);
|
const [sessionToDelete, setSessionToDelete] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
// Immersive mode (fullscreen)
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||||
|
|
||||||
// Build filter object
|
// Build filter object
|
||||||
const filter: SessionsFilter = React.useMemo(
|
const filter: SessionsFilter = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -149,7 +156,7 @@ export function SessionsPage() {
|
|||||||
const hasActiveFilters = statusFilter.length > 0 || searchQuery.length > 0;
|
const hasActiveFilters = statusFilter.length > 0 || searchQuery.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -168,6 +175,18 @@ export function SessionsPage() {
|
|||||||
<RefreshCw className={cn('h-4 w-4 mr-2', isFetching && 'animate-spin')} />
|
<RefreshCw className={cn('h-4 w-4 mr-2', isFetching && 'animate-spin')} />
|
||||||
{formatMessage({ id: 'common.actions.refresh' })}
|
{formatMessage({ id: 'common.actions.refresh' })}
|
||||||
</Button>
|
</Button>
|
||||||
|
<button
|
||||||
|
onClick={toggleImmersiveMode}
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-md transition-colors',
|
||||||
|
isImmersiveMode
|
||||||
|
? 'bg-primary/10 text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
title={isImmersiveMode ? formatMessage({ id: 'common.exitFullscreen', defaultMessage: 'Exit Fullscreen' }) : formatMessage({ id: 'common.fullscreen', defaultMessage: 'Fullscreen' })}
|
||||||
|
>
|
||||||
|
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import {
|
|||||||
Folder,
|
Folder,
|
||||||
User,
|
User,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
@@ -128,6 +131,10 @@ export function SkillsManagerPage() {
|
|||||||
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
||||||
const [isDetailPanelOpen, setIsDetailPanelOpen] = useState(false);
|
const [isDetailPanelOpen, setIsDetailPanelOpen] = useState(false);
|
||||||
|
|
||||||
|
// Immersive mode state
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
skills,
|
skills,
|
||||||
categories,
|
categories,
|
||||||
@@ -233,7 +240,7 @@ export function SkillsManagerPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className={cn("space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
@@ -256,6 +263,18 @@ export function SkillsManagerPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={toggleImmersiveMode}
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-md transition-colors',
|
||||||
|
isImmersiveMode
|
||||||
|
? 'bg-primary/10 text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
|
||||||
|
>
|
||||||
|
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
|
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
|
||||||
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
|
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
|
||||||
{formatMessage({ id: 'common.actions.refresh' })}
|
{formatMessage({ id: 'common.actions.refresh' })}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
// Main page for team execution - list/detail dual view with tabbed detail
|
// Main page for team execution - list/detail dual view with tabbed detail
|
||||||
|
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { Package, MessageSquare } from 'lucide-react';
|
import { Package, MessageSquare, Maximize2, Minimize2 } from 'lucide-react';
|
||||||
import { Card, CardContent } from '@/components/ui/Card';
|
import { Card, CardContent } from '@/components/ui/Card';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
||||||
import { useTeamStore } from '@/stores/teamStore';
|
import { useTeamStore } from '@/stores/teamStore';
|
||||||
import type { TeamDetailTab } from '@/stores/teamStore';
|
import type { TeamDetailTab } from '@/stores/teamStore';
|
||||||
@@ -33,6 +35,8 @@ export function TeamPage() {
|
|||||||
setDetailTab,
|
setDetailTab,
|
||||||
backToList,
|
backToList,
|
||||||
} = useTeamStore();
|
} = useTeamStore();
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||||
|
|
||||||
// Data hooks (only active in detail mode)
|
// Data hooks (only active in detail mode)
|
||||||
const { messages, total: messageTotal } = useTeamMessages(
|
const { messages, total: messageTotal } = useTeamMessages(
|
||||||
@@ -67,16 +71,30 @@ export function TeamPage() {
|
|||||||
|
|
||||||
// Detail view
|
// Detail view
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className={cn("p-6 space-y-6", isImmersiveMode && "h-screen overflow-hidden")}>
|
||||||
{/* Detail Header: back button + team name + stats + controls */}
|
{/* Detail Header: back button + team name + stats + controls */}
|
||||||
<TeamHeader
|
<div className="flex items-center justify-between">
|
||||||
selectedTeam={selectedTeam}
|
<TeamHeader
|
||||||
onBack={backToList}
|
selectedTeam={selectedTeam}
|
||||||
members={members}
|
onBack={backToList}
|
||||||
totalMessages={totalMessages}
|
members={members}
|
||||||
autoRefresh={autoRefresh}
|
totalMessages={totalMessages}
|
||||||
onToggleAutoRefresh={toggleAutoRefresh}
|
autoRefresh={autoRefresh}
|
||||||
/>
|
onToggleAutoRefresh={toggleAutoRefresh}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={toggleImmersiveMode}
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-md transition-colors',
|
||||||
|
isImmersiveMode
|
||||||
|
? 'bg-primary/10 text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
|
||||||
|
>
|
||||||
|
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Overview: Pipeline + Members (always visible) */}
|
{/* Overview: Pipeline + Members (always visible) */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// Right sidebar: FileSidebarPanel (file tree, resizable)
|
// Right sidebar: FileSidebarPanel (file tree, resizable)
|
||||||
// Top: DashboardToolbar with panel toggles and layout presets
|
// Top: DashboardToolbar with panel toggles and layout presets
|
||||||
// Floating panels: Issues, Queue, Inspector (overlay, mutually exclusive)
|
// Floating panels: Issues, Queue, Inspector (overlay, mutually exclusive)
|
||||||
// Fullscreen mode: Hides all sidebars for maximum terminal space
|
// Fullscreen mode: Uses global isImmersiveMode to hide app chrome (Header + Sidebar)
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -24,6 +24,7 @@ import { QueuePanel } from '@/components/terminal-dashboard/QueuePanel';
|
|||||||
import { InspectorContent } from '@/components/terminal-dashboard/BottomInspector';
|
import { InspectorContent } from '@/components/terminal-dashboard/BottomInspector';
|
||||||
import { FileSidebarPanel } from '@/components/terminal-dashboard/FileSidebarPanel';
|
import { FileSidebarPanel } from '@/components/terminal-dashboard/FileSidebarPanel';
|
||||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
|
|
||||||
// ========== Main Page Component ==========
|
// ========== Main Page Component ==========
|
||||||
|
|
||||||
@@ -32,10 +33,13 @@ export function TerminalDashboardPage() {
|
|||||||
const [activePanel, setActivePanel] = useState<PanelId | null>(null);
|
const [activePanel, setActivePanel] = useState<PanelId | null>(null);
|
||||||
const [isFileSidebarOpen, setIsFileSidebarOpen] = useState(true);
|
const [isFileSidebarOpen, setIsFileSidebarOpen] = useState(true);
|
||||||
const [isSessionSidebarOpen, setIsSessionSidebarOpen] = useState(true);
|
const [isSessionSidebarOpen, setIsSessionSidebarOpen] = useState(true);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
||||||
|
|
||||||
const projectPath = useWorkflowStore(selectProjectPath);
|
const projectPath = useWorkflowStore(selectProjectPath);
|
||||||
|
|
||||||
|
// Use global immersive mode state (only affects AppShell chrome)
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||||
|
|
||||||
const togglePanel = useCallback((panelId: PanelId) => {
|
const togglePanel = useCallback((panelId: PanelId) => {
|
||||||
setActivePanel((prev) => (prev === panelId ? null : panelId));
|
setActivePanel((prev) => (prev === panelId ? null : panelId));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -44,17 +48,8 @@ export function TerminalDashboardPage() {
|
|||||||
setActivePanel(null);
|
setActivePanel(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(() => {
|
|
||||||
setIsFullscreen((prev) => !prev);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// In fullscreen mode, hide all sidebars and panels
|
|
||||||
const showSessionSidebar = isSessionSidebarOpen && !isFullscreen;
|
|
||||||
const showFileSidebar = isFileSidebarOpen && !isFullscreen;
|
|
||||||
const showFloatingPanels = !isFullscreen;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col overflow-hidden ${isFullscreen ? 'h-screen -m-0' : 'h-[calc(100vh-56px)] -m-4 md:-m-6'}`}>
|
<div className={`flex flex-col overflow-hidden ${isImmersiveMode ? 'h-screen' : 'h-[calc(100vh-56px)]'}`}>
|
||||||
<AssociationHighlightProvider>
|
<AssociationHighlightProvider>
|
||||||
{/* Global toolbar */}
|
{/* Global toolbar */}
|
||||||
<DashboardToolbar
|
<DashboardToolbar
|
||||||
@@ -64,15 +59,15 @@ export function TerminalDashboardPage() {
|
|||||||
onToggleFileSidebar={() => setIsFileSidebarOpen((prev) => !prev)}
|
onToggleFileSidebar={() => setIsFileSidebarOpen((prev) => !prev)}
|
||||||
isSessionSidebarOpen={isSessionSidebarOpen}
|
isSessionSidebarOpen={isSessionSidebarOpen}
|
||||||
onToggleSessionSidebar={() => setIsSessionSidebarOpen((prev) => !prev)}
|
onToggleSessionSidebar={() => setIsSessionSidebarOpen((prev) => !prev)}
|
||||||
isFullscreen={isFullscreen}
|
isFullscreen={isImmersiveMode}
|
||||||
onToggleFullscreen={toggleFullscreen}
|
onToggleFullscreen={toggleImmersiveMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main content with three-column layout */}
|
{/* Main content with three-column layout */}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
<Allotment className="h-full">
|
<Allotment className="h-full">
|
||||||
{/* Session sidebar (conditional) */}
|
{/* Session sidebar (controlled by local state, not immersive mode) */}
|
||||||
{showSessionSidebar && (
|
{isSessionSidebarOpen && (
|
||||||
<Allotment.Pane preferredSize={240} minSize={180} maxSize={320}>
|
<Allotment.Pane preferredSize={240} minSize={180} maxSize={320}>
|
||||||
<div className="h-full flex flex-col border-r border-border">
|
<div className="h-full flex flex-col border-r border-border">
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||||
@@ -90,8 +85,8 @@ export function TerminalDashboardPage() {
|
|||||||
<TerminalGrid />
|
<TerminalGrid />
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
|
|
||||||
{/* File sidebar (conditional, default 280px) */}
|
{/* File sidebar (controlled by local state, not immersive mode) */}
|
||||||
{showFileSidebar && (
|
{isFileSidebarOpen && (
|
||||||
<Allotment.Pane preferredSize={280} minSize={200} maxSize={400}>
|
<Allotment.Pane preferredSize={280} minSize={200} maxSize={400}>
|
||||||
<FileSidebarPanel
|
<FileSidebarPanel
|
||||||
rootPath={projectPath ?? '/'}
|
rootPath={projectPath ?? '/'}
|
||||||
@@ -103,40 +98,36 @@ export function TerminalDashboardPage() {
|
|||||||
</Allotment>
|
</Allotment>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating panels (conditional, overlay) */}
|
{/* Floating panels (always available) */}
|
||||||
{showFloatingPanels && (
|
<FloatingPanel
|
||||||
<>
|
isOpen={activePanel === 'issues'}
|
||||||
<FloatingPanel
|
onClose={closePanel}
|
||||||
isOpen={activePanel === 'issues'}
|
title={formatMessage({ id: 'terminalDashboard.toolbar.issues' })}
|
||||||
onClose={closePanel}
|
side="left"
|
||||||
title={formatMessage({ id: 'terminalDashboard.toolbar.issues' })}
|
width={380}
|
||||||
side="left"
|
>
|
||||||
width={380}
|
<IssuePanel />
|
||||||
>
|
</FloatingPanel>
|
||||||
<IssuePanel />
|
|
||||||
</FloatingPanel>
|
|
||||||
|
|
||||||
<FloatingPanel
|
<FloatingPanel
|
||||||
isOpen={activePanel === 'queue'}
|
isOpen={activePanel === 'queue'}
|
||||||
onClose={closePanel}
|
onClose={closePanel}
|
||||||
title={formatMessage({ id: 'terminalDashboard.toolbar.queue' })}
|
title={formatMessage({ id: 'terminalDashboard.toolbar.queue' })}
|
||||||
side="right"
|
side="right"
|
||||||
width={400}
|
width={400}
|
||||||
>
|
>
|
||||||
<QueuePanel />
|
<QueuePanel />
|
||||||
</FloatingPanel>
|
</FloatingPanel>
|
||||||
|
|
||||||
<FloatingPanel
|
<FloatingPanel
|
||||||
isOpen={activePanel === 'inspector'}
|
isOpen={activePanel === 'inspector'}
|
||||||
onClose={closePanel}
|
onClose={closePanel}
|
||||||
title={formatMessage({ id: 'terminalDashboard.toolbar.inspector' })}
|
title={formatMessage({ id: 'terminalDashboard.toolbar.inspector' })}
|
||||||
side="right"
|
side="right"
|
||||||
width={360}
|
width={360}
|
||||||
>
|
>
|
||||||
<InspectorContent />
|
<InspectorContent />
|
||||||
</FloatingPanel>
|
</FloatingPanel>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AssociationHighlightProvider>
|
</AssociationHighlightProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
Library,
|
Library,
|
||||||
Play,
|
Play,
|
||||||
Activity,
|
Activity,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
@@ -24,6 +26,7 @@ import { Input } from '@/components/ui/Input';
|
|||||||
import { useFlowStore, toast } from '@/stores';
|
import { useFlowStore, toast } from '@/stores';
|
||||||
import { useExecutionStore } from '@/stores/executionStore';
|
import { useExecutionStore } from '@/stores/executionStore';
|
||||||
import { useExecuteFlow } from '@/hooks/useFlows';
|
import { useExecuteFlow } from '@/hooks/useFlows';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
import type { Flow } from '@/types/flow';
|
import type { Flow } from '@/types/flow';
|
||||||
|
|
||||||
interface FlowToolbarProps {
|
interface FlowToolbarProps {
|
||||||
@@ -37,6 +40,10 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
|
|||||||
const [flowName, setFlowName] = useState('');
|
const [flowName, setFlowName] = useState('');
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
// Immersive mode state
|
||||||
|
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
|
||||||
|
const toggleImmersiveMode = useAppStore((s) => s.toggleImmersiveMode);
|
||||||
|
|
||||||
// Flow store
|
// Flow store
|
||||||
const currentFlow = useFlowStore((state) => state.currentFlow);
|
const currentFlow = useFlowStore((state) => state.currentFlow);
|
||||||
const isModified = useFlowStore((state) => state.isModified);
|
const isModified = useFlowStore((state) => state.isModified);
|
||||||
@@ -363,6 +370,22 @@ export function FlowToolbar({ className, onOpenTemplateLibrary }: FlowToolbarPro
|
|||||||
)}
|
)}
|
||||||
{formatMessage({ id: 'orchestrator.toolbar.runWorkflow' })}
|
{formatMessage({ id: 'orchestrator.toolbar.runWorkflow' })}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<div className="w-px h-6 bg-border" />
|
||||||
|
|
||||||
|
{/* Fullscreen Toggle */}
|
||||||
|
<button
|
||||||
|
onClick={toggleImmersiveMode}
|
||||||
|
className={cn(
|
||||||
|
'p-2 rounded-md transition-colors',
|
||||||
|
isImmersiveMode
|
||||||
|
? 'bg-primary/10 text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||||
|
)}
|
||||||
|
title={isImmersiveMode ? 'Exit Fullscreen' : 'Fullscreen'}
|
||||||
|
>
|
||||||
|
{isImmersiveMode ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight, Maximize2, Minimize2 } from 'lucide-react';
|
||||||
import { useFlowStore } from '@/stores';
|
import { useFlowStore } from '@/stores';
|
||||||
|
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
|
||||||
import { useExecutionStore } from '@/stores/executionStore';
|
import { useExecutionStore } from '@/stores/executionStore';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { FlowCanvas } from './FlowCanvas';
|
import { FlowCanvas } from './FlowCanvas';
|
||||||
|
|||||||
@@ -236,6 +236,9 @@ const initialState = {
|
|||||||
themeSlots: [DEFAULT_SLOT] as ThemeSlot[],
|
themeSlots: [DEFAULT_SLOT] as ThemeSlot[],
|
||||||
activeSlotId: 'default' as ThemeSlotId,
|
activeSlotId: 'default' as ThemeSlotId,
|
||||||
deletedSlotBuffer: null as ThemeSlot | null,
|
deletedSlotBuffer: null as ThemeSlot | null,
|
||||||
|
|
||||||
|
// Immersive fullscreen mode (hides app shell chrome)
|
||||||
|
isImmersiveMode: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAppStore = create<AppStore>()(
|
export const useAppStore = create<AppStore>()(
|
||||||
@@ -670,6 +673,16 @@ export const useAppStore = create<AppStore>()(
|
|||||||
}
|
}
|
||||||
get().setBackgroundConfig(updated);
|
get().setBackgroundConfig(updated);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ========== Immersive Mode Actions ==========
|
||||||
|
|
||||||
|
setImmersiveMode: (enabled: boolean) => {
|
||||||
|
set({ isImmersiveMode: enabled }, false, 'setImmersiveMode');
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleImmersiveMode: () => {
|
||||||
|
set((state) => ({ isImmersiveMode: !state.isImmersiveMode }), false, 'toggleImmersiveMode');
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'ccw-app-store',
|
name: 'ccw-app-store',
|
||||||
@@ -807,3 +820,4 @@ export const selectError = (state: AppStore) => state.error;
|
|||||||
export const selectThemeSlots = (state: AppStore) => state.themeSlots;
|
export const selectThemeSlots = (state: AppStore) => state.themeSlots;
|
||||||
export const selectActiveSlotId = (state: AppStore) => state.activeSlotId;
|
export const selectActiveSlotId = (state: AppStore) => state.activeSlotId;
|
||||||
export const selectDeletedSlotBuffer = (state: AppStore) => state.deletedSlotBuffer;
|
export const selectDeletedSlotBuffer = (state: AppStore) => state.deletedSlotBuffer;
|
||||||
|
export const selectIsImmersiveMode = (state: AppStore) => state.isImmersiveMode;
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ export interface AppState {
|
|||||||
themeSlots: ThemeSlot[];
|
themeSlots: ThemeSlot[];
|
||||||
activeSlotId: ThemeSlotId;
|
activeSlotId: ThemeSlotId;
|
||||||
deletedSlotBuffer: ThemeSlot | null;
|
deletedSlotBuffer: ThemeSlot | null;
|
||||||
|
|
||||||
|
// Immersive mode (fullscreen)
|
||||||
|
isImmersiveMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppActions {
|
export interface AppActions {
|
||||||
@@ -170,6 +173,10 @@ export interface AppActions {
|
|||||||
updateBackgroundEffect: <K extends keyof BackgroundEffects>(key: K, value: BackgroundEffects[K]) => void;
|
updateBackgroundEffect: <K extends keyof BackgroundEffects>(key: K, value: BackgroundEffects[K]) => void;
|
||||||
setBackgroundMode: (mode: BackgroundMode) => void;
|
setBackgroundMode: (mode: BackgroundMode) => void;
|
||||||
setBackgroundImage: (url: string | null, attribution: UnsplashAttribution | null) => void;
|
setBackgroundImage: (url: string | null, attribution: UnsplashAttribution | null) => void;
|
||||||
|
|
||||||
|
// Immersive mode actions
|
||||||
|
setImmersiveMode: (enabled: boolean) => void;
|
||||||
|
toggleImmersiveMode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppStore = AppState & AppActions;
|
export type AppStore = AppState & AppActions;
|
||||||
|
|||||||
Reference in New Issue
Block a user