Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions frontend/components/Layouts/DefaultLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)";

Expand All @@ -22,6 +22,8 @@ export default function DefaultLayout({
// No GoF pattern applies; this layout tracks simple responsive disclosure state.
const [sidebarState, setSidebarState] = useState<SidebarDisclosureState>("responsive");
const [isDesktopSidebarViewport, setIsDesktopSidebarViewport] = useState(false);
const shellRef = useRef<HTMLElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
const sidebarSwitcherRef = useRef<HTMLButtonElement>(null);
const isWorkspaceShell = variant === "workspace";
const sidebarOpen =
Expand All @@ -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 (
<main
ref={shellRef}
id="main-content"
className={`min-h-screen overflow-x-hidden text-slate-900 dark:text-slate-100 ${
isWorkspaceShell ? WORKSPACE_SHELL_METRICS_CLASS : PUBLIC_SHELL_METRICS_CLASS
}`}
>
<div className="fixed left-0 top-0 z-50 w-full">
<div ref={headerRef} id="workspace-header" className="fixed left-0 top-0 z-50 w-full">
<Header
sidebarOpen={sidebarOpen}
setSidebarOpen={setSidebarOpen}
Expand All @@ -62,6 +98,7 @@ export default function DefaultLayout({
/>
)}
<div
data-workspace-main-panel="true"
className={`min-h-[calc(100vh_-_var(--workspace-header-offset))] min-w-0 transition-[margin] duration-200 ${
isWorkspaceShell && sidebarState !== "closed" ? "lg:ml-[var(--workspace-sidebar-width)]" : ""
}`}
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/Workspace/WorkspaceHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ const WorkspaceHome = () => {
</h1>
<p className="mt-4 text-lg leading-8 text-slate-600 dark:text-slate-300">
{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.`}
</p>
<div className="mt-6 flex flex-wrap gap-3">
Expand Down
29 changes: 29 additions & 0 deletions frontend/e2e/route-shells.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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);
});
}
Expand All @@ -200,6 +223,7 @@ test.describe("workspace shell reflow", () => {
"aria-expanded",
"false",
);
await expectWorkspaceShellBelowHeader(page);
await expectSidebarOffCanvas(page);
await expectNoHorizontalOverflow(page);

Expand All @@ -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));
Expand All @@ -217,6 +242,7 @@ test.describe("workspace shell reflow", () => {
"aria-expanded",
"false",
);
await expectWorkspaceShellBelowHeader(page);
await expectSidebarOffCanvas(page);
await expectNoHorizontalOverflow(page);
});
Expand All @@ -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);
});
Expand Down
Loading