From 00624f230cd9c2c4d05f23203d16d7094fe731c6 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 12 Aug 2026 01:25:30 -0700 Subject: [PATCH] [eric] canvas: a card tiled to fill the screen hides the floating chrome the same way fullscreen does, so tidy and zoom stop sitting on its composer (ENG-259) --- .../Dashboard/canvas/DashboardCanvas.tsx | 8 ++- .../src/shared/state/dashboardLayoutSlice.ts | 18 ++++- .../selectViewportCoveringCardId.test.ts | 66 +++++++++++++++++++ 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 frontend/src/shared/state/selectViewportCoveringCardId.test.ts diff --git a/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx b/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx index de4c7146..b9638191 100644 --- a/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx +++ b/frontend/src/app/pages/Dashboard/canvas/DashboardCanvas.tsx @@ -1,7 +1,7 @@ import React, { useEffect, type RefObject } from 'react'; import Box from '@mui/material/Box'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; -import { addViewCard, addBrowserTab, clearTiledCard, toggleMinimizeCard, selectFullscreenCardId } from '@/shared/state/dashboardLayoutSlice'; +import { addViewCard, addBrowserTab, clearTiledCard, toggleMinimizeCard, selectFullscreenCardId, selectViewportCoveringCardId } from '@/shared/state/dashboardLayoutSlice'; import { store } from '@/shared/state/store'; import { buildDockEntries } from '../desktop/dockEntries'; import DashboardHeader from './DashboardHeader'; @@ -188,6 +188,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); + // Chrome hides because a card OWNS THE SCREEN, which `fill` does just as much as `fullscreen`. + const screenOwnedByCard = !!useAppSelector(selectViewportCoveringCardId); const minimizedCards = useAppSelector((s) => s.dashboardLayout.minimizedCards); const anyFullscreen = !!fullscreenCardId; const [headerRevealed, setHeaderRevealed] = React.useState(false); @@ -458,7 +460,7 @@ const DashboardCanvas: React.FC = ({ willChange: 'transform', transform: `translate3d(${canvas.panX % dotSpacing}px, ${canvas.panY % dotSpacing}px, 0)`, // Arc fullscreen: the float sits on a clean themed ground, the dot texture is canvas-only. - display: anyFullscreen ? 'none' : undefined, + display: screenOwnedByCard ? 'none' : undefined, backgroundImage: gridTileUrl, backgroundSize: `${dotSpacing}px ${dotSpacing}px`, }} @@ -517,7 +519,7 @@ const DashboardCanvas: React.FC = ({ {/* display:contents when visible so the overlays' absolute children keep positioning against the canvas root; display:none (not unmount) so the toolbar composer draft survives fullscreen. */} - + monitor -> browser row evenly spaced. @@ -256,7 +257,10 @@ interface LayoutPayload { function tileOwnerExists(s: DashboardLayoutState, id: string): boolean { return id in s.cards || id in s.viewCards || id in s.browserCards || id in s.workflowCards || (id === WORKFLOWS_HUB_ID && !!s.workflowsHub) || (id === SETTINGS_CARD_ID && !!s.settingsCard) - || (id === MARKETPLACE_CARD_ID && !!s.marketplaceCard); + || (id === MARKETPLACE_CARD_ID && !!s.marketplaceCard) + // The Run Monitor is a real tileable surface everywhere else in this file; leaving it out here + // would silently report its fullscreen as "owner gone" and leave the canvas chrome on top of it. + || (id === WORKFLOWS_MONITOR_ID && !!s.workflowsMonitorCard); } function ledgerAdd(ledger: string[], id: string): void { @@ -2138,6 +2142,18 @@ export const reopenLastClosed = createAsyncThunk( } ); +// A card in the `fill` zone covers the whole viewport just like `fullscreen` does, but it is not +// 'fullscreen', so the canvas chrome stayed up and the tidy/zoom cluster sat directly on the card's +// composer (measured: 4 controls over a filled card, 0 over a fullscreen one). Anything that hides +// chrome because a card owns the screen must ask THIS, not the fullscreen question. +export const selectViewportCoveringCardId = (state: { dashboardLayout: DashboardLayoutState }): string | null => { + const s = state.dashboardLayout; + const entry = Object.entries(s.tiledCards).find(([, zone]) => zone === 'fullscreen' || zone === 'fill'); + if (!entry) return null; + const id = entry[0]; + return tileOwnerExists(s, id) && !s.minimizedCards[id] ? id : null; +}; + export const selectFullscreenCardId = (state: { dashboardLayout: DashboardLayoutState }): string | null => { const s = state.dashboardLayout; const entry = Object.entries(s.tiledCards).find(([, zone]) => zone === 'fullscreen'); diff --git a/frontend/src/shared/state/selectViewportCoveringCardId.test.ts b/frontend/src/shared/state/selectViewportCoveringCardId.test.ts new file mode 100644 index 00000000..fb2ea6b4 --- /dev/null +++ b/frontend/src/shared/state/selectViewportCoveringCardId.test.ts @@ -0,0 +1,66 @@ +// Run: node --test frontend/src/shared/state/selectViewportCoveringCardId.test.ts +// +// The canvas hides its floating chrome when a card owns the screen. That used to be keyed on the +// 'fullscreen' zone alone, but TILE_ZONES also has `fill`, which covers the whole viewport and is a +// different string. Measured live before the fix: 4 controls (minimap, Tidy layout, Zoom out, Zoom +// in) sitting on top of a filled card, versus 0 on a fullscreen one. These pin the predicate. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { selectViewportCoveringCardId, selectFullscreenCardId } from './dashboardLayoutSlice.ts'; + +function stateWith(tiledCards: Record, extra: Record = {}): any { + return { + dashboardLayout: { + tiledCards, + minimizedCards: {}, + cards: { chat1: { x: 0, y: 0, width: 480, height: 300 } }, + viewCards: {}, browserCards: {}, workflowCards: {}, + workflowsHub: null, settingsCard: null, marketplaceCard: null, workflowsMonitorCard: null, + ...extra, + }, + }; +} + +test('a fullscreen card owns the screen', () => { + assert.equal(selectViewportCoveringCardId(stateWith({ chat1: 'fullscreen' })), 'chat1'); +}); + +test('a FILLED card owns the screen too, which is the whole bug', () => { + assert.equal(selectViewportCoveringCardId(stateWith({ chat1: 'fill' })), 'chat1'); +}); + +test('a half-screen tile does not own the screen, so chrome stays', () => { + for (const zone of ['left', 'right', 'top', 'bottom', 'tl', 'tr', 'bl', 'br', 't3l', 't3c', 't3r']) { + assert.equal(selectViewportCoveringCardId(stateWith({ chat1: zone })), null, `zone ${zone}`); + } +}); + +test('nothing tiled means nothing owns the screen', () => { + assert.equal(selectViewportCoveringCardId(stateWith({})), null); +}); + +test('a minimized card cannot own the screen', () => { + const s = stateWith({ chat1: 'fill' }, { minimizedCards: { chat1: true } }); + assert.equal(selectViewportCoveringCardId(s), null); +}); + +test('a tile whose card is gone cannot own the screen', () => { + const s = stateWith({ ghost: 'fill' }); + assert.equal(selectViewportCoveringCardId(s), null); +}); + +test('the Run Monitor counts as a real tile owner', () => { + // It was missing from tileOwnerExists, so its fullscreen read as "owner gone" and left the canvas + // chrome on top of it. Latent today (no tiling UI on that card) but a hole in a load-bearing check. + const s = stateWith({ 'workflows-monitor': 'fullscreen' }, + { workflowsMonitorCard: { x: 0, y: 0, width: 900, height: 600, zOrder: 1 } }); + assert.equal(selectViewportCoveringCardId(s), 'workflows-monitor'); + assert.equal(selectFullscreenCardId(s), 'workflows-monitor'); +}); + +test('fullscreen and covering stay different questions', () => { + // Only chrome-hiding should treat `fill` as fullscreen; window buttons and the exit pill must not. + const filled = stateWith({ chat1: 'fill' }); + assert.equal(selectViewportCoveringCardId(filled), 'chat1'); + assert.equal(selectFullscreenCardId(filled), null); +});