💄 day/night themes
This commit is contained in:
@@ -7,12 +7,13 @@ import { useEditorStore } from './store/editorStore'
|
|||||||
import './styles/app.css'
|
import './styles/app.css'
|
||||||
|
|
||||||
export default function App(): JSX.Element {
|
export default function App(): JSX.Element {
|
||||||
const { setFileTree, activeFilePath, isDirty, markSaved, activeFileContent } =
|
const { setFileTree, activeFilePath, isDirty, markSaved, activeFileContent, theme, toggleTheme } =
|
||||||
useEditorStore()
|
useEditorStore()
|
||||||
|
|
||||||
// Load file tree on mount
|
// Load file tree on mount and apply persisted theme
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.api.listFiles().then(setFileTree)
|
window.api.listFiles().then(setFileTree)
|
||||||
|
document.documentElement.classList.toggle('light', theme === 'light')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Handle Cmd+S / Ctrl+S
|
// Handle Cmd+S / Ctrl+S
|
||||||
@@ -34,6 +35,13 @@ export default function App(): JSX.Element {
|
|||||||
<div className="app-layout">
|
<div className="app-layout">
|
||||||
<div className="app-titlebar">
|
<div className="app-titlebar">
|
||||||
<span className="app-titlebar-title">Hohoff Editor</span>
|
<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>
|
</div>
|
||||||
<aside className="sidebar">
|
<aside className="sidebar">
|
||||||
<FileTree />
|
<FileTree />
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const annotationField = StateField.define<DecorationSet>({
|
|||||||
|
|
||||||
const themeCompartment = new Compartment()
|
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(
|
return EditorView.theme(
|
||||||
{
|
{
|
||||||
'&': {
|
'&': {
|
||||||
@@ -67,8 +67,8 @@ function buildTheme(fontSize: number): ReturnType<typeof EditorView.theme> {
|
|||||||
caretColor: 'var(--accent)'
|
caretColor: 'var(--accent)'
|
||||||
},
|
},
|
||||||
'.cm-cursor': { borderLeftColor: 'var(--accent)' },
|
'.cm-cursor': { borderLeftColor: 'var(--accent)' },
|
||||||
'.cm-selectionBackground': { backgroundColor: 'rgba(167,139,95,0.2)' },
|
'.cm-selectionBackground': { backgroundColor: 'var(--active-bg)' },
|
||||||
'&.cm-focused .cm-selectionBackground': { backgroundColor: 'rgba(167,139,95,0.3)' },
|
'&.cm-focused .cm-selectionBackground': { backgroundColor: 'var(--active-bg)' },
|
||||||
'.cm-line': { padding: '0', marginBottom: '6px' },
|
'.cm-line': { padding: '0', marginBottom: '6px' },
|
||||||
'.cm-gutters': { display: 'none' },
|
'.cm-gutters': { display: 'none' },
|
||||||
'.cm-activeLine': { backgroundColor: 'transparent' },
|
'.cm-activeLine': { backgroundColor: 'transparent' },
|
||||||
@@ -98,14 +98,14 @@ function buildTheme(fontSize: number): ReturnType<typeof EditorView.theme> {
|
|||||||
borderRadius: '2px'
|
borderRadius: '2px'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ dark: true }
|
{ dark }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MarkdownEditor(): JSX.Element {
|
export function MarkdownEditor(): JSX.Element {
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const viewRef = useRef<EditorView | null>(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
|
// Initialize CodeMirror once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -120,7 +120,7 @@ export function MarkdownEditor(): JSX.Element {
|
|||||||
markdown(),
|
markdown(),
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
annotationField,
|
annotationField,
|
||||||
themeCompartment.of(buildTheme(fontSize)),
|
themeCompartment.of(buildTheme(fontSize, theme === 'dark')),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
@@ -161,12 +161,12 @@ export function MarkdownEditor(): JSX.Element {
|
|||||||
view.dispatch({ effects: setAnnotationsEffect.of(annotations) })
|
view.dispatch({ effects: setAnnotationsEffect.of(annotations) })
|
||||||
}, [annotations])
|
}, [annotations])
|
||||||
|
|
||||||
// Reconfigure theme when font size changes
|
// Reconfigure theme when font size or colour theme changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const view = viewRef.current
|
const view = viewRef.current
|
||||||
if (!view) return
|
if (!view) return
|
||||||
view.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize)) })
|
view.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark')) })
|
||||||
}, [fontSize])
|
}, [fontSize, theme])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="editor-container">
|
<div className="editor-container">
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ interface EditorState {
|
|||||||
// Editor font size
|
// Editor font size
|
||||||
fontSize: number
|
fontSize: number
|
||||||
setFontSize: (size: number) => void
|
setFontSize: (size: number) => void
|
||||||
|
|
||||||
|
// Theme
|
||||||
|
theme: 'dark' | 'light'
|
||||||
|
toggleTheme: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useEditorStore = create<EditorState>((set, get) => ({
|
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))
|
const clamped = Math.max(11, Math.min(24, size))
|
||||||
localStorage.setItem('editorFontSize', String(clamped))
|
localStorage.setItem('editorFontSize', String(clamped))
|
||||||
set({ fontSize: 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 }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -28,6 +28,25 @@
|
|||||||
text-transform: uppercase;
|
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 ──────────────────────────────────────────────────── */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
background: var(--sidebar-bg);
|
background: var(--sidebar-bg);
|
||||||
|
|||||||
@@ -38,6 +38,30 @@
|
|||||||
--font-sans: -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;
|
--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 ─────────────────────────────────────────────────────── */
|
/* ─── Base ─────────────────────────────────────────────────────── */
|
||||||
html, body, #root {
|
html, body, #root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user