let a maximized panel take the whole window, header included

Maximize had one depth: fill the content region, leave the nav header visible. That was never a choice
about how much room to take — the region is an `absolute z-2` stacking context and the header is a
`fixed z-10` sibling, so no z-index a panel gives itself can paint over the nav. `top-[56px]` was the
workaround.

So full screen is cooperative rather than a bigger overlay. The panel asks, and the shell hides its own
header for it; `inset-0` is then genuinely the window. Still the same element and the same class swap —
no portal, no remount, so scroll position and playback survive the step between depths the way they
already survived maximize.

The mode rides beside the maximized panel id in sessionStorage as one value, so the two cannot drift;
a tab open across this change reads the old bare string, gets undefined for `.id`, and lands on
"nothing is maximized".

The toggle is offered from every state, so taking the window is one click from a tiled panel, and it
steps back to a maximized panel rather than all the way out. The amber light is present at both depths
and always goes all the way out, so neither is a trap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 21:56:58 +01:00
co-authored by Claude Opus 5
parent 9974736587
commit 417860b892
10 changed files with 301 additions and 65 deletions
@@ -1,6 +1,6 @@
import { useMemo, useRef } from 'react';
import { useLocation } from 'react-router';
import { useDock, MusicPlayerHost } from 'officerdev';
import { useDock, MusicPlayerHost, usePanelFullscreen } from 'officerdev';
import { useCapabilities } from 'hooks/useCapabilities';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { ScreenErrorFallback } from './ScreenErrorFallback';
@@ -24,13 +24,17 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
const isTouch = useIsTouch();
const { pathname } = useLocation();
usePageTitleSync();
// A panel can maximize into the content region on its own, but it cannot paint over this header — the
// region below is an `absolute z-2` stacking context and the header is a `fixed z-10` sibling of it. So
// "full screen" is cooperative: the panel asks, and the chrome steps aside.
const panelFullscreen = usePanelFullscreen();
// The content region shrinks when the (in-flow) music dock takes its space; the nav dock measures its
// reveal boundary from this element, so it always sits just above whatever's at the bottom.
const regionRef = useRef<HTMLElement | null>(null);
return (
<div className="relative flex h-dvh flex-col overflow-hidden outline-none inset-0">
<Header dockItems={visibleItems} />
<Header dockItems={visibleItems} hidden={panelFullscreen} />
{/* overflow-CLIP, not hidden: `hidden` still makes this a scroll container, and the nav Dock —
absolute, parked below the bottom edge by translateY while hidden — adds its transformed box to
the scrollable overflow. So clicking a link let the browser scroll this section ~70px to reveal
@@ -53,14 +53,20 @@ function EditablePageTitle() {
type HeaderProps = {
dockItems?: DockItem[];
/**
* Stand down for a panel that has taken the whole window. `display: none` rather than an unmount: the
* header holds a half-typed tab rename and its own popovers, and a panel going fullscreen is not a
* reason to throw those away. It is `fixed`, so hiding it costs no layout shift either.
*/
hidden?: boolean;
};
export function Header({ dockItems }: HeaderProps) {
export function Header({ dockItems, hidden }: HeaderProps) {
const [open, setOpen] = useState(false);
const isTouch = useIsTouch();
return (
<header className="fixed z-10 w-full">
<header className={`fixed z-10 w-full ${hidden ? 'hidden' : ''}`}>
<div
className="relative shrink-0 border-b backdrop-blur-xl shadow-lg px-3 py-2 md:px-6 md:py-3 flex items-center justify-between"
style={{ backgroundColor: 'rgba(255, 255, 255, 0.25)', borderColor: 'rgba(255, 255, 255, 0.35)' }}