diff --git a/frontend/components/Layouts/DefaultLayout.tsx b/frontend/components/Layouts/DefaultLayout.tsx index 0630aea..4ba78a0 100644 --- a/frontend/components/Layouts/DefaultLayout.tsx +++ b/frontend/components/Layouts/DefaultLayout.tsx @@ -6,7 +6,7 @@ import type React from "react"; import { useEffect, useRef, useState } from "react"; const WORKSPACE_SHELL_METRICS_CLASS = - "[--workspace-header-offset:9.75rem] [--workspace-sidebar-width:15.5rem] sm:[--workspace-header-offset:8.75rem] lg:[--workspace-header-offset:4.5rem]"; + "[--workspace-header-offset:9.75rem] [--workspace-sidebar-width:15.5rem]"; const PUBLIC_SHELL_METRICS_CLASS = "[--workspace-header-offset:5rem]"; const DESKTOP_SIDEBAR_QUERY = "(min-width: 1024px)"; @@ -22,6 +22,8 @@ export default function DefaultLayout({ // No GoF pattern applies; this layout tracks simple responsive disclosure state. const [sidebarState, setSidebarState] = useState("responsive"); const [isDesktopSidebarViewport, setIsDesktopSidebarViewport] = useState(false); + const shellRef = useRef(null); + const headerRef = useRef(null); const sidebarSwitcherRef = useRef(null); const isWorkspaceShell = variant === "workspace"; const sidebarOpen = @@ -38,14 +40,48 @@ export default function DefaultLayout({ return () => desktopQuery.removeEventListener("change", syncDesktopViewport); }, []); + useEffect(() => { + const shell = shellRef.current; + const header = headerRef.current; + + if (!shell || !header) { + return; + } + + // No GoF pattern applies; mirror the measured fixed header height into shell CSS. + const updateHeaderOffset = () => { + const measuredHeight = Math.ceil(header.getBoundingClientRect().height); + + if (measuredHeight > 0) { + shell.style.setProperty("--workspace-header-offset", `${measuredHeight}px`); + } + }; + + updateHeaderOffset(); + + const ResizeObserverConstructor = window.ResizeObserver as typeof ResizeObserver | undefined; + + if (!ResizeObserverConstructor) { + window.addEventListener("resize", updateHeaderOffset); + + return () => window.removeEventListener("resize", updateHeaderOffset); + } + + const resizeObserver = new ResizeObserverConstructor(updateHeaderOffset); + resizeObserver.observe(header); + + return () => resizeObserver.disconnect(); + }, []); + return (
-
+
)}
{

{snapshot - ? `Current context: ${snapshot.context_label}. Deterministic highlights, advisory items, and attention states below are coming from the backend snapshot contract.` + ? `Current context: ${snapshot.context_label}. Highlights, advisory items, and attention states below come from the Insights workspace snapshot for this context.` : `Tutor is preparing the latest ${roleConfig.label.toLowerCase()} snapshot for this context.`}

diff --git a/frontend/e2e/route-shells.spec.ts b/frontend/e2e/route-shells.spec.ts index 9b8d79d..4db24d4 100644 --- a/frontend/e2e/route-shells.spec.ts +++ b/frontend/e2e/route-shells.spec.ts @@ -132,6 +132,28 @@ const expectSidebarFixed = async (page: Page) => { .toBe("fixed"); }; +const expectWorkspaceShellBelowHeader = async (page: Page) => { + await expect + .poll(async () => + page.evaluate(() => { + const header = document.querySelector("#workspace-header"); + const sidebar = document.querySelector("#sidebar"); + const mainPanel = document.querySelector("[data-workspace-main-panel]"); + + if (!header || !sidebar || !mainPanel) { + return Number.NEGATIVE_INFINITY; + } + + const headerBottom = Math.ceil(header.getBoundingClientRect().bottom); + const sidebarTop = Math.floor(sidebar.getBoundingClientRect().top); + const mainPanelTop = Math.floor(mainPanel.getBoundingClientRect().top); + + return Math.min(sidebarTop - headerBottom, mainPanelTop - headerBottom); + }), + ) + .toBeGreaterThanOrEqual(-1); +}; + const setWorkspaceRole = async (page: Page, role: string) => { await page.addInitScript((workspaceRole) => { window.localStorage.setItem("tutor.workspace.role", JSON.stringify(workspaceRole)); @@ -182,6 +204,7 @@ test.describe("workspace shell reflow", () => { await page.goto(reflowCase.path); await expect(page.locator("#main-content")).toBeVisible(); + await expectWorkspaceShellBelowHeader(page); await expectNoHorizontalOverflow(page); }); } @@ -200,6 +223,7 @@ test.describe("workspace shell reflow", () => { "aria-expanded", "false", ); + await expectWorkspaceShellBelowHeader(page); await expectSidebarOffCanvas(page); await expectNoHorizontalOverflow(page); @@ -209,6 +233,7 @@ test.describe("workspace shell reflow", () => { "aria-expanded", "true", ); + await expectWorkspaceShellBelowHeader(page); await expectSidebarOnCanvas(page); await page.mouse.click(viewport.width - 16, Math.min(320, viewport.height - 16)); @@ -217,6 +242,7 @@ test.describe("workspace shell reflow", () => { "aria-expanded", "false", ); + await expectWorkspaceShellBelowHeader(page); await expectSidebarOffCanvas(page); await expectNoHorizontalOverflow(page); }); @@ -230,17 +256,20 @@ test.describe("workspace shell reflow", () => { await expect(page.locator("#main-content")).toBeVisible(); await expect(page.locator('button[aria-controls="sidebar"]')).toHaveAttribute("aria-expanded", "true"); + await expectWorkspaceShellBelowHeader(page); await expectSidebarOnCanvas(page); await expectSidebarFixed(page); await expectNoHorizontalOverflow(page); await page.locator('button[aria-controls="sidebar"]').click(); await expect(page.locator('button[aria-controls="sidebar"]')).toHaveAttribute("aria-expanded", "false"); + await expectWorkspaceShellBelowHeader(page); await expectSidebarOffCanvas(page); await expectNoHorizontalOverflow(page); await page.locator('button[aria-controls="sidebar"]').click(); await expect(page.locator('button[aria-controls="sidebar"]')).toHaveAttribute("aria-expanded", "true"); + await expectWorkspaceShellBelowHeader(page); await expectSidebarOnCanvas(page); await expectNoHorizontalOverflow(page); });