feat(app): Workbench-Dock statt Zoom-Landkarte + bedienbare VNC-Konsole (0.2.2.0)

Cockpit neu gedacht — das Pinch-Zoom-Landkarten-Konzept war auf 5 Zoll fummelig:
- WorkspaceDeck + WorkspaceDock: Taskleiste unten (Chat · Code · Desktop),
  Ein-Tap-Panelwechsel im Daumenbereich, animierter Indikator, Aktivitaets-
  Badges (Code blau / Desktop gruen), Safe-Area, blendet bei offener Tastatur
  aus. Panels bildschirmfuellend + immer gemountet (kein Remount).
- noVNC bedienbar: novncHtml bekommt Tastatur-Bridge (verstecktes Input-Feld →
  rfb.sendKey inkl. Enter/Backspace/Pfeile), Strg-Alt-Entf, Fit↔1:1. VncTile
  zeigt eine Steuerungs-Leiste (⌨ / Strg+Alt+Entf / ⤢).
- Entfernt: WorkspaceCanvas, Tile, PreviewTile, CockpitOverviewButton,
  cockpitNav (Pinch-Canvas + Uebersichts-Button obsolet). layout.ts auf
  Panel-Metadaten geschlankt. Header nur noch mit Kompakt/Cockpit-Umschalter.

tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 21:44:32 +02:00
co-authored by Claude Opus 4.8
parent 7d46d8e531
commit 0110526622
14 changed files with 317 additions and 551 deletions
+3 -84
View File
@@ -1,95 +1,14 @@
/**
* layout — Kachel-Geometrie + Kamera-Mathematik fuer den Workspace-Canvas.
*
* Welt-Koordinaten = Pixel bei Scale 1. Kacheln liegen auf einem festen 2x2-
* Raster. Die "Welt-View" (Animated.View) ist exakt die Bounding-Box der gerade
* sichtbaren Kacheln; Kinder werden relativ zu deren Ursprung positioniert.
*
* RN 0.73 kennt noch kein transformOrigin — Scale dreht um die View-MITTE.
* Alle Kamera-Formeln rechnen deshalb mit Center-Origin:
* screen = center + scale*(p - center) + translate
* wobei center = (worldW/2, worldH/2) (die Welt-View sitzt bei screen 0,0).
* layout — Panel-Definitionen der Workbench (Metadaten fuer das Dock).
*/
export type TileId = 'chat' | 'editor' | 'vnc' | 'preview';
export interface TileRect { x: number; y: number; w: number; h: number; }
export interface TileDef { id: TileId; title: string; icon: string; }
// Feste Kachelgroesse im Welt-Raster (Pixel bei Scale 1).
const TILE_W = 1100;
const TILE_H = 1500;
const GAP = 140;
/** Absolute Welt-Rects pro Kachel (2x2-Raster). */
export const TILE_RECTS: Record<TileId, TileRect> = {
chat: { x: 0, y: 0, w: TILE_W, h: TILE_H },
editor: { x: TILE_W + GAP, y: 0, w: TILE_W, h: TILE_H },
vnc: { x: 0, y: TILE_H + GAP, w: TILE_W, h: TILE_H },
preview: { x: TILE_W + GAP, y: TILE_H + GAP, w: TILE_W, h: TILE_H },
};
export interface TileDef { id: TileId; title: string; icon: string }
export const TILE_META: Record<TileId, TileDef> = {
chat: { id: 'chat', title: 'Chat', icon: '💬' },
editor: { id: 'editor', title: 'Editor', icon: '📝' },
editor: { id: 'editor', title: 'Code', icon: '📝' },
vnc: { id: 'vnc', title: 'Desktop', icon: '🖥️' },
preview: { id: 'preview', title: 'Vorschau', icon: '🖼️' },
};
// Reihenfolge fuer stabiles Rendering.
export const TILE_ORDER: TileId[] = ['chat', 'editor', 'vnc', 'preview'];
export interface VisibilitySignals {
kind: 'code' | 'chat';
hasCode: boolean; // schon ein code_file empfangen
hasDesktop: boolean; // Desktop verfuegbar gemeldet
}
/** Welche Kacheln sind fuer den aktuellen Kontext sichtbar?
* Chat ist immer da; Code-Projekt (explizit ODER durch ein Live-Signal)
* blendet Editor/Desktop/Vorschau ein. */
export function visibleTilesFor(sig: VisibilitySignals): TileId[] {
const isCode = sig.kind === 'code' || sig.hasCode || sig.hasDesktop;
if (!isCode) return ['chat'];
return ['chat', 'editor', 'vnc', 'preview'];
}
/** Bounding-Box mehrerer Kacheln. */
export function boundsOf(ids: TileId[]): TileRect {
if (ids.length === 0) return { x: 0, y: 0, w: TILE_W, h: TILE_H };
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const id of ids) {
const r = TILE_RECTS[id];
minX = Math.min(minX, r.x);
minY = Math.min(minY, r.y);
maxX = Math.max(maxX, r.x + r.w);
maxY = Math.max(maxY, r.y + r.h);
}
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
}
/** Rect in Welt-View-lokale Koordinaten (relativ zur Bounds-Ecke) umrechnen. */
export function toLocal(rect: TileRect, bounds: TileRect): TileRect {
return { x: rect.x - bounds.x, y: rect.y - bounds.y, w: rect.w, h: rect.h };
}
export interface Camera { scale: number; tx: number; ty: number; }
/** Kamera, die ein (lokales) Rect bildschirmfuellend zeigt (Fokus-Modus). */
export function focusCamera(localRect: TileRect, worldW: number, worldH: number, vw: number, vh: number): Camera {
const s = Math.min(vw / localRect.w, vh / localRect.h);
const pcx = localRect.x + localRect.w / 2;
const pcy = localRect.y + localRect.h / 2;
const tx = vw / 2 - worldW / 2 - s * (pcx - worldW / 2);
const ty = vh / 2 - worldH / 2 - s * (pcy - worldH / 2);
return { scale: s, tx, ty };
}
/** Kamera, die die gesamte Welt zentriert einpasst (Uebersicht). */
export function overviewCamera(worldW: number, worldH: number, vw: number, vh: number, pad = 0.86): Camera {
const s = Math.min(vw / worldW, vh / worldH) * pad;
const tx = vw / 2 - worldW / 2;
const ty = vh / 2 - worldH / 2;
return { scale: s, tx, ty };
}