diff --git a/docs/changelogs/cinder-v0.2.md b/docs/changelogs/cinder-v0.2.md index 6b07394..c0a4b46 100644 --- a/docs/changelogs/cinder-v0.2.md +++ b/docs/changelogs/cinder-v0.2.md @@ -107,4 +107,3 @@ Allow dragging `.md` files from Finder/Explorer into the workspace to import the ### 10. Note Pinning Allow users to pin frequently accessed notes to the top of the explorer or tabs. - diff --git a/package.json b/package.json index 752f04f..5b51c3e 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/settings/Settings.tsx b/src/components/features/settings/Settings.tsx index 7413119..b0136c6 100644 --- a/src/components/features/settings/Settings.tsx +++ b/src/components/features/settings/Settings.tsx @@ -1,4 +1,6 @@ import { useState, useEffect } from 'react'; +import { useAppStore } from '../../../store/useAppStore'; +import { getTranslation } from '../../../utils/i18n'; import { Sun, Moon, @@ -138,24 +140,60 @@ export function Settings() { () => localStorage.getItem('cinder-theme') || '' ); const [colorMode, setColorMode] = useState<'light' | 'dark' | 'system'>( - 'dark' + () => + (localStorage.getItem('cinder-color-mode') as + | 'light' + | 'dark' + | 'system') || 'dark' ); const [showAllThemes, setShowAllThemes] = useState(false); + const { + defaultView, + setDefaultView, + sidebarPosition, + setSidebarPosition, + isAutoSave, + toggleAutoSave, + language, + setLanguage, + } = useAppStore(); - useEffect(() => { - THEME_PRESETS.forEach((t) => { - if (t.value) document.documentElement.classList.remove(t.value); - }); + const t = (key: string) => getTranslation(language, key); - THEME_VARIABLES.forEach((v) => - document.documentElement.style.removeProperty(v) - ); + useEffect(() => { + const applyTheme = (theme: string) => { + THEME_PRESETS.forEach((t) => { + if (t.value) document.documentElement.classList.remove(t.value); + }); + THEME_VARIABLES.forEach((v) => + document.documentElement.style.removeProperty(v) + ); + if (theme) { + document.documentElement.classList.add(theme); + } + }; - if (currentTheme) { - document.documentElement.classList.add(currentTheme); + if (colorMode === 'system') { + const mediaQuery = window.matchMedia('(prefers-color-scheme: light)'); + const handleChange = (e: MediaQueryListEvent | MediaQueryList) => { + if (e.matches) { + applyTheme('theme-cinder-light'); + } else { + applyTheme(currentTheme || ''); + } + }; + handleChange(mediaQuery); + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + } else if (colorMode === 'light') { + applyTheme('theme-cinder-light'); + } else { + applyTheme(currentTheme); } + localStorage.setItem('cinder-theme', currentTheme); - }, [currentTheme]); + localStorage.setItem('cinder-color-mode', colorMode); + }, [currentTheme, colorMode]); return (
@@ -163,21 +201,21 @@ export function Settings() { {/* Sidebar-style Tabs */}

- Settings + {t('settings')}

@@ -188,135 +226,150 @@ export function Settings() {

- General Settings + {t('general')}

- Customize your editor experience + {t('generalDesc') || 'Customize your editor experience'}

- Editor + {t('editor')}

-
+
-

- Auto-save - - Coming Soon - +

+ {t('autoSave')}

- Automatically save changes while typing + {t('autoSaveDesc')}

- {[ - { - 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) => ( -
+
+
+ +
+
+

+ {t('defaultView')} +

+

+ {t('defaultViewDesc')} +

+
+
+ +
+ +
+
+
+ +
+
+

+ {t('sidebarPosition')} +

+

+ {t('sidebarPositionDesc')} +

-
- ))} + +

- System + {t('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) => ( -
+
+
+ +
+
+

+ {t('language')} +

+

+ {t('languageDesc')} +

+
+
+ +
+ +
+
+
+ +
+
+

+ {t('notifications')} + + Coming Soon + +

+

+ {t('notificationsDesc')} +

-
- ))} + +
@@ -324,22 +377,23 @@ export function Settings() {

- Themes + {t('themes')}

- Manage app appearance and customization + {t('themesDesc') || + 'Manage app appearance and customization'}

- Color Mode + {t('colorMode')}

{[ - { id: 'light', label: 'Light mode', icon: Sun }, - { id: 'dark', label: 'Dark mode', icon: Moon }, - { id: 'system', label: 'System', icon: Monitor }, + { id: 'light', label: t('lightMode'), icon: Sun }, + { id: 'dark', label: t('darkMode'), icon: Moon }, + { id: 'system', label: t('system'), icon: Monitor }, ].map((mode) => ( @@ -160,12 +172,35 @@ export function FileExplorer() { } }} > - {filteredFiles.length === 0 ? ( + {filteredFiles.length === 0 && pinnedNodes.length === 0 ? (
- No matches found + {t('noMatches')}
) : ( -
+
+ {pinnedNodes.length > 0 && ( +
+
+ Pinned +
+ {pinnedNodes.map((node) => ( + + ))} +
+ )} + + {pinnedNodes.length > 0 && filteredFiles.length > 0 && ( +
+ Files +
+ )} + {filteredFiles.map((node) => ( ))} diff --git a/src/components/layout/explorer/FileTreeItem.tsx b/src/components/layout/explorer/FileTreeItem.tsx index a5ca97c..8f27c25 100644 --- a/src/components/layout/explorer/FileTreeItem.tsx +++ b/src/components/layout/explorer/FileTreeItem.tsx @@ -80,6 +80,8 @@ export function FileTreeItem({ node, depth = 0 }: FileTreeItemProps) { closeAllFiles, createFile, findFile, + pinnedFiles, + togglePinFile, } = useAppStore(); // Derived state from store @@ -230,6 +232,8 @@ export function FileTreeItem({ node, depth = 0 }: FileTreeItemProps) { closeOtherFiles, closeAllFiles, findFile, + togglePinFile, + isPinned: (id: string) => pinnedFiles.includes(id), }; if (node.type === 'folder') { showFolderContextMenu(node, actions); @@ -335,6 +339,24 @@ export function FileTreeItem({ node, depth = 0 }: FileTreeItemProps) { {node.name.replace(/\.md$/, '')} )} + + {node.type === 'file' && pinnedFiles.includes(node.id) && ( + + + + + )}
diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index af8c898..299ea90 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -24,6 +24,13 @@ interface AppState { expandedFolderIds: string[]; // List of folder IDs that are expanded pendingFileId: string | null; isAutoSave: boolean; + defaultView: 'editor' | 'preview'; + sidebarPosition: 'left' | 'right'; + language: string; + + // Pinned Files + pinnedFiles: string[]; + togglePinFile: (fileId: string) => void; // Search State isSearchOpen: boolean; @@ -70,6 +77,9 @@ interface AppState { ) => void; toggleAutoSave: () => void; openSystemTab: (tabId: string) => void; + setDefaultView: (view: 'editor' | 'preview') => void; + setSidebarPosition: (position: 'left' | 'right') => void; + setLanguage: (lang: string) => void; // Context Menu Actions deleteFile: (fileId: string) => void; @@ -95,6 +105,14 @@ export const useAppStore = create((set, get) => ({ lastSidebarWidth: 20, expandedFolderIds: [], isAutoSave: true, + defaultView: + (localStorage.getItem('cinder-default-view') as 'editor' | 'preview') || + 'editor', + sidebarPosition: + (localStorage.getItem('cinder-sidebar-position') as 'left' | 'right') || + 'left', + language: localStorage.getItem('cinder-language') || 'English', + pinnedFiles: JSON.parse(localStorage.getItem('cinder-pinned-files') || '[]'), isSearchOpen: false, searchQuery: '', @@ -103,6 +121,16 @@ export const useAppStore = create((set, get) => ({ // Workspace actions setWorkspacePath: (path: string | null) => set({ workspacePath: path }), + togglePinFile: (fileId: string) => + set((state) => { + const isPinned = state.pinnedFiles.includes(fileId); + const newPinned = isPinned + ? state.pinnedFiles.filter((id) => id !== fileId) + : [...state.pinnedFiles, fileId]; + localStorage.setItem('cinder-pinned-files', JSON.stringify(newPinned)); + return { pinnedFiles: newPinned }; + }), + setSearchOpen: (isOpen: boolean) => set({ isSearchOpen: isOpen }), setSearchQuery: (query: string) => set({ searchQuery: query }), setSearchResults: (results: SearchResult[]) => @@ -127,14 +155,23 @@ export const useAppStore = create((set, get) => ({ expandedFolderIds: [], }), - findFile: (id: string, nodes = get().files): FileNode | null => { - for (const node of nodes) { + findFile: (id: string, nodes?: FileNode[]): FileNode | null => { + const isRootCall = nodes === undefined; + const searchNodes = nodes || get().files; + + for (const node of searchNodes) { if (node.id === id) return node; if (node.children) { const found = get().findFile(id, node.children); if (found) return found; } } + + if (isRootCall && get().pinnedFiles?.includes(id)) { + const name = id.split(/[/\\]/).pop() || 'Unknown'; + return { id, name, type: 'file', path: id, content: '' }; + } + return null; }, @@ -911,6 +948,21 @@ export const useAppStore = create((set, get) => ({ set((state) => ({ isAutoSave: !state.isAutoSave })); }, + setDefaultView: (view: 'editor' | 'preview') => { + localStorage.setItem('cinder-default-view', view); + set({ defaultView: view }); + }, + + setSidebarPosition: (position: 'left' | 'right') => { + localStorage.setItem('cinder-sidebar-position', position); + set({ sidebarPosition: position }); + }, + + setLanguage: (lang: string) => { + localStorage.setItem('cinder-language', lang); + set({ language: lang }); + }, + // --- Context Menu Actions --- deleteFile: (fileId: string) => { diff --git a/src/util/contextMenu.ts b/src/util/contextMenu.ts index ad4e9a8..6c28dcd 100644 --- a/src/util/contextMenu.ts +++ b/src/util/contextMenu.ts @@ -27,6 +27,8 @@ type StoreActions = { closeOtherFiles: (fileId: string) => void; closeAllFiles: () => void; findFile: (id: string) => FileNode | null; + togglePinFile?: (fileId: string) => void; + isPinned?: (fileId: string) => boolean; }; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -57,6 +59,12 @@ export async function showFileContextMenu( action: () => actions.openFileInNewTab(node.id), }), await sep(), + await MenuItem.new({ + id: 'pin', + text: actions.isPinned?.(node.id) ? 'Unpin' : 'Pin', + action: () => actions.togglePinFile?.(node.id), + }), + await sep(), await MenuItem.new({ id: 'rename', text: 'Rename', @@ -178,7 +186,20 @@ export async function showTabContextMenu( const file = actions.findFile(fileId); const filePath = file?.path; - const items: (MenuItem | PredefinedMenuItem)[] = [ + const items: (MenuItem | PredefinedMenuItem)[] = []; + + if (actions.togglePinFile && actions.isPinned) { + items.push( + await MenuItem.new({ + id: 'pin', + text: actions.isPinned(fileId) ? 'Unpin' : 'Pin', + action: () => actions.togglePinFile?.(fileId), + }), + await sep() + ); + } + + items.push( await MenuItem.new({ id: 'close', text: 'Close', @@ -193,8 +214,8 @@ export async function showTabContextMenu( id: 'close-all', text: 'Close All', action: () => actions.closeAllFiles(), - }), - ]; + }) + ); if (filePath) { items.push( diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 0000000..23dfba1 --- /dev/null +++ b/src/utils/i18n.ts @@ -0,0 +1,167 @@ +export const TRANSLATIONS: Record> = { + English: { + settings: 'Settings', + general: 'General', + generalDesc: 'Customize your editor experience', + appearance: 'Appearance', + editor: 'Editor', + system: 'System', + autoSave: 'Auto-save', + autoSaveDesc: 'Automatically save changes while typing', + defaultView: 'Default View', + defaultViewDesc: 'Choose default view for new files', + sidebarPosition: 'Sidebar Position', + sidebarPositionDesc: 'Change the location of the sidebar', + language: 'Language', + languageDesc: 'Change interface language', + notifications: 'Notifications', + notificationsDesc: 'Configure desktop notifications', + themes: 'Themes', + themesDesc: 'Manage app appearance and customization', + colorMode: 'Color Mode', + mainThemes: 'Main Themes', + preview: 'Preview', + editing: 'Editing', + lines: 'lines', + words: 'words', + manual: 'Manual', + searchPlaceholder: 'Search...', + newNote: 'New Note', + noMatches: 'No matches found', + lightMode: 'Light mode', + darkMode: 'Dark mode', + }, + Spanish: { + settings: 'Ajustes', + general: 'General', + generalDesc: 'Personaliza tu experiencia con el editor', + appearance: 'Apariencia', + editor: 'Editor', + system: 'Sistema', + autoSave: 'Guardado automático', + autoSaveDesc: 'Guardar cambios automáticamente al escribir', + defaultView: 'Vista predeterminada', + defaultViewDesc: 'Elegir vista predeterminada para archivos nuevos', + sidebarPosition: 'Posición de la barra lateral', + sidebarPositionDesc: 'Cambiar la ubicación de la barra lateral', + language: 'Idioma', + languageDesc: 'Cambiar el idioma de la interfaz', + notifications: 'Notificaciones', + notificationsDesc: 'Configurar notificaciones de escritorio', + themes: 'Temas', + themesDesc: 'Gestionar la aparência y personalización de la aplicación', + colorMode: 'Modo de color', + mainThemes: 'Temas principales', + preview: 'Vista previa', + editing: 'Editando', + lines: 'líneas', + words: 'palabras', + manual: 'Manual', + searchPlaceholder: 'Buscar...', + newNote: 'Nueva nota', + noMatches: 'No se encontraron coincidencias', + lightMode: 'Modo claro', + darkMode: 'Modo oscuro', + }, + French: { + settings: 'Paramètres', + general: 'Général', + generalDesc: "Personnalisez votre expérience d'édition", + appearance: 'Apparence', + editor: 'Éditeur', + system: 'Système', + autoSave: 'Enregistrement automatique', + autoSaveDesc: + 'Enregistrer automatiquement les modifications lors de la saisie', + defaultView: 'Vue par défaut', + defaultViewDesc: 'Choisir la vue par défaut pour les nouveaux fichiers', + sidebarPosition: 'Position de la barre latérale', + sidebarPositionDesc: "Changer l'emplacement de la barre latérale", + language: 'Langue', + languageDesc: "Changer la langue de l'interface", + notifications: 'Notifications', + notificationsDesc: 'Configurer les notifications de bureau', + themes: 'Thèmes', + themesDesc: "Gérer l'apparence et la personnalisation de l'application", + colorMode: 'Mode de couleur', + mainThemes: 'Thèmes principaux', + preview: 'Aperçu', + editing: 'Édition', + lines: 'lignes', + words: 'mots', + manual: 'Manuel', + searchPlaceholder: 'Rechercher...', + newNote: 'Nouvelle note', + noMatches: 'Aucune correspondance trouvée', + lightMode: 'Mode clair', + darkMode: 'Mode sombre', + }, + German: { + settings: 'Einstellungen', + general: 'Allgemein', + generalDesc: 'Passen Sie Ihr Editor-Erlebnis an', + appearance: 'Aussehen', + editor: 'Editor', + system: 'System', + autoSave: 'Automatisch speichern', + autoSaveDesc: 'Änderungen beim Tippen automatisch speichern', + defaultView: 'Standardansicht', + defaultViewDesc: 'Standardansicht für neue Dateien wählen', + sidebarPosition: 'Position der Seitenleiste', + sidebarPositionDesc: 'Position der Seitenleiste ändern', + language: 'Sprache', + languageDesc: 'Oberflächensprache ändern', + notifications: 'Benachrichtigungen', + notificationsDesc: 'Desktop-Benachrichtigungen konfigurieren', + themes: 'Themes', + themesDesc: 'App-Erscheinungsbild und Anpassung verwalten', + colorMode: 'Farbmodus', + mainThemes: 'Haupt-Themes', + preview: 'Vorschau', + editing: 'Bearbeiten', + lines: 'Zeilen', + words: 'Wörter', + manual: 'Manuell', + searchPlaceholder: 'Suchen...', + newNote: 'Neue Notiz', + noMatches: 'Keine Treffer gefunden', + lightMode: 'Heller Modus', + darkMode: 'Dunkler Modus', + }, + Japanese: { + settings: '設定', + general: '一般', + generalDesc: 'エディタのエクスペリエンスをカスタマイズします', + appearance: '外観', + editor: 'エディタ', + system: 'システム', + autoSave: '自動保存', + autoSaveDesc: '入力中に変更を自動的に保存します', + defaultView: 'デフォルトビュー', + defaultViewDesc: '新しいファイルのデフォルトビューを選択します', + sidebarPosition: 'サイドバーの位置', + sidebarPositionDesc: 'サイドバーの場所を変更します', + language: '言語', + languageDesc: 'インターフェース言語を変更します', + notifications: '通知', + notificationsDesc: 'デスクトップ通知を設定します', + themes: 'テーマ', + themesDesc: 'アプリの外観とカスタマイズを管理します', + colorMode: 'カラーモード', + mainThemes: 'メインテーマ', + preview: 'プレビュー', + editing: '編集中', + lines: '行', + words: '単語', + manual: '手動', + searchPlaceholder: '検索...', + newNote: '新しいノート', + noMatches: '一致する項目が見つかりません', + lightMode: 'ライトモード', + darkMode: 'ダークモード', + }, +}; + +export const getTranslation = (lang: string, key: string) => { + return TRANSLATIONS[lang]?.[key] || TRANSLATIONS['English'][key] || key; +};