fix: settings navigation

This commit is contained in:
2026-02-10 17:37:26 +01:00
parent 0876c294bd
commit fbe0efb088
5 changed files with 191 additions and 192 deletions

View File

@@ -79,69 +79,43 @@
opacity: 1; opacity: 1;
} }
/* Body layout */ /* Body layout - simplified, no internal sidebar */
.settings-body {
display: flex;
flex: 1;
overflow: hidden;
}
/* Category navigation */
.settings-nav {
display: flex;
flex-direction: column;
width: 180px;
min-width: 180px;
padding: 12px 0;
border-right: 1px solid var(--vscode-panel-border);
overflow-y: auto;
}
.settings-nav-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: transparent;
border: none;
border-left: 2px solid transparent;
color: var(--vscode-foreground);
font-size: 13px;
cursor: pointer;
text-align: left;
transition: background-color 0.1s;
}
.settings-nav-item:hover {
background-color: var(--vscode-list-hoverBackground);
}
.settings-nav-item.active {
background-color: var(--vscode-list-activeSelectionBackground);
border-left-color: var(--vscode-focusBorder);
font-weight: 500;
}
.settings-nav-icon {
font-size: 16px;
width: 20px;
text-align: center;
flex-shrink: 0;
}
.settings-nav-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Settings content area */
.settings-content { .settings-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 16px 24px 40px; padding: 16px 24px 40px;
} }
/* No results message */
.settings-no-results {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 48px 24px;
color: var(--vscode-descriptionForeground);
}
.settings-no-results p {
margin: 0;
font-size: 14px;
}
.settings-no-results button {
padding: 6px 14px;
font-size: 12px;
border: none;
border-radius: 4px;
cursor: pointer;
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
}
.settings-no-results button:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
/* Setting section */ /* Setting section */
.setting-section { .setting-section {
margin-bottom: 32px; margin-bottom: 32px;
@@ -431,23 +405,4 @@
color: var(--vscode-input-placeholderForeground); color: var(--vscode-input-placeholderForeground);
} }
/* Responsive - narrow sidebar */
@media (max-width: 600px) {
.settings-nav {
width: 48px;
min-width: 48px;
}
.settings-nav-label {
display: none;
}
.settings-nav-item {
justify-content: center;
padding: 10px;
}
.settings-nav-icon {
width: auto;
}
}

View File

