mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] dashboard: minimize Settings and the Workflows hub to the rail instead of closing them
This commit is contained in:
@@ -13,7 +13,7 @@ import ChatInput from '@/app/pages/AgentChat/ChatInput';
|
||||
import type { ContextPath } from '@/app/components/editor/DirectoryBrowser';
|
||||
import SchedulePopover from '@/app/pages/Workflows/SchedulePopover';
|
||||
import { openWorkflowCard, fetchAllRuns, upsertRun } from '@/shared/state/workflowsSlice';
|
||||
import { addWorkflowCard, openWorkflowsApp, closeWorkflowsApp } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { addWorkflowCard, openWorkflowsApp, closeWorkflowsApp, WORKFLOWS_HUB_ID } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { useElementSelection } from '@/app/components/editor/ElementSelectionContext';
|
||||
import { useClaudeTokens, DarkTokensScope } from '@/shared/styles/ThemeContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
@@ -171,7 +171,8 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
const allRuns = useAppSelector((s) => s.workflows.allRuns);
|
||||
const allRunsLoading = useAppSelector((s) => s.workflows.allRunsLoading);
|
||||
const workflowItems = useAppSelector((s) => s.workflows.items);
|
||||
const workflowsHubOpen = useAppSelector((s) => Boolean(s.dashboardLayout.workflowsHub));
|
||||
// Parked counts as not-showing, so the pill restores the window instead of throwing its state away.
|
||||
const workflowsHubOpen = useAppSelector((s) => Boolean(s.dashboardLayout.workflowsHub) && !s.dashboardLayout.minimizedCards[WORKFLOWS_HUB_ID]);
|
||||
|
||||
const outputList = useMemo(() => Object.values(outputs), [outputs]);
|
||||
const filteredOutputs = useMemo(() => {
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { TILE_ZONES, useTiledStyle } from './tileZones';
|
||||
import { useCanvasWindowResize } from './useCanvasWindowResize';
|
||||
import { useDragEndBackstops } from '../hooks/interaction/useDragEndBackstops';
|
||||
import type { CardType } from '@/shared/state/dashboardLayoutSlice';
|
||||
|
||||
type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
||||
|
||||
const EDGE = 6;
|
||||
const CORNER = 14;
|
||||
const DRAG_THRESHOLD = 3;
|
||||
const SNAP_GRID = 24;
|
||||
const TILE_GAP = 8;
|
||||
|
||||
const CURSOR_MAP: Record<ResizeDir, string> = {
|
||||
n: 'ns-resize', s: 'ns-resize', e: 'ew-resize', w: 'ew-resize',
|
||||
nw: 'nwse-resize', se: 'nwse-resize', ne: 'nesw-resize', sw: 'nesw-resize',
|
||||
};
|
||||
const HANDLE_DEFS: { dir: ResizeDir; css: React.CSSProperties }[] = [
|
||||
{ dir: 'n', css: { top: -EDGE / 2, left: CORNER, right: CORNER, height: EDGE } },
|
||||
{ dir: 's', css: { bottom: -EDGE / 2, left: CORNER, right: CORNER, height: EDGE } },
|
||||
{ dir: 'w', css: { left: -EDGE / 2, top: CORNER, bottom: CORNER, width: EDGE } },
|
||||
{ dir: 'e', css: { right: -EDGE / 2, top: CORNER, bottom: CORNER, width: EDGE } },
|
||||
{ dir: 'nw', css: { top: -EDGE / 2, left: -EDGE / 2, width: CORNER, height: CORNER } },
|
||||
{ dir: 'ne', css: { top: -EDGE / 2, right: -EDGE / 2, width: CORNER, height: CORNER } },
|
||||
{ dir: 'sw', css: { bottom: -EDGE / 2, left: -EDGE / 2, width: CORNER, height: CORNER } },
|
||||
{ dir: 'se', css: { bottom: -EDGE / 2, right: -EDGE / 2, width: CORNER, height: CORNER } },
|
||||
];
|
||||
|
||||
/** Drag handlers the window hands down to whatever renders its title bar. */
|
||||
export interface CanvasWindowHeader {
|
||||
onPointerDown: (e: React.PointerEvent) => void;
|
||||
@@ -50,6 +32,8 @@ interface CanvasWindowCardProps {
|
||||
selectName: string;
|
||||
cardX: number; cardY: number; cardWidth: number; cardHeight: number; cardZOrder?: number;
|
||||
fullscreen?: boolean;
|
||||
/** Parked in the minimized rail: stays mounted (and keeps its state) off-canvas instead of unmounting. */
|
||||
minimized?: boolean;
|
||||
minWidth: number; minHeight: number;
|
||||
background: string; highlightColor: string;
|
||||
getCanvasState: () => { panX: number; panY: number; zoom: number };
|
||||
@@ -71,7 +55,7 @@ interface CanvasWindowCardProps {
|
||||
const CanvasWindowCard: React.FC<CanvasWindowCardProps> = ({
|
||||
cardId, cardType, selectType, selectName,
|
||||
cardX, cardY, cardWidth, cardHeight, cardZOrder = 0,
|
||||
fullscreen = false, minWidth, minHeight, background, highlightColor,
|
||||
fullscreen = false, minimized = false, minWidth, minHeight, background, highlightColor,
|
||||
getCanvasState,
|
||||
isSelected = false, isHighlighted = false, multiDragDelta = null,
|
||||
onCardSelect, onDragStart, onDragMove, onDragEnd, onBringToFront,
|
||||
@@ -166,53 +150,10 @@ const CanvasWindowCard: React.FC<CanvasWindowCardProps> = ({
|
||||
}, [finalizeDrag]);
|
||||
useDragEndBackstops(isDragging, finalizeDrag, abortDrag);
|
||||
|
||||
// ---- Resize ----
|
||||
const resizeRef = useRef<{ dir: ResizeDir; sx0: number; sy0: number; ox: number; oy: number; ow: number; oh: number } | null>(null);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [localResize, setLocalResize] = useState<{ x: number; y: number; w: number; h: number } | null>(null);
|
||||
|
||||
const onResizeDown = useCallback((dir: ResizeDir) => (e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resizeRef.current = { dir, sx0: e.clientX, sy0: e.clientY, ox: cardX, oy: cardY, ow: cardWidth, oh: cardHeight };
|
||||
setIsResizing(true);
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
}, [cardX, cardY, cardWidth, cardHeight]);
|
||||
|
||||
const compute = useCallback((e: React.PointerEvent) => {
|
||||
if (!resizeRef.current) return null;
|
||||
const { dir, sx0, sy0, ox, oy, ow, oh } = resizeRef.current;
|
||||
const z2 = getCanvasState().zoom;
|
||||
const dx = (e.clientX - sx0) / z2;
|
||||
const dy = (e.clientY - sy0) / z2;
|
||||
let nx = ox, ny = oy, nw = ow, nh = oh;
|
||||
if (dir.includes('e')) nw = ow + dx;
|
||||
if (dir.includes('w')) { nw = ow - dx; nx = ox + dx; }
|
||||
if (dir.includes('s')) nh = oh + dy;
|
||||
if (dir.includes('n')) { nh = oh - dy; ny = oy + dy; }
|
||||
if (nw < minWidth) { if (dir.includes('w')) nx = ox + ow - minWidth; nw = minWidth; }
|
||||
if (nh < minHeight) { if (dir.includes('n')) ny = oy + oh - minHeight; nh = minHeight; }
|
||||
return { x: nx, y: ny, w: nw, h: nh };
|
||||
}, [getCanvasState, minWidth, minHeight]);
|
||||
|
||||
const onResizeMove = useCallback((e: React.PointerEvent) => {
|
||||
const r = compute(e);
|
||||
if (r) setLocalResize(r);
|
||||
}, [compute]);
|
||||
|
||||
const onResizeUp = useCallback((e: React.PointerEvent) => {
|
||||
if (!resizeRef.current) return;
|
||||
const r = compute(e);
|
||||
if (r) {
|
||||
onCommitPosition(r.x, r.y);
|
||||
onCommitSize(r.w, r.h);
|
||||
}
|
||||
resizeRef.current = null;
|
||||
setLocalResize(null);
|
||||
setIsResizing(false);
|
||||
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
|
||||
}, [compute, onCommitPosition, onCommitSize]);
|
||||
const { isResizing, live: localResize, handles } = useCanvasWindowResize({
|
||||
cardX, cardY, cardWidth, cardHeight, minWidth, minHeight,
|
||||
getCanvasState, onCommitPosition, onCommitSize,
|
||||
});
|
||||
|
||||
const onTileZone = useCallback((zone: string) => {
|
||||
const z = TILE_ZONES[zone];
|
||||
@@ -249,15 +190,20 @@ const CanvasWindowCard: React.FC<CanvasWindowCardProps> = ({
|
||||
if (target.closest('[data-no-drag]')) return;
|
||||
onCardSelect?.(cardId, cardType, e.shiftKey);
|
||||
}}
|
||||
data-keepalive-hidden={minimized ? '1' : undefined}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
contain: 'layout style',
|
||||
willChange: 'transform',
|
||||
left: fsStyle ? fsStyle.left : dx,
|
||||
top: fsStyle ? fsStyle.top : dy,
|
||||
// Parked windows go off-canvas rather than unmounting, so Settings keeps its form and Workflows its view state.
|
||||
pointerEvents: minimized ? 'none' : undefined,
|
||||
// Belt and braces: leaving fullscreen tears down the tiled-style hook, whose cleanup strips the inline left/top React just wrote, and visibility is the one park signal it never touches.
|
||||
visibility: minimized ? 'hidden' : undefined,
|
||||
left: minimized ? -100000 : fsStyle ? fsStyle.left : dx,
|
||||
top: minimized ? -100000 : fsStyle ? fsStyle.top : dy,
|
||||
width: fsStyle ? fsStyle.width : dw,
|
||||
height: fsStyle ? fsStyle.height : dh,
|
||||
transform: fsStyle ? fsStyle.transform : undefined,
|
||||
transform: minimized ? undefined : fsStyle ? fsStyle.transform : undefined,
|
||||
transformOrigin: fsStyle ? fsStyle.transformOrigin : undefined,
|
||||
background,
|
||||
border: fsStyle ? 'none' : border,
|
||||
@@ -282,14 +228,14 @@ const CanvasWindowCard: React.FC<CanvasWindowCardProps> = ({
|
||||
onTileZone,
|
||||
})}
|
||||
|
||||
{!fullscreen && HANDLE_DEFS.map(({ dir, css }) => (
|
||||
{!fullscreen && !minimized && handles.map((h) => (
|
||||
<div
|
||||
key={dir}
|
||||
key={h.dir}
|
||||
data-no-drag
|
||||
onPointerDown={onResizeDown(dir)}
|
||||
onPointerMove={onResizeMove}
|
||||
onPointerUp={onResizeUp}
|
||||
style={{ position: 'absolute', cursor: CURSOR_MAP[dir], zIndex: 25, ...css }}
|
||||
onPointerDown={h.onPointerDown}
|
||||
onPointerMove={h.onPointerMove}
|
||||
onPointerUp={h.onPointerUp}
|
||||
style={h.style}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -124,9 +124,14 @@ export function useTiledStyle(
|
||||
: (el.parentElement as HTMLElement | null) ?? el;
|
||||
const posProps = ['left', 'top'];
|
||||
const sizeProps = ['width', 'height', 'transform', 'transform-origin', 'transition'];
|
||||
// Drop ONLY the overrides this hook still owns: React re-asserts these props without the important
|
||||
// flag when a card leaves its zone, and deleting those left the window with no geometry at all.
|
||||
const clearOwn = (target: HTMLElement, props: string[]): void => {
|
||||
props.forEach((pr) => { if (target.style.getPropertyPriority(pr) === 'important') target.style.removeProperty(pr); });
|
||||
};
|
||||
const clearAll = (): void => {
|
||||
posProps.forEach((pr) => posEl.style.removeProperty(pr));
|
||||
sizeProps.forEach((pr) => el.style.removeProperty(pr));
|
||||
clearOwn(posEl, posProps);
|
||||
clearOwn(el, sizeProps);
|
||||
};
|
||||
const apply = (): void => {
|
||||
// A parked card (kept alive off-screen / minimized) must keep its sx parking position.
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
|
||||
type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
||||
|
||||
const EDGE = 6;
|
||||
const CORNER = 14;
|
||||
|
||||
const CURSOR_MAP: Record<ResizeDir, string> = {
|
||||
n: 'ns-resize', s: 'ns-resize', e: 'ew-resize', w: 'ew-resize',
|
||||
nw: 'nwse-resize', se: 'nwse-resize', ne: 'nesw-resize', sw: 'nesw-resize',
|
||||
};
|
||||
const HANDLE_DEFS: { dir: ResizeDir; css: React.CSSProperties }[] = [
|
||||
{ dir: 'n', css: { top: -EDGE / 2, left: CORNER, right: CORNER, height: EDGE } },
|
||||
{ dir: 's', css: { bottom: -EDGE / 2, left: CORNER, right: CORNER, height: EDGE } },
|
||||
{ dir: 'w', css: { left: -EDGE / 2, top: CORNER, bottom: CORNER, width: EDGE } },
|
||||
{ dir: 'e', css: { right: -EDGE / 2, top: CORNER, bottom: CORNER, width: EDGE } },
|
||||
{ dir: 'nw', css: { top: -EDGE / 2, left: -EDGE / 2, width: CORNER, height: CORNER } },
|
||||
{ dir: 'ne', css: { top: -EDGE / 2, right: -EDGE / 2, width: CORNER, height: CORNER } },
|
||||
{ dir: 'sw', css: { bottom: -EDGE / 2, left: -EDGE / 2, width: CORNER, height: CORNER } },
|
||||
{ dir: 'se', css: { bottom: -EDGE / 2, right: -EDGE / 2, width: CORNER, height: CORNER } },
|
||||
];
|
||||
|
||||
export interface CanvasWindowResizeHandle {
|
||||
dir: string;
|
||||
style: React.CSSProperties;
|
||||
onPointerDown: (e: React.PointerEvent) => void;
|
||||
onPointerMove: (e: React.PointerEvent) => void;
|
||||
onPointerUp: (e: React.PointerEvent) => void;
|
||||
}
|
||||
|
||||
export interface CanvasWindowResizeState {
|
||||
isResizing: boolean;
|
||||
/** Live geometry while the pointer is down; null once committed to the slice. */
|
||||
live: { x: number; y: number; w: number; h: number } | null;
|
||||
handles: CanvasWindowResizeHandle[];
|
||||
}
|
||||
|
||||
interface CanvasWindowResizeArgs {
|
||||
cardX: number; cardY: number; cardWidth: number; cardHeight: number;
|
||||
minWidth: number; minHeight: number;
|
||||
getCanvasState: () => { panX: number; panY: number; zoom: number };
|
||||
onCommitPosition: (x: number, y: number) => void;
|
||||
onCommitSize: (width: number, height: number) => void;
|
||||
}
|
||||
|
||||
/** The 8 edge/corner grips of a canvas window: preview the new rect locally, commit it on release. */
|
||||
export function useCanvasWindowResize({
|
||||
cardX, cardY, cardWidth, cardHeight, minWidth, minHeight,
|
||||
getCanvasState, onCommitPosition, onCommitSize,
|
||||
}: CanvasWindowResizeArgs): CanvasWindowResizeState {
|
||||
const resizeRef = useRef<{ dir: ResizeDir; sx0: number; sy0: number; ox: number; oy: number; ow: number; oh: number } | null>(null);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [live, setLive] = useState<{ x: number; y: number; w: number; h: number } | null>(null);
|
||||
|
||||
const onResizeDown = useCallback((dir: ResizeDir) => (e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resizeRef.current = { dir, sx0: e.clientX, sy0: e.clientY, ox: cardX, oy: cardY, ow: cardWidth, oh: cardHeight };
|
||||
setIsResizing(true);
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
}, [cardX, cardY, cardWidth, cardHeight]);
|
||||
|
||||
const compute = useCallback((e: React.PointerEvent) => {
|
||||
if (!resizeRef.current) return null;
|
||||
const { dir, sx0, sy0, ox, oy, ow, oh } = resizeRef.current;
|
||||
const zoom = getCanvasState().zoom;
|
||||
const dx = (e.clientX - sx0) / zoom;
|
||||
const dy = (e.clientY - sy0) / zoom;
|
||||
let nx = ox, ny = oy, nw = ow, nh = oh;
|
||||
if (dir.includes('e')) nw = ow + dx;
|
||||
if (dir.includes('w')) { nw = ow - dx; nx = ox + dx; }
|
||||
if (dir.includes('s')) nh = oh + dy;
|
||||
if (dir.includes('n')) { nh = oh - dy; ny = oy + dy; }
|
||||
if (nw < minWidth) { if (dir.includes('w')) nx = ox + ow - minWidth; nw = minWidth; }
|
||||
if (nh < minHeight) { if (dir.includes('n')) ny = oy + oh - minHeight; nh = minHeight; }
|
||||
return { x: nx, y: ny, w: nw, h: nh };
|
||||
}, [getCanvasState, minWidth, minHeight]);
|
||||
|
||||
const onResizeMove = useCallback((e: React.PointerEvent) => {
|
||||
const r = compute(e);
|
||||
if (r) setLive(r);
|
||||
}, [compute]);
|
||||
|
||||
const onResizeUp = useCallback((e: React.PointerEvent) => {
|
||||
if (!resizeRef.current) return;
|
||||
const r = compute(e);
|
||||
if (r) {
|
||||
onCommitPosition(r.x, r.y);
|
||||
onCommitSize(r.w, r.h);
|
||||
}
|
||||
resizeRef.current = null;
|
||||
setLive(null);
|
||||
setIsResizing(false);
|
||||
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
|
||||
}, [compute, onCommitPosition, onCommitSize]);
|
||||
|
||||
const handles = HANDLE_DEFS.map(({ dir, css }) => ({
|
||||
dir,
|
||||
style: { position: 'absolute' as const, cursor: CURSOR_MAP[dir], zIndex: 25, ...css },
|
||||
onPointerDown: onResizeDown(dir),
|
||||
onPointerMove: onResizeMove,
|
||||
onPointerUp: onResizeUp,
|
||||
}));
|
||||
|
||||
return { isResizing, live, handles };
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { toggleMinimizeCard, setTiledCard, recordClosedCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import {
|
||||
toggleMinimizeCard, setTiledCard, recordClosedCard,
|
||||
closeSettingsCard, closeWorkflowsApp, toggleSettingsCardFullscreen, toggleWorkflowsHubFullscreen,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import { removeBrowserCardCleanly } from '@/shared/browserTeardown';
|
||||
import { removeViewCardCleanly } from '@/shared/viewTeardown';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
@@ -27,7 +30,9 @@ function MinimizedStack({ browserCards, viewCards, outputs, selectedIds, onResto
|
||||
const dispatch = useAppDispatch();
|
||||
const c = useClaudeTokens();
|
||||
const minimizedCards = useAppSelector((s) => s.dashboardLayout.minimizedCards);
|
||||
const entries = buildMinimizedEntries({ browserCards, viewCards, outputs, minimizedCards });
|
||||
const workflowsHub = useAppSelector((s) => s.dashboardLayout.workflowsHub);
|
||||
const settingsCard = useAppSelector((s) => s.dashboardLayout.settingsCard);
|
||||
const entries = buildMinimizedEntries({ browserCards, viewCards, outputs, workflowsHub, settingsCard, minimizedCards });
|
||||
if (entries.length === 0) return null;
|
||||
|
||||
const restore = (entry: MinimizedEntry): void => {
|
||||
@@ -40,11 +45,22 @@ function MinimizedStack({ browserCards, viewCards, outputs, selectedIds, onResto
|
||||
if (entry.kind === 'browser') {
|
||||
dispatch(recordClosedCard({ kind: 'browser', id: entry.id }));
|
||||
void removeBrowserCardCleanly(entry.id, dispatch);
|
||||
} else {
|
||||
} else if (entry.kind === 'view') {
|
||||
dispatch(recordClosedCard({ kind: 'view', id: entry.id }));
|
||||
void removeViewCardCleanly(entry.id, dispatch);
|
||||
} else if (entry.kind === 'workflows') {
|
||||
dispatch(closeWorkflowsApp());
|
||||
} else {
|
||||
dispatch(closeSettingsCard());
|
||||
}
|
||||
};
|
||||
// Singletons own a fullscreen flag instead of a tiledCards entry, so a setTiledCard here would strand a ghost fullscreen owner nothing renders.
|
||||
const tile = (entry: MinimizedEntry, zone: string): void => {
|
||||
restore(entry);
|
||||
if (entry.kind === 'workflows') { if (zone === 'fullscreen') dispatch(toggleWorkflowsHubFullscreen()); return; }
|
||||
if (entry.kind === 'settings') { if (zone === 'fullscreen') dispatch(toggleSettingsCardFullscreen()); return; }
|
||||
if (zone !== 'restore') dispatch(setTiledCard({ cardId: entry.id, zone }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -89,12 +105,14 @@ function MinimizedStack({ browserCards, viewCards, outputs, selectedIds, onResto
|
||||
selected={selectedIds.includes(entry.id)}
|
||||
onRestore={() => restore(entry)}
|
||||
onClose={() => close(entry)}
|
||||
onTile={(zone: string) => { restore(entry); if (zone !== 'restore') dispatch(setTiledCard({ cardId: entry.id, zone })); }}
|
||||
onTile={(zone: string) => tile(entry, zone)}
|
||||
onContextMenu={(e: React.MouseEvent) => openCardContextMenu(e, {
|
||||
items: [
|
||||
{ label: 'Restore', onClick: () => restore(entry) },
|
||||
{ label: 'Restore full screen', onClick: () => { restore(entry); dispatch(setTiledCard({ cardId: entry.id, zone: 'fullscreen' })); } },
|
||||
{ label: 'Tile to zone', submenu: tileMenuRows((zone) => { restore(entry); if (zone !== 'restore') dispatch(setTiledCard({ cardId: entry.id, zone })); }) },
|
||||
{ label: 'Restore full screen', onClick: () => tile(entry, 'fullscreen') },
|
||||
...(entry.kind === 'browser' || entry.kind === 'view'
|
||||
? [{ label: 'Tile to zone', submenu: tileMenuRows((zone) => tile(entry, zone)) }]
|
||||
: []),
|
||||
{ kind: 'separator' },
|
||||
{ label: 'Close', danger: true, onClick: () => close(entry) },
|
||||
],
|
||||
|
||||
@@ -3,6 +3,8 @@ import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded';
|
||||
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import { pickIcon } from '../canvas/DashboardGlyph';
|
||||
import WindowControls, { ARC_CHIP_SX } from '../cards/WindowControls';
|
||||
import { getMinimizedShot } from './minimizedShots';
|
||||
@@ -46,6 +48,9 @@ function MinimizedTile({ entry, accent, selected, onRestore, onClose, onTile, on
|
||||
);
|
||||
}
|
||||
if (entry.kind === 'browser') return <LanguageIcon sx={{ fontSize: size, color: 'rgba(255,255,255,0.55)', display: 'block' }} />;
|
||||
// The singleton windows never have a thumbnail, so their own app glyph is the whole identity of the tile.
|
||||
if (entry.kind === 'workflows') return <EventRepeatIcon sx={{ fontSize: size, color: 'rgba(255,255,255,0.55)', display: 'block' }} />;
|
||||
if (entry.kind === 'settings') return <SettingsIcon sx={{ fontSize: size, color: 'rgba(255,255,255,0.55)', display: 'block' }} />;
|
||||
// Never a letter here: a glyph initial reads as a bug, so an unmatched name falls back to a real symbol.
|
||||
const AppIcon = pickIcon(entry.label);
|
||||
if (AppIcon) return <AppIcon size={size} strokeWidth={1.75} color="rgba(255,255,255,0.6)" />;
|
||||
@@ -108,7 +113,8 @@ function MinimizedTile({ entry, accent, selected, onRestore, onClose, onTile, on
|
||||
opacity: 0, pointerEvents: 'none', transition: 'opacity 140ms ease',
|
||||
}}
|
||||
>
|
||||
<WindowControls onClose={onClose} onMinimize={onRestore} onTile={onTile} tiled={false} />
|
||||
{/* The singleton windows only know fullscreen, so their green light must not offer half/quarter zones it can't honor. */}
|
||||
<WindowControls onClose={onClose} onMinimize={onRestore} onTile={onTile} tiled={false} noTileMenu={entry.kind === 'workflows' || entry.kind === 'settings'} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BrowserCardPosition, ViewCardPosition } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { SETTINGS_CARD_ID, WORKFLOWS_HUB_ID } from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { BrowserCardPosition, ViewCardPosition, WorkflowsHubPosition } from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { Output } from '@/shared/state/outputsSlice';
|
||||
|
||||
export interface MinimizedRect {
|
||||
@@ -8,7 +9,7 @@ export interface MinimizedRect {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export type MinimizedKind = 'browser' | 'view';
|
||||
export type MinimizedKind = 'browser' | 'view' | 'workflows' | 'settings';
|
||||
|
||||
export interface MinimizedEntry {
|
||||
id: string;
|
||||
@@ -25,14 +26,18 @@ export interface MinimizedSlices {
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
viewCards: Record<string, ViewCardPosition>;
|
||||
outputs: Record<string, Output>;
|
||||
workflowsHub: WorkflowsHubPosition | null;
|
||||
settingsCard: WorkflowsHubPosition | null;
|
||||
minimizedCards: Record<string, boolean>;
|
||||
}
|
||||
|
||||
/** Every window parked in the minimized rail, browsers and apps in one list so both share a small state. */
|
||||
/** Every window parked in the minimized rail, browsers, apps and the singleton windows in one list so they all share a small state. */
|
||||
export function buildMinimizedEntries({
|
||||
browserCards,
|
||||
viewCards,
|
||||
outputs,
|
||||
workflowsHub,
|
||||
settingsCard,
|
||||
minimizedCards,
|
||||
}: MinimizedSlices): MinimizedEntry[] {
|
||||
const list: MinimizedEntry[] = [];
|
||||
@@ -59,5 +64,11 @@ export function buildMinimizedEntries({
|
||||
thumbnail: outputs[vc.output_id]?.thumbnail,
|
||||
});
|
||||
}
|
||||
if (workflowsHub && minimizedCards[WORKFLOWS_HUB_ID]) {
|
||||
list.push({ id: WORKFLOWS_HUB_ID, kind: 'workflows', label: 'Workflows', rect: workflowsHub });
|
||||
}
|
||||
if (settingsCard && minimizedCards[SETTINGS_CARD_ID]) {
|
||||
list.push({ id: SETTINGS_CARD_ID, kind: 'settings', label: 'Settings', rect: settingsCard });
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
closeSettingsCard,
|
||||
setSettingsCardPosition,
|
||||
setSettingsCardSize,
|
||||
toggleMinimizeCard,
|
||||
toggleSettingsCardFullscreen,
|
||||
SETTINGS_CARD_ID,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
@@ -44,6 +45,7 @@ const SettingsAppCard: React.FC<Props> = ({
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const isFullscreen = useAppSelector((s) => !!s.dashboardLayout.settingsCard?.fullscreen);
|
||||
const isMinimized = useAppSelector((s) => !!s.dashboardLayout.minimizedCards[SETTINGS_CARD_ID]);
|
||||
|
||||
const commitPosition = useCallback((x: number, y: number) => {
|
||||
dispatch(setSettingsCardPosition({ x, y }));
|
||||
@@ -52,6 +54,7 @@ const SettingsAppCard: React.FC<Props> = ({
|
||||
dispatch(setSettingsCardSize({ width, height }));
|
||||
}, [dispatch]);
|
||||
const close = useCallback(() => { dispatch(closeSettingsCard()); }, [dispatch]);
|
||||
const minimize = useCallback(() => { dispatch(toggleMinimizeCard({ cardId: SETTINGS_CARD_ID })); }, [dispatch]);
|
||||
|
||||
return (
|
||||
<CanvasWindowCard
|
||||
@@ -65,6 +68,7 @@ const SettingsAppCard: React.FC<Props> = ({
|
||||
cardHeight={cardHeight}
|
||||
cardZOrder={cardZOrder}
|
||||
fullscreen={isFullscreen}
|
||||
minimized={isMinimized}
|
||||
minWidth={MIN_W}
|
||||
minHeight={MIN_H}
|
||||
background={c.bg.page}
|
||||
@@ -104,7 +108,7 @@ const SettingsAppCard: React.FC<Props> = ({
|
||||
>
|
||||
<WindowControls
|
||||
onClose={close}
|
||||
onMinimize={close}
|
||||
onMinimize={minimize}
|
||||
onTile={(zone) => {
|
||||
if (zone === 'fullscreen' || zone === 'restore') { dispatch(toggleSettingsCardFullscreen()); return; }
|
||||
if (isFullscreen) dispatch(toggleSettingsCardFullscreen());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { setWorkflowsHubPosition, setWorkflowsHubSize } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { setWorkflowsHubPosition, setWorkflowsHubSize, WORKFLOWS_HUB_ID } from '@/shared/state/dashboardLayoutSlice';
|
||||
import CanvasWindowCard from '@/app/pages/Dashboard/cards/CanvasWindowCard';
|
||||
import type { CardType } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { useWC } from './uiKit';
|
||||
@@ -35,6 +35,7 @@ const WorkflowsAppCard: React.FC<Props> = ({
|
||||
const WC = useWC();
|
||||
const dispatch = useAppDispatch();
|
||||
const isFullscreen = useAppSelector((s) => !!s.dashboardLayout.workflowsHub?.fullscreen);
|
||||
const isMinimized = useAppSelector((s) => !!s.dashboardLayout.minimizedCards[WORKFLOWS_HUB_ID]);
|
||||
|
||||
// Keep fonts/keyframes available while the card is mounted.
|
||||
useEffect(() => { ensureAssets(); }, []);
|
||||
@@ -48,7 +49,7 @@ const WorkflowsAppCard: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<CanvasWindowCard
|
||||
cardId="workflows-hub"
|
||||
cardId={WORKFLOWS_HUB_ID}
|
||||
cardType="workflows-hub"
|
||||
selectType="workflows-hub-card"
|
||||
selectName="Workflows"
|
||||
@@ -58,6 +59,7 @@ const WorkflowsAppCard: React.FC<Props> = ({
|
||||
cardHeight={cardHeight}
|
||||
cardZOrder={cardZOrder}
|
||||
fullscreen={isFullscreen}
|
||||
minimized={isMinimized}
|
||||
minWidth={MIN_W}
|
||||
minHeight={MIN_H}
|
||||
background={WC.page}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { clearWorkflowsAppTarget, closeWorkflowsApp, toggleWorkflowsHubFullscreen } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { clearWorkflowsAppTarget, closeWorkflowsApp, toggleMinimizeCard, toggleWorkflowsHubFullscreen, WORKFLOWS_HUB_ID } from '@/shared/state/dashboardLayoutSlice';
|
||||
import WindowControls from '@/app/pages/Dashboard/cards/WindowControls';
|
||||
import {
|
||||
fetchWorkflows, fetchAllRuns, fetchPausedState, fetchActiveRuns, fetchDeletedWorkflows,
|
||||
@@ -84,7 +84,7 @@ const WorkflowsAppContent: React.FC<{ header: CardHeader; onTileZone?: (zone: st
|
||||
>
|
||||
<WindowControls
|
||||
onClose={() => dispatch(closeWorkflowsApp())}
|
||||
onMinimize={() => dispatch(closeWorkflowsApp())}
|
||||
onMinimize={() => dispatch(toggleMinimizeCard({ cardId: WORKFLOWS_HUB_ID }))}
|
||||
onTile={(zone) => {
|
||||
if (zone === 'fullscreen' || zone === 'restore') { dispatch(toggleWorkflowsHubFullscreen()); return; }
|
||||
if (isFullscreen) dispatch(toggleWorkflowsHubFullscreen());
|
||||
|
||||
@@ -26,7 +26,9 @@ export const DEFAULT_WORKFLOWS_HUB_W = DEFAULT_BROWSER_CARD_W;
|
||||
export const DEFAULT_WORKFLOWS_HUB_H = DEFAULT_BROWSER_CARD_H;
|
||||
export const DEFAULT_SETTINGS_CARD_W = 900;
|
||||
export const DEFAULT_SETTINGS_CARD_H = 640;
|
||||
// The two singleton windows have no card map to key off, so they own these fixed ids everywhere (selection, minimize, z-order).
|
||||
export const SETTINGS_CARD_ID = 'settings';
|
||||
export const WORKFLOWS_HUB_ID = 'workflows-hub';
|
||||
export const EXPANDED_CARD_MIN_H = 620;
|
||||
export const GRID_GAP = 24;
|
||||
// Gap between the Workflows window and the cards it spawns (run monitor, that monitor's browser). Keeps the hub -> monitor -> browser row evenly spaced.
|
||||
@@ -579,6 +581,9 @@ const dashboardLayoutSlice = createSlice({
|
||||
} else {
|
||||
state.minimizedCards[id] = true;
|
||||
if (state.tiledCards[id]) delete state.tiledCards[id];
|
||||
// The singletons hold their own fullscreen flag instead of a tiledCards entry, so parking one has to drop that too.
|
||||
if (id === SETTINGS_CARD_ID && state.settingsCard) state.settingsCard.fullscreen = false;
|
||||
if (id === WORKFLOWS_HUB_ID && state.workflowsHub) state.workflowsHub.fullscreen = false;
|
||||
}
|
||||
},
|
||||
setTiledCard(state, action: PayloadAction<{ cardId: string; zone: string }>) {
|
||||
@@ -1115,6 +1120,7 @@ const dashboardLayoutSlice = createSlice({
|
||||
openWorkflowsHub(state, action: PayloadAction<{ expandedSessionIds?: string[] } | undefined>) {
|
||||
if (state.workflowsHub) {
|
||||
state.workflowsHub.zOrder = state.nextZOrder++;
|
||||
delete state.minimizedCards[WORKFLOWS_HUB_ID];
|
||||
state.pendingFocusWorkflowsHub = true;
|
||||
return;
|
||||
}
|
||||
@@ -1136,6 +1142,7 @@ const dashboardLayoutSlice = createSlice({
|
||||
|
||||
closeWorkflowsHub(state) {
|
||||
state.workflowsHub = null;
|
||||
delete state.minimizedCards[WORKFLOWS_HUB_ID];
|
||||
},
|
||||
|
||||
// The Workflows app is an on-canvas card (like chat/browser/view cards), backed by the singleton workflowsHub geometry. Opening it creates or raises that card and pans to it; an optional workflowId deep-links to that workflow's detail once the card mounts.
|
||||
@@ -1143,6 +1150,8 @@ const dashboardLayoutSlice = createSlice({
|
||||
state.workflowsAppTarget = action.payload?.workflowId ?? null;
|
||||
if (state.workflowsHub) {
|
||||
state.workflowsHub.zOrder = state.nextZOrder++;
|
||||
// Opening means visible: a parked window must come back to the canvas, or the focus pan flies to empty space.
|
||||
delete state.minimizedCards[WORKFLOWS_HUB_ID];
|
||||
state.pendingFocusWorkflowsHub = true;
|
||||
return;
|
||||
}
|
||||
@@ -1160,6 +1169,7 @@ const dashboardLayoutSlice = createSlice({
|
||||
|
||||
closeWorkflowsApp(state) {
|
||||
state.workflowsHub = null;
|
||||
delete state.minimizedCards[WORKFLOWS_HUB_ID];
|
||||
state.workflowsAppTarget = null;
|
||||
state.workflowsMonitorId = null;
|
||||
state.workflowsMonitorRunId = null;
|
||||
@@ -1232,6 +1242,7 @@ const dashboardLayoutSlice = createSlice({
|
||||
openSettingsCard(state, action: PayloadAction<{ expandedSessionIds?: string[] } | undefined>) {
|
||||
if (state.settingsCard) {
|
||||
state.settingsCard.zOrder = state.nextZOrder++;
|
||||
delete state.minimizedCards[SETTINGS_CARD_ID];
|
||||
state.pendingFocusSettingsCard = true;
|
||||
return;
|
||||
}
|
||||
@@ -1249,6 +1260,7 @@ const dashboardLayoutSlice = createSlice({
|
||||
|
||||
closeSettingsCard(state) {
|
||||
state.settingsCard = null;
|
||||
delete state.minimizedCards[SETTINGS_CARD_ID];
|
||||
state.pendingFocusSettingsCard = false;
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user