💄 day/night themes

This commit is contained in:
2026-02-21 10:26:50 +10:00
parent 9bd46127a6
commit 740faae8c8
5 changed files with 76 additions and 11 deletions

View File

@@ -7,12 +7,13 @@ import { useEditorStore } from './store/editorStore'
import './styles/app.css'
export default function App(): JSX.Element {
const { setFileTree, activeFilePath, isDirty, markSaved, activeFileContent } =
const { setFileTree, activeFilePath, isDirty, markSaved, activeFileContent, theme, toggleTheme } =
useEditorStore()
// Load file tree on mount
// Load file tree on mount and apply persisted theme
useEffect(() => {
window.api.listFiles().then(setFileTree)
document.documentElement.classList.toggle('light', theme === 'light')
}, [])
// Handle Cmd+S / Ctrl+S
@@ -34,6 +35,13 @@ export default function App(): JSX.Element {
<div className="app-layout">
<div className="app-titlebar">
<span className="app-titlebar-title">Hohoff Editor</span>
<button
className="app-titlebar-theme-btn"
onClick={toggleTheme}
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
>
{theme === 'dark' ? '☀' : '☾'}
</button>
</div>
<aside className="sidebar">
<FileTree />

View File

@@ -46,7 +46,7 @@ const annotationField = StateField.define<DecorationSet>({
const themeCompartment = new Compartment()
function buildTheme(fontSize: number): ReturnType<typeof EditorView.theme> {
function buildTheme(fontSize: number, dark: boolean): ReturnType<typeof EditorView.theme> {
return EditorView.theme(
{
'&': {
@@ -67,8 +67,8 @@ function buildTheme(fontSize: number): ReturnType<typeof EditorView.theme> {
caretColor: 'var(--accent)'
},
'.cm-cursor': { borderLeftColor: 'var(--accent)' },
'.cm-selectionBackground': { backgroundColor: 'rgba(167,139,95,0.2)' },
'&.cm-focused .cm-selectionBackground': { backgroundColor: 'rgba(167,139,95,0.3)' },
'.cm-selectionBackground': { backgroundColor: 'var(--active-bg)' },
'&.cm-focused .cm-selectionBackground': { backgroundColor: 'var(--active-bg)' },
'.cm-line': { padding: '0', marginBottom: '6px' },
'.cm-gutters': { display: 'none' },
'.cm-activeLine': { backgroundColor: 'transparent' },
@@ -98,14 +98,14 @@ function buildTheme(fontSize: number): ReturnType<typeof EditorView.theme> {
borderRadius: '2px'
}
},
{ dark: true }
{ dark }
)
}
export function MarkdownEditor(): JSX.Element {
const containerRef = useRef<HTMLDivElement>(null)
const viewRef = useRef<EditorView | null>(null)
const { activeFilePath, activeFileContent, setContent, annotations, fontSize } = useEditorStore()
const { activeFilePath, activeFileContent, setContent, annotations, fontSize, theme } = useEditorStore()
// Initialize CodeMirror once
useEffect(() => {
@@ -120,7 +120,7 @@ export function MarkdownEditor(): JSX.Element {
markdown(),
syntaxHighlighting(defaultHighlightStyle),
annotationField,
themeCompartment.of(buildTheme(fontSize)),
themeCompartment.of(buildTheme(fontSize, theme === 'dark')),
EditorView.lineWrapping,
EditorView.updateListener.of((update) => {
if (update.docChanged) {
@@ -161,12 +161,12 @@ export function MarkdownEditor(): JSX.Element {
view.dispatch({ effects: setAnnotationsEffect.of(annotations) })
}, [annotations])
// Reconfigure theme when font size changes
// Reconfigure theme when font size or colour theme changes
useEffect(() => {
const view = viewRef.current
if (!view) return
view.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize)) })
}, [fontSize])
view.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark')) })
}, [fontSize, theme])
return (
<div className="editor-container">

View File

@@ -42,6 +42,10 @@ interface EditorState {
// Editor font size
fontSize: number
setFontSize: (size: number) => void
// Theme
theme: 'dark' | 'light'
toggleTheme: () => void
}
export const useEditorStore = create<EditorState>((set, get) => ({
@@ -133,5 +137,15 @@ export const useEditorStore = create<EditorState>((set, get) => ({
const clamped = Math.max(11, Math.min(24, size))
localStorage.setItem('editorFontSize', String(clamped))
set({ fontSize: clamped })
},
theme: (localStorage.getItem('editorTheme') as 'dark' | 'light') || 'dark',
toggleTheme: () => {
set((s) => {
const next = s.theme === 'dark' ? 'light' : 'dark'
localStorage.setItem('editorTheme', next)
document.documentElement.classList.toggle('light', next === 'light')
return { theme: next }
})
}
}))

View File

@@ -28,6 +28,25 @@
text-transform: uppercase;
}
.app-titlebar-theme-btn {
position: absolute;
right: 12px;
background: none;
border: none;
color: var(--text-muted);
font-size: 13px;
cursor: pointer;
padding: 2px 4px;
border-radius: 4px;
line-height: 1;
-webkit-app-region: no-drag;
transition: color 0.15s;
}
.app-titlebar-theme-btn:hover {
color: var(--text-primary);
}
/* ─── Sidebar ──────────────────────────────────────────────────── */
.sidebar {
background: var(--sidebar-bg);

View File

@@ -38,6 +38,30 @@
--font-sans: -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
}
/* ─── Light mode token overrides (sepia) ──────────────────────── */
html.light {
--bg-base: #f2ead8;
--sidebar-bg: #e8dfc8;
--toolbar-bg: #ddd4bc;
--editor-bg: #f2ead8;
--message-bg: #e8dfc8;
--user-message-bg:#ddd4bc;
--input-bg: #e8dfc8;
--border: #c4b89a;
--hover-bg: rgba(120,88,40,0.08);
--active-bg: rgba(120,88,40,0.15);
--text-primary: #241c0e;
--text-secondary: #52401e;
--text-muted: #7a6640;
--heading-color: #3c2c0e;
--accent: #7a5c28;
--accent-hover: #9a7030;
}
/* ─── Base ─────────────────────────────────────────────────────── */
html, body, #root {
height: 100%;