[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)

This commit is contained in:
ciregenz
2026-08-12 01:25:30 -07:00
parent 25fb3677ca
commit 00624f230c
3 changed files with 88 additions and 4 deletions
@@ -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<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);
// 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<DashboardCanvasProps> = ({
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<DashboardCanvasProps> = ({
</Box>
{/* 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. */}
<Box sx={{ display: fullscreenCardId ? 'none' : 'contents' }}>
<Box sx={{ display: screenOwnedByCard ? 'none' : 'contents' }}>
<DashboardOverlays
anyFullscreen={anyFullscreen}
canvas={canvas}
@@ -34,6 +34,7 @@ export const MARKETPLACE_CARD_ID = 'marketplace';
export const DEFAULT_MARKETPLACE_CARD_W = DEFAULT_BROWSER_CARD_W;
export const DEFAULT_MARKETPLACE_CARD_H = DEFAULT_BROWSER_CARD_H;
export const WORKFLOWS_HUB_ID = 'workflows-hub';
export const WORKFLOWS_MONITOR_ID = 'workflows-monitor';
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.
@@ -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');
@@ -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<string, string>, extra: Record<string, unknown> = {}): 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);
});