@@ -145,7 +105,6 @@ export function DynamicPropertiesPanel({
?
setShowAddDialog(false)} vaultStatuses={vaultStatuses} />
: setShowAddDialog(true)} disabled={!onAddProperty} />
}
-
)
}
diff --git a/src/components/Inspector.tsx b/src/components/Inspector.tsx
index 742df6ed..73ba932e 100644
--- a/src/components/Inspector.tsx
+++ b/src/components/Inspector.tsx
@@ -6,7 +6,7 @@ import { SlidersHorizontal, X, Sparkle, WarningCircle, PencilSimple } from '@pho
import { Separator } from './ui/separator'
import { parseFrontmatter, detectFrontmatterState } from '../utils/frontmatter'
import { DynamicPropertiesPanel } from './DynamicPropertiesPanel'
-import { DynamicRelationshipsPanel, BacklinksPanel, ReferencedByPanel, GitHistoryPanel, InstancesPanel } from './InspectorPanels'
+import { DynamicRelationshipsPanel, BacklinksPanel, ReferencedByPanel, GitHistoryPanel, InstancesPanel, NoteInfoPanel } from './InspectorPanels'
import { wikilinkTarget } from '../utils/wikilink'
import type { ReferencedByItem, BacklinkItem } from './InspectorPanels'
@@ -189,7 +189,7 @@ export function Inspector({
{fmState === 'valid' ? (
<>
0 && }
+
+
{gitHistory.length > 0 && }
>
diff --git a/src/components/InspectorPanels.tsx b/src/components/InspectorPanels.tsx
index 38d4c8fd..feb0250e 100644
--- a/src/components/InspectorPanels.tsx
+++ b/src/components/InspectorPanels.tsx
@@ -5,3 +5,4 @@ export { ReferencedByPanel } from './inspector/ReferencedByPanel'
export type { ReferencedByItem } from './inspector/ReferencedByPanel'
export { GitHistoryPanel } from './inspector/GitHistoryPanel'
export { InstancesPanel } from './inspector/InstancesPanel'
+export { NoteInfoPanel } from './inspector/NoteInfoPanel'
diff --git a/src/components/inspector/NoteInfoPanel.test.tsx b/src/components/inspector/NoteInfoPanel.test.tsx
new file mode 100644
index 00000000..90a79e5f
--- /dev/null
+++ b/src/components/inspector/NoteInfoPanel.test.tsx
@@ -0,0 +1,81 @@
+import { describe, it, expect } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { NoteInfoPanel } from './NoteInfoPanel'
+import type { VaultEntry } from '../../types'
+
+function makeEntry(overrides: Partial = {}): VaultEntry {
+ return {
+ path: '/vault/test.md', filename: 'test.md', title: 'Test', isA: 'Note',
+ aliases: [], outgoingLinks: [], relationships: {}, tags: [],
+ modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 1024,
+ icon: null, color: null, archived: false, trashed: false, favorite: false,
+ ...overrides,
+ }
+}
+
+describe('NoteInfoPanel', () => {
+ it('renders Info section header with icon', () => {
+ render()
+ expect(screen.getByText('Info')).toBeInTheDocument()
+ })
+
+ it('renders Modified and Words in read-only Info section', () => {
+ render()
+ const readOnlyRows = screen.getAllByTestId('readonly-property')
+ const labels = readOnlyRows.map(row => row.querySelector('span')?.textContent)
+ expect(labels).toContain('Modified')
+ expect(labels).toContain('Words')
+ })
+
+ it('renders Created date', () => {
+ render()
+ const readOnlyRows = screen.getAllByTestId('readonly-property')
+ const labels = readOnlyRows.map(row => row.querySelector('span')?.textContent)
+ expect(labels).toContain('Created')
+ })
+
+ it('renders file size', () => {
+ render()
+ expect(screen.getByText('Size')).toBeInTheDocument()
+ expect(screen.getByText('4.2 KB')).toBeInTheDocument()
+ })
+
+ it('shows em dash for null timestamps', () => {
+ render()
+ const dashes = screen.getAllByText('\u2014')
+ expect(dashes.length).toBeGreaterThanOrEqual(2)
+ })
+
+ it('read-only rows do not have hover styling', () => {
+ render()
+ const readOnlyRows = screen.getAllByTestId('readonly-property')
+ readOnlyRows.forEach(row => {
+ expect(row.className).not.toContain('hover:bg-muted')
+ })
+ })
+
+ it('formats file sizes correctly', () => {
+ const { rerender } = render()
+ expect(screen.getByText('500 B')).toBeInTheDocument()
+
+ rerender()
+ expect(screen.getByText('2.0 KB')).toBeInTheDocument()
+
+ rerender()
+ expect(screen.getByText('1.0 MB')).toBeInTheDocument()
+ })
+
+ it('uses CSS grid with two equal columns on read-only rows', () => {
+ render()
+ const readOnlyRows = screen.getAllByTestId('readonly-property')
+ readOnlyRows.forEach(row => {
+ expect(row.className).toContain('grid')
+ expect(row.className).toContain('grid-cols-2')
+ })
+ })
+
+ it('renders word count from content', () => {
+ render()
+ expect(screen.getByText('Words')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/inspector/NoteInfoPanel.tsx b/src/components/inspector/NoteInfoPanel.tsx
new file mode 100644
index 00000000..5d2604d4
--- /dev/null
+++ b/src/components/inspector/NoteInfoPanel.tsx
@@ -0,0 +1,44 @@
+import type { VaultEntry } from '../../types'
+import { Info } from '@phosphor-icons/react'
+import { countWords } from '../../utils/wikilinks'
+
+function formatDate(timestamp: number | null): string {
+ if (!timestamp) return '\u2014'
+ const d = new Date(timestamp * 1000)
+ return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })
+}
+
+function formatFileSize(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`
+ const kb = bytes / 1024
+ if (kb < 1024) return `${kb.toFixed(1)} KB`
+ const mb = kb / 1024
+ return `${mb.toFixed(1)} MB`
+}
+
+function InfoRow({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ )
+}
+
+export function NoteInfoPanel({ entry, content }: { entry: VaultEntry; content: string | null }) {
+ const wordCount = countWords(content ?? '')
+ return (
+
+
+
+ Info
+
+
+
+
+
+
+
+
+ )
+}