✨ distraction free mode
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'] {
|
||||
|
||||
Reference in New Issue
Block a user