From 4a26cafb5b775607272502078694cba7c6ef5661 Mon Sep 17 00:00:00 2001 From: sarah <129242944+sarah-inkeep@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:46:50 -0700 Subject: [PATCH] fix(open-knowledge): align fullscreen graph header with macOS traffic lights + restore scoped window-drag (#2127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(open-knowledge): align fullscreen graph header with macOS traffic lights + restore scoped window-drag The fullscreen graph overlay (`fixed inset-0`) starts at the raw window top, 8px above the editor chrome row (which sits inside SidebarInset's `m-2`). Its header now reproduces that 8px inset (`mt-2`), matches the chrome row's `h-12` band, and drops PanelHeader's inherited `py-3` so content centers on the same y=32 midline the traffic lights are tuned to (verified by measuring the rendered rects). A structural spacer reserves the traffic-light footprint so the title no longer overlaps them horizontally. The overlay paints over the editor's `-webkit-app-region: drag` chrome. Drag regions composite at the OS layer and show through an opaque overlay, so clicks on the Explore/Orphans/Hubs tabs were being converted to window drags. Scope it like EditorHeader: the header row is the drag region, the controls cluster opts back out with `[&>*]:no-drag`. The window stays draggable by the header while the tabs/buttons receive clicks. * fix(open-knowledge): use precedent-#49 pl- reserve instead of spacer in graph header Address PR review: the spacer approach removed the `pl-[var(--ok-titlebar-reserve-left,1rem)]` marker that precedent #49's class-level guard (fullscreen-overlay-safe-area-coverage.test.ts) requires within ±6 lines of every `fixed inset-0`, which would have failed CI. Measured the premise the spacer was built on: `pl-[var(--ok-titlebar-reserve-left,1rem)]` resolves to 78px even with PanelHeader's base `px-4` (the arbitrary `pl-` wins by Tailwind emit order) — so the cascade-collapse the spacer guarded against does not occur. Reverted to the precedent-compliant `pl-` reserve on the chrome row; dropped the spacer. Keeps the vertical fix (`mt-2 h-12 py-0` → title at y=32) and the header-scoped window-drag (header is the drag region, controls opt out with `[&>*]:[-webkit-app-region:no-drag]`). Also fixes a comment that quoted the invalid `[&>*]:no-drag` shorthand. * fix(open-knowledge): restore traffic-light clearance lost in spacer→pl- revert The spacer stacked on the header's base px-4 (16px + 78px = 94px to the title), but `pl-` *replaces* px-4's left, giving only 78px — which leaves the title touching the traffic lights (overlap regression). Re-add the 16px via `ml-4` on the title cluster, restoring the 94px clearance the spacer version had (measured: pl-[…]=78px + ml-4=16px → title at 94px, identical to the approved spacer). Also reword the header comment so it no longer contains the literal `fixed inset-0` string — the safe-area coverage guard regex scans comment lines, and the prose was being matched as a spurious class-pattern hit without a nearby marker once the comment grew. --------- GitOrigin-RevId: 6c3f321523cb9f5cef1c047982d3c4232040413c --- .../graph-fullscreen-header-alignment.md | 7 +++ ...aphPanel.fullscreen-safe-area.dom.test.tsx | 58 ++++++++++++++++--- packages/app/src/components/GraphPanel.tsx | 41 ++++++++++--- 3 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 .changeset/graph-fullscreen-header-alignment.md diff --git a/.changeset/graph-fullscreen-header-alignment.md b/.changeset/graph-fullscreen-header-alignment.md new file mode 100644 index 000000000..5d5ee8b46 --- /dev/null +++ b/.changeset/graph-fullscreen-header-alignment.md @@ -0,0 +1,7 @@ +--- +"@inkeep/open-knowledge-app": patch +--- + +Fix the fullscreen graph overlay's header on macOS desktop. The "GRAPH" title row now vertically aligns with the window's traffic lights: the overlay is `fixed inset-0` so it starts at the raw window top, 8px above where the normal editor chrome row sits (inside `SidebarInset`'s `m-2`), so its header reproduces that 8px inset and matches the chrome row's height — landing the title on the same midline the traffic lights are tuned to. The header continues to reserve the traffic-light footprint so the title never overlaps the buttons. + +Window dragging works again in fullscreen graph mode, and the Explore/Orphans/Hubs tabs are reliably clickable. The overlay paints over the editor's `-webkit-app-region: drag` chrome; previously those drag regions showed through and silently converted clicks on the mode tabs into window drags. The header is now scoped like the editor header — the header row is the drag region and the controls cluster opts back out with `no-drag` — so the window stays draggable by the header while the tabs and buttons receive clicks. diff --git a/packages/app/src/components/GraphPanel.fullscreen-safe-area.dom.test.tsx b/packages/app/src/components/GraphPanel.fullscreen-safe-area.dom.test.tsx index e0880f55f..d1e5af9c9 100644 --- a/packages/app/src/components/GraphPanel.fullscreen-safe-area.dom.test.tsx +++ b/packages/app/src/components/GraphPanel.fullscreen-safe-area.dom.test.tsx @@ -1,4 +1,4 @@ -import { afterEach, describe, mock, test } from 'bun:test'; +import { afterEach, describe, expect, mock, test } from 'bun:test'; import { cleanup, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import type { ReactNode } from 'react'; @@ -44,8 +44,17 @@ mock.module('@/components/GraphView', () => ({ ), })); +function setElectronHost(on: boolean) { + const w = window as unknown as { okDesktop?: unknown }; + if (on) w.okDesktop = {}; + else delete w.okDesktop; +} + describe('GraphPanel fullscreen safe-area behavior', () => { - afterEach(() => cleanup()); + afterEach(() => { + cleanup(); + setElectronHost(false); + }); async function renderExpandedGraphPanel() { const { GraphPanel } = await import('./GraphPanel'); @@ -70,16 +79,43 @@ describe('GraphPanel fullscreen safe-area behavior', () => { 'overflow-hidden', 'bg-background', ]); + expectVisualClassTokensAbsent(panel?.className, ['[-webkit-app-region:no-drag]']); const header = panel?.querySelector('[data-slot="panel-header"]'); + expectVisualClassTokens(header?.className, ['mt-2', 'h-12', 'py-0']); + expectVisualClassTokens(header?.className, ['pl-[var(--ok-titlebar-reserve-left,1rem)]']); expectVisualClassTokensAbsent(header?.className, ['pl-[var(--ok-titlebar-reserve-left)]']); const titleCluster = header?.querySelector('[data-slot="graph-title-cluster"]'); - expectVisualClassTokens(titleCluster?.className, ['ml-2']); + expectVisualClassTokens(titleCluster?.className, ['ml-4']); + }); + + test('fullscreen on Electron scopes window-drag to the header, controls opt out', async () => { + setElectronHost(true); + await renderExpandedGraphPanel(); + + const panel = screen.getByTestId('graph-view').closest('[data-slot="panel"]'); + const header = panel?.querySelector('[data-slot="panel-header"]'); + expectVisualClassTokens(header?.className, ['[-webkit-app-region:drag]']); + expect(header?.getAttribute('data-electron-drag')).toBe(''); + + const controls = header?.querySelector('[data-slot="graph-controls"]'); + expectVisualClassTokens(controls?.className, ['[&>*]:[-webkit-app-region:no-drag]']); + }); + + test('fullscreen off Electron declares no drag region', async () => { + await renderExpandedGraphPanel(); + + const header = screen + .getByTestId('graph-view') + .closest('[data-slot="panel"]') + ?.querySelector('[data-slot="panel-header"]'); + expectVisualClassTokensAbsent(header?.className, ['[-webkit-app-region:drag]']); + expect(header?.getAttribute('data-electron-drag')).toBeNull(); }); - test('docked (non-expanded) graph does not indent the title cluster', async () => { + test('docked (non-expanded) graph does not reserve the traffic-light footprint', async () => { const { GraphPanel } = await import('./GraphPanel'); render( @@ -87,10 +123,14 @@ describe('GraphPanel fullscreen safe-area behavior', () => { , ); - const titleCluster = screen - .getByTestId('graph-view') - .closest('[data-slot="panel"]') - ?.querySelector('[data-slot="graph-title-cluster"]'); - expectVisualClassTokensAbsent(titleCluster?.className, ['ml-2']); + const panel = screen.getByTestId('graph-view').closest('[data-slot="panel"]'); + const header = panel?.querySelector('[data-slot="panel-header"]'); + expectVisualClassTokensAbsent(header?.className, [ + 'pl-[var(--ok-titlebar-reserve-left,1rem)]', + 'mt-2', + 'h-12', + ]); + const titleCluster = header?.querySelector('[data-slot="graph-title-cluster"]'); + expectVisualClassTokensAbsent(titleCluster?.className, ['ml-4']); }); }); diff --git a/packages/app/src/components/GraphPanel.tsx b/packages/app/src/components/GraphPanel.tsx index 1bd35e61f..226ef252e 100644 --- a/packages/app/src/components/GraphPanel.tsx +++ b/packages/app/src/components/GraphPanel.tsx @@ -284,6 +284,7 @@ export function GraphPanel({ activeDocName }: { activeDocName: string }) { pagesBySlug, pagesByBasename, } = usePageList(); + const isElectronHost = typeof window !== 'undefined' && window.okDesktop != null; const [isExpanded, setIsExpanded] = useState(false); const [fullscreenMode, setFullscreenMode] = useState('explore'); const [orphanMode, setOrphanMode] = useState('both'); @@ -397,18 +398,34 @@ export function GraphPanel({ activeDocName }: { activeDocName: string }) { return ( - {/* In fullscreen the header reserve clears the macOS traffic-light - footprint; this small margin keeps the flush-left "GRAPH" title from - crowding the buttons (the reserve alone leaves it touching them). */} + {/* Fullscreen header anatomy (expanded only): + • `pl-[var(--ok-titlebar-reserve-left,1rem)]` reserves the macOS + traffic-light footprint on the chrome row (precedent #49). The + arbitrary `pl-` wins over PanelHeader's base `px-4` by Tailwind + emit order (measured: resolves to 78px under electron-mode); the + `,1rem` fallback keeps web layout at the base `px-4`. Because `pl-` + *replaces* the base `px-4` rather than stacking on it, 78px alone + leaves the title touching the buttons — the title cluster adds + `ml-4` below for the 16px of breathing room (94px total, measured). + • `mt-2 h-12 py-0` land the row on the editor chrome midline: the + overlay is pinned to the whole window, so it starts at the raw + window top, 8px above EditorHeader's SidebarInset-`m-2` band. + `mt-2` reproduces that inset, `h-12` matches the 48px band, `py-0` + drops the inherited `py-3` so content centers in the full band — + title at y=32, exactly where the traffic lights are tuned (measured). + • Electron: the header row is the window-drag region (so graph mode + stays draggable); the controls cluster opts back out below. */}
Graph @@ -424,7 +441,13 @@ export function GraphPanel({ activeDocName }: { activeDocName: string }) {
) : null} -
+
*]:[-webkit-app-region:no-drag]', + )} + > {isExpanded ? (