mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-30 19:59:38 +02:00
[eric] workflows: traffic-light window controls + fullscreen (hides dock)
This commit is contained in:
@@ -172,6 +172,8 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
// 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<DashboardCanvasProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{!fullscreenCardId && (
|
||||
{!anyFullscreen && (
|
||||
<DesktopDock
|
||||
sessions={sessions}
|
||||
cards={cards}
|
||||
|
||||
@@ -11,6 +11,9 @@ interface WindowControlsProps {
|
||||
// Full size view: the native macOS lights sit right above this corner, so a second dot cluster
|
||||
// reads as double chrome; collapse to ONE exit control and let the natives own window ops.
|
||||
fullscreen?: boolean;
|
||||
// Green = direct fullscreen toggle, no Fill/Halves/Quarters submenu (for surfaces that only
|
||||
// support fullscreen, like the Workflows window, where half-tiling has nowhere to land).
|
||||
noTileMenu?: boolean;
|
||||
}
|
||||
|
||||
// macOS-style traffic lights on every card = the "AI OS" window feel. Grey at rest so a canvas
|
||||
@@ -54,10 +57,10 @@ export const ARC_CHIP_SX: Record<string, unknown> = {
|
||||
'.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<number | null>(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(); };
|
||||
|
||||
|
||||
@@ -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<Props> = ({
|
||||
}) => {
|
||||
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<Props> = ({
|
||||
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
{HANDLE_DEFS.map(({ dir, css }) => (
|
||||
{!isFullscreen && HANDLE_DEFS.map(({ dir, css }) => (
|
||||
<div
|
||||
key={dir}
|
||||
data-no-drag
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { clearWorkflowsAppTarget, closeWorkflowsApp } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { clearWorkflowsAppTarget, closeWorkflowsApp, toggleWorkflowsHubFullscreen } from '@/shared/state/dashboardLayoutSlice';
|
||||
import WindowControls from '@/app/pages/Dashboard/cards/WindowControls';
|
||||
import {
|
||||
fetchWorkflows, fetchAllRuns, fetchPausedState, fetchActiveRuns, fetchDeletedWorkflows,
|
||||
} from '@/shared/state/workflowsSlice';
|
||||
import { fetchMissedRuns } from '@/shared/state/missedRunsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import ShareButton from '@/app/components/share/ShareButton';
|
||||
import { FONT_SANS, FONT_SERIF, useWC } from './uiKit';
|
||||
import type { AppMode, CalView, AppNav, CardHeader } from './types';
|
||||
@@ -22,9 +20,9 @@ import TrashView from './TrashView';
|
||||
// The three-pane Workflows body plus its title bar. The card wraps this with drag/resize geometry and passes the drag handlers in; the title bar lives here because Share needs to know which workflow is open.
|
||||
const WorkflowsAppContent: React.FC<{ header: CardHeader }> = ({ 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<AppMode>('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. */}
|
||||
<span
|
||||
className="osw-card"
|
||||
data-no-drag
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<WindowControls
|
||||
onClose={() => dispatch(closeWorkflowsApp())}
|
||||
onMinimize={() => dispatch(closeWorkflowsApp())}
|
||||
onTile={() => dispatch(toggleWorkflowsHubFullscreen())}
|
||||
tiled={isFullscreen}
|
||||
fullscreen={isFullscreen}
|
||||
noTileMenu
|
||||
/>
|
||||
</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<EventRepeatIcon sx={{ fontSize: 18, color: WC.accent, display: 'block' }} />
|
||||
<span style={{ fontFamily: FONT_SERIF, fontSize: 14.5, fontWeight: 500, color: WC.ink, letterSpacing: '-0.01em', lineHeight: 1, transform: 'translateY(2.5px)' }}>Workflows</span>
|
||||
@@ -93,16 +108,6 @@ const WorkflowsAppContent: React.FC<{ header: CardHeader }> = ({ header }) => {
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<IconButton
|
||||
aria-label="Close"
|
||||
data-no-drag
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); dispatch(closeWorkflowsApp()); }}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
sx={{ color: c.text.tertiary, '&:hover': { color: c.status.error, bgcolor: `${c.status.error}14` } }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user