mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-29 13:04:51 +02:00
[eric] canvas: the fill/halves/quarters tiling menus are gone; the green dot is full screen or restore, nothing else
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d68ee068e6
commit
08f4c03a88
@@ -6,7 +6,6 @@ import { useCardTiling } from './useCardTiling';
|
||||
import { useCanvasWindowResize } from './useCanvasWindowResize';
|
||||
import { useDragEndBackstops } from '../hooks/interaction/useDragEndBackstops';
|
||||
import { openCardContextMenu, isNativeMenuTarget, type CardMenuRow } from '../desktop/openCardContextMenu';
|
||||
import { tileMenuRows } from './tileMenuRows';
|
||||
import type { CardType } from '@/shared/state/dashboardLayoutSlice';
|
||||
|
||||
const DRAG_THRESHOLD = 3;
|
||||
@@ -193,7 +192,6 @@ const CanvasWindowCard: React.FC<CanvasWindowCardProps> = ({
|
||||
const items: CardMenuRow[] = [
|
||||
{ kind: 'header', label: selectName },
|
||||
{ label: tiling.isFullscreen ? 'Exit Full Screen' : 'Full Screen', onClick: () => tiling.applyZone(tiling.isFullscreen ? 'restore' : 'fullscreen') },
|
||||
{ label: 'Tile to zone', submenu: tileMenuRows(tiling.applyZone, tiling.zone) },
|
||||
];
|
||||
if (onMinimize) items.push({ label: minimized ? 'Restore' : 'Minimize', onClick: onMinimize });
|
||||
if (onBringToFront) items.push({ label: 'Bring to front', onClick: () => onBringToFront(cardId, cardType) });
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { TILE_ZONES } from '../canvas/tiledGeometry';
|
||||
import { TILE_GROUPS, ZONE_LABELS } from './tileZones';
|
||||
|
||||
interface WindowControlsProps {
|
||||
onClose: () => void;
|
||||
onMinimize: () => void;
|
||||
onTile: (zone: string) => void; // a TILE_ZONES key, 'fullscreen', or 'restore'
|
||||
onTile: (zone: string) => void; // 'fullscreen' or 'restore'
|
||||
tiled?: boolean;
|
||||
/** Onboarding anchor riding the close dot (the settings flow points its cursor here). */
|
||||
closeDataOnboarding?: string;
|
||||
@@ -14,8 +12,7 @@ interface WindowControlsProps {
|
||||
|
||||
// macOS-style traffic lights on every card = the "AI OS" window feel. Grey at rest so a canvas
|
||||
// full of cards isn't a wall of color; they colorize when the parent .osw-card is hovered, and the
|
||||
// close/minus/plus symbols reveal on hovering the group. Hovering the GREEN dot opens the tiling menu (Fill,
|
||||
// Halves, Quarters, Thirds), exactly like macOS; clicking green = Full Screen (or restore if tiled).
|
||||
// close/minus/plus symbols reveal on hovering the group. Clicking green = Full Screen (or restore if tiled).
|
||||
const RED = '#ff5f57';
|
||||
const YELLOW = '#febc2e';
|
||||
const GREEN = '#28c840';
|
||||
@@ -50,21 +47,6 @@ export const ARC_CHIP_SX: Record<string, unknown> = {
|
||||
};
|
||||
|
||||
function WindowControls({ onClose, onMinimize, onTile, tiled, closeDataOnboarding }: WindowControlsProps): React.ReactElement {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
// Menu DOM (12 tiles + labels, ~30 nodes) mounts on first green-dot hover, not per card at boot.
|
||||
const [menuHot, setMenuHot] = useState(false);
|
||||
// Right-edge hosts (the minimized stack) open the menu leftward so it never clips off-window.
|
||||
const [alignRight, setAlignRight] = useState(false);
|
||||
const greenRef = useRef<HTMLDivElement | null>(null);
|
||||
const closeTimer = useRef<number | null>(null);
|
||||
const openMenu = (): void => {
|
||||
if (closeTimer.current) window.clearTimeout(closeTimer.current);
|
||||
const rect = greenRef.current?.getBoundingClientRect();
|
||||
if (rect) setAlignRight(rect.left + 224 > window.innerWidth);
|
||||
if (!menuHot) { setMenuHot(true); requestAnimationFrame(() => setMenuOpen(true)); return; }
|
||||
setMenuOpen(true);
|
||||
};
|
||||
const scheduleClose = (): void => { closeTimer.current = window.setTimeout(() => setMenuOpen(false), 550); };
|
||||
const stop = (e: React.PointerEvent | React.MouseEvent): void => { e.stopPropagation(); };
|
||||
|
||||
const btn = (color: string, symbol: string, onClick: () => void, label: string, slot: string): React.ReactElement => (
|
||||
@@ -76,7 +58,7 @@ function WindowControls({ onClose, onMinimize, onTile, tiled, closeDataOnboardin
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className="osw-window-lights" data-tilemenu-open={menuOpen ? '1' : undefined} onPointerDown={stop}
|
||||
<Box className="osw-window-lights" onPointerDown={stop}
|
||||
sx={{
|
||||
display: 'flex', gap: '8px', alignItems: 'center', flex: 'none', '&:hover span': { opacity: 1 },
|
||||
// Inert until the card is hovered: crossing a card can't hit-test or fire React enter/leave
|
||||
@@ -85,44 +67,12 @@ function WindowControls({ onClose, onMinimize, onTile, tiled, closeDataOnboardin
|
||||
}}>
|
||||
{btn(RED, '×', onClose, 'Close', 'close')}
|
||||
{btn(YELLOW, '−', onMinimize, 'Minimize', 'minimize')}
|
||||
<Box ref={greenRef} data-light="zoom" sx={{ position: 'relative', display: 'flex', alignItems: 'center' }}
|
||||
onMouseEnter={openMenu} onMouseLeave={scheduleClose}>
|
||||
<Box data-light="zoom" sx={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
|
||||
<Box component="button" type="button" aria-label={tiled ? 'Exit Full Screen' : 'Full Screen'}
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); onTile(tiled ? 'restore' : 'fullscreen'); }}
|
||||
onPointerDown={stop} sx={dotSx(GREEN)}>
|
||||
<span>{tiled ? '−' : '+'}</span>
|
||||
</Box>
|
||||
{menuHot && <Box className="osw-tilemenu" onPointerDown={stop} onClick={stop}
|
||||
onMouseEnter={openMenu} onMouseLeave={scheduleClose}
|
||||
sx={{
|
||||
position: 'absolute', top: 19, width: 216, background: '#FFFFFF',
|
||||
// Invisible runway over the button-to-menu gap: without it the pointer crosses dead
|
||||
// space, mouseleave fires, and the menu vanishes mid-approach ("baited").
|
||||
'&::before': { content: '""', position: 'absolute', top: -8, left: 0, right: 0, height: 8 },
|
||||
...(alignRight ? { right: -8 } : { left: -8 }),
|
||||
border: '1px solid rgba(0,0,0,0.06)', borderRadius: '12px', boxShadow: '0 .5rem 2rem rgba(0,0,0,.14)',
|
||||
p: 1.25, zIndex: 1200, transformOrigin: alignRight ? 'top right' : 'top left',
|
||||
opacity: menuOpen ? 1 : 0, transform: menuOpen ? 'none' : 'translateY(-6px) scale(0.96)',
|
||||
pointerEvents: menuOpen ? 'auto' : 'none', transition: 'opacity .16s, transform .18s cubic-bezier(.3,.9,.3,1)',
|
||||
}}>
|
||||
{TILE_GROUPS.map((g) => (
|
||||
<Box key={g.label} sx={{ mb: 0.75, '&:last-of-type': { mb: 0 } }}>
|
||||
<Box sx={{ fontSize: '0.625rem', fontWeight: 700, letterSpacing: '0.08em', color: 'rgba(115,114,108,0.65)', textTransform: 'uppercase', mb: 0.5 }}>{g.label}</Box>
|
||||
<Box sx={{ display: 'flex', gap: '7px' }}>
|
||||
{g.zones.map((zone) => {
|
||||
const z = TILE_ZONES[zone];
|
||||
return (
|
||||
<Box key={zone} role="button" aria-label={ZONE_LABELS[zone]}
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setMenuOpen(false); onTile(zone); }}
|
||||
sx={{ position: 'relative', flex: 1, height: 32, border: '1px solid rgba(0,0,0,0.08)', borderRadius: '6px', background: '#F5F4ED', cursor: 'pointer', overflow: 'hidden', transition: 'border-color .12s, background .12s', '&:hover': { borderColor: '#ae5630', background: '#ae56300d' } }}>
|
||||
<Box sx={{ position: 'absolute', left: `${z.x * 100 + 8}%`, top: `${z.y * 100 + 14}%`, width: `${z.w * 100 - 16}%`, height: `${z.h * 100 - 28}%`, background: '#ae5630', opacity: 0.8, borderRadius: '2px' }} />
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { handleSlashCommand } from '@/app/pages/AgentChat/ChatInput/hooks/slashC
|
||||
import type { AppDispatch } from '@/shared/state/store';
|
||||
import type { CardMenuRow } from '../desktop/openCardContextMenu';
|
||||
import { chord } from '../desktop/chord';
|
||||
import { tileMenuRows } from './tileMenuRows';
|
||||
|
||||
interface AgentMenuArgs {
|
||||
session: AgentSession;
|
||||
@@ -26,7 +25,6 @@ export function agentCardMenuRows({ session, dispatch, expanded, tileZone, expan
|
||||
return [
|
||||
{ label: expanded ? 'Collapse' : 'Open', shortcut: chord('enter'), onClick: () => dispatch(expanded ? collapseSession(session.id) : expandSession(session.id)) },
|
||||
{ label: tileZone === 'fullscreen' ? 'Exit Full Screen' : 'Full Screen', onClick: () => onTile(tileZone === 'fullscreen' ? 'restore' : 'fullscreen') },
|
||||
{ label: 'Tile to zone', submenu: tileMenuRows(onTile, tileZone) },
|
||||
{ label: 'Bring to front', onClick: () => dispatch(bringToFront({ id: session.id, type: 'agent' })) },
|
||||
{ kind: 'separator' },
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@ import { setClipboardCards } from '@/shared/dashboardClipboard';
|
||||
import type { AppDispatch } from '@/shared/state/store';
|
||||
import type { CardMenuRow } from '../desktop/openCardContextMenu';
|
||||
import { chord } from '../desktop/chord';
|
||||
import { tileMenuRows } from './tileMenuRows';
|
||||
|
||||
interface BrowserNav {
|
||||
reload: () => void;
|
||||
@@ -49,7 +48,6 @@ export function browserCardMenuRows({
|
||||
{ label: 'Copy URL', disabled: !activeUrl, onClick: () => { void navigator.clipboard.writeText(activeUrl); } },
|
||||
{ kind: 'separator' },
|
||||
{ label: tileZone === 'fullscreen' ? 'Exit Full Screen' : 'Full Screen', onClick: () => onTile(tileZone === 'fullscreen' ? 'restore' : 'fullscreen') },
|
||||
{ label: 'Tile to zone', submenu: tileMenuRows(onTile, tileZone) },
|
||||
{ label: isMinimized ? 'Restore' : 'Minimize', onClick: onMinimize },
|
||||
{ label: 'Bring to front', onClick: () => dispatch(bringToFront({ id: browserId, type: 'browser' })) },
|
||||
{
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { CardMenuRow } from '../desktop/openCardContextMenu';
|
||||
import { TILE_GROUPS, ZONE_LABELS } from './tileZones';
|
||||
|
||||
// The green-dot tiling grid, spelled out as words for the right-click path. Same catalog, so the two
|
||||
// surfaces always offer the same zones.
|
||||
export function tileMenuRows(onTile: (zone: string) => void, currentZone?: string): CardMenuRow[] {
|
||||
const rows: CardMenuRow[] = [];
|
||||
for (const group of TILE_GROUPS) {
|
||||
rows.push({ kind: 'header', label: group.label });
|
||||
for (const zone of group.zones) {
|
||||
rows.push({ label: ZONE_LABELS[zone], checked: currentZone === zone, onClick: () => onTile(zone) });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// The zone catalog the tiling MENUS render. The geometry itself lives in canvas/tiledGeometry.
|
||||
|
||||
export const ZONE_LABELS: Record<string, string> = {
|
||||
fill: 'Fill', left: 'Left half', right: 'Right half', top: 'Top half', bottom: 'Bottom half',
|
||||
tl: 'Top left', tr: 'Top right', bl: 'Bottom left', br: 'Bottom right',
|
||||
};
|
||||
|
||||
// The one grid every tiling surface renders: the green-dot hover menu and the right-click "Tile to
|
||||
// zone" submenu both map this, so they can never drift apart on which zones exist. Thirds were cut
|
||||
// (Eric 2026-08-09): the geometry still restores an old t3* card fine, the menus just stop offering it.
|
||||
export const TILE_GROUPS: { label: string; zones: string[] }[] = [
|
||||
{ label: 'Fill and halves', zones: ['fill', 'left', 'right', 'top', 'bottom'] },
|
||||
{ label: 'Quarters', zones: ['tl', 'tr', 'bl', 'br'] },
|
||||
];
|
||||
@@ -6,7 +6,6 @@ import { API_BASE } from '@/shared/config';
|
||||
import type { AppDispatch } from '@/shared/state/store';
|
||||
import type { CardMenuRow } from '../desktop/openCardContextMenu';
|
||||
import { chord } from '../desktop/chord';
|
||||
import { tileMenuRows } from './tileMenuRows';
|
||||
|
||||
interface ViewMenuArgs {
|
||||
output: Output;
|
||||
@@ -31,7 +30,6 @@ export function viewCardMenuRows({
|
||||
{ label: 'Open another instance', onClick: () => dispatch(addViewCard({ outputId: output.id, newInstance: true })) },
|
||||
{ kind: 'separator' },
|
||||
{ label: tileZone === 'fullscreen' ? 'Exit Full Screen' : 'Full Screen', onClick: () => onTile(tileZone === 'fullscreen' ? 'restore' : 'fullscreen') },
|
||||
{ label: 'Tile to zone', submenu: tileMenuRows(onTile, tileZone) },
|
||||
{ label: isMinimized ? 'Restore' : 'Minimize', onClick: onMinimize },
|
||||
{ label: 'Bring to front', onClick: () => dispatch(bringToFront({ id: cardKey, type: 'view' })) },
|
||||
{ kind: 'separator' },
|
||||
|
||||
@@ -12,7 +12,6 @@ import { dropMinimizedShot } from './minimizedShots';
|
||||
import { buildMinimizedEntries, MinimizedEntry, MinimizedRect } from './minimizedEntries';
|
||||
import MinimizedTile, { MINIMIZED_TILE_W } from './MinimizedTile';
|
||||
import { openCardContextMenu } from './openCardContextMenu';
|
||||
import { tileMenuRows } from '../cards/tileMenuRows';
|
||||
import type { BrowserCardPosition, ViewCardPosition } from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { Output } from '@/shared/state/outputsSlice';
|
||||
|
||||
@@ -113,9 +112,6 @@ function MinimizedStack({ browserCards, viewCards, outputs, selectedIds, onResto
|
||||
items: [
|
||||
{ label: 'Restore', onClick: () => restore(entry) },
|
||||
{ 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) },
|
||||
],
|
||||
|
||||
@@ -5,10 +5,3 @@ body.dashboard-marquee-active * {
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
/* While a traffic-light tile menu is open, its lights cluster stays visible and interactive even
|
||||
when the hover that revealed it has moved on (to the menu itself); otherwise the menu fades
|
||||
mid-approach and the click lands on nothing. */
|
||||
.osw-pill-lights:has(.osw-window-lights[data-tilemenu-open="1"]) {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user