From 669deb3076b88218400dbdba19d82876eb270a4e Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 21 Jul 2026 20:17:50 -0700 Subject: [PATCH] [eric] workflows: traffic-light window controls + fullscreen (hides dock) --- .../Dashboard/canvas/DashboardCanvas.tsx | 4 ++- .../pages/Dashboard/cards/WindowControls.tsx | 7 ++-- .../pages/Workflows/app/WorkflowsAppCard.tsx | 29 ++++++++++++--- .../Workflows/app/WorkflowsAppContent.tsx | 35 +++++++++++-------- .../src/shared/state/dashboardLayoutSlice.ts | 10 ++++++ 5 files changed, 62 insertions(+), 23 deletions(-) diff --git a/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx b/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx index 3c11a1eb..7eb4b036 100644 --- a/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx +++ b/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx @@ -172,6 +172,8 @@ const DashboardCanvas: React.FC = ({ // macOS full screen: one card owns the whole window, every piece of chrome steps aside; Esc exits. const dispatch = useAppDispatch(); const fullscreenCardId = useAppSelector(selectFullscreenCardId); + // The Workflows window has its own fullscreen flag (not a tiledCard); its fill also hides the dock. + const anyFullscreen = !!fullscreenCardId || !!workflowsHub?.fullscreen; const [headerRevealed, setHeaderRevealed] = React.useState(false); const [appsWindowOpen, setAppsWindowOpen] = React.useState(false); useEffect(() => { @@ -263,7 +265,7 @@ const DashboardCanvas: React.FC = ({ /> )} - {!fullscreenCardId && ( + {!anyFullscreen && ( = { '.osw-pill-host:hover & .osw-window-lights > :nth-of-type(3)': { transform: 'translate(calc(-50% + 11px), calc(-50% + 5px)) scale(1)', opacity: 1, transitionDelay: '80ms' }, }; -function WindowControls({ onClose, onMinimize, onTile, tiled, fullscreen }: WindowControlsProps): React.ReactElement { +function WindowControls({ onClose, onMinimize, onTile, tiled, fullscreen, noTileMenu }: WindowControlsProps): React.ReactElement { const [menuOpen, setMenuOpen] = useState(false); const closeTimer = useRef(null); - const openMenu = (): void => { if (closeTimer.current) window.clearTimeout(closeTimer.current); setMenuOpen(true); }; + const openMenu = (): void => { if (noTileMenu) return; if (closeTimer.current) window.clearTimeout(closeTimer.current); setMenuOpen(true); }; const scheduleClose = (): void => { closeTimer.current = window.setTimeout(() => setMenuOpen(false), 180); }; const stop = (e: React.PointerEvent | React.MouseEvent): void => { e.stopPropagation(); }; diff --git a/frontend/src/app/pages/Workflows/app/WorkflowsAppCard.tsx b/frontend/src/app/pages/Workflows/app/WorkflowsAppCard.tsx index 58a58adc..9570afde 100644 --- a/frontend/src/app/pages/Workflows/app/WorkflowsAppCard.tsx +++ b/frontend/src/app/pages/Workflows/app/WorkflowsAppCard.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useAppDispatch } from '@/shared/hooks'; +import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { setWorkflowsHubPosition, setWorkflowsHubSize } from '@/shared/state/dashboardLayoutSlice'; +import { useTiledStyle } from '@/app/pages/Dashboard/cards/tileZones'; import { useWC } from './uiKit'; import WorkflowsAppContent from './WorkflowsAppContent'; @@ -53,6 +54,18 @@ const WorkflowsAppCard: React.FC = ({ }) => { const WC = useWC(); const dispatch = useAppDispatch(); + const isFullscreen = useAppSelector((s) => !!s.dashboardLayout.workflowsHub?.fullscreen); + // Fullscreen pins the card to the viewport, so its geometry must track pan/zoom like the tiled + // agent/browser cards; reuse the exact same helper. Subscribe to pan only while fullscreen. + const [, forceTick] = useState(0); + useEffect(() => { + if (!isFullscreen) return undefined; + const onPan = (): void => forceTick((t) => t + 1); + window.addEventListener('openswarm:canvas-pan-changed', onPan); + return () => window.removeEventListener('openswarm:canvas-pan-changed', onPan); + }, [isFullscreen]); + const cam = getCanvasState(); + const fsStyle = useTiledStyle(isFullscreen ? 'fullscreen' : undefined, cam.panX, cam.panY, cam.zoom); // ---- Drag (title bar is the handle) ---- @@ -67,6 +80,7 @@ const WorkflowsAppCard: React.FC = ({ const onHeaderPointerDown = useCallback((e: React.PointerEvent) => { if (e.button !== 0) return; + if (isFullscreen) return; // pinned to the viewport, no drag until restored const target = e.target as HTMLElement; if (target.closest('[data-no-drag], button, [role="button"], input, textarea, select')) return; e.preventDefault(); @@ -197,15 +211,20 @@ const WorkflowsAppCard: React.FC = ({ position: 'absolute', contain: 'layout style', willChange: 'transform', - left: dx, top: dy, width: dw, height: dh, + left: fsStyle ? fsStyle.left : dx, + top: fsStyle ? fsStyle.top : dy, + width: fsStyle ? fsStyle.width : dw, + height: fsStyle ? fsStyle.height : dh, + transform: fsStyle ? fsStyle.transform : undefined, + transformOrigin: fsStyle ? fsStyle.transformOrigin : undefined, background: WC.page, - border, + border: fsStyle ? 'none' : border, borderRadius: WC.radius.lg, boxShadow: (isDragging || isResizing) ? WC.shadow.lg : WC.shadow.md, overflow: 'hidden', display: 'flex', flexDirection: 'column', - zIndex: (isDragging || isResizing) ? 999999 : cardZOrder, + zIndex: fsStyle ? 999990 : (isDragging || isResizing) ? 999999 : cardZOrder, transition: noTransition ? 'none' : 'box-shadow 0.3s ease, border-color 0.2s ease', }} > @@ -218,7 +237,7 @@ const WorkflowsAppCard: React.FC = ({ }} /> - {HANDLE_DEFS.map(({ dir, css }) => ( + {!isFullscreen && HANDLE_DEFS.map(({ dir, css }) => (
= ({ header }) => { const WC = useWC(); - const c = useClaudeTokens(); const dispatch = useAppDispatch(); const target = useAppSelector((s) => s.dashboardLayout.workflowsAppTarget); + const isFullscreen = useAppSelector((s) => !!s.dashboardLayout.workflowsHub?.fullscreen); const dashboardId = useAppSelector((s) => s.tempState.lastDashboardId) || undefined; const [mode, setMode] = useState('home'); @@ -74,6 +72,23 @@ const WorkflowsAppContent: React.FC<{ header: CardHeader }> = ({ header }) => { onPointerUp={header.onPointerUp} style={{ height: 42, flex: 'none', display: 'flex', alignItems: 'center', padding: '0 16px', borderBottom: `1px solid ${WC.line}`, background: WC.panel, gap: 14, cursor: header.dragging ? 'grabbing' : 'grab', touchAction: 'none', userSelect: 'none' }} > + {/* macOS traffic lights: the whole window gets close / minimize / full size view like every card. */} + e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + style={{ display: 'flex', alignItems: 'center' }} + > + dispatch(closeWorkflowsApp())} + onMinimize={() => dispatch(closeWorkflowsApp())} + onTile={() => dispatch(toggleWorkflowsHubFullscreen())} + tiled={isFullscreen} + fullscreen={isFullscreen} + noTileMenu + /> +
Workflows @@ -93,16 +108,6 @@ const WorkflowsAppContent: React.FC<{ header: CardHeader }> = ({ header }) => { /> )} - { e.stopPropagation(); dispatch(closeWorkflowsApp()); }} - onPointerDown={(e) => e.stopPropagation()} - sx={{ color: c.text.tertiary, '&:hover': { color: c.status.error, bgcolor: `${c.status.error}14` } }} - > - -
diff --git a/frontend/src/shared/state/dashboardLayoutSlice.ts b/frontend/src/shared/state/dashboardLayoutSlice.ts index d1281c2b..34ec48bb 100644 --- a/frontend/src/shared/state/dashboardLayoutSlice.ts +++ b/frontend/src/shared/state/dashboardLayoutSlice.ts @@ -100,6 +100,8 @@ export interface WorkflowsHubPosition { width: number; height: number; zOrder: number; + // Full size view: the card fills the whole dashboard (reuses the fullscreen tile geometry). + fullscreen?: boolean; } @@ -1125,6 +1127,13 @@ const dashboardLayoutSlice = createSlice({ state.workflowsMonitorCard = null; }, + toggleWorkflowsHubFullscreen(state) { + if (state.workflowsHub) { + state.workflowsHub.fullscreen = !state.workflowsHub.fullscreen; + state.workflowsHub.zOrder = state.nextZOrder++; + } + }, + clearWorkflowsAppTarget(state) { state.workflowsAppTarget = null; }, @@ -1823,6 +1832,7 @@ export const { closeWorkflowsHub, openWorkflowsApp, closeWorkflowsApp, + toggleWorkflowsHubFullscreen, clearWorkflowsAppTarget, openWorkflowMonitor, closeWorkflowMonitor,