@@ -1,10 +1,20 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useAppStore } from '../../store'; import { useAppStore } from '../../store';
import { showToast } from '../Toast'; import { showToast } from '../Toast';
import './SettingsView.css'; import './SettingsView.css';
// Settings categories matching VS Code style // Export category IDs for sidebar navigation
type SettingsCategory = 'editor' | 'content' | 'sync' | 'publishing' | 'data'; export type SettingsCategory = 'editor' | 'content' | 'sync' | 'publishing' | 'data';
// Scroll to a settings section by category ID
export const scrollToSettingsSection = (category: SettingsCategory) => {
const element = document.getElementById(`settings-section-${category}`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
// Settings categories
interface Credentials { interface Credentials {
// Turso Cloud Sync // Turso Cloud Sync
@@ -48,15 +58,6 @@ const SearchIcon = () => (
// Default post categories based on VISION.md // Default post categories based on VISION.md
const DEFAULT_POST_CATEGORIES = ['article', 'picture', 'aside', 'page']; const DEFAULT_POST_CATEGORIES = ['article', 'picture', 'aside', 'page'];
// Category definitions
const categories: { id: SettingsCategory; label: string; icon: string }[] = [
{ id: 'editor', label: 'Editor', icon: '📝' },
{ id: 'content', label: 'Content', icon: '📋' },
{ id: 'sync', label: 'Sync', icon: '🔄' },
{ id: 'publishing', label: 'Publishing', icon: '🚀' },
{ id: 'data', label: 'Data Management', icon: '🗄️' },
];
// Individual setting row component (VS Code style) // Individual setting row component (VS Code style)
const SettingRow: React.FC<{ const SettingRow: React.FC<{
id: string; id: string;
@@ -75,36 +76,55 @@ const SettingRow: React.FC<{
</div> </div>
); );
// Section header component // Section header component with optional ID for scrolling
const SettingSection: React.FC<{ const SettingSection: React.FC<{
id?: string;
title: string; title: string;
description?: string; description?: string;
children: React.ReactNode; children: React.ReactNode;
}> = ({ title, description, children }) => ( hidden?: boolean;
<div className="setting-section"> }> = ({ id, title, description, children, hidden }) => {
<div className="setting-section-header"> if (hidden) return null;
<h3>{title}</h3> return (
{description && <p className="setting-section-description">{description}</p>} <div className="setting-section" id={id}>
<div className="setting-section-header">
<h3>{title}</h3>
{description && <p className="setting-section-description">{description}</p>}
</div>
<div className="setting-section-content">
{children}
</div>
</div> </div>
<div className="setting-section-content"> );
{children} };
</div>
</div>
);
export const SettingsView: React.FC = () => { export const SettingsView: React.FC = () => {
const { preferredEditorMode, setPreferredEditorMode, syncConfigured } = useAppStore(); const { preferredEditorMode, setPreferredEditorMode, syncConfigured } = useAppStore();
const [activeCategory, setActiveCategory] = useState<SettingsCategory>('editor');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [credentials, setCredentials] = useState<Credentials>(defaultCredentials); const [credentials, setCredentials] = useState<Credentials>(defaultCredentials);
const [showSecrets, setShowSecrets] = useState(false); const [showSecrets, setShowSecrets] = useState(false);
const [dropboxConfigured, setDropboxConfigured] = useState(false); const [dropboxConfigured, setDropboxConfigured] = useState(false);
const [dropboxLastSync, setDropboxLastSync] = useState<string | null>(null); const [dropboxLastSync, setDropboxLastSync] = useState<string | null>(null);
const contentRef = useRef<HTMLDivElement>(null);
// Post categories management // Post categories management
const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES); const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES);
const [newCategoryInput, setNewCategoryInput] = useState(''); const [newCategoryInput, setNewCategoryInput] = useState('');
// Check if a setting matches the search query
const matchesSearch = useCallback((text: string) => {
if (!searchQuery) return true;
return text.toLowerCase().includes(searchQuery.toLowerCase());
}, [searchQuery]);
// Check if a section has any matching settings
const sectionHasMatches = useCallback((sectionKeywords: string[]) => {
if (!searchQuery) return true;
return sectionKeywords.some(keyword =>
keyword.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [searchQuery]);
// Load saved credentials and categories // Load saved credentials and categories
useEffect(() => { useEffect(() => {
const loadSettings = async () => { const loadSettings = async () => {
@@ -261,34 +281,36 @@ export const SettingsView: React.FC = () => {
} }
}; };
// Filter categories if searching // Keywords for each section for search filtering
const filteredCategories = searchQuery const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual'];
? categories.filter(c => c.label.toLowerCase().includes(searchQuery.toLowerCase())) const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page'];
: categories; const syncKeywords = ['sync', 'turso', 'libsql', 'cloud', 'database', 'dropbox', 'file', 'backup', 'token', 'remote'];
const publishingKeywords = ['publishing', 'ftp', 'ssh', 'deploy', 'server', 'host', 'upload'];
const dataKeywords = ['data', 'database', 'rebuild', 'maintenance', 'posts', 'media', 'links', 'folder', 'filesystem'];
const renderEditorSettings = () => ( const renderEditorSettings = () => (
<> <SettingSection
<SettingSection id="settings-section-editor"
title="Editor" title="Editor"
description="Configure the blog post editor behavior and appearance." description="Configure the blog post editor behavior and appearance."
hidden={!sectionHasMatches(editorKeywords)}
>
<SettingRow
id="editor-mode"
label="Default Editor Mode"
description="Choose the default mode when opening posts. You can switch modes at any time using the editor toolbar."
> >
<SettingRow <select
id="editor-mode" id="editor-mode"
label="Default Editor Mode" value={preferredEditorMode}
description="Choose the default mode when opening posts. You can switch modes at any time using the editor toolbar." onChange={(e) => setPreferredEditorMode(e.target.value as 'wysiwyg' | 'markdown' | 'preview')}
> >
<select <option value="wysiwyg">WYSIWYG (Visual Editor)</option>
id="editor-mode" <option value="markdown">Markdown (Source)</option>
value={preferredEditorMode} <option value="preview">Preview (Read-only)</option>
onChange={(e) => setPreferredEditorMode(e.target.value as 'wysiwyg' | 'markdown' | 'preview')} </select>
> </SettingRow>
<option value="wysiwyg">WYSIWYG (Visual Editor)</option> </SettingSection>
<option value="markdown">Markdown (Source)</option>
<option value="preview">Preview (Read-only)</option>
</select>
</SettingRow>
</SettingSection>
</>
); );
// Handlers for post categories management // Handlers for post categories management
@@ -323,11 +345,12 @@ export const SettingsView: React.FC = () => {
}; };
const renderContentSettings = () => ( const renderContentSettings = () => (
<> <SettingSection
<SettingSection id="settings-section-content"
title="Post Categories" title="Post Categories"
description="Manage the available categories for blog posts. Each post can have one category that determines its display template." description="Manage the available categories for blog posts. Each post can have one category that determines its display template."
> hidden={!sectionHasMatches(contentKeywords)}
>
<div className="categories-list"> <div className="categories-list">
{postCategories.map((cat) => ( {postCategories.map((cat) => (
<div key={cat} className="category-item"> <div key={cat} className="category-item">
@@ -366,15 +389,16 @@ export const SettingsView: React.FC = () => {
Reset to Defaults Reset to Defaults
</button> </button>
</div> </div>
</SettingSection> </SettingSection>
</>
); );
const renderSyncSettings = () => ( const renderSyncSettings = () => (
<> <>
<SettingSection <SettingSection
id="settings-section-sync"
title="Cloud Sync — Turso/LibSQL" title="Cloud Sync — Turso/LibSQL"
description="Sync post and media metadata to a Turso cloud database for backup and multi-device access." description="Sync post and media metadata to a Turso cloud database for backup and multi-device access."
hidden={!sectionHasMatches(syncKeywords)}
> >
<SettingRow <SettingRow
id="turso-url" id="turso-url"
@@ -436,6 +460,7 @@ export const SettingsView: React.FC = () => {
<SettingSection <SettingSection
title="File Sync — Dropbox" title="File Sync — Dropbox"
description="Synchronize your blog files (posts and media) to Dropbox for backup and cross-device access." description="Synchronize your blog files (posts and media) to Dropbox for backup and cross-device access."
hidden={!sectionHasMatches(syncKeywords)}
> >
<SettingRow <SettingRow
id="dropbox-token" id="dropbox-token"
@@ -523,8 +548,10 @@ export const SettingsView: React.FC = () => {
const renderPublishingSettings = () => ( const renderPublishingSettings = () => (
<> <>
<SettingSection <SettingSection
id="settings-section-publishing"
title="FTP Publishing" title="FTP Publishing"
description="Configure FTP credentials for publishing your blog to a web server." description="Configure FTP credentials for publishing your blog to a web server."
hidden={!sectionHasMatches(publishingKeywords)}
> >
<SettingRow <SettingRow
id="ftp-host" id="ftp-host"
@@ -577,6 +604,7 @@ export const SettingsView: React.FC = () => {
<SettingSection <SettingSection
title="SSH Publishing" title="SSH Publishing"
description="Configure SSH credentials for secure deployment to your server." description="Configure SSH credentials for secure deployment to your server."
hidden={!sectionHasMatches(publishingKeywords)}
> >
<SettingRow <SettingRow
id="ssh-host" id="ssh-host"
@@ -631,8 +659,10 @@ export const SettingsView: React.FC = () => {
const renderDataSettings = () => ( const renderDataSettings = () => (
<> <>
<SettingSection <SettingSection
id="settings-section-data"
title="Database Maintenance" title="Database Maintenance"
description="Rebuild the local database index from source files. Useful if post or media files were edited externally." description="Rebuild the local database index from source files. Useful if post or media files were edited externally."
hidden={!sectionHasMatches(dataKeywords)}
> >
<SettingRow <SettingRow
id="rebuild-posts" id="rebuild-posts"
@@ -715,6 +745,7 @@ export const SettingsView: React.FC = () => {
<SettingSection <SettingSection
title="File System" title="File System"
description="Access project data files and directories." description="Access project data files and directories."
hidden={!sectionHasMatches(dataKeywords)}
> >
<SettingRow <SettingRow
id="open-data" id="open-data"
@@ -737,35 +768,13 @@ export const SettingsView: React.FC = () => {
</> </>
); );
const renderContent = () => { // Check if any results match the search
if (searchQuery) { const hasAnyMatches = !searchQuery ||
// Show all matching settings when searching sectionHasMatches(editorKeywords) ||
return ( sectionHasMatches(contentKeywords) ||
<> sectionHasMatches(syncKeywords) ||
{renderEditorSettings()} sectionHasMatches(publishingKeywords) ||
{renderContentSettings()} sectionHasMatches(dataKeywords);
{renderSyncSettings()}
{renderPublishingSettings()}
{renderDataSettings()}
</>
);
}
switch (activeCategory) {
case 'editor':
return renderEditorSettings();
case 'content':
return renderContentSettings();
case 'sync':
return renderSyncSettings();
case 'publishing':
return renderPublishingSettings();
case 'data':
return renderDataSettings();
default:
return renderEditorSettings();
}
};
return ( return (
<div className="settings-view"> <div className="settings-view">
@@ -791,28 +800,22 @@ export const SettingsView: React.FC = () => {
</div> </div>
</div> </div>
<div className="settings-body"> {/* Settings content - all sections in scrollable list */}
{/* Category navigation sidebar */} <div className="settings-content" ref={contentRef}>
<nav className="settings-nav"> {hasAnyMatches ? (
{filteredCategories.map((cat) => ( <>
<button {renderEditorSettings()}
key={cat.id} {renderContentSettings()}
className={`settings-nav-item ${activeCategory === cat.id && !searchQuery ? 'active' : ''}`} {renderSyncSettings()}
onClick={() => { {renderPublishingSettings()}
setActiveCategory(cat.id); {renderDataSettings()}
setSearchQuery(''); </>
}} ) : (
> <div className="settings-no-results">
<span className="settings-nav-icon">{cat.icon}</span> <p>No settings found matching "{searchQuery}"</p>
<span className="settings-nav-label">{cat.label}</span> <button onClick={() => setSearchQuery('')}>Clear search</button>
</button> </div>
))} )}
</nav>
{/* Settings content */}
<div className="settings-content">
{renderContent()}
</div>
</div> </div>
</div> </div>
); );

View File

@@ -1 +1,2 @@
export { SettingsView } from './SettingsView'; export { SettingsView, scrollToSettingsSection } from './SettingsView';
export type { SettingsCategory } from './SettingsView';

View File

@@ -216,6 +216,21 @@
font-size: 12px; font-size: 12px;
color: var(--vscode-sideBar-foreground); color: var(--vscode-sideBar-foreground);
border-radius: 3px; border-radius: 3px;
background: transparent;
border: none;
cursor: pointer;
text-align: left;
width: 100%;
transition: background-color 0.1s;
}
.settings-nav-entry:hover {
background-color: var(--vscode-list-hoverBackground);
}
.settings-nav-entry.active {
background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
} }
.settings-nav-entry-icon { .settings-nav-entry-icon {

View File

@@ -578,8 +578,16 @@ const MediaList: React.FC = () => {
); );
}; };
import { scrollToSettingsSection, SettingsCategory } from '../SettingsView/SettingsView';
const SettingsNav: React.FC = () => { const SettingsNav: React.FC = () => {
const { syncConfigured } = useAppStore(); const { syncConfigured } = useAppStore();
const [activeSection, setActiveSection] = useState<SettingsCategory | null>(null);
const handleNavClick = (category: SettingsCategory) => {
setActiveSection(category);
scrollToSettingsSection(category);
};
return ( return (
<div className="sidebar-content settings-panel"> <div className="sidebar-content settings-panel">
@@ -590,26 +598,43 @@ const SettingsNav: React.FC = () => {
</div> </div>
<div className="settings-nav-list"> <div className="settings-nav-list">
<div className="settings-nav-entry"> <button
className={`settings-nav-entry ${activeSection === 'editor' ? 'active' : ''}`}
onClick={() => handleNavClick('editor')}
>
<span className="settings-nav-entry-icon">📝</span> <span className="settings-nav-entry-icon">📝</span>
<span>Editor</span> <span>Editor</span>
</div> </button>
<div className="settings-nav-entry"> <button
className={`settings-nav-entry ${activeSection === 'content' ? 'active' : ''}`}
onClick={() => handleNavClick('content')}
>
<span className="settings-nav-entry-icon">📋</span>
<span>Content</span>
</button>
<button
className={`settings-nav-entry ${activeSection === 'sync' ? 'active' : ''}`}
onClick={() => handleNavClick('sync')}
>
<span className="settings-nav-entry-icon">🔄</span> <span className="settings-nav-entry-icon">🔄</span>
<span>Sync</span> <span>Sync</span>
{syncConfigured && <span className="settings-nav-badge"></span>} {syncConfigured && <span className="settings-nav-badge"></span>}
</div> </button>
<div className="settings-nav-entry"> <button
className={`settings-nav-entry ${activeSection === 'publishing' ? 'active' : ''}`}
onClick={() => handleNavClick('publishing')}
>
<span className="settings-nav-entry-icon">🚀</span> <span className="settings-nav-entry-icon">🚀</span>
<span>Publishing</span> <span>Publishing</span>
</div> </button>
<div className="settings-nav-entry"> <button
className={`settings-nav-entry ${activeSection === 'data' ? 'active' : ''}`}
onClick={() => handleNavClick('data')}
>
<span className="settings-nav-entry-icon">🗄</span> <span className="settings-nav-entry-icon">🗄</span>
<span>Data</span> <span>Data</span>
</div> </button>
</div> </div>
<p className="settings-hint">Configure settings in the main editor area.</p>
</div> </div>
); );
}; };