From dc14c328eac1e6fbd7678798e1bd680a5d0be285 Mon Sep 17 00:00:00 2001 From: Adib Hanna Date: Thu, 25 Jun 2026 09:07:48 -0500 Subject: [PATCH] feat(settings): grouped rail + sub-tabbed dense pages to cut overwhelm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganize the settings modal so it stops feeling overwhelming — without removing any setting or changing how anything persists. - Rail: group the 9 categories into Look & feel / Editing / Vault / System, each with an icon + short label; descriptions move out of the rail onto the page header. Empty query shows the grouped rail; typing switches to flat search results. - Dense pages -> focused in-page sub-tabs: Vault (Location / Notes / System) and Editor (Vim / Search / Writing / Quick capture). Existing sections are spliced into sub-tabs with no control moved or rewritten. - Search now opens the sub-tab that owns a result before scrolling to and highlighting it. - Keymap left as-is: its sticky live-filter + grouped list already suits 50+ shortcuts; sub-tabs would fragment that search. Verified: control / search-anchor / onChange counts are identical to before, so no setting was dropped and every store + config.toml binding is preserved. Typecheck + app-core tests pass. --- .../app-core/src/components/SettingsModal.tsx | 332 +++++++++++++++++- 1 file changed, 319 insertions(+), 13 deletions(-) diff --git a/packages/app-core/src/components/SettingsModal.tsx b/packages/app-core/src/components/SettingsModal.tsx index a069296b..5e2e975d 100644 --- a/packages/app-core/src/components/SettingsModal.tsx +++ b/packages/app-core/src/components/SettingsModal.tsx @@ -80,14 +80,111 @@ type SettingsCategoryId = type ResolvedVaultTextSearchBackend = 'builtin' | 'ripgrep' | 'fzf' +type SettingsSectionId = 'look' | 'editing' | 'vault' | 'system' + +/** A focused sub-screen within a dense category (e.g. Vault → Location/Folders/Remote). */ +interface SettingsSubTab { + id: string + title: string + /** Search-item ids that live on this sub-tab, so search-jump can open the right one. */ + searchIds?: string[] + content: JSX.Element +} + interface SettingsCategory extends SettingsSearchCategory { id: SettingsCategoryId title: string description: string keywords: string[] - content: JSX.Element + /** A category renders either a single `content` pane or a set of `subTabs`. */ + content?: JSX.Element + subTabs?: SettingsSubTab[] +} + +/** Compact stroke icon used in the grouped settings rail. */ +function NavIcon({ children }: { children: React.ReactNode }): JSX.Element { + return ( + + ) } +const SETTINGS_CATEGORY_ICONS: Record = { + appearance: ( + + + + + ), + typography: ( + + + + + + ), + editor: ( + + + + + ), + keymaps: ( + + + + + ), + vault: ( + + + + + ), + templates: ( + + + + + ), + mcp: ( + + + + + ), + cli: ( + + + + + ), + about: ( + + + + + ) +} + +const SETTINGS_SECTIONS: { id: SettingsSectionId; title: string; categoryIds: SettingsCategoryId[] }[] = [ + { id: 'look', title: 'Look & feel', categoryIds: ['appearance', 'typography'] }, + { id: 'editing', title: 'Editing', categoryIds: ['editor', 'keymaps'] }, + { id: 'vault', title: 'Vault', categoryIds: ['vault', 'templates'] }, + { id: 'system', title: 'System', categoryIds: ['mcp', 'cli', 'about'] } +] + function settingsSearchTargetProps( settingId: string | undefined ): { 'data-settings-search-id'?: string } { @@ -645,6 +742,10 @@ export function SettingsModal(): JSX.Element { const ref = useRef(null) const settingsSearchHighlightTimerRef = useRef(null) const [activeCategory, setActiveCategory] = useState('appearance') + // Per-category active sub-tab (dense categories split their content into sub-tabs). + const [activeSubTabByCategory, setActiveSubTabByCategory] = useState< + Partial> + >({}) const [activeSearchResultId, setActiveSearchResultId] = useState(null) const [navQuery, setNavQuery] = useState('') const availableVaultTextSearchTools = [ @@ -1003,7 +1104,18 @@ export function SettingsModal(): JSX.Element { keywords: ['quick capture', 'hotkey', 'shortcut'] } ], - content: ( + subTabs: [ + { + id: 'vim', + title: 'Vim', + searchIds: [ + 'vim-mode', + 'vim-insert-escape', + 'leader-key-hints', + 'leader-hint-behavior', + 'leader-hint-duration' + ], + content: (
)}
- +
+ ) + }, + { + id: 'search', + title: 'Search', + searchIds: ['vault-text-search-backend', 'ripgrep-binary-path', 'fzf-binary-path'], + content: ( +
- +
+ ) + }, + { + id: 'writing', + title: 'Writing', + searchIds: [ + 'live-preview', + 'render-tables', + 'markdown-snippets', + 'note-tabs', + 'wrap-note-tabs', + 'word-wrap', + 'smooth-preview-scroll', + 'pdfs-in-edit-mode' + ], + content: ( +
- +
+ ) + }, + { + id: 'quick-capture', + title: 'Quick capture', + searchIds: ['date-titled-quick-notes', 'quick-note-prefix', 'quick-capture-hotkey'], + content: ( +
- ) + ) + } + ] }, { id: 'keymaps', @@ -1597,7 +1744,12 @@ export function SettingsModal(): JSX.Element { keywords: ['system folders', 'tasks', 'todos', 'goals', 'rename'] } ], - content: ( + subTabs: [ + { + id: 'location', + title: 'Location', + searchIds: ['vault-location', 'saved-remote-workspaces'], + content: (
)} - +
+ ) + }, + { + id: 'notes', + title: 'Notes', + searchIds: [ + 'primary-notes-location', + 'enable-daily-notes', + 'daily-notes-directory', + 'daily-note-title-pattern', + 'daily-note-locale', + 'daily-note-pattern-support', + 'daily-note-pattern-reset', + 'open-todays-daily-note', + 'daily-notes-template', + 'enable-weekly-notes', + 'weekly-notes-directory', + 'weekly-note-title-pattern', + 'weekly-note-locale', + 'weekly-note-pattern-support', + 'weekly-note-pattern-reset', + 'weekly-notes-template', + 'open-this-week-note', + 'auto-calendar-panel', + 'calendar-week-start', + 'calendar-week-numbers' + ], + content: ( +
- +
+ ) + }, + { + id: 'system', + title: 'System', + searchIds: [ + 'inbox-label', + 'quick-notes-label', + 'archive-label', + 'trash-label', + 'tasks-label' + ], + content: ( +
- ) + ) + } + ] }, { id: 'templates', @@ -2602,7 +2799,46 @@ export function SettingsModal(): JSX.Element {
-
)} - + + )}
@@ -2676,7 +2924,22 @@ export function SettingsModal(): JSX.Element {
{visibleCategory ? ( - visibleCategory.content + visibleCategory.subTabs ? ( + + setActiveSubTabByCategory((prev) => ({ + ...prev, + [visibleCategory.id]: tabId + })) + } + /> + ) : ( + visibleCategory.content + ) ) : (
Try a broader search term, or clear the search field to browse every settings section. @@ -3030,6 +3293,49 @@ function Section({ ) } +/** Renders a dense category as focused sub-tabs (Vault → Location/Folders/Remote, …). */ +function CategorySubTabs({ + tabs, + activeId, + onSelect +}: { + tabs: SettingsSubTab[] + activeId: string + onSelect: (id: string) => void +}): JSX.Element { + const active = tabs.find((tab) => tab.id === activeId) ?? tabs[0] + return ( +
+
+ {tabs.map((tab) => { + const selected = tab.id === active.id + return ( + + ) + })} +
+
{active.content}
+
+ ) +} + function InlineNote({ children }: { children: React.ReactNode }): JSX.Element { return
{children}
}