diff --git a/src/components/gastown/TerminalBar.module.css b/src/components/gastown/TerminalBar.module.css new file mode 100644 index 0000000000..db2e57584b --- /dev/null +++ b/src/components/gastown/TerminalBar.module.css @@ -0,0 +1,12 @@ +.fullscreen { + position: fixed; + inset: 0; + z-index: 100; + padding: 0; + margin: 0; + overflow: hidden; +} + +.fullscreenTransition { + transition: all 0.3s ease-in-out; +} diff --git a/src/components/gastown/TerminalBar.tsx b/src/components/gastown/TerminalBar.tsx index 284758cdf8..0756ce9325 100644 --- a/src/components/gastown/TerminalBar.tsx +++ b/src/components/gastown/TerminalBar.tsx @@ -33,6 +33,7 @@ import { MessageCircle, } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; +import styles from './TerminalBar.module.css'; type TerminalBarProps = { townId: string; @@ -146,13 +147,60 @@ export function TerminalBar({ townId, basePath: basePathOverride }: TerminalBarP const effectiveActiveId = activeTabId ?? 'mayor'; const activeTab = allTabs.find(t => t.id === effectiveActiveId) ?? allTabs[0]; + // ── Fullscreen state (purely local — toggled via double-click / Escape) ── + const [isFullscreen, setLocalFullscreen] = useState(false); + const previousSizeRef = useRef(size); + + const enterFullscreen = useCallback(() => { + previousSizeRef.current = size; + setLocalFullscreen(true); + }, [size]); + + const exitFullscreen = useCallback(() => { + setSize(previousSizeRef.current); + setLocalFullscreen(false); + }, [setSize]); + + const toggleFullscreen = useCallback(() => { + if (isFullscreen) { + exitFullscreen(); + } else { + enterFullscreen(); + } + }, [isFullscreen, enterFullscreen, exitFullscreen]); + + // Escape key exits fullscreen + useEffect(() => { + if (!isFullscreen) return; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + exitFullscreen(); + } + }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isFullscreen, exitFullscreen]); + // ── Resize drag logic ────────────────────────────────────────────── const isDragging = useRef(false); const startPos = useRef(0); const startSize = useRef(0); + const lastClickTime = useRef(0); const onResizePointerDown = useCallback( (e: React.PointerEvent) => { + // Prevent drag on double-click (detected by < 300ms between clicks) + const now = Date.now(); + if (now - lastClickTime.current < 300) { + return; + } + lastClickTime.current = now; + + if (isFullscreen) { + exitFullscreen(); + return; + } + e.preventDefault(); isDragging.current = true; startSize.current = size; @@ -184,9 +232,16 @@ export function TerminalBar({ townId, basePath: basePathOverride }: TerminalBarP document.addEventListener('pointermove', onPointerMove); document.addEventListener('pointerup', onPointerUp); }, - [size, position, horizontal, setSize] + [size, position, horizontal, setSize, isFullscreen, exitFullscreen] ); + // Double-click handler for resize bar + const onResizeDoubleClick = useCallback(() => { + if (!collapsed) { + toggleFullscreen(); + } + }, [collapsed, toggleFullscreen]); + // ── Compute container styles ─────────────────────────────────────── const totalSize = collapsed ? COLLAPSED_SIZE : COLLAPSED_SIZE + size; @@ -270,14 +325,18 @@ export function TerminalBar({ townId, basePath: basePathOverride }: TerminalBarP return (
{position === 'bottom' && ( <> {!collapsed && ( -
+
)} @@ -300,6 +359,7 @@ export function TerminalBar({ townId, basePath: basePathOverride }: TerminalBarP size={size} townId={townId} alarmWs={alarmWs} + fullscreen={isFullscreen} /> )} @@ -312,6 +372,7 @@ export function TerminalBar({ townId, basePath: basePathOverride }: TerminalBarP size={size} townId={townId} alarmWs={alarmWs} + fullscreen={isFullscreen} /> {!collapsed && ( -
+
)} @@ -335,7 +400,11 @@ export function TerminalBar({ townId, basePath: basePathOverride }: TerminalBarP {position === 'right' && ( <> {!collapsed && ( -
+
)} @@ -358,6 +427,7 @@ export function TerminalBar({ townId, basePath: basePathOverride }: TerminalBarP size={size} townId={townId} alarmWs={alarmWs} + fullscreen={isFullscreen} /> )} @@ -382,9 +452,14 @@ export function TerminalBar({ townId, basePath: basePathOverride }: TerminalBarP size={size} townId={townId} alarmWs={alarmWs} + fullscreen={isFullscreen} /> {!collapsed && ( -
+
)} @@ -758,6 +833,7 @@ function TerminalContent({ size, townId, alarmWs, + fullscreen, }: { activeTab: TabDef; collapsed: boolean; @@ -765,6 +841,7 @@ function TerminalContent({ size: number; townId: string; alarmWs: AlarmWsResult; + fullscreen?: boolean; }) { if (collapsed) return null; @@ -776,8 +853,8 @@ function TerminalContent({ animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }} - style={horizontal ? { height: size } : { width: size }} - className={`overflow-hidden ${horizontal ? '' : 'h-full'}`} + style={fullscreen ? {} : horizontal ? { height: size } : { width: size }} + className={`overflow-hidden ${horizontal ? '' : 'h-full'} ${fullscreen ? 'h-full' : ''}`} > {activeTab.kind === 'mayor' ? (