distraction free mode

This commit is contained in:
2026-05-01 11:02:21 +10:00
parent e6e3e52f10
commit 5a5d3dd682
6 changed files with 88 additions and 14 deletions

View File

@@ -112,6 +112,12 @@ function buildAppMenu(win: BrowserWindow): void {
{
label: 'View',
submenu: [
{
label: 'Focus Mode',
accelerator: 'CmdOrCtrl+Shift+G',
click: () => send(win, 'toggleFocusMode')
},
{ type: 'separator' },
{
label: 'Toggle File Tree',
accelerator: 'CmdOrCtrl+Shift+1',

View File

@@ -14,7 +14,7 @@ export default function App(): JSX.Element {
const {
setFileTree, activeFilePath, isDirty, markSaved, activeFileContent, theme, toggleTheme,
loadSession, revisionPanelOpen, toggleRevisionPanel, fontSize, setFontSize,
openProjectSearch, clearActiveFile, initPrefs
openProjectSearch, clearActiveFile, initPrefs, focusMode, toggleFocusMode
} = useEditorStore()
const [sidebarOpen, setSidebarOpen] = useState(
() => localStorage.getItem('sidebarOpen') !== 'false'
@@ -72,6 +72,8 @@ export default function App(): JSX.Element {
openProjectSearch()
} else if (action === 'openSettings') {
setSettingsOpen(true)
} else if (action === 'toggleFocusMode') {
toggleFocusMode()
} else if (action === 'openProject') {
const picked = await window.api.pickProjectFolder()
if (picked) await switchProject(picked)
@@ -92,17 +94,23 @@ export default function App(): JSX.Element {
} else if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'f') {
e.preventDefault()
openProjectSearch()
} else if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'g') {
e.preventDefault()
toggleFocusMode()
} else if (e.key === 'Escape' && focusMode) {
toggleFocusMode()
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [activeFilePath, isDirty, activeFileContent])
}, [activeFilePath, isDirty, activeFileContent, focusMode])
return (
<div
className="app-layout"
data-sidebar={sidebarOpen ? 'open' : 'closed'}
data-chat={chatOpen ? 'open' : 'closed'}
data-focus={focusMode ? 'on' : 'off'}
>
<div className="app-titlebar">
<span className="app-titlebar-title">Hohoff Editor</span>
@@ -121,6 +129,11 @@ export default function App(): JSX.Element {
/>
</div>
<div className="app-titlebar-icon-group">
<button
className={`app-titlebar-theme-btn${focusMode ? ' active' : ''}`}
onClick={toggleFocusMode}
title="Focus mode (⌘⇧G)"
></button>
<button
className="app-titlebar-theme-btn"
onClick={toggleTheme}
@@ -145,6 +158,9 @@ export default function App(): JSX.Element {
<aside className="chat-area">
<ChatPanel />
</aside>
{focusMode && (
<button className="focus-exit-btn" onClick={toggleFocusMode} title="Exit focus mode (Esc)"></button>
)}
<ProjectSearchModal />
{settingsOpen && (
<SettingsDialog

View File

@@ -424,23 +424,27 @@ const hrPlugin = ViewPlugin.fromClass(
{ decorations: v => v.decorations }
)
function buildTheme(fontSize: number, dark: boolean): ReturnType<typeof EditorView.theme> {
function buildTheme(fontSize: number, dark: boolean, focusMode = false): ReturnType<typeof EditorView.theme> {
const displaySize = focusMode ? Math.max(fontSize + 3, 18) : fontSize
const maxWidth = focusMode ? '92vw' : '740px'
const padding = focusMode ? '40px 4vw' : '16px 20px'
const lineHeight = focusMode ? '1.9' : '1.8'
return EditorView.theme(
{
'&': {
height: '100%',
fontSize: `${fontSize}px`,
fontSize: `${displaySize}px`,
backgroundColor: 'transparent',
color: 'var(--text-primary)'
},
'.cm-scroller': {
fontFamily: 'var(--font-serif)',
lineHeight: '1.8',
lineHeight,
overflow: 'auto'
},
'.cm-content': {
padding: '16px 20px',
maxWidth: '740px',
padding,
maxWidth,
margin: '0 auto',
caretColor: 'var(--accent)'
},
@@ -619,7 +623,7 @@ export function MarkdownEditor(): JSX.Element {
const [hasSelection, setHasSelection] = useState(false)
const [pendingComment, setPendingComment] = useState<{ from: number; to: number; text: string } | null>(null)
const [commentDraft, setCommentDraft] = useState('')
const { activeFilePath, activeFileContent, setContent, annotations, fontSize, theme, scrollPositions, pendingScrollToLine, clearPendingScrollToLine } = useEditorStore()
const { activeFilePath, activeFileContent, setContent, annotations, fontSize, theme, focusMode, scrollPositions, pendingScrollToLine, clearPendingScrollToLine } = useEditorStore()
function saveComment(): void {
if (!pendingComment || !commentDraft.trim()) return
@@ -679,7 +683,7 @@ export function MarkdownEditor(): JSX.Element {
annotationHoverTooltip,
annotationHistory,
hrPlugin,
themeCompartment.of(buildTheme(fontSize, theme === 'dark')),
themeCompartment.of(buildTheme(fontSize, theme === 'dark', focusMode)),
EditorView.lineWrapping,
EditorView.updateListener.of((update) => {
if (update.docChanged) {
@@ -832,12 +836,12 @@ export function MarkdownEditor(): JSX.Element {
clearPendingScrollToLine()
}, [pendingScrollToLine])
// Reconfigure theme when font size or colour theme changes
// Reconfigure theme when font size, colour theme, or focus mode changes
useEffect(() => {
const view = viewRef.current
if (!view) return
view.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark')) })
}, [fontSize, theme])
view.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark', focusMode)) })
}, [fontSize, theme, focusMode])
useEffect(() => {
return window.api.onMenuAction((action) => {

View File

@@ -83,6 +83,10 @@ interface EditorState {
rightPanelTab: 'chat' | 'feedback'
setRightPanelTab: (tab: 'chat' | 'feedback') => void
// Focus / distraction-free mode
focusMode: boolean
toggleFocusMode: () => void
// Document outline panel
outlineOpen: boolean
toggleOutline: () => void
@@ -595,6 +599,13 @@ export const useEditorStore = create<EditorState>((set, get) => ({
rightPanelTab: 'chat',
setRightPanelTab: (tab) => set({ rightPanelTab: tab }),
focusMode: localStorage.getItem('focusMode') === 'true',
toggleFocusMode: () => set((s) => {
const next = !s.focusMode
localStorage.setItem('focusMode', String(next))
return { focusMode: next }
}),
outlineOpen: localStorage.getItem('outlineOpen') === 'true',
toggleOutline: () => set((s) => {
const next = !s.outlineOpen

View File

@@ -4,7 +4,44 @@
grid-template-rows: 38px calc(100vh - 38px);
height: 100vh;
overflow: hidden;
transition: grid-template-columns 0.2s ease;
transition: grid-template-columns 0.25s ease, grid-template-rows 0.25s ease;
}
/* ─── Focus / distraction-free mode ───────────────────────────── */
.app-layout[data-focus='on'] {
grid-template-columns: 0 1fr 0;
grid-template-rows: 0 100vh;
}
.app-layout[data-focus='on'] .app-titlebar {
overflow: hidden;
border: none;
}
.app-layout[data-focus='on'] .toolbar {
display: none;
}
.focus-exit-btn {
position: fixed;
top: 10px;
right: 14px;
z-index: 9999;
background: none;
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 13px;
padding: 3px 7px;
border-radius: 4px;
cursor: pointer;
opacity: 0.12;
transition: opacity 0.2s;
-webkit-app-region: no-drag;
}
.focus-exit-btn:hover {
opacity: 0.6;
color: var(--text-primary);
}
.app-layout[data-sidebar='closed'] {