/** * `ViewWindow` — the panel-only entry (ticket #23, reworked in #47). * * A detached OS window loads the same SPA with `?panel=` (no project id); * {@link main} routes to this component instead of the full {@link App} when a * `panel` param is present. It re-instantiates the DI (same adapters/gateways as * the main window — views are gateway/read-model-driven, so no heavy state is * transferred) and renders **only** the requested view for the project the main * window currently has in focus. * * Focus, not embedding (#47): the window never carries — nor reopens — a project * id. It reads the current focus from the {@link FocusedProjectGateway} at mount * and then tracks {@link FocusedProjectGateway.onFocusedProjectChanged}. While no * project is focused it mounts **no** project-scoped panel and shows an "open a * project" shell, so a window restored at restart never surfaces a stale * project's data. * * Pure presentation over the ports: no `invoke()`, no cross-window messaging, * and crucially no `openProject()` — opening a project is the main window's job. */ import { useEffect, useState } from "react"; import type { FocusedProject } from "@/ports"; import { Panel } from "@/shared"; import { PANEL_TITLE, type PanelId } from "@/features/projects"; import { ViewPanelBody, type ViewPanelId } from "@/features/projects"; import { useGateways } from "./di"; /** Panels that may be shown in a detached window (everything but "projects"). */ const DETACHABLE = new Set( (Object.keys(PANEL_TITLE) as PanelId[]).filter((p) => p !== "projects"), ); /** Parsed `?panel=` params, or `null` when absent/invalid. */ export interface ViewWindowParams { panel: ViewPanelId; } /** Reads and validates the panel-only param from a query string (#47: no project). */ export function parseViewWindowParams( search: string, ): ViewWindowParams | null { const params = new URLSearchParams(search); const panel = params.get("panel"); if (!panel || !DETACHABLE.has(panel)) return null; return { panel: panel as ViewPanelId }; } export interface ViewWindowProps { panel: ViewPanelId; } export function ViewWindow({ panel }: ViewWindowProps) { const { focusedProject } = useGateways(); // The focused project this window follows. `undefined` = not yet resolved // (initial read in flight); `null` = resolved, no project focused. const [focus, setFocus] = useState( undefined, ); // Read the current focus at mount, then track changes. Never opens a project: // the window is a pure follower of the main window's focus (#47). useEffect(() => { let cancelled = false; let unsubscribe: (() => void) | undefined; focusedProject .getFocusedProject() .then((project) => { if (!cancelled) setFocus(project); }) .catch(() => { if (!cancelled) setFocus(null); }); void focusedProject .onFocusedProjectChanged((project) => { if (!cancelled) setFocus(project); }) .then((un) => { if (cancelled) un(); else unsubscribe = un; }); return () => { cancelled = true; unsubscribe?.(); }; }, [focusedProject]); return (

{PANEL_TITLE[panel]}

{focus && ( · {focus.name} )}
{focus ? ( // Only mount a project-scoped panel when a project is actually focused // — the panels require a real `projectRoot`, never a placeholder. ) : (

Ouvrez un projet dans la fenêtre principale pour afficher ce panneau.

)}
); }