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