From 732a90aeab42c67760cd77f624cee5df685bac29 Mon Sep 17 00:00:00 2001 From: jongtae park Date: Wed, 20 May 2026 14:36:42 +0900 Subject: [PATCH] feat: toggle the settings panel to free up canvas space Add a slim handle on the canvas/panel boundary that collapses the right panel so the presenter can use the full window for the whiteboard. The handle relocates to the bottom edge on narrow viewports where the panel stacks below, and the collapsed state is persisted in localStorage so it survives reloads. Co-Authored-By: Claude Opus 4.7 (1M context) --- public/app.js | 39 ++++++++++++++++++++++++- public/style.css | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/public/app.js b/public/app.js index 712f2af..0b6c63e 100644 --- a/public/app.js +++ b/public/app.js @@ -20,6 +20,7 @@ const OPENAI_TRANSCRIPTION_MODELS = [ ]; const MOONSHINE_MODELS = ["tiny", "small", "medium"]; const MIC_STORAGE_KEY = "autopreso.mic"; +const PANEL_HIDDEN_STORAGE_KEY = "autopreso.panelHidden"; const STARTER_STAGING_ELEMENTS = []; @@ -57,6 +58,14 @@ function saveStoredMic(mic) { localStorage.setItem(MIC_STORAGE_KEY, JSON.stringify(mic)); } +function loadStoredPanelHidden() { + try { + return localStorage.getItem(PANEL_HIDDEN_STORAGE_KEY) === "1"; + } catch { + return false; + } +} + function App() { const [api, setApi] = React.useState(null); const [mode, setMode] = React.useState("staging"); @@ -74,6 +83,7 @@ function App() { const [sttError, setSttError] = React.useState(false); const [expandedRow, setExpandedRow] = React.useState(null); const [mic, setMic] = React.useState(loadStoredMic); + const [panelHidden, setPanelHidden] = React.useState(loadStoredPanelHidden); const [analyser, setAnalyser] = React.useState(null); const [resetConfirming, setResetConfirming] = React.useState(false); const [resetting, setResetting] = React.useState(false); @@ -107,6 +117,12 @@ function App() { React.useEffect(() => { listeningRef.current = listening; }, [listening]); + + React.useEffect(() => { + try { + localStorage.setItem(PANEL_HIDDEN_STORAGE_KEY, panelHidden ? "1" : "0"); + } catch {} + }, [panelHidden]); const [isFullscreen, setIsFullscreen] = React.useState(false); React.useEffect(() => { @@ -649,10 +665,31 @@ function App() { return React.createElement( "main", - { className: `shell mode-${mode}`, ref: shellRef }, + { + className: `shell mode-${mode}${panelHidden ? " panel-hidden" : ""}`, + ref: shellRef, + }, React.createElement( "section", { className: "canvas-wrap", ref: canvasWrapRef }, + React.createElement( + "button", + { + type: "button", + className: "panel-toggle", + onClick: () => setPanelHidden((v) => !v), + title: panelHidden ? "Show settings panel" : "Hide settings panel", + "aria-label": panelHidden + ? "Show settings panel" + : "Hide settings panel", + "aria-expanded": !panelHidden, + }, + React.createElement( + "span", + { className: "panel-toggle-icon", "aria-hidden": "true" }, + panelHidden ? "‹" : "›", + ), + ), React.createElement(Excalidraw, { excalidrawAPI: setApi, initialData: { diff --git a/public/style.css b/public/style.css index 9a3160a..07bcb6f 100644 --- a/public/style.css +++ b/public/style.css @@ -22,6 +22,12 @@ body { grid-template-columns: minmax(0, 1fr) 360px; height: 100vh; width: 100vw; + overflow: hidden; + transition: grid-template-columns 240ms ease; +} + +.shell.panel-hidden { + grid-template-columns: minmax(0, 1fr) 0; } .canvas-wrap { @@ -31,6 +37,38 @@ body { overflow: hidden; } +.panel-toggle { + position: absolute; + top: 50%; + right: 0; + transform: translateY(-50%); + z-index: 50; + width: 18px; + height: 56px; + padding: 0; + border-radius: 8px 0 0 8px; + background: rgba(255, 253, 248, 0.92); + color: #6d675e; + border: 1px solid #dedbd2; + border-right: none; + font-weight: 600; + font-size: 14px; + line-height: 1; + box-shadow: -2px 2px 8px rgba(30, 30, 30, 0.06); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + transition: background 160ms ease, color 160ms ease; +} + +.panel-toggle:hover { + background: #fffdf8; + color: #1e1e1e; +} + +.panel-toggle-icon { + display: inline-block; +} + .stage-overlay { position: absolute; left: 0; @@ -96,6 +134,15 @@ body { padding: 22px; overflow: auto; position: relative; + min-width: 0; + min-height: 0; + transition: opacity 200ms ease; +} + +.shell.panel-hidden .panel { + opacity: 0; + pointer-events: none; + overflow: hidden; } .brand h1 { @@ -646,13 +693,40 @@ button:disabled { grid-template-rows: minmax(0, 1fr) auto; } + .shell.panel-hidden { + grid-template-columns: 1fr; + grid-template-rows: minmax(0, 1fr) 0; + } + .canvas-wrap { height: 62vh; } + .shell.panel-hidden .canvas-wrap { + height: 100vh; + } + .panel { height: 38vh; border-left: 0; border-top: 1px solid #dedbd2; } + + .panel-toggle { + top: auto; + right: auto; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 56px; + height: 18px; + border-radius: 8px 8px 0 0; + border: 1px solid #dedbd2; + border-bottom: none; + box-shadow: 0 -2px 8px rgba(30, 30, 30, 0.06); + } + + .panel-toggle .panel-toggle-icon { + transform: rotate(90deg); + } }