feat: add focus mode to maximize scenario panel
Signed-off-by: Abhinav Sinha <[email protected]>
This commit is contained in:
@@ -54,6 +54,36 @@ export default function App() {
|
|||||||
const tmDragY0 = useRef(0)
|
const tmDragY0 = useRef(0)
|
||||||
const tmDragH0 = useRef(0)
|
const tmDragH0 = useRef(0)
|
||||||
|
|
||||||
|
// Focus mode — collapses sidebar, bundles nav, terminal for distraction-free reading
|
||||||
|
const [focusMode, setFocusMode] = useState(false)
|
||||||
|
const focusSavedState = useRef(null)
|
||||||
|
|
||||||
|
const toggleFocusMode = useCallback(() => {
|
||||||
|
if (!focusMode) {
|
||||||
|
// Save current states then collapse everything
|
||||||
|
focusSavedState.current = { sidebarCollapsed, bundlesCollapsed, termCollapsed }
|
||||||
|
setSidebarCollapsed(true)
|
||||||
|
setBundlesCollapsed(true)
|
||||||
|
setTermCollapsed(true)
|
||||||
|
setFocusMode(true)
|
||||||
|
} else {
|
||||||
|
// Restore previous states
|
||||||
|
const saved = focusSavedState.current || {}
|
||||||
|
setSidebarCollapsed(saved.sidebarCollapsed ?? false)
|
||||||
|
setBundlesCollapsed(saved.bundlesCollapsed ?? false)
|
||||||
|
setTermCollapsed(saved.termCollapsed ?? false)
|
||||||
|
setFocusMode(false)
|
||||||
|
}
|
||||||
|
}, [focusMode, sidebarCollapsed, bundlesCollapsed, termCollapsed])
|
||||||
|
|
||||||
|
// If the user manually opens any panel while in focus mode, exit focus mode
|
||||||
|
// so the button icon reverts to "expand" (next click re-collapses everything)
|
||||||
|
useEffect(() => {
|
||||||
|
if (focusMode && (!sidebarCollapsed || !bundlesCollapsed || !termCollapsed)) {
|
||||||
|
setFocusMode(false)
|
||||||
|
}
|
||||||
|
}, [sidebarCollapsed, bundlesCollapsed, termCollapsed, focusMode])
|
||||||
|
|
||||||
// Track previous scenario id to teardown on switch
|
// Track previous scenario id to teardown on switch
|
||||||
const prevActiveIdRef = useRef(null)
|
const prevActiveIdRef = useRef(null)
|
||||||
|
|
||||||
@@ -330,6 +360,8 @@ export default function App() {
|
|||||||
isExamMode={!!examSession}
|
isExamMode={!!examSession}
|
||||||
examProgress={examProgress}
|
examProgress={examProgress}
|
||||||
totalExamWeight={totalExamWeight}
|
totalExamWeight={totalExamWeight}
|
||||||
|
focusMode={focusMode}
|
||||||
|
onToggleFocus={toggleFocusMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ async function resetProgress(scope, opts) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ScenarioPanel({ scenario, onProgressUpdate, onScenarioStart, isExamMode, examProgress, totalExamWeight }) {
|
export default function ScenarioPanel({ scenario, onProgressUpdate, onScenarioStart, isExamMode, examProgress, totalExamWeight, focusMode, onToggleFocus }) {
|
||||||
const [tab, setTab] = useState('problem')
|
const [tab, setTab] = useState('problem')
|
||||||
const [setupState, setSetupState] = useState('idle') // idle | running | done | error
|
const [setupState, setSetupState] = useState('idle') // idle | running | done | error
|
||||||
const [validating, setValidating] = useState(false)
|
const [validating, setValidating] = useState(false)
|
||||||
@@ -216,6 +216,29 @@ export default function ScenarioPanel({ scenario, onProgressUpdate, onScenarioSt
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.titleRow}>
|
<div className={styles.titleRow}>
|
||||||
<div className={styles.scenarioTitle}>{scenario.title}</div>
|
<div className={styles.scenarioTitle}>{scenario.title}</div>
|
||||||
|
{onToggleFocus && !isExamMode && (
|
||||||
|
<button
|
||||||
|
className={focusMode ? styles.focusBtnActive : styles.focusBtn}
|
||||||
|
onClick={onToggleFocus}
|
||||||
|
title={focusMode ? 'Exit focus mode' : 'Focus mode — hide sidebar, terminal & nav'}
|
||||||
|
>
|
||||||
|
{focusMode ? (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="4 14 10 14 10 20" />
|
||||||
|
<polyline points="20 10 14 10 14 4" />
|
||||||
|
<line x1="10" y1="14" x2="3" y2="21" />
|
||||||
|
<line x1="21" y1="3" x2="14" y2="10" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="15 3 21 3 21 9" />
|
||||||
|
<polyline points="9 21 3 21 3 15" />
|
||||||
|
<line x1="21" y1="3" x2="14" y2="10" />
|
||||||
|
<line x1="3" y1="21" x2="10" y2="14" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{isExamMode && scenario.progress?.started_at && (
|
{isExamMode && scenario.progress?.started_at && (
|
||||||
<div className={styles.progressStats}>
|
<div className={styles.progressStats}>
|
||||||
<div className={styles.statItem}>
|
<div className={styles.statItem}>
|
||||||
@@ -238,6 +261,29 @@ export default function ScenarioPanel({ scenario, onProgressUpdate, onScenarioSt
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{onToggleFocus && isExamMode && (
|
||||||
|
<button
|
||||||
|
className={focusMode ? styles.focusBtnActive : styles.focusBtn}
|
||||||
|
onClick={onToggleFocus}
|
||||||
|
title={focusMode ? 'Exit focus mode' : 'Focus mode — hide sidebar, terminal & nav'}
|
||||||
|
>
|
||||||
|
{focusMode ? (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="4 14 10 14 10 20" />
|
||||||
|
<polyline points="20 10 14 10 14 4" />
|
||||||
|
<line x1="10" y1="14" x2="3" y2="21" />
|
||||||
|
<line x1="21" y1="3" x2="14" y2="10" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="15 3 21 3 21 9" />
|
||||||
|
<polyline points="9 21 3 21 3 15" />
|
||||||
|
<line x1="21" y1="3" x2="14" y2="10" />
|
||||||
|
<line x1="3" y1="21" x2="10" y2="14" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{scenario.progress?.status !== 'not_started' && scenario.progress?.attempts > 0 && (
|
{scenario.progress?.status !== 'not_started' && scenario.progress?.attempts > 0 && (
|
||||||
<button
|
<button
|
||||||
className={styles.resetBtn}
|
className={styles.resetBtn}
|
||||||
|
|||||||
@@ -128,6 +128,34 @@
|
|||||||
background: var(--red-dim);
|
background: var(--red-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.focusBtn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border2);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-3);
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
align-self: center;
|
||||||
|
transition: color 0.12s, border-color 0.12s, background 0.12s;
|
||||||
|
}
|
||||||
|
.focusBtn:hover {
|
||||||
|
color: var(--green);
|
||||||
|
border-color: color-mix(in srgb, var(--green) 40%, transparent);
|
||||||
|
background: var(--green-dim);
|
||||||
|
}
|
||||||
|
.focusBtnActive {
|
||||||
|
composes: focusBtn;
|
||||||
|
color: var(--green);
|
||||||
|
border-color: color-mix(in srgb, var(--green) 40%, transparent);
|
||||||
|
background: var(--green-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.completedBanner {
|
.completedBanner {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user