From ec367549b4807784bba10ad38b9ef717a733b29b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:13:56 +0200 Subject: [PATCH] feat(terminal): add mobile shell controls Add a compact mobile toolbar for fullscreen terminal control keys and adjust terminal sizing so the toolbar does not cover rows. --- resources/css/app.css | 7 ++ resources/js/terminal.js | 80 ++++++++++++++-- .../project/shared/terminal.blade.php | 26 +++++- .../Feature/RealtimeTerminalPackagingTest.php | 93 ++++++++++++++++++- 4 files changed, 194 insertions(+), 12 deletions(-) diff --git a/resources/css/app.css b/resources/css/app.css index 936e0c713..de92bf0c9 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -53,6 +53,13 @@ If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ + +@layer components { + .terminal-mobile-key { + @apply min-h-10 rounded-md border border-white/10 bg-white/10 px-2 py-2 text-sm font-semibold text-white shadow-inner active:bg-white/25; + } +} + @layer base { *, diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 7a7fc8536..8498f4356 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -52,6 +52,7 @@ export function initializeTerminalComponent() { // Visibility handling - prevent disconnects when tab loses focus isDocumentVisible: true, wasConnectedBeforeHidden: false, + mobileToolbarCollapsed: false, init() { this.setupTerminal(); @@ -538,6 +539,64 @@ export function initializeTerminalComponent() { }); }, + + sendTerminalInput(data) { + if (!this.term || !this.terminalActive) { + return; + } + + this.term.focus(); + this.sendMessage({ message: data }); + }, + + sendTerminalControl(sequence) { + const terminalSequences = { + arrowUp: '\x1b[A', + arrowDown: '\x1b[B', + arrowRight: '\x1b[C', + arrowLeft: '\x1b[D', + tab: '\t', + escape: '\x1b', + ctrlC: '\x03' + }; + + if (terminalSequences[sequence]) { + this.sendTerminalInput(terminalSequences[sequence]); + } + }, + + async pasteFromClipboard() { + if (!navigator.clipboard?.readText) { + this.$wire.dispatch('error', 'Clipboard paste is not available in this browser.'); + return; + } + + try { + const text = await navigator.clipboard.readText(); + if (text) { + this.sendTerminalInput(text); + } + } catch (error) { + logTerminal('warn', '[Terminal] Clipboard paste failed:', error); + this.$wire.dispatch('error', 'Clipboard paste permission was denied.'); + } + }, + + async copyTerminalSelection() { + const selection = this.term?.getSelection(); + if (!selection) { + this.$wire.dispatch('error', 'Select terminal text before copying.'); + return; + } + + try { + await navigator.clipboard.writeText(selection); + } catch (error) { + logTerminal('warn', '[Terminal] Clipboard copy failed:', error); + this.$wire.dispatch('error', 'Clipboard copy permission was denied.'); + } + }, + keepAlive() { if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.sendMessage({ ping: true }); @@ -629,15 +688,20 @@ export function initializeTerminalComponent() { // Force a refresh of the fit addon dimensions this.fitAddon.fit(); - // Get fresh dimensions after fit - const wrapperHeight = this.$refs.terminalWrapper.clientHeight; - const wrapperWidth = this.$refs.terminalWrapper.clientWidth; + // Get fresh dimensions from the terminal element itself. The mobile + // toolbar can live beside the terminal in normal flow, so wrapper dimensions + // would include controls that should not be counted as terminal rows. + const terminalElement = document.getElementById('terminal'); + const terminalHeight = terminalElement?.clientHeight || this.$refs.terminalWrapper.clientHeight; + const terminalWidth = terminalElement?.clientWidth || this.$refs.terminalWrapper.clientWidth; - // Account for terminal container padding (px-2 py-1 = 8px left/right, 4px top/bottom) - const horizontalPadding = 16; // 8px * 2 (left + right) - const verticalPadding = 8; // 4px * 2 (top + bottom) - const height = wrapperHeight - verticalPadding; - const width = wrapperWidth - horizontalPadding; + // Account for terminal container padding. In fullscreen mobile mode, + // the fixed toolbar sits over the terminal container, so reserve its height + // when calculating rows to keep the prompt above the controls. + const horizontalPadding = 16; // px-2 = 8px * 2 (left + right) + const verticalPadding = 8; // py-1 = 4px * 2 (top + bottom) + const height = terminalHeight - verticalPadding; + const width = terminalWidth - horizontalPadding; // Check if dimensions are valid if (height <= 0 || width <= 0) { diff --git a/resources/views/livewire/project/shared/terminal.blade.php b/resources/views/livewire/project/shared/terminal.blade.php index c46c5f316..0adb92db6 100644 --- a/resources/views/livewire/project/shared/terminal.blade.php +++ b/resources/views/livewire/project/shared/terminal.blade.php @@ -18,10 +18,32 @@ @endif
+ :class="fullscreen ? 'fixed inset-0 z-[9999] m-0 h-[100dvh] w-screen max-w-none overflow-hidden rounded-none !bg-black p-0' : 'relative w-full h-full py-4 mx-auto max-h-[510px]'">
+ :class="fullscreen ? (mobileToolbarCollapsed ? 'h-[calc(100dvh-3.5rem)] mb-14 px-2 py-1 bg-black' : 'h-[calc(100dvh-6rem)] mb-[6rem] px-2 py-1 bg-black') : 'h-[510px] max-h-[calc(100dvh-10rem)] overflow-hidden px-2 py-1 rounded-sm bg-black'" + x-show="terminalActive"> +
+
+
+
+ Terminal keys + +
+
+ + + + + + +
+