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
7 changes: 7 additions & 0 deletions .changeset/graph-fullscreen-header-alignment.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
Expand All @@ -70,27 +79,58 @@ 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(
<TooltipProvider>
<GraphPanel activeDocName="docs/Active" />
</TooltipProvider>,
);

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']);
});
});
41 changes: 32 additions & 9 deletions packages/app/src/components/GraphPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<FullscreenGraphMode>('explore');
const [orphanMode, setOrphanMode] = useState<OrphanMode>('both');
Expand Down Expand Up @@ -397,18 +398,34 @@ export function GraphPanel({ activeDocName }: { activeDocName: string }) {
return (
<Panel className={isExpanded ? 'fixed inset-0 z-50 overflow-hidden bg-background' : undefined}>
<PanelHeader
className={
data-electron-drag={isExpanded && isElectronHost ? '' : undefined}
className={cn(
isExpanded
? 'flex-wrap gap-3 pl-[var(--ok-titlebar-reserve-left,1rem)]'
: 'flex-wrap gap-3'
}
? 'mt-2 h-12 gap-3 py-0 pl-[var(--ok-titlebar-reserve-left,1rem)]'
: 'flex-wrap gap-3',
isExpanded && isElectronHost && '[-webkit-app-region:drag]',
)}
>
{/* 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. */}
<div
data-slot="graph-title-cluster"
className={cn('flex min-w-0 items-center gap-1.5', isExpanded && 'ml-2')}
className={cn('flex min-w-0 items-center gap-1.5', isExpanded && 'ml-4')}
>
<PanelTitle>
<Trans>Graph</Trans>
Expand All @@ -424,7 +441,13 @@ export function GraphPanel({ activeDocName }: { activeDocName: string }) {
</div>
) : null}
</div>
<div className="ml-auto flex items-center gap-2">
<div
data-slot="graph-controls"
className={cn(
'ml-auto flex items-center gap-2',
isExpanded && isElectronHost && '[&>*]:[-webkit-app-region:no-drag]',
)}
>
{isExpanded ? (
<ToggleGroup
type="single"
Expand Down