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. */}