✨ distraction free mode
This commit is contained in:
@@ -112,6 +112,12 @@ function buildAppMenu(win: BrowserWindow): void {
|
|||||||
{
|
{
|
||||||
label: 'View',
|
label: 'View',
|
||||||
submenu: [
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Focus Mode',
|
||||||
|
accelerator: 'CmdOrCtrl+Shift+G',
|
||||||
|
click: () => send(win, 'toggleFocusMode')
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Toggle File Tree',
|
label: 'Toggle File Tree',
|
||||||
accelerator: 'CmdOrCtrl+Shift+1',
|
accelerator: 'CmdOrCtrl+Shift+1',
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default function App(): JSX.Element {
|
|||||||
const {
|
const {
|
||||||
setFileTree, activeFilePath, isDirty, markSaved, activeFileContent, theme, toggleTheme,
|
setFileTree, activeFilePath, isDirty, markSaved, activeFileContent, theme, toggleTheme,
|
||||||
loadSession, revisionPanelOpen, toggleRevisionPanel, fontSize, setFontSize,
|
loadSession, revisionPanelOpen, toggleRevisionPanel, fontSize, setFontSize,
|
||||||
openProjectSearch, clearActiveFile, initPrefs
|
openProjectSearch, clearActiveFile, initPrefs, focusMode, toggleFocusMode
|
||||||
} = useEditorStore()
|
} = useEditorStore()
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(
|
const [sidebarOpen, setSidebarOpen] = useState(
|
||||||
() => localStorage.getItem('sidebarOpen') !== 'false'
|
() => localStorage.getItem('sidebarOpen') !== 'false'
|
||||||
@@ -72,6 +72,8 @@ export default function App(): JSX.Element {
|
|||||||
openProjectSearch()
|
openProjectSearch()
|
||||||
} else if (action === 'openSettings') {
|
} else if (action === 'openSettings') {
|
||||||
setSettingsOpen(true)
|
setSettingsOpen(true)
|
||||||
|
} else if (action === 'toggleFocusMode') {
|
||||||
|
toggleFocusMode()
|
||||||
} else if (action === 'openProject') {
|
} else if (action === 'openProject') {
|
||||||
const picked = await window.api.pickProjectFolder()
|
const picked = await window.api.pickProjectFolder()
|
||||||
if (picked) await switchProject(picked)
|
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') {
|
} else if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'f') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
openProjectSearch()
|
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)
|
window.addEventListener('keydown', handler)
|
||||||
return () => window.removeEventListener('keydown', handler)
|
return () => window.removeEventListener('keydown', handler)
|
||||||
}, [activeFilePath, isDirty, activeFileContent])
|
}, [activeFilePath, isDirty, activeFileContent, focusMode])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="app-layout"
|
className="app-layout"
|
||||||
data-sidebar={sidebarOpen ? 'open' : 'closed'}
|
data-sidebar={sidebarOpen ? 'open' : 'closed'}
|
||||||
data-chat={chatOpen ? 'open' : 'closed'}
|
data-chat={chatOpen ? 'open' : 'closed'}
|
||||||
|
data-focus={focusMode ? 'on' : 'off'}
|
||||||
>
|
>
|
||||||
<div className="app-titlebar">
|
<div className="app-titlebar">
|
||||||
<span className="app-titlebar-title">Hohoff Editor</span>
|
<span className="app-titlebar-title">Hohoff Editor</span>
|
||||||
@@ -121,6 +129,11 @@ export default function App(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="app-titlebar-icon-group">
|
<div className="app-titlebar-icon-group">
|
||||||
|
<button
|
||||||
|
className={`app-titlebar-theme-btn${focusMode ? ' active' : ''}`}
|
||||||
|
onClick={toggleFocusMode}
|
||||||
|
title="Focus mode (⌘⇧G)"
|
||||||
|
>⊡</button>
|
||||||
<button
|
<button
|
||||||
className="app-titlebar-theme-btn"
|
className="app-titlebar-theme-btn"
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
@@ -145,6 +158,9 @@ export default function App(): JSX.Element {
|
|||||||
<aside className="chat-area">
|
<aside className="chat-area">
|
||||||
<ChatPanel />
|
<ChatPanel />
|
||||||
</aside>
|
</aside>
|
||||||
|
{focusMode && (
|
||||||
|
<button className="focus-exit-btn" onClick={toggleFocusMode} title="Exit focus mode (Esc)">✕</button>
|
||||||
|
)}
|
||||||
<ProjectSearchModal />
|
<ProjectSearchModal />
|
||||||
{settingsOpen && (
|
{settingsOpen && (
|
||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
|
|||||||
@@ -424,23 +424,27 @@ const hrPlugin = ViewPlugin.fromClass(
|
|||||||
{ decorations: v => v.decorations }
|
{ 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(
|
return EditorView.theme(
|
||||||
{
|
{
|
||||||
'&': {
|
'&': {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
fontSize: `${fontSize}px`,
|
fontSize: `${displaySize}px`,
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
color: 'var(--text-primary)'
|
color: 'var(--text-primary)'
|
||||||
},
|
},
|
||||||
'.cm-scroller': {
|
'.cm-scroller': {
|
||||||
fontFamily: 'var(--font-serif)',
|
fontFamily: 'var(--font-serif)',
|
||||||
lineHeight: '1.8',
|
lineHeight,
|
||||||
overflow: 'auto'
|
overflow: 'auto'
|
||||||
},
|
},
|
||||||
'.cm-content': {
|
'.cm-content': {
|
||||||
padding: '16px 20px',
|
padding,
|
||||||
maxWidth: '740px',
|
maxWidth,
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
caretColor: 'var(--accent)'
|
caretColor: 'var(--accent)'
|
||||||
},
|
},
|
||||||
@@ -619,7 +623,7 @@ export function MarkdownEditor(): JSX.Element {
|
|||||||
const [hasSelection, setHasSelection] = useState(false)
|
const [hasSelection, setHasSelection] = useState(false)
|
||||||
const [pendingComment, setPendingComment] = useState<{ from: number; to: number; text: string } | null>(null)
|
const [pendingComment, setPendingComment] = useState<{ from: number; to: number; text: string } | null>(null)
|
||||||
const [commentDraft, setCommentDraft] = useState('')
|
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 {
|
function saveComment(): void {
|
||||||
if (!pendingComment || !commentDraft.trim()) return
|
if (!pendingComment || !commentDraft.trim()) return
|
||||||
@@ -679,7 +683,7 @@ export function MarkdownEditor(): JSX.Element {
|
|||||||
annotationHoverTooltip,
|
annotationHoverTooltip,
|
||||||
annotationHistory,
|
annotationHistory,
|
||||||
hrPlugin,
|
hrPlugin,
|
||||||
themeCompartment.of(buildTheme(fontSize, theme === 'dark')),
|
themeCompartment.of(buildTheme(fontSize, theme === 'dark', focusMode)),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
@@ -832,12 +836,12 @@ export function MarkdownEditor(): JSX.Element {
|
|||||||
clearPendingScrollToLine()
|
clearPendingScrollToLine()
|
||||||
}, [pendingScrollToLine])
|
}, [pendingScrollToLine])
|
||||||
|
|
||||||
// Reconfigure theme when font size or colour theme changes
|
// Reconfigure theme when font size, colour theme, or focus mode changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const view = viewRef.current
|
const view = viewRef.current
|
||||||
if (!view) return
|
if (!view) return
|
||||||
view.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark')) })
|
view.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark', focusMode)) })
|
||||||
}, [fontSize, theme])
|
}, [fontSize, theme, focusMode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return window.api.onMenuAction((action) => {
|
return window.api.onMenuAction((action) => {
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ interface EditorState {
|
|||||||
rightPanelTab: 'chat' | 'feedback'
|
rightPanelTab: 'chat' | 'feedback'
|
||||||
setRightPanelTab: (tab: 'chat' | 'feedback') => void
|
setRightPanelTab: (tab: 'chat' | 'feedback') => void
|
||||||
|
|
||||||
|
// Focus / distraction-free mode
|
||||||
|
focusMode: boolean
|
||||||
|
toggleFocusMode: () => void
|
||||||
|
|
||||||
// Document outline panel
|
// Document outline panel
|
||||||
outlineOpen: boolean
|
outlineOpen: boolean
|
||||||
toggleOutline: () => void
|
toggleOutline: () => void
|
||||||
@@ -595,6 +599,13 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||||||
rightPanelTab: 'chat',
|
rightPanelTab: 'chat',
|
||||||
setRightPanelTab: (tab) => set({ rightPanelTab: tab }),
|
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',
|
outlineOpen: localStorage.getItem('outlineOpen') === 'true',
|
||||||
toggleOutline: () => set((s) => {
|
toggleOutline: () => set((s) => {
|
||||||
const next = !s.outlineOpen
|
const next = !s.outlineOpen
|
||||||
|
|||||||
@@ -4,7 +4,44 @@
|
|||||||
grid-template-rows: 38px calc(100vh - 38px);
|
grid-template-rows: 38px calc(100vh - 38px);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
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'] {
|
.app-layout[data-sidebar='closed'] {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user