diff --git a/react/src/main.tsx b/react/src/main.tsx index 774e22d..c77dc5b 100644 --- a/react/src/main.tsx +++ b/react/src/main.tsx @@ -201,6 +201,7 @@ type NvimPanelSummary = { detail?: string; syncMode: boolean; syncReady: boolean; + controlMode: boolean; controlEnabled: boolean; }; @@ -258,6 +259,14 @@ function Topbar({ setScale, nvimVisible, setNvimVisible, + nvimSyncMode, + setNvimSyncMode, + nvimControlMode, + setNvimControlMode, + nvimExpanded, + setNvimExpanded, + nvimFit, + setNvimFit, nvimSummary, }: { toc: CardModel["toc"]; @@ -265,6 +274,14 @@ function Topbar({ setScale: (value: number) => void; nvimVisible: boolean; setNvimVisible: (value: boolean) => void; + nvimSyncMode: boolean; + setNvimSyncMode: (value: boolean) => void; + nvimControlMode: boolean; + setNvimControlMode: (value: boolean) => void; + nvimExpanded: boolean; + setNvimExpanded: (value: boolean) => void; + nvimFit: boolean; + setNvimFit: (value: boolean) => void; nvimSummary: NvimPanelSummary; }) { return ( @@ -286,7 +303,7 @@ function Topbar({ aria-pressed={nvimVisible} onClick={() => setNvimVisible(!nvimVisible)} > - {nvimVisible ? "F1 · Ukryj Neovim" : "F1 · Pokaż Neovim"} + {nvimVisible ? "Alt+1 · Ukryj Neovim" : "Alt+1 · Pokaż Neovim"} {nvimVisible && ( <> @@ -297,14 +314,40 @@ function Topbar({ {nvimSummary.instance ?? "Neovim MCP"} {nvimSummary.detail && {nvimSummary.detail}} - - {nvimSummary.syncMode ? "F2 · SYNC ON" : "F2 · SYNC OFF"} - - - {nvimSummary.controlEnabled ? "STEROWANIE NVIM" : "NAJEDŹ · PRACUJ"} - + + + + - F3 pełny · F4 odśwież · F12 skróty + Alt+W okna · F12 skróty )} @@ -822,11 +865,17 @@ function keyForNvim(event: React.KeyboardEvent) { function NeovimPanel({ visible, syncMode, + controlMode, + expanded, + fit, scale, onSummaryChange, }: { visible: boolean; syncMode: boolean; + controlMode: boolean; + expanded: boolean; + fit: boolean; scale: number; onSummaryChange: (summary: NvimPanelSummary) => void; }) { @@ -853,7 +902,7 @@ function NeovimPanel({ const [checkpoint, setCheckpoint] = useState({ state: "idle" }); const checkpointBusy = checkpoint.state === "replaying" || checkpoint.state === "verifying"; const syncReady = syncMode && status.state === "connected" && !checkpointBusy; - const controlEnabled = pointerInside && status.state === "connected" && !checkpointBusy; + const controlEnabled = controlMode && pointerInside && status.state === "connected" && !checkpointBusy; const redraw = () => { if (canvasRef.current) { @@ -1037,8 +1086,9 @@ function NeovimPanel({ }, [panelHeightVh]); useEffect(() => { - redraw(); - }, [scale]); + const frame = window.requestAnimationFrame(redraw); + return () => window.cancelAnimationFrame(frame); + }, [expanded, fit, scale]); useEffect(() => { const refresh = () => { @@ -1070,19 +1120,20 @@ function NeovimPanel({ ].filter(Boolean).join(" · "), syncMode, syncReady, + controlMode, controlEnabled, }); - }, [activeStep, checkpoint, controlEnabled, onSummaryChange, status, syncMode, syncReady]); + }, [activeStep, checkpoint, controlEnabled, controlMode, onSummaryChange, status, syncMode, syncReady]); useEffect(() => { if (!controlEnabled) return; const protectBrowserShortcuts = (event: KeyboardEvent) => { const socket = websocketRef.current; if (socket?.readyState !== WebSocket.OPEN) return; - if (event.ctrlKey && !event.altKey && !event.metaKey && event.key.toLowerCase() === "w") { + if (event.altKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === "w") { event.preventDefault(); event.stopImmediatePropagation(); - if (!event.repeat) socket.send(JSON.stringify({ type: "input", keys: "" })); + if (!event.repeat) socket.send(JSON.stringify({ type: "input", keys: "" })); return; } const directions: Record = { @@ -1095,7 +1146,7 @@ function NeovimPanel({ event.preventDefault(); event.stopImmediatePropagation(); if (!event.repeat) { - socket.send(JSON.stringify({ type: "input", keys: `${directions[event.key]}` })); + socket.send(JSON.stringify({ type: "input", keys: `${directions[event.key]}` })); } } }; @@ -1200,7 +1251,7 @@ function NeovimPanel({
@@ -2711,11 +2762,14 @@ function ShortcutHelp({ open, onClose }: { open: boolean; onClose: () => void }) ["Ctrl + Shift + ← / →", "poprzedni / następny unikalny SNAPSHOT"], ["Alt + ← / →", "poprzedni / następny BLOCK"], ["Ctrl + ← / →", "poprzedni / następny TASK"], - ["F1", "pokaż lub ukryj panel Neovima"], - ["F2", "przełącz SYNC OFF / SYNC ON"], - ["F3", "włącz lub wyłącz pełny ekran Neovima bez suwaka"], - ["F4", "odśwież layout i ekran Neovima"], - ["Najedź na Neovim", "przekaż klawiaturę i mysz do aktywnej sesji"], + ["Alt + 1", "pokaż lub ukryj panel Neovima"], + ["Alt + 2", "przełącz SYNC OFF / SYNC ON"], + ["Alt + 3", "uzbrój lub rozbrój sterowanie Neovimem"], + ["Alt + 4", "wysoki panel 90% / zapisana wysokość normalna"], + ["Alt + 5", "dopasuj cały ekran Neovima; wyłącz ręczny suwak"], + ["Alt + 3, potem najedź", "przekaż klawiaturę i mysz do aktywnej sesji"], + ["Alt + W", "prefiks poleceń okien Neovima ()"], + ["Alt + strzałka", "przejdź do sąsiedniego okna Neovima"], ["Zatwierdź krok", "zapisz bieżący step i timestamp do raportu"], ["F12", "pokaż lub ukryj ten skorowidz"], ["Esc", "zamknij skorowidz"], @@ -2764,11 +2818,16 @@ function App({ model }: { model: CardModel }) { const [nvimSyncMode, setNvimSyncModeState] = useState( () => localStorage.getItem("stem-card-nvim-sync-mode") === "true", ); - const [nvimFullscreen, setNvimFullscreen] = useState(false); + const [nvimControlMode, setNvimControlModeState] = useState( + () => localStorage.getItem("stem-card-nvim-control-mode") === "true", + ); + const [nvimExpanded, setNvimExpandedState] = useState(false); + const [nvimFit, setNvimFitState] = useState(false); const [nvimSummary, setNvimSummary] = useState({ state: "hidden", syncMode: false, syncReady: false, + controlMode: false, controlEnabled: false, }); const [progress, setProgress] = useState(null); @@ -2787,8 +2846,41 @@ function App({ model }: { model: CardModel }) { }, [nvimSyncMode]); const setNvimVisible = (value: boolean) => { setNvimVisibleState(value); + if (!value) setNvimExpandedState(false); localStorage.setItem("stem-card-nvim-visible", String(value)); }; + const setNvimControlMode = (value: boolean) => { + setNvimControlModeState(value); + localStorage.setItem("stem-card-nvim-control-mode", String(value)); + }; + const setNvimSyncMode = (value: boolean) => { + setNvimSyncModeState(value); + localStorage.setItem("stem-card-nvim-sync-mode", String(value)); + }; + const setNvimExpanded = (value: boolean) => { + if (value) setNvimVisible(true); + if (value) setNvimFitState(false); + setNvimExpandedState(value); + }; + const setNvimFit = (value: boolean) => { + if (value) { + setNvimVisible(true); + setNvimExpandedState(false); + } + setNvimFitState(value); + }; + useEffect(() => { + let second = 0; + const first = window.requestAnimationFrame(() => { + second = window.requestAnimationFrame(() => { + window.dispatchEvent(new Event("stem:nvim-refresh")); + }); + }); + return () => { + window.cancelAnimationFrame(first); + window.cancelAnimationFrame(second); + }; + }, [nvimExpanded, nvimFit]); useEffect(() => { fetch("/api/progress") .then((response) => @@ -2806,17 +2898,21 @@ function App({ model }: { model: CardModel }) { useEffect(() => { const navigate = (event: KeyboardEvent) => { if (event.repeat) return; - if (event.key === "F1") { + const browserCommand = event.altKey && !event.ctrlKey && !event.metaKey + ? event.code + : ""; + if (browserCommand === "Digit1") { event.preventDefault(); event.stopImmediatePropagation(); setNvimVisibleState(current => { const next = !current; + if (!next) setNvimExpandedState(false); localStorage.setItem("stem-card-nvim-visible", String(next)); return next; }); return; } - if (event.key === "F2") { + if (browserCommand === "Digit2") { event.preventDefault(); event.stopImmediatePropagation(); setNvimSyncModeState(current => { @@ -2826,18 +2922,40 @@ function App({ model }: { model: CardModel }) { }); return; } - if (event.key === "F3") { + if (browserCommand === "Digit3") { event.preventDefault(); event.stopImmediatePropagation(); setNvimVisibleState(true); localStorage.setItem("stem-card-nvim-visible", "true"); - setNvimFullscreen(current => !current); + setNvimControlModeState(current => { + const next = !current; + localStorage.setItem("stem-card-nvim-control-mode", String(next)); + return next; + }); return; } - if (event.key === "F4") { + if (browserCommand === "Digit4") { event.preventDefault(); event.stopImmediatePropagation(); - window.dispatchEvent(new Event("stem:nvim-refresh")); + setNvimVisibleState(true); + localStorage.setItem("stem-card-nvim-visible", "true"); + setNvimExpandedState(current => { + const next = !current; + if (next) setNvimFitState(false); + return next; + }); + return; + } + if (browserCommand === "Digit5") { + event.preventDefault(); + event.stopImmediatePropagation(); + setNvimVisibleState(true); + localStorage.setItem("stem-card-nvim-visible", "true"); + setNvimFitState(current => { + const next = !current; + if (next) setNvimExpandedState(false); + return next; + }); return; } if (event.key === "F12") { @@ -2925,7 +3043,7 @@ function App({ model }: { model: CardModel }) { <>
diff --git a/react/src/react.css b/react/src/react.css index 4178d1e..606406e 100644 --- a/react/src/react.css +++ b/react/src/react.css @@ -8,39 +8,59 @@ position: relative; top: auto; } -.viewer-chrome.is-nvim-fullscreen { - position: fixed; - z-index: 300; - inset: 0; +.viewer-chrome.is-nvim-fit { display: flex; - width: 100vw; + width: 100%; height: 100vh; + height: 100dvh; + max-height: 100dvh; flex-direction: column; overflow: hidden; background: #11171a; } -.viewer-chrome.is-nvim-fullscreen .topbar { +.viewer-chrome.is-nvim-fit .topbar { flex: none; } -.viewer-chrome.is-nvim-fullscreen .nvim-panel { +.viewer-chrome.is-nvim-fit .nvim-panel { display: flex; min-height: 0; flex: 1; flex-direction: column; } -.viewer-chrome.is-nvim-fullscreen .nvim-screen-scroll { +.viewer-chrome.is-nvim-fit .nvim-screen-scroll { min-height: 0; height: auto !important; flex: 1; } -.viewer-chrome.is-nvim-fullscreen .nvim-resize-handle { +.viewer-chrome.is-nvim-fit .nvim-resize-handle { display: none; } -.viewer-chrome.is-nvim-fullscreen .nvim-screen-content { - overflow-y: hidden; +.viewer-chrome.is-nvim-fit .nvim-screen-stage { + width: 100%; + height: 100%; + margin: 0; + zoom: 1; +} +.viewer-chrome.is-nvim-fit .nvim-screen-content { + right: 1mm; + overflow: hidden; scrollbar-width: none; } -.viewer-chrome.is-nvim-fullscreen .nvim-screen-content::-webkit-scrollbar { +.viewer-chrome.is-nvim-fit .nvim-screen-grid { + display: flex; + width: 100%; + min-height: 0; + height: 100%; + align-items: center; + justify-content: center; +} +.viewer-chrome.is-nvim-fit .nvim-screen { + width: auto !important; + height: auto !important; + max-width: 100%; + max-height: 100%; +} +.viewer-chrome.is-nvim-fit .nvim-screen-content::-webkit-scrollbar { display: none; } .topbar-nvim-section { @@ -132,6 +152,7 @@ font: 700 10px/1.2 ui-monospace, monospace; letter-spacing: 0.04em; white-space: nowrap; + cursor: pointer; } .nvim-control-indicator.is-active { border-color: #3cb3e2; @@ -151,12 +172,33 @@ font: 700 10px/1.2 ui-monospace, monospace; letter-spacing: .03em; white-space: nowrap; + cursor: pointer; } .nvim-work-indicator.is-active { border-color: #cf3d31; background: #fff0ee; color: #9f241b; } +.nvim-work-indicator.is-engaged { + box-shadow: inset 0 0 0 1px #cf3d31; +} +.nvim-height-toggle { + flex: none; + border: 1px solid #75848b; + border-radius: 3px; + background: #f4f6f7; + color: #425159; + padding: 3px 8px; + font: 700 10px/1.2 ui-monospace, monospace; + letter-spacing: .03em; + white-space: nowrap; + cursor: pointer; +} +.nvim-height-toggle.is-active { + border-color: #267697; + background: #eaf6fb; + color: #174d64; +} .nvim-shortcut-strip { flex: none; color: #536168;