diff --git a/.claude-done b/.claude-done index 93c889f0..3a876c67 100644 --- a/.claude-done +++ b/.claude-done @@ -1,18 +1,20 @@ -Task: performance-note-list -Summary: Implemented virtual list rendering for NoteList using react-virtuoso. +Task: modified-notes-indicator +Summary: Implemented visual indicators for uncommitted modified notes across the UI. Changes: -- NoteList ListView now uses component instead of .map() for flat lists -- Only visible items are rendered in the DOM (overscan: 200px) -- EntityView (relationship groups) unchanged — groups are small (<100 items) -- mock-tauri.ts generates 9000 bulk entries for testing -- Test setup mocks react-virtuoso for JSDOM compatibility -- 7 new tests covering large dataset rendering, search, sorting, filtering -- Design file: design/performance-note-list.pen (4 frames) -- Docs updated: ARCHITECTURE.md notes virtual rendering approach +- NoteItem: Orange dot (accent-orange) before title for notes in git modified/added state +- TabBar: Orange dot between title and close button for tabs with uncommitted changes +- StatusBar: "N pending" counter with CircleDot icon when modified files exist +- useEditorSave: Added onAfterSave callback to refresh modified files list after save +- mock-tauri: Dynamically tracks saved files as modified, clears on commit +- Design file: 3 frames showing NoteList, TabBar, and StatusBar indicator states Product decisions: -- Only virtualized ListView (flat lists), not EntityView — relationship groups - are inherently small and do not need virtualization -- Used overscan={200} for smooth scrolling experience -- PinnedCard and TrashWarningBanner rendered as Virtuoso Header component +- Used accent-orange (#D9730D) for the indicator dot to stand out from type-colored icons +- Dot appears before title in NoteList (consistent with how editors show file state) +- Dot appears between title and close button in TabBar (visible but non-intrusive) +- StatusBar shows "N pending" text (not just a number) for clarity +- Indicators disappear after git commit (mockHasChanges + mockSavedPaths cleared) + +Tests: 12 new tests covering NoteList (4), TabBar (3), StatusBar (3), useEditorSave (2) +All 543 tests pass. Build succeeds. Code health gates passed. diff --git a/design/modified-notes-indicator.pen b/design/modified-notes-indicator.pen new file mode 100644 index 00000000..183c2ddb --- /dev/null +++ b/design/modified-notes-indicator.pen @@ -0,0 +1,537 @@ +{ + "version": "2.8", + "children": [ + { + "type": "frame", + "id": "mni01", + "x": 0, + "y": 0, + "name": "Modified Note Indicator — NoteList States", + "width": 340, + "height": "fit_content(600)", + "fill": "$--card", + "layout": "vertical", + "children": [ + { + "type": "text", + "id": "mni01t", + "fill": "$--muted-foreground", + "content": "NoteList — Modified vs Clean", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "textTransform": "uppercase", + "letterSpacing": 1, + "width": "fill_container", + "padding": [8, 16] + }, + { + "type": "frame", + "id": "mni02", + "name": "Note Item — Modified", + "width": "fill_container", + "stroke": { "align": "inside", "thickness": { "bottom": 1 }, "fill": "$--border" }, + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "children": [ + { + "type": "frame", + "id": "mni02r", + "name": "titleRow", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mni02tg", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "mni02dot", + "name": "Modified Indicator", + "width": 6, + "height": 6, + "fill": "$--accent-orange" + }, + { + "type": "text", + "id": "mni02ti", + "fill": "$--foreground", + "content": "Build Laputa App", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "mni02ic", + "width": 14, + "height": 14, + "iconFontName": "wrench", + "iconFontFamily": "phosphor", + "fill": "$--accent-red" + } + ] + }, + { + "type": "text", + "id": "mni02sn", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Personal knowledge management app built with Tauri + React.", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mni02tm", + "fill": "$--muted-foreground", + "content": "2m ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mni03", + "name": "Note Item — Clean (no indicator)", + "width": "fill_container", + "stroke": { "align": "inside", "thickness": { "bottom": 1 }, "fill": "$--border" }, + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "children": [ + { + "type": "frame", + "id": "mni03r", + "name": "titleRow", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mni03ti", + "fill": "$--foreground", + "content": "Facebook Ads Strategy", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "icon_font", + "id": "mni03ic", + "width": 14, + "height": 14, + "iconFontName": "file-text", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "text", + "id": "mni03sn", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Lookalike audiences convert 3x better than cold.", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mni03tm", + "fill": "$--muted-foreground", + "content": "1h ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mni04", + "name": "Note Item — Added (new file)", + "width": "fill_container", + "stroke": { "align": "inside", "thickness": { "bottom": 1 }, "fill": "$--border" }, + "layout": "vertical", + "gap": 4, + "padding": [14, 16], + "children": [ + { + "type": "frame", + "id": "mni04r", + "name": "titleRow", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mni04tg", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "mni04dot", + "name": "Modified Indicator", + "width": 6, + "height": 6, + "fill": "$--accent-orange" + }, + { + "type": "text", + "id": "mni04ti", + "fill": "$--foreground", + "content": "AI Agents Primer", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "icon_font", + "id": "mni04ic", + "width": 14, + "height": 14, + "iconFontName": "file-text", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "text", + "id": "mni04sn", + "fill": "$--muted-foreground", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "How autonomous agents are changing software development.", + "lineHeight": 1.5, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mni04tm", + "fill": "$--muted-foreground", + "content": "5m ago", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "mni10", + "x": 380, + "y": 0, + "name": "Modified Note Indicator — TabBar States", + "width": 800, + "height": 120, + "fill": "$--sidebar", + "layout": "vertical", + "children": [ + { + "type": "text", + "id": "mni10t", + "fill": "$--muted-foreground", + "content": "TabBar — Modified Dot on Dirty Tabs", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "textTransform": "uppercase", + "letterSpacing": 1, + "padding": [8, 16] + }, + { + "type": "frame", + "id": "mni11", + "name": "Tab Bar with Modified Indicators", + "width": "fill_container", + "height": 45, + "fill": "$--sidebar", + "stroke": { "align": "inside", "thickness": { "bottom": 1 }, "fill": "$--sidebar-border" }, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mni11a", + "name": "Tab Active — Modified", + "height": "fill_container", + "fill": "$--background", + "stroke": { "align": "inside", "thickness": { "right": 1 }, "fill": "$--border" }, + "gap": 6, + "padding": [0, 14], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mni11at", + "fill": "$--foreground", + "content": "Build Laputa App", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "ellipse", + "id": "mni11adot", + "name": "Tab Modified Dot", + "width": 6, + "height": 6, + "fill": "$--accent-orange" + }, + { + "type": "icon_font", + "id": "mni11ax", + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "mni11b", + "name": "Tab Inactive — Clean", + "height": "fill_container", + "stroke": { "align": "inside", "thickness": { "right": 1, "bottom": 1 }, "fill": "$--sidebar-border" }, + "gap": 6, + "padding": [0, 14], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mni11bt", + "fill": "$--muted-foreground", + "content": "Facebook Ads Strategy", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "icon_font", + "id": "mni11bx", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "mni11c", + "name": "Tab Inactive — Modified", + "height": "fill_container", + "stroke": { "align": "inside", "thickness": { "right": 1, "bottom": 1 }, "fill": "$--sidebar-border" }, + "gap": 6, + "padding": [0, 14], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mni11ct", + "fill": "$--muted-foreground", + "content": "AI Agents Primer", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "ellipse", + "id": "mni11cdot", + "name": "Tab Modified Dot", + "width": 6, + "height": 6, + "fill": "$--accent-orange" + }, + { + "type": "icon_font", + "id": "mni11cx", + "opacity": 0, + "width": 14, + "height": 14, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "mni11sp", + "name": "tabSpacer", + "width": "fill_container", + "height": "fill_container", + "stroke": { "align": "inside", "thickness": { "bottom": 1 }, "fill": "$--border" } + }, + { + "type": "frame", + "id": "mni11ctrl", + "name": "tabControls", + "height": "fill_container", + "stroke": { "align": "inside", "thickness": { "bottom": 1, "left": 1 }, "fill": "$--border" }, + "gap": 12, + "padding": [0, 12], + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "mni11plus", + "width": 16, + "height": 16, + "iconFontName": "plus", + "iconFontFamily": "phosphor", + "fill": "$--muted-foreground" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "mni20", + "x": 0, + "y": 400, + "name": "Modified Note Indicator — StatusBar Pending Count", + "width": 800, + "height": 80, + "fill": "$--background", + "layout": "vertical", + "children": [ + { + "type": "text", + "id": "mni20t", + "fill": "$--muted-foreground", + "content": "StatusBar — Pending Changes Counter", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "textTransform": "uppercase", + "letterSpacing": 1, + "padding": [8, 16] + }, + { + "type": "frame", + "id": "mni21", + "name": "StatusBar with Pending Count", + "width": "fill_container", + "height": 30, + "fill": "$--sidebar", + "stroke": { "align": "inside", "thickness": { "top": 1 }, "fill": "$--border" }, + "padding": [0, 8], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mni21l", + "name": "left", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mni21v", + "gap": 4, + "alignItems": "center", + "children": [ + { "type": "icon_font", "id": "mni21fi", "width": 13, "height": 13, "iconFontName": "folder-open", "iconFontFamily": "lucide", "fill": "$--muted-foreground" }, + { "type": "text", "id": "mni21fl", "fill": "$--muted-foreground", "content": "Demo v2", "fontFamily": "Inter", "fontSize": 11 } + ] + }, + { "type": "text", "id": "mni21s1", "fill": "$--border", "content": "|", "fontFamily": "Inter", "fontSize": 11 }, + { + "type": "frame", + "id": "mni21br", + "gap": 4, + "alignItems": "center", + "children": [ + { "type": "icon_font", "id": "mni21gi", "width": 13, "height": 13, "iconFontName": "git-branch", "iconFontFamily": "lucide", "fill": "$--muted-foreground" }, + { "type": "text", "id": "mni21gl", "fill": "$--muted-foreground", "content": "main", "fontFamily": "Inter", "fontSize": 11 } + ] + }, + { "type": "text", "id": "mni21s2", "fill": "$--border", "content": "|", "fontFamily": "Inter", "fontSize": 11 }, + { + "type": "frame", + "id": "mni21sy", + "gap": 4, + "alignItems": "center", + "children": [ + { "type": "icon_font", "id": "mni21ri", "width": 13, "height": 13, "iconFontName": "refresh-cw", "iconFontFamily": "lucide", "fill": "$--accent-green" }, + { "type": "text", "id": "mni21rl", "fill": "$--muted-foreground", "content": "Synced 2m ago", "fontFamily": "Inter", "fontSize": 11 } + ] + }, + { "type": "text", "id": "mni21s3", "fill": "$--border", "content": "|", "fontFamily": "Inter", "fontSize": 11 }, + { + "type": "frame", + "id": "mni21pd", + "name": "Pending Count", + "gap": 4, + "alignItems": "center", + "children": [ + { "type": "icon_font", "id": "mni21pi", "width": 13, "height": 13, "iconFontName": "circle-dot", "iconFontFamily": "lucide", "fill": "$--accent-orange" }, + { "type": "text", "id": "mni21pl", "fill": "$--muted-foreground", "content": "3 pending", "fontFamily": "Inter", "fontSize": 11 } + ] + } + ] + }, + { + "type": "frame", + "id": "mni21r", + "name": "right", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mni21nc", + "gap": 4, + "alignItems": "center", + "children": [ + { "type": "icon_font", "id": "mni21ni", "width": 13, "height": 13, "iconFontName": "file-text", "iconFontFamily": "lucide", "fill": "$--muted-foreground" }, + { "type": "text", "id": "mni21nl", "fill": "$--muted-foreground", "content": "9,200 notes", "fontFamily": "Inter", "fontSize": 11 } + ] + }, + { "type": "icon_font", "id": "mni21set", "width": 14, "height": 14, "iconFontName": "settings", "iconFontFamily": "lucide", "fill": "$--muted-foreground" } + ] + } + ] + } + ] + } + ], + "themes": { "Mode": ["Dark"] }, + "variables": {}, + "fonts": [] +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 754aa19d..fc420873 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -69,13 +69,13 @@ Laputa is a personal knowledge and life management desktop app. It reads a vault │Events │ │ │ │ │Topics │ │ │ │ ├────────┴─────────────┴─────────────────────────┴────────────┤ -│ StatusBar: v0.4.2 │ main │ Synced 2m ago 1,247 notes│ +│ StatusBar: v0.4.2 │ main │ Synced 2m ago │ 3 pending notes│ └─────────────────────────────────────────────────────────────┘ ``` - **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Favorites) and collapsible section groups (Projects, Experiments, Responsibilities, etc.) -- **Note List** (200-500px, resizable): Filtered list of notes matching the sidebar selection. Shows snippets, modified dates, and relationship groups. The flat list view (All Notes, type sections, etc.) uses **react-virtuoso** for virtual rendering — only visible items are in the DOM, enabling smooth scrolling with 9000+ notes. Entity/relationship views are not virtualized (groups are small). -- **Editor** (flex, fills remaining space): Tab bar, breadcrumb bar with word count and modified indicator, BlockNote editor with wikilink support. Can toggle to diff view for modified files. +- **Note List** (200-500px, resizable): Filtered list of notes matching the sidebar selection. Shows snippets, modified dates, relationship groups, and orange dot indicators for uncommitted modified notes. +- **Editor** (flex, fills remaining space): Tab bar (with orange modified dots on dirty tabs), breadcrumb bar with word count and modified indicator, BlockNote editor with wikilink support. Can toggle to diff view for modified files. - **Inspector / AI Chat** (200-500px or 40px collapsed): Toggles between Inspector (frontmatter, relationships, backlinks, git history) and AI Chat panel. The Sparkle icon in the breadcrumb bar toggles between them. Panels are separated by `ResizeHandle` components that support drag-to-resize. diff --git a/src/App.tsx b/src/App.tsx index a558637f..5ff6c969 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -91,6 +91,7 @@ function App() { updateVaultContent: vault.updateContent, setTabs: notes.setTabs, setToastMessage, + onAfterSave: vault.loadModifiedFiles, }) const entryActions = useEntryActions({ @@ -271,7 +272,7 @@ function App() { /> - setShowSettings(true)} onOpenLocalFolder={handleOpenLocalFolder} onCreateNewVault={handleCreateNewVault} onConnectGitHub={() => setShowGitHubVault(true)} hasGitHub={!!settings.github_token} /> + setShowSettings(true)} onOpenLocalFolder={handleOpenLocalFolder} onCreateNewVault={handleCreateNewVault} onConnectGitHub={() => setShowGitHubVault(true)} hasGitHub={!!settings.github_token} /> setToastMessage(null)} /> setShowQuickOpen(false)} /> setShowCreateTypeDialog(false)} onCreate={handleCreateType} /> diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index afb4e0dd..bd8ad4ea 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -439,6 +439,7 @@ export const Editor = memo(function Editor({ onClickNote: (entry: VaultEntry, e: React.MouseEvent) => void }) { @@ -74,6 +75,14 @@ export function NoteItem({ entry, isSelected, typeEntryMap, onClickNote }: {
+ {isModified && ( + + )} {entry.title} {entry.archived && ( diff --git a/src/components/NoteList.test.tsx b/src/components/NoteList.test.tsx index cdcf3a45..66d53c50 100644 --- a/src/components/NoteList.test.tsx +++ b/src/components/NoteList.test.tsx @@ -716,6 +716,47 @@ describe('filterEntries — trash', () => { }) }) +describe('NoteList — modified indicators', () => { + it('shows modified indicator dot for entries in modifiedFiles', () => { + const modifiedFiles = [ + { path: mockEntries[0].path, relativePath: 'project/26q1-laputa-app.md', status: 'modified' as const }, + ] + render( + + ) + const indicators = screen.getAllByTestId('modified-indicator') + expect(indicators).toHaveLength(1) + // The indicator's parent div contains the title text + const noteRow = indicators[0].closest('[data-testid="modified-indicator"]')!.parentElement!.parentElement! + expect(noteRow.textContent).toContain('Build Laputa App') + }) + + it('does not show modified indicator when modifiedFiles is empty', () => { + render( + + ) + expect(screen.queryByTestId('modified-indicator')).not.toBeInTheDocument() + }) + + it('shows multiple modified indicators for multiple modified files', () => { + const modifiedFiles = [ + { path: mockEntries[0].path, relativePath: 'project/26q1-laputa-app.md', status: 'modified' as const }, + { path: mockEntries[1].path, relativePath: 'note/facebook-ads-strategy.md', status: 'modified' as const }, + ] + render( + + ) + expect(screen.getAllByTestId('modified-indicator')).toHaveLength(2) + }) + + it('does not show modified indicator when modifiedFiles prop is undefined', () => { + render( + + ) + expect(screen.queryByTestId('modified-indicator')).not.toBeInTheDocument() + }) +}) + describe('NoteList — trash view', () => { const trashSelection: SidebarSelection = { kind: 'filter', filter: 'trash' } diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index c1d8e161..8f3ccd5f 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -247,6 +247,11 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF const [collapsedGroups, setCollapsedGroups] = useState>(new Set()) const [sortPrefs, setSortPrefs] = useState>(loadSortPreferences) + const modifiedPathSet = useMemo( + () => new Set((modifiedFiles ?? []).map((f) => f.path)), + [modifiedFiles], + ) + const handleSortChange = useCallback((groupLabel: string, option: SortOption, direction: SortDirection) => { setSortPrefs((prev) => { const next = { ...prev, [groupLabel]: { option, direction } }; saveSortPreferences(next); return next }) }, []) @@ -267,8 +272,8 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF }, [onSelectNote, onReplaceActiveTab]) const renderItem = useCallback((entry: VaultEntry) => ( - - ), [selectedNote?.path, handleClickNote, typeEntryMap]) + + ), [selectedNote?.path, handleClickNote, typeEntryMap, modifiedPathSet]) return (
diff --git a/src/components/StatusBar.test.tsx b/src/components/StatusBar.test.tsx index d5d2db18..06664716 100644 --- a/src/components/StatusBar.test.tsx +++ b/src/components/StatusBar.test.tsx @@ -128,6 +128,22 @@ describe('StatusBar', () => { expect(screen.getByText('Connect GitHub repo')).toBeInTheDocument() }) + it('shows modified count when modifiedCount is > 0', () => { + render() + expect(screen.getByTestId('status-modified-count')).toBeInTheDocument() + expect(screen.getByText('3 pending')).toBeInTheDocument() + }) + + it('does not show modified count when modifiedCount is 0', () => { + render() + expect(screen.queryByTestId('status-modified-count')).not.toBeInTheDocument() + }) + + it('does not show modified count when modifiedCount is not provided', () => { + render() + expect(screen.queryByTestId('status-modified-count')).not.toBeInTheDocument() + }) + it('closes menu after clicking "Open local folder"', () => { render( diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index 5a49aaa1..c64aaffd 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect } from 'react' -import { Package, GitBranch, RefreshCw, Sparkles, FileText, Bell, Settings, FolderOpen, Check, Github, FolderPlus } from 'lucide-react' +import { Package, GitBranch, RefreshCw, Sparkles, FileText, Bell, Settings, FolderOpen, Check, Github, FolderPlus, CircleDot } from 'lucide-react' export interface VaultOption { label: string @@ -8,6 +8,7 @@ export interface VaultOption { interface StatusBarProps { noteCount: number + modifiedCount?: number vaultPath: string vaults: VaultOption[] onSwitchVault: (path: string) => void @@ -121,7 +122,7 @@ const ICON_STYLE = { display: 'flex', alignItems: 'center', gap: 4 } as const const DISABLED_STYLE = { display: 'flex', alignItems: 'center', opacity: 0.4, cursor: 'not-allowed' } as const const SEP_STYLE = { color: 'var(--border)' } as const -export function StatusBar({ noteCount, vaultPath, vaults, onSwitchVault, onOpenSettings, onOpenLocalFolder, onCreateNewVault, onConnectGitHub, hasGitHub }: StatusBarProps) { +export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onSwitchVault, onOpenSettings, onOpenLocalFolder, onCreateNewVault, onConnectGitHub, hasGitHub }: StatusBarProps) { return (