feat(ui): fenêtres View système séparées — WebviewWindow Tauri + port WindowGateway (#23)

Détache les vues dans de vraies fenêtres système Tauri (multi-écran,
fullscreen). Backend : commandes de gestion de WebviewWindow, capabilities
et composition root. Frontend : nouveau port WindowGateway et son adaptateur
window, entrée panel-only ViewWindow/ViewPanelBody, détachement câblé dans
ProjectsView. QA vert : app-tauri 63, frontend typecheck + vitest 546/546.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 16:53:43 +02:00
parent 2323a71f01
commit 69e785ec7e
21 changed files with 962 additions and 55 deletions

View File

@ -881,6 +881,44 @@ export interface TicketGateway {
): Promise<void>;
}
/**
* Payload of the OS window-lifecycle event that fires when a detached view
* window closes (ticket #23). Lets the main window re-toggle the view's
* placement out of `"detached"`.
*/
export interface ViewWindowClosed {
/** The detached panel id (a `PanelId`, kept as `string` at the port seam). */
panel: string;
/** The project the detached window was showing. */
projectId: string;
}
/**
* Detaching a View into its own OS window (ticket #23).
*
* The backend owns the Tauri `WebviewWindow` lifecycle (create/focus/close,
* anti-duplicate registry). This port is the UI's only door to it — components
* never call `invoke()` directly. `panel` is a `PanelId`-shaped string; it is
* typed as `string` here so ports need not depend on the `features/` layer.
*/
export interface WindowGateway {
/**
* Opens — or focuses, if already open — a separate OS window rendering only
* `panel` for `projectId` (the backend's `open_view_window` command).
*/
openViewWindow(panel: string, projectId: string): Promise<void>;
/** Closes the detached window for `panel`/`projectId` if one is open. */
closeViewWindow(panel: string, projectId: string): Promise<void>;
/**
* Subscribes to detached-window close events (OS close or programmatic). The
* handler receives the `{ panel, projectId }` that closed. Returns an
* unsubscribe.
*/
onViewWindowClosed(
handler: (event: ViewWindowClosed) => void,
): Promise<Unsubscribe>;
}
/**
* The full set of gateways the app depends on, injected via the DI provider.
* The composition (real vs mock) is chosen in `app/`.
@ -903,4 +941,5 @@ export interface Gateways {
workState: WorkStateGateway;
conversation: ConversationGateway;
ticket: TicketGateway;
window: WindowGateway;
}