- {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}
-
{
- e.stopPropagation();
- closeFile(fileId);
- }}
- className="ml-2 p-0.5 rounded-md opacity-0 group-hover:opacity-100 transition-all hover:bg-[var(--bg-active)]"
- style={{
- color: 'var(--text-tertiary)',
- }}
- >
-
-
+
+
+
{
+ e.stopPropagation();
+ closeFile(fileId);
+ }}
+ className={`absolute inset-0 p-0.5 flex items-center justify-center rounded-md transition-all hover:bg-[var(--bg-active)] ${dirtyFiles.has(fileId) ? 'opacity-0 group-hover:opacity-100' : 'opacity-0 group-hover:opacity-100'}`}
+ style={{
+ color: 'var(--text-tertiary)',
+ }}
+ >
+
+
+
);
})}
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);
}
/* =====================================================