From c66a7055ce47b285c846baeb911d98da446041d2 Mon Sep 17 00:00:00 2001 From: tab11pm Date: Fri, 25 Sep 2026 21:38:27 +0700 Subject: [PATCH] feat(terminal): add fullscreen mode for terminal sessions Implement CSS-only fullscreen overlay that expands a terminal session to fill the entire viewport without persisting bounds to the backend. Key design decisions: - Fullscreen layer rendered outside the canvas camera transform to avoid coordinate-system conflicts with pan/zoom - No IPC bounds changes: crash-safe, original layout preserved on restart - Single-session constraint: entering fullscreen for another session auto-exits the current one - Escape key exits fullscreen (bubble phase to avoid stealing from modals) - Exit button in top-right corner and restore button in header Includes i18n strings (en/ru) and feature documentation. --- docs/fullscreen-mode.md | 44 ++ package-lock.json | 523 ------------------ src/renderer/src/App.tsx | 18 + .../src/features/terminal/TerminalCard.tsx | 40 +- .../features/workspace/WorkspaceCanvas.tsx | 51 +- src/renderer/src/lib/i18n.ts | 4 + src/renderer/src/styles/app.css | 26 + 7 files changed, 180 insertions(+), 526 deletions(-) create mode 100644 docs/fullscreen-mode.md diff --git a/docs/fullscreen-mode.md b/docs/fullscreen-mode.md new file mode 100644 index 00000000..32b52d58 --- /dev/null +++ b/docs/fullscreen-mode.md @@ -0,0 +1,44 @@ +# Fullscreen Mode for Terminal Sessions + +CanvasTTY supports fullscreen mode for terminal sessions, allowing you to expand any session to fill the entire viewport for focused work. + +## How to Use + +### Enter Fullscreen + +- **Header button:** Click the maximize icon (⛶) in the terminal card header. +- **Keyboard:** Not yet supported via shortcut (coming soon). + +### Exit Fullscreen + +- **Close button:** Click the X button in the top-right corner of the fullscreen session. +- **Header button:** Click the restore icon (❐) in the terminal card header. +- **Keyboard:** Press `Escape`. + +## Behavior + +- **Camera preserved:** When you exit fullscreen, the canvas camera (pan/zoom) stays where you left it. If you navigated during fullscreen, the camera won't snap back. +- **Single session only:** Only one session can be fullscreen at a time. Opening fullscreen for another session automatically closes the previous one. +- **No bounds persistence:** Fullscreen mode is CSS-only — it does not modify the session's saved position or size. If the app crashes during fullscreen, the session restores at its original layout on restart. +- **Header hidden:** The terminal card header and resize handles are hidden in fullscreen, maximizing the terminal surface area. + +## Architecture + +Fullscreen is implemented as a **CSS overlay layer** separate from the canvas camera transform: + +1. `App.tsx` tracks `fullscreenSessionId` state and passes it down to `WorkspaceCanvas`. +2. `WorkspaceCanvas` splits rendering into two groups: + - **Normal layer** (`workspace__windows`): all sessions *except* the fullscreen one. + - **Fullscreen layer** (`workspace__fullscreen-layer`): only the fullscreen session, rendered outside the camera transform with `position: absolute; inset: 0`. +3. `TerminalCard` receives a `fullscreen` prop and applies the `terminal-card--fullscreen` CSS class, which overrides dimensions, removes borders/shadows, and hides the header. + +This design avoids coordinate-system conflicts between the canvas pan/zoom transform and the fullscreen overlay. + +## Internationalization + +Fullscreen UI strings are localized in `src/renderer/src/lib/i18n.ts`: + +| Key | English | Russian | +|------------------|----------------------|-------------------------------| +| `enterFullscreen`| Enter fullscreen | Развернуть на весь экран | +| `exitFullscreen` | Exit fullscreen | Свернуть из полноэкранного | \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5dcf52db..c28ce21f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,474 +53,6 @@ "vite": "8.2.2" } }, - "integrations/even-g2/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "integrations/even-g2/node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, "integrations/even-g2/node_modules/@evenrealities/evenhub-cli": { "version": "0.1.14", "resolved": "https://registry.npmjs.org/@evenrealities/evenhub-cli/-/evenhub-cli-0.1.14.tgz", @@ -567,50 +99,6 @@ "node": ">=20" } }, - "integrations/even-g2/node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, "integrations/even-g2/node_modules/postcss": { "version": "8.5.28", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", @@ -6353,7 +5841,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -6375,7 +5862,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -6397,7 +5883,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -6419,7 +5904,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -6441,7 +5925,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -6463,7 +5946,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -6485,7 +5967,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -6507,7 +5988,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -6529,7 +6009,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -6551,7 +6030,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -6573,7 +6051,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 37589a88..71577a55 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -210,6 +210,7 @@ export function App(): React.JSX.Element { const [activeSessionId, setActiveSessionId] = useState(null); const [browserSelected, setBrowserSelected] = useState(false); const [renamingSessionId, setRenamingSessionId] = useState(null); + const [fullscreenSessionId, setFullscreenSessionId] = useState(null); const [pendingTerminalUrl, setPendingTerminalUrl] = useState(null); const [toast, setToast] = useState(null); const [ready, setReady] = useState(false); @@ -457,6 +458,21 @@ export function App(): React.JSX.Element { window.canvasTTY.terminal.setBounds(id, bounds); }, []); + const toggleSessionFullscreen = useCallback((id: string): void => { + if (fullscreenSessionId === id) { + // Exit fullscreen: only update state, no bounds persistence + // Note: Camera position is preserved intentionally (BUG 3 mitigation). + // If the user panned/zoomed during fullscreen, the camera stays where they left it. + setFullscreenSessionId(null); + } else { + // Enter fullscreen: exit current fullscreen first if switching sessions (BUG 2) + if (fullscreenSessionId !== null) { + setFullscreenSessionId(null); + } + setFullscreenSessionId(id); + } + }, [fullscreenSessionId]); + const changePluginCanvasBounds = useCallback((id: string, bounds: SessionBounds): void => { const pluginCanvas = settingsRef.current.pluginCanvas.map((instance) => instance.id === id ? { ...instance, position: bounds.position, size: bounds.size } @@ -1074,6 +1090,8 @@ export function App(): React.JSX.Element { activeSessionId={activeSessionId} browserSelected={browserSelected} renamingSessionId={renamingSessionId} + fullscreenSessionId={fullscreenSessionId} + onToggleFullscreen={toggleSessionFullscreen} onSelectSession={(id) => { setBrowserSelected(false); setActiveSessionId(id); diff --git a/src/renderer/src/features/terminal/TerminalCard.tsx b/src/renderer/src/features/terminal/TerminalCard.tsx index 0a3a0081..ecd62185 100644 --- a/src/renderer/src/features/terminal/TerminalCard.tsx +++ b/src/renderer/src/features/terminal/TerminalCard.tsx @@ -58,7 +58,9 @@ interface TerminalCardProps { /** Multi-select group member: gets the selected outline without focus/WebGL side effects. */ groupSelected?: boolean; renaming: boolean; + fullscreen: boolean; snapTargets: readonly SessionBounds[]; + onToggleFullscreen(): void; onActivate(session: SessionSnapshot): void; onSelect(id: string): void; onRename(id: string, title: string): Promise; @@ -111,7 +113,9 @@ export function TerminalCard({ selected, groupSelected, renaming, + fullscreen, snapTargets, + onToggleFullscreen, onActivate, onSelect, onRename, @@ -432,6 +436,20 @@ export function TerminalCard({ if (summaryMode) closeSearch(); }, [summaryMode]); + // Exit fullscreen mode on Escape key + useEffect(() => { + if (!fullscreen) return; + const handleEscape = (event: KeyboardEvent): void => { + if (event.key === "Escape") { + event.preventDefault(); + event.stopPropagation(); + onToggleFullscreen(); + } + }; + window.addEventListener("keydown", handleEscape, false); + return () => window.removeEventListener("keydown", handleEscape, false); + }, [fullscreen, onToggleFullscreen]); + const bindRenameInput = useCallback((input: HTMLInputElement | null): void => { renameInput.current = input; if (!input) return; @@ -621,7 +639,7 @@ export function TerminalCard({ const searchCount = `${searchMatches.current}/${searchMatches.total}`; return (
+ {fullscreen && ( + + )}
)} +
diff --git a/src/renderer/src/features/workspace/WorkspaceCanvas.tsx b/src/renderer/src/features/workspace/WorkspaceCanvas.tsx index 70237eb8..7df95aa6 100644 --- a/src/renderer/src/features/workspace/WorkspaceCanvas.tsx +++ b/src/renderer/src/features/workspace/WorkspaceCanvas.tsx @@ -160,6 +160,8 @@ interface WorkspaceCanvasProps { activeSessionId: string | null; browserSelected: boolean; renamingSessionId: string | null; + fullscreenSessionId: string | null; + onToggleFullscreen(id: string): void; onSelectSession(id: string): void; onSelectBrowser(): void; onClearCanvasSelection(): void; @@ -196,7 +198,7 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element settings, mediaData, sessions, limits, limitsLoadState, plugins, browser, browserViewVisible, homeEditing, camera, onCameraChange, onGoHome, onOpenSettings, onOpenAgent, onOpenTerminal, onOpenBrowser, onOpenTerminalUrl, onFocusSession, - activeSessionId, browserSelected, renamingSessionId, onSelectSession, + activeSessionId, browserSelected, renamingSessionId, fullscreenSessionId, onToggleFullscreen, onSelectSession, onSelectBrowser, onClearCanvasSelection, onRenameSession, onRenameEnd, onRequestMedia, onRemoveMedia, onHomeLayoutChange, onHomeGridSizeChange, onFinishHomeEdit, onResetHomeLayout, onPluginError, onPluginCanvasBoundsChange, @@ -822,7 +824,7 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element onPluginCanvasWheel={wheelNavigation.applyCanvasWheel} />
- {renderedSessions.map((session) => ( + {renderedSessions.filter((session) => fullscreenSessionId !== session.id).map((session) => ( onToggleFullscreen(session.id)} snapTargets={[ homeBounds, ...renderedCanvasRegions.map((candidate) => ({ position: candidate.position, size: candidate.size })), @@ -974,6 +978,49 @@ export function WorkspaceCanvas(props: WorkspaceCanvasProps): React.JSX.Element
+ {/* Fullscreen layer: rendered outside workspace__scene to avoid camera transformation */} +
+ {renderedSessions + .filter((session) => fullscreenSessionId === session.id) + .map((session) => ( + onToggleFullscreen(session.id)} + snapTargets={[]} + onActivate={(selectedSession) => { + focusController.focus(terminalCanvasWidgetId(selectedSession.id), "explicit"); + onFocusSession(selectedSession); + }} + onSelect={(id) => { + focusController.cancelHover(); + focusController.focus(terminalCanvasWidgetId(id), "explicit"); + onSelectSession(id); + }} + onRename={onRenameSession} + onRenameEnd={onRenameEnd} + onBoundsChange={() => {}} + onRestart={onRestartSession} + onDispose={onDisposeSession} + onOpenUrl={onOpenTerminalUrl} + /> + ))} +
+ {pointerNavigation.marquee && (
= { close: "Close", focus: "Focus", expand: "Expand", + enterFullscreen: "Enter fullscreen", + exitFullscreen: "Exit fullscreen", sessionStarted: "Session created", terminalStarted: "Terminal created", launchFailed: "Could not launch session", diff --git a/src/renderer/src/styles/app.css b/src/renderer/src/styles/app.css index 2407e41a..b79be05d 100644 --- a/src/renderer/src/styles/app.css +++ b/src/renderer/src/styles/app.css @@ -63,6 +63,8 @@ button { border: 0; } .workspace__regions { position: absolute; left: 0; top: 0; } .workspace__windows { position: absolute; left: 0; top: 0; } .workspace__windows--hidden { visibility: hidden; pointer-events: none; } +.workspace__fullscreen-layer { position: absolute; inset: 0; z-index: 9999; pointer-events: none; } +.workspace__fullscreen-layer .terminal-card--fullscreen { pointer-events: auto; } .radial-launcher { position: absolute; @@ -396,6 +398,30 @@ button { border: 0; } .terminal-card__summary span { color: rgba(255,255,255,.58); font-size: 21px; font-weight: 750; line-height: 1; } .terminal-card--summary .terminal-card__surface { visibility: hidden; } .terminal-card--summary .terminal-card__summary { display: flex; } +/* Fullscreen mode: full viewport overlay */ +.terminal-card--fullscreen { + position: absolute !important; + inset: 0 !important; + width: 100% !important; + height: 100% !important; + left: 0 !important; + top: 0 !important; + z-index: 9999 !important; + border-radius: 0 !important; + border: none !important; + max-width: none !important; + max-height: none !important; + transform: none !important; + box-shadow: none !important; +} +.terminal-card--fullscreen .terminal-card__header { display: none; } +.terminal-card--fullscreen .terminal-card__surface { height: 100%; } +.terminal-card--fullscreen .terminal-card__resize-handle { display: none; } + +/* Exit fullscreen button */ +.terminal-card__exit-fullscreen { position: fixed; top: calc(var(--titlebar-height) + 16px); right: 16px; z-index: 10000; width: 36px; height: 36px; display: grid; place-items: center; border-radius: 8px; color: rgba(255,255,255,.8); background: rgba(0,0,0,.6); cursor: pointer; font-size: 16px; opacity: 1; transition: opacity .2s ease, background .2s ease; border: none; } +.terminal-card__exit-fullscreen:hover { color: white; background: rgba(0,0,0,.8); } +.terminal-card:not(.terminal-card--fullscreen) .terminal-card__exit-fullscreen { display: none; } .terminal-card__resize-handle { position: absolute; z-index: 20; touch-action: none; } .terminal-card__resize-handle--n { top: 0; left: 10px; right: 10px; height: 8px; cursor: ns-resize; } .terminal-card__resize-handle--e { top: 10px; right: 0; bottom: 10px; width: 8px; cursor: ew-resize; }