diff --git a/package.json b/package.json index 5b51c3e..752f04f 100644 --- a/package.json +++ b/package.json @@ -58,4 +58,4 @@ "typescript-eslint": "^8.46.4", "vite": "^7.2.4" } -} +} \ No newline at end of file diff --git a/src/components/features/FloatingHub.tsx b/src/components/features/FloatingHub.tsx index a3277f2..f49dffb 100644 --- a/src/components/features/FloatingHub.tsx +++ b/src/components/features/FloatingHub.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; -import { User, Palette, Settings } from 'lucide-react'; +import { Info, Settings } from 'lucide-react'; import { useAppStore } from '../../store/useAppStore'; export function FloatingHub() { @@ -86,41 +86,28 @@ export function FloatingHub() {
-
diff --git a/src/components/features/settings/AccountSettings.tsx b/src/components/features/settings/AccountSettings.tsx deleted file mode 100644 index 70bc31e..0000000 --- a/src/components/features/settings/AccountSettings.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { useRef } from 'react'; -import { User, Shield, KeyRound, Globe, Save } from 'lucide-react'; - -export function AccountSettings() { - const nameRef = useRef(null); - const emailRef = useRef(null); - - return ( -
-
- {/* Header */} -
-
-

- Account Settings -

-

- Manage your profile and security preferences -

-
- -
- - {/* Profile Section */} -
-
-
- 👤 -
-
- -

- JPG, GIF or PNG. Max size of 800K -

-
-
- -
-
- -
- - -
-
-
- -
- - -
-
-
-
- - {/* Integration List / Security (Placeholder) */} -
-

- Security -

- -
- {[ - { - icon: KeyRound, - title: 'Password', - desc: 'Last changed 3 months ago', - action: 'Update', - }, - { - icon: Shield, - title: 'Two-Factor Authentication', - desc: 'Currently disabled', - action: 'Enable', - }, - ].map((item, idx) => ( -
-
-
- -
-
-

- {item.title} -

-

- {item.desc} -

-
-
- -
- ))} -
-
-
-
- ); -} diff --git a/src/components/features/settings/GeneralSettings.tsx b/src/components/features/settings/GeneralSettings.tsx deleted file mode 100644 index 605f1ce..0000000 --- a/src/components/features/settings/GeneralSettings.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Monitor, Bell, FolderOpen, Globe } from 'lucide-react'; - -export function GeneralSettings() { - return ( -
-
- {/* Header */} -
-
-

- General Settings -

-

- Customize your editor experience -

-
-
- - {/* Content */} -
-

- Appearance -

-
- {[ - { - icon: Monitor, - title: 'Default View', - desc: 'Choose default view for new files', - action: 'Split View', - options: ['Split View', 'Editor Only', 'Preview Only'], - }, - { - icon: FolderOpen, - title: 'Sidebar Position', - desc: 'Change the location of the sidebar', - action: 'Left', - options: ['Left', 'Right'], - }, - ].map((item, idx) => ( -
-
-
- -
-
-

- {item.title} -

-

- {item.desc} -

-
-
- -
- ))} -
- -

- System -

-
- {[ - { - icon: Globe, - title: 'Language', - desc: 'Change interface language', - action: 'English', - options: ['English', 'Spanish', 'French', 'German', 'Japanese'], - }, - { - icon: Bell, - title: 'Notifications', - desc: 'Configure desktop notifications', - action: 'All', - options: ['All', 'Important Only', 'None'], - }, - ].map((item, idx) => ( -
-
-
- -
-
-

- {item.title} -

-

- {item.desc} -

-
-
- -
- ))} -
-
-
-
- ); -} diff --git a/src/components/features/settings/Info.tsx b/src/components/features/settings/Info.tsx new file mode 100644 index 0000000..9731891 --- /dev/null +++ b/src/components/features/settings/Info.tsx @@ -0,0 +1,139 @@ +import { Github, ExternalLink, Mail, Info as InfoIcon } from 'lucide-react'; + +export function Info() { + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

+ About Cinder Notes +

+

+ The minimalist, distraction-free markdown powerhouse. +

+
+
+
+ + {/* Content Section */} +
+
+
+

+ What is Cinder? +

+

+ Cinder Notes is a high-performance markdown editor designed for + speed and focus. Built with a "less is more" philosophy, it + provides a clean canvas for your thoughts while keeping powerful + tools just a click away in the Floating Hub. +

+
+ +
+

+ Core Philosophy +

+
    +
  • +
    + Personalized through themes, not bloat. +
  • +
  • +
    + Privacy first: Your notes stay on your machine. +
  • +
  • +
    + Native performance with lightweight components. +
  • +
+
+
+ +
+ + +
+

+ Connect +

+

+ Have feedback or want to request a feature? We'd love to hear + from you. +

+ +
+
+
+ + {/* Footer */} +
+

+ Cinder Notes v1.0.0 • Built with focus by Aurelius Labs +

+
+
+
+ ); +} diff --git a/src/components/features/settings/Settings.tsx b/src/components/features/settings/Settings.tsx new file mode 100644 index 0000000..e98283f --- /dev/null +++ b/src/components/features/settings/Settings.tsx @@ -0,0 +1,392 @@ +import { useState, useEffect } from 'react'; +import { + Sun, + Moon, + Monitor, + Lock, + Settings as SettingsIcon, + Palette, + Bell, + FolderOpen, + Globe, +} from 'lucide-react'; + +interface ThemePreset { + id: string; + name: string; + value: string; + gradient: string; + accent: string; + disabled?: boolean; +} + +const THEME_PRESETS: ThemePreset[] = [ + { + id: 'cinder-dark', + name: 'Cinder Dark', + value: '', + gradient: 'linear-gradient(135deg, #1f1f23 0%, #141417 100%)', + accent: '#f48c25', + }, + { + id: 'cinder-light', + name: 'Cinder Light', + value: 'theme-cinder-light', + gradient: 'linear-gradient(135deg, #fdfaf0 0%, #f2efe7 100%)', + accent: '#d97706', + }, + { + id: 'zen-black', + name: 'Zen Black', + value: 'theme-zen-black', + gradient: 'linear-gradient(135deg, #0a0a0a 0%, #000000 100%)', + accent: '#333', + }, + { + id: 'synthwave', + name: "Synthwave '84", + value: 'theme-synthwave', + gradient: 'linear-gradient(135deg, #2a2139 0%, #262335 100%)', + accent: '#ff7edb', + }, + { + id: 'github-dark', + name: 'GitHub Dark', + value: 'theme-github-dark', + gradient: 'linear-gradient(135deg, #161b22 0%, #0d1117 100%)', + accent: '#58a6ff', + }, + { + id: 'monokai', + name: 'Monokai Pro', + value: 'theme-monokai', + gradient: 'linear-gradient(135deg, #272822 0%, #1e1f1c 100%)', + accent: '#a6e22e', + }, + { + id: 'dracula', + name: 'Dracula', + value: 'theme-dracula', + gradient: 'linear-gradient(135deg, #282a36 0%, #21222c 100%)', + accent: '#bd93f9', + }, + { + id: 'nord', + name: 'Nord', + value: 'theme-nord', + gradient: 'linear-gradient(135deg, #3b4252 0%, #2e3440 100%)', + accent: '#88c0d0', + }, + { + id: 'forest', + name: 'Forest', + value: 'theme-forest', + gradient: 'linear-gradient(135deg, #273329 0%, #1e2820 100%)', + accent: '#a7c957', + }, + { + id: 'mustard', + name: 'Muddy Mustard', + value: 'theme-mustard', + gradient: 'linear-gradient(135deg, #3b3728 0%, #2d2a1e 100%)', + accent: '#e9c46a', + }, + { + id: 'marine', + name: 'Marine', + value: 'theme-marine', + gradient: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)', + accent: '#38bdf8', + }, + { + id: 'ember', + name: 'Ember', + value: 'theme-ember', + gradient: 'linear-gradient(135deg, #3f1818 0%, #2b1111 100%)', + accent: '#f87171', + }, +]; + +const THEME_VARIABLES = [ + '--bg-primary', + '--bg-secondary', + '--bg-tertiary', + '--bg-hover', + '--bg-active', + '--text-primary', + '--text-secondary', + '--text-tertiary', + '--editor-header-accent', + '--accent-glow', + '--border-primary', + '--border-secondary', + '--markdown-heading', + '--markdown-link', + '--markdown-code', + '--markdown-code-bg', + '--editor-bg', + '--editor-text', + '--editor-selection-bg', + '--activity-bar-bg', +]; + +export function Settings() { + const [activeTab, setActiveTab] = useState<'general' | 'theme'>('general'); + const [currentTheme, setCurrentTheme] = useState( + () => localStorage.getItem('cinder-theme') || '' + ); + const [colorMode, setColorMode] = useState<'light' | 'dark' | 'system'>( + 'dark' + ); + + useEffect(() => { + THEME_PRESETS.forEach((t) => { + if (t.value) document.documentElement.classList.remove(t.value); + }); + + THEME_VARIABLES.forEach((v) => + document.documentElement.style.removeProperty(v) + ); + + if (currentTheme) { + document.documentElement.classList.add(currentTheme); + } + localStorage.setItem('cinder-theme', currentTheme); + }, [currentTheme]); + + return ( +
+ {/* Sidebar-style Tabs */} +
+
+

+ Settings +

+ + +
+ + {/* Content Area */} +
+
+ {activeTab === 'general' ? ( +
+
+

+ General Settings +

+

+ Customize your editor experience +

+
+ +
+

+ Editor +

+
+ {[ + { + icon: Monitor, + title: 'Default View', + desc: 'Choose default view for new files', + options: ['Split View', 'Editor Only', 'Preview Only'], + }, + { + icon: FolderOpen, + title: 'Sidebar Position', + desc: 'Change the location of the sidebar', + options: ['Left', 'Right'], + }, + ].map((item, idx) => ( +
+
+
+ +
+
+

+ {item.title} +

+

+ {item.desc} +

+
+
+ +
+ ))} +
+ +

+ System +

+
+ {[ + { + icon: Globe, + title: 'Language', + desc: 'Change interface language', + options: [ + 'English', + 'Spanish', + 'French', + 'German', + 'Japanese', + ], + }, + { + icon: Bell, + title: 'Notifications', + desc: 'Configure desktop notifications', + options: ['All', 'Important Only', 'None'], + }, + ].map((item, idx) => ( +
+
+
+ +
+
+

+ {item.title} +

+

+ {item.desc} +

+
+
+ +
+ ))} +
+
+
+ ) : ( +
+
+

+ Themes +

+

+ Manage app appearance and customization +

+
+ +
+

+ Color Mode +

+
+ {[ + { id: 'light', label: 'Light mode', icon: Sun }, + { id: 'dark', label: 'Dark mode', icon: Moon }, + { id: 'system', label: 'System', icon: Monitor }, + ].map((mode) => ( + + ))} +
+
+ +
+

+ Preset themes +

+
+ {THEME_PRESETS.map((theme) => { + const isActive = + currentTheme === theme.value && !theme.disabled; + return ( + + ); + })} +
+
+
+ )} +
+
+
+
+ ); +} diff --git a/src/components/features/settings/ThemeSettings.tsx b/src/components/features/settings/ThemeSettings.tsx deleted file mode 100644 index e401306..0000000 --- a/src/components/features/settings/ThemeSettings.tsx +++ /dev/null @@ -1,304 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Upload, Shuffle, Sun, Moon, Monitor, Lock } from 'lucide-react'; - -interface ThemePreset { - id: string; - name: string; - value: string; - gradient: string; - accent: string; - disabled?: boolean; -} - -const THEME_PRESETS: ThemePreset[] = [ - { - id: 'cinder-dark', - name: 'Cinder Dark', - value: '', - gradient: 'linear-gradient(135deg, #1f1f23 0%, #141417 100%)', - accent: '#f48c25', - }, - { - id: 'cinder-light', - name: 'Cinder Light', - value: 'theme-cinder-light', - gradient: 'linear-gradient(135deg, #fdfaf0 0%, #f2efe7 100%)', - accent: '#d97706', - }, - { - id: 'zen-black', - name: 'Zen Black', - value: 'theme-zen-black', - gradient: 'linear-gradient(135deg, #0a0a0a 0%, #000000 100%)', - accent: '#333', - }, - // New Functional Themes - { - id: 'synthwave', - name: "Synthwave '84", - value: 'theme-synthwave', - gradient: 'linear-gradient(135deg, #2a2139 0%, #262335 100%)', - accent: '#ff7edb', - }, - { - id: 'github-dark', - name: 'GitHub Dark', - value: 'theme-github-dark', - gradient: 'linear-gradient(135deg, #161b22 0%, #0d1117 100%)', - accent: '#58a6ff', - }, - { - id: 'monokai', - name: 'Monokai Pro', - value: 'theme-monokai', - gradient: 'linear-gradient(135deg, #272822 0%, #1e1f1c 100%)', - accent: '#a6e22e', - }, - { - id: 'dracula', - name: 'Dracula', - value: 'theme-dracula', - gradient: 'linear-gradient(135deg, #282a36 0%, #21222c 100%)', - accent: '#bd93f9', - }, - { - id: 'nord', - name: 'Nord', - value: 'theme-nord', - gradient: 'linear-gradient(135deg, #3b4252 0%, #2e3440 100%)', - accent: '#88c0d0', - }, - { - id: 'forest', - name: 'Forest', - value: 'theme-forest', - gradient: 'linear-gradient(135deg, #273329 0%, #1e2820 100%)', - accent: '#a7c957', - }, - { - id: 'mustard', - name: 'Muddy Mustard', - value: 'theme-mustard', - gradient: 'linear-gradient(135deg, #3b3728 0%, #2d2a1e 100%)', - accent: '#e9c46a', - }, - { - id: 'marine', - name: 'Marine', - value: 'theme-marine', - gradient: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)', - accent: '#38bdf8', - }, - { - id: 'ember', - name: 'Ember', - value: 'theme-ember', - gradient: 'linear-gradient(135deg, #3f1818 0%, #2b1111 100%)', - accent: '#f87171', - }, -]; - -export function ThemeSettings() { - const [currentTheme, setCurrentTheme] = useState( - () => localStorage.getItem('cinder-theme') || '' - ); - const [colorMode, setColorMode] = useState<'light' | 'dark' | 'system'>( - 'dark' - ); // Simplified for now - - // Apply theme changes - useEffect(() => { - // Reset classes - THEME_PRESETS.forEach((t) => { - if (t.value) document.documentElement.classList.remove(t.value); - }); - - // Add new theme class - if (currentTheme) { - document.documentElement.classList.add(currentTheme); - } - localStorage.setItem('cinder-theme', currentTheme); - }, [currentTheme]); - - return ( -
-
- {/* Header */} -
-
-

- Themes -

-

- Manage app appearance and customization -

-
-
- - -
-
- - {/* Color Mode Section */} -
-

- Color Mode -

-

- Choose if app's appearance should be light or dark, or follow your - computer's settings. -

- -
- {[ - { id: 'light', label: 'Light mode', icon: Sun }, - { id: 'dark', label: 'Dark mode', icon: Moon }, - { id: 'system', label: 'System', icon: Monitor }, - ].map((mode) => ( - - ))} -
-
- - {/* Preset Themes Grid */} -
-

- Preset themes -

-

- Choose a preset theme from our library. -

- -
- {THEME_PRESETS.map((theme) => { - const isActive = currentTheme === theme.value && !theme.disabled; - return ( - - ); - })} -
-
- - {/* Custom Themes (Placeholder UI) */} -
-
-
-

- Custom themes -

-

- Set your own app theme by changing the color for each interface - element. -

-
- {/* Toggle switch placeholder */} -
-
-
-
- -
- {[ - { label: 'Window background', hex: '#F1F5F9' }, - { label: 'Selected items', hex: '#F472B6' }, - { label: 'Online indication', hex: '#84CC16' }, - { label: 'Notifications', hex: '#6366F1' }, - { label: 'New inbox', hex: '#F97316' }, - { label: 'Sidebar', hex: '#7C3AED' }, - ].map((item, idx) => ( -
-
-
- {item.label} -
-
- {item.hex} -
-
-
-
- -
-
- ))} -
-
-
-
- ); -} diff --git a/src/components/layout/editor/Editor.tsx b/src/components/layout/editor/Editor.tsx index 3c9c986..888debe 100644 --- a/src/components/layout/editor/Editor.tsx +++ b/src/components/layout/editor/Editor.tsx @@ -1,10 +1,10 @@ -import type { MutableRefObject } from 'react'; +import { useEffect, type MutableRefObject } from 'react'; import { useAppStore } from '../../../store/useAppStore'; import { MarkdownPreview } from './MarkdownPreview'; import { Eye, ChevronLeft, FileText, Save } from 'lucide-react'; -import { AccountSettings } from '../../features/settings/AccountSettings'; -import { ThemeSettings } from '../../features/settings/ThemeSettings'; -import { GeneralSettings } from '../../features/settings/GeneralSettings'; + +import { Settings } from '../../features/settings/Settings'; +import { Info } from '../../features/settings/Info'; import { CodeMirrorEditor } from './CodeMirrorEditor'; import type { EditorView } from '@codemirror/view'; @@ -20,7 +20,21 @@ export function Editor({ editorViewRef, onCursorChange, }: EditorProps) { - const { activeFileId, activeFileContent, updateFileContent } = useAppStore(); + const { activeFileId, activeFileContent, updateFileContent, saveFile } = + useAppStore(); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + if (activeFileId) { + saveFile(activeFileId); + } + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [activeFileId, saveFile]); return (
- {activeFileId === 'cinder-account' && } - {activeFileId === 'cinder-theme' && } - {activeFileId === 'cinder-settings' && } + {activeFileId === 'cinder-settings' && } + {activeFileId === 'cinder-info' && } {(!activeFileId || activeFileId === 'welcome') && (
diff --git a/src/components/layout/editor/EditorTabs.tsx b/src/components/layout/editor/EditorTabs.tsx index 4182e65..b8d7ba8 100644 --- a/src/components/layout/editor/EditorTabs.tsx +++ b/src/components/layout/editor/EditorTabs.tsx @@ -6,8 +6,7 @@ import { Gift, Maximize2, Minimize2, - User, - Palette, + Info, Settings, } from 'lucide-react'; import { useAppStore } from '../../../store/useAppStore'; @@ -33,6 +32,7 @@ export function EditorTabs() { createFolder, closeOtherFiles, closeAllFiles, + dirtyFiles, } = useAppStore(); return ( @@ -55,9 +55,8 @@ export function EditorTabs() { let tabName = ''; if (isWelcomeTab) tabName = 'Welcome'; else if (isBlankTab) tabName = 'Untitled'; - else if (fileId === 'cinder-account') tabName = 'Account'; - else if (fileId === 'cinder-theme') tabName = 'Themes'; else if (fileId === 'cinder-settings') tabName = 'Settings'; + else if (fileId === 'cinder-info') tabName = 'About'; else tabName = file?.name.replace(/\.md$/, '') || 'Unknown'; return ( @@ -118,9 +117,8 @@ export function EditorTabs() { color: isActive ? 'var(--editor-header-accent)' : 'inherit', }} > - {fileId === 'cinder-account' && } - {fileId === 'cinder-theme' && } {fileId === 'cinder-settings' && } + {fileId === 'cinder-info' && } )} @@ -130,18 +128,23 @@ export function EditorTabs() { {tabName} - +
+
+ +
); })} diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index af8c898..2cb0499 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -1,5 +1,6 @@ import { create } from 'zustand'; import { invoke } from '@tauri-apps/api/core'; +import { ask } from '@tauri-apps/plugin-dialog'; import type { FileNode } from '../types/fileSystem'; export interface SearchResult { @@ -24,6 +25,7 @@ interface AppState { expandedFolderIds: string[]; // List of folder IDs that are expanded pendingFileId: string | null; isAutoSave: boolean; + dirtyFiles: Set; // Search State isSearchOpen: boolean; @@ -43,7 +45,8 @@ interface AppState { // Actions selectFile: (fileId: string) => void; openFileInNewTab: (fileId: string) => void; - closeFile: (fileId: string) => void; + closeFile: (fileId: string) => Promise; + saveFile: (fileId?: string) => Promise; updateFileContent: (fileId: string, content: string) => void; findFile: (id: string, nodes?: FileNode[]) => FileNode | null; getFileBreadcrumb: (fileId: string) => FileNode[]; @@ -77,8 +80,8 @@ interface AppState { duplicateFile: (fileId: string) => void; createFileInFolder: (folderId: string) => void; createFolder: (parentFolderId?: string | null) => void; - closeOtherFiles: (fileId: string) => void; - closeAllFiles: () => void; + closeOtherFiles: (fileId: string) => Promise; + closeAllFiles: () => Promise; } export const useAppStore = create((set, get) => ({ @@ -95,6 +98,7 @@ export const useAppStore = create((set, get) => ({ lastSidebarWidth: 20, expandedFolderIds: [], isAutoSave: true, + dirtyFiles: new Set(), isSearchOpen: false, searchQuery: '', @@ -344,10 +348,22 @@ export const useAppStore = create((set, get) => ({ } }, - closeFile: (fileId: string) => { + closeFile: async (fileId: string) => { + const state = get(); + if (state.dirtyFiles.has(fileId)) { + const confirmed = await ask( + 'You have unsaved changes. Are you sure you want to close without saving?', + { title: 'Unsaved Changes', kind: 'warning' } + ); + if (!confirmed) return; + } + const { openFiles, activeFileId } = get(); const newOpenFiles = openFiles.filter((id) => id !== fileId); + const newDirtyFiles = new Set(get().dirtyFiles); + newDirtyFiles.delete(fileId); + if (activeFileId === fileId) { const nextActive = newOpenFiles.length > 0 ? newOpenFiles[newOpenFiles.length - 1] : null; @@ -355,12 +371,34 @@ export const useAppStore = create((set, get) => ({ get().selectFile(nextActive); } else { set({ activeFileId: null, activeFileContent: '' }); - // If we closed the last tab, we might want to show empty state or Welcome? - // For now, empty state is fine. } } - set({ openFiles: newOpenFiles }); + set({ openFiles: newOpenFiles, dirtyFiles: newDirtyFiles }); + }, + + saveFile: async (fileId?: string) => { + const state = get(); + const targetFileId = fileId || state.activeFileId; + if (!targetFileId) return; + + const file = state.findFile(targetFileId); + const contentToSave = + targetFileId === state.activeFileId + ? state.activeFileContent + : file?.content || ''; + + if (file && file.path) { + try { + await invoke('write_note', { path: file.path, content: contentToSave }); + console.log('File saved manually:', file.path); + const newDirty = new Set(get().dirtyFiles); + newDirty.delete(targetFileId); + set({ dirtyFiles: newDirty }); + } catch (err) { + console.error('Failed to save file:', err); + } + } }, updateFileContent: (fileId: string, content: string) => { @@ -408,20 +446,28 @@ export const useAppStore = create((set, get) => ({ }; // Create file on disk with content - invoke('write_note', { path: filePath, content }) - .then(() => console.log('File created on disk:', filePath)) - .catch((err) => console.error('Failed to create file:', err)); + if (state.isAutoSave) { + invoke('write_note', { path: filePath, content }) + .then(() => console.log('File created on disk:', filePath)) + .catch((err) => console.error('Failed to create file:', err)); + } const newFiles = [...files, newFile]; const newOpenFiles = openFiles.map((id) => id === fileId ? newFileId : id ); + const newDirtyFiles = new Set(state.dirtyFiles); + if (!state.isAutoSave) { + newDirtyFiles.add(newFileId); + } + set({ files: newFiles, activeFileId: newFileId, openFiles: newOpenFiles, activeFileContent: content, + dirtyFiles: newDirtyFiles, }); return; } @@ -432,13 +478,28 @@ export const useAppStore = create((set, get) => ({ // Write to disk if we have a path if (filePath) { - invoke('write_note', { path: filePath, content }) - .then(() => console.log('File saved:', filePath)) - .catch((err) => console.error('Failed to save file:', err)); + if (state.isAutoSave) { + invoke('write_note', { path: filePath, content }) + .then(() => { + console.log('File saved:', filePath); + const stateNow = get(); + if (stateNow.dirtyFiles.has(fileId)) { + const newDirty = new Set(stateNow.dirtyFiles); + newDirty.delete(fileId); + set({ dirtyFiles: newDirty }); + } + }) + .catch((err) => console.error('Failed to save file:', err)); + } } // Normal update - Persist to store AND active state set((state) => { + const newDirty = new Set(state.dirtyFiles); + if (!state.isAutoSave) { + newDirty.add(fileId); + } + const updateContentRecursive = (nodes: FileNode[]): FileNode[] => { return nodes.map((node) => { if (node.id === fileId) { @@ -455,6 +516,7 @@ export const useAppStore = create((set, get) => ({ activeFileContent: state.activeFileId === fileId ? content : state.activeFileContent, files: updateContentRecursive(state.files), + dirtyFiles: newDirty, }; }); }, @@ -908,7 +970,15 @@ export const useAppStore = create((set, get) => ({ }, toggleAutoSave: () => { - set((state) => ({ isAutoSave: !state.isAutoSave })); + set((state) => { + const newAutoSave = !state.isAutoSave; + if (newAutoSave && state.dirtyFiles.size > 0) { + Array.from(state.dirtyFiles).forEach((fileId) => { + get().saveFile(fileId); + }); + } + return { isAutoSave: newAutoSave }; + }); }, // --- Context Menu Actions --- @@ -1255,24 +1325,57 @@ export const useAppStore = create((set, get) => ({ } }, - closeOtherFiles: (fileId: string) => { + closeOtherFiles: async (fileId: string) => { const state = get(); + const otherDirtyFiles = Array.from(state.dirtyFiles).filter( + (id) => id !== fileId && state.openFiles.includes(id) + ); + if (otherDirtyFiles.length > 0) { + const confirmed = await ask( + 'You have unsaved changes in other files. Are you sure you want to close them without saving?', + { title: 'Unsaved Changes', kind: 'warning' } + ); + if (!confirmed) return; + } + + const newDirtyFiles = new Set(state.dirtyFiles); + state.openFiles.forEach((id) => { + if (id !== fileId) newDirtyFiles.delete(id); + }); + set({ openFiles: state.openFiles.includes(fileId) ? [fileId] : [], activeFileId: state.openFiles.includes(fileId) ? fileId : null, activeFileContent: state.activeFileId === fileId ? state.activeFileContent : '', + dirtyFiles: newDirtyFiles, }); if (state.activeFileId !== fileId && state.openFiles.includes(fileId)) { get().selectFile(fileId); } }, - closeAllFiles: () => { + closeAllFiles: async () => { + const state = get(); + const dirtyOpenFiles = Array.from(state.dirtyFiles).filter((id) => + state.openFiles.includes(id) + ); + if (dirtyOpenFiles.length > 0) { + const confirmed = await ask( + 'You have unsaved changes. Are you sure you want to close all files without saving?', + { title: 'Unsaved Changes', kind: 'warning' } + ); + if (!confirmed) return; + } + + const newDirtyFiles = new Set(state.dirtyFiles); + state.openFiles.forEach((id) => newDirtyFiles.delete(id)); + set({ openFiles: [], activeFileId: null, activeFileContent: '', + dirtyFiles: newDirtyFiles, }); }, })); diff --git a/src/theme/cinderTheme.ts b/src/theme/cinderTheme.ts index 6d10615..4dc52b9 100644 --- a/src/theme/cinderTheme.ts +++ b/src/theme/cinderTheme.ts @@ -42,7 +42,7 @@ const cinderEditorTheme = EditorView.theme( backgroundColor: 'var(--editor-selection-bg) !important', }, '.cm-activeLine': { - backgroundColor: 'rgba(255, 255, 255, 0.03)', + backgroundColor: 'var(--bg-active)', }, '.cm-gutters': { backgroundColor: 'var(--editor-bg)', @@ -76,21 +76,20 @@ const cinderEditorTheme = EditorView.theme( opacity: '0.75', }, '.cm-md-inline-code': { - backgroundColor: 'rgba(255, 255, 255, 0.06)', + backgroundColor: 'var(--bg-tertiary)', padding: '0.25em 0.5em', borderRadius: '6px', fontSize: '0.85em', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', - border: '1px solid rgba(255, 255, 255, 0.08)', + border: '1px solid var(--border-secondary)', color: 'var(--text-primary)', - boxShadow: '0 1px 2px rgba(0, 0, 0, 0.1)', }, '.cm-codeblock-line': { - backgroundColor: 'rgba(0, 0, 0, 0.2)', + backgroundColor: 'var(--bg-secondary)', paddingLeft: '1.3em', paddingRight: '1.3em', - borderLeft: '1px solid rgba(255, 255, 255, 0.08)', - borderRight: '1px solid rgba(255, 255, 255, 0.08)', + borderLeft: '1px solid var(--border-secondary)', + borderRight: '1px solid var(--border-secondary)', }, '.cm-codeblock-line *': { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', @@ -118,15 +117,14 @@ const cinderEditorTheme = EditorView.theme( fontSize: '1.1rem !important', }, '.cm-blockquote-line': { - background: - 'linear-gradient(to right, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.01))', + backgroundColor: 'var(--bg-active)', boxShadow: 'inset 4px 0 0 var(--editor-header-accent)', color: 'var(--text-secondary)', paddingLeft: '1.6em', paddingTop: '0.2em', paddingBottom: '0.2em', fontStyle: 'italic', - border: '1px solid rgba(255, 255, 255, 0.03)', + border: '1px solid var(--border-secondary)', borderLeft: 'none', }, diff --git a/src/theme/markdown.css b/src/theme/markdown.css index 0edce41..4434c55 100644 --- a/src/theme/markdown.css +++ b/src/theme/markdown.css @@ -126,14 +126,13 @@ INLINE CODE ===================================================== */ .markdown-preview code:not(pre code) { - background: rgba(255, 255, 255, 0.06); + background: var(--bg-tertiary); padding: 0.25em 0.5em; border-radius: 6px; font-size: 0.85em; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - border: 1px solid rgba(255, 255, 255, 0.08); + border: 1px solid var(--border-secondary); color: var(--text-primary); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } /* ===================================================== @@ -143,11 +142,9 @@ margin: 2em 0; border-radius: 12px; overflow: hidden; - background: rgba(0, 0, 0, 0.2); - border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: - 0 10px 40px rgba(0, 0, 0, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.05); + background: var(--bg-secondary); + border: 1px solid var(--border-secondary); + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); } .markdown-preview .code-block-header { @@ -155,8 +152,8 @@ align-items: center; justify-content: space-between; padding: 0.6em 1em; - background: rgba(255, 255, 255, 0.02); - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + background: var(--bg-tertiary); + border-bottom: 1px solid var(--border-secondary); font-size: 0.72em; letter-spacing: 0.1em; text-transform: uppercase; @@ -192,18 +189,14 @@ BLOCKQUOTE ===================================================== */ .markdown-preview blockquote { - background: linear-gradient( - to right, - rgba(255, 255, 255, 0.05), - rgba(255, 255, 255, 0.01) - ); + background: var(--bg-active); padding: 1.2em 1.6em; border-radius: 4px 12px 12px 4px; margin: 2em 0; box-shadow: inset 4px 0 0 var(--editor-header-accent); color: var(--text-secondary); font-style: italic; - border: 1px solid rgba(255, 255, 255, 0.03); + border: 1px solid var(--border-secondary); border-left: none; } @@ -226,16 +219,16 @@ .markdown-preview th, .markdown-preview td { padding: 0.75em 1em; - border: 1px solid rgba(255, 255, 255, 0.06); + border: 1px solid var(--border-secondary); } .markdown-preview th { - background: rgba(255, 255, 255, 0.04); + background: var(--bg-tertiary); font-weight: 600; } .markdown-preview tr:nth-child(even) { - background: rgba(255, 255, 255, 0.015); + background: var(--bg-active); } /* ===================================================== @@ -282,7 +275,7 @@ SELECTION ===================================================== */ .markdown-preview ::selection { - background: rgba(100, 150, 255, 0.25); + background: var(--editor-selection-bg); } /* =====================================================