diff --git a/src/App.tsx b/src/App.tsx index 44aca661..17761277 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -159,6 +159,10 @@ function App() { setInspectorCollapsed((c) => !c)} + entry={activeTab?.entry ?? null} + content={activeTab?.content ?? null} + entries={entries} + onNavigate={handleNavigateWikilink} /> diff --git a/src/components/Inspector.css b/src/components/Inspector.css index 928ce897..a3151747 100644 --- a/src/components/Inspector.css +++ b/src/components/Inspector.css @@ -61,3 +61,53 @@ color: #555; margin: 0; } + +/* Properties */ +.inspector__props { + display: flex; + flex-direction: column; + gap: 8px; +} + +.inspector__prop { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 13px; +} + +.inspector__prop-label { + color: #888; + flex-shrink: 0; +} + +.inspector__prop-value { + color: #ccc; + text-align: right; +} + +.inspector__status-pill { + display: inline-block; + padding: 2px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + color: #fff; +} + +.inspector__add-prop { + margin-top: 12px; + background: none; + border: 1px dashed #444; + color: #666; + padding: 6px 12px; + border-radius: 6px; + cursor: not-allowed; + font-size: 12px; + width: 100%; +} + +.inspector__add-prop:hover { + border-color: #555; + color: #888; +} diff --git a/src/components/Inspector.test.tsx b/src/components/Inspector.test.tsx index 2a5bc7be..20f4b280 100644 --- a/src/components/Inspector.test.tsx +++ b/src/components/Inspector.test.tsx @@ -1,25 +1,98 @@ import { render, screen, fireEvent } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import { Inspector } from './Inspector' +import type { VaultEntry } from '../types' + +const mockEntry: VaultEntry = { + path: '/vault/project/test.md', + filename: 'test.md', + title: 'Test Project', + isA: 'Project', + aliases: [], + belongsTo: ['[[responsibility/grow-newsletter]]'], + relatedTo: ['[[topic/software-development]]'], + status: 'Active', + owner: 'Luca Rossi', + cadence: null, + modifiedAt: 1707900000, + fileSize: 1024, +} + +const mockContent = `--- +title: Test Project +is_a: Project +status: Active +--- + +# Test Project + +This is a test note with some words to count. +` + +const defaultProps = { + collapsed: false, + onToggle: () => {}, + entry: null as VaultEntry | null, + content: null as string | null, + entries: [] as VaultEntry[], + onNavigate: () => {}, +} describe('Inspector', () => { - it('renders expanded state with sections', () => { - render( {}} />) + it('renders expanded state with "no note selected"', () => { + render() expect(screen.getByText('Inspector')).toBeInTheDocument() expect(screen.getByText('Properties')).toBeInTheDocument() - expect(screen.getByText('Relationships')).toBeInTheDocument() + expect(screen.getByText('No note selected')).toBeInTheDocument() }) it('renders collapsed state without sections', () => { - render( {}} />) + render() expect(screen.queryByText('Inspector')).not.toBeInTheDocument() expect(screen.queryByText('Properties')).not.toBeInTheDocument() }) it('calls onToggle when toggle button clicked', () => { const onToggle = vi.fn() - render() + render() fireEvent.click(screen.getByRole('button')) expect(onToggle).toHaveBeenCalledOnce() }) + + it('shows properties when a note is selected', () => { + render() + expect(screen.getByText('Project')).toBeInTheDocument() + expect(screen.getByText('Active')).toBeInTheDocument() + expect(screen.getByText('Luca Rossi')).toBeInTheDocument() + expect(screen.getByText('Type')).toBeInTheDocument() + expect(screen.getByText('Status')).toBeInTheDocument() + expect(screen.getByText('Owner')).toBeInTheDocument() + expect(screen.getByText('Words')).toBeInTheDocument() + }) + + it('renders status as a colored pill', () => { + render() + const pill = screen.getByText('Active') + expect(pill).toHaveClass('inspector__status-pill') + expect(pill).toHaveStyle({ backgroundColor: '#4caf50' }) + }) + + it('computes word count from content minus frontmatter', () => { + render() + // "# Test Project" + "This is a test note with some words to count." = 13 words + expect(screen.getByText('13')).toBeInTheDocument() + }) + + it('shows "Add property" button as disabled placeholder', () => { + render() + const btn = screen.getByText('+ Add property') + expect(btn).toBeDisabled() + }) + + it('shows cadence when present', () => { + const entryWithCadence = { ...mockEntry, cadence: 'Weekly' } + render() + expect(screen.getByText('Cadence')).toBeInTheDocument() + expect(screen.getByText('Weekly')).toBeInTheDocument() + }) }) diff --git a/src/components/Inspector.tsx b/src/components/Inspector.tsx index 5656edc0..8e1487c2 100644 --- a/src/components/Inspector.tsx +++ b/src/components/Inspector.tsx @@ -1,11 +1,91 @@ +import type { VaultEntry } from '../types' import './Inspector.css' interface InspectorProps { collapsed: boolean onToggle: () => void + entry: VaultEntry | null + content: string | null + entries: VaultEntry[] + onNavigate: (target: string) => void } -export function Inspector({ collapsed, onToggle }: InspectorProps) { +const STATUS_COLORS: Record = { + Active: '#4caf50', + Done: '#2196f3', + Paused: '#ff9800', + Archived: '#9e9e9e', + Dropped: '#f44336', +} + +function formatDate(timestamp: number | null): string { + if (!timestamp) return '—' + const d = new Date(timestamp * 1000) + return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) +} + +function countWords(content: string | null): number { + if (!content) return 0 + // Strip YAML frontmatter + const stripped = content.replace(/^---[\s\S]*?---\n?/, '') + const words = stripped.trim().split(/\s+/).filter((w) => w.length > 0) + return words.length +} + +function PropertiesPanel({ entry, content }: { entry: VaultEntry; content: string | null }) { + const statusColor = entry.status ? STATUS_COLORS[entry.status] ?? '#888' : undefined + const wordCount = countWords(content) + + return ( +
+

Properties

+
+ {entry.isA && ( +
+ Type + {entry.isA} +
+ )} + {entry.status && ( +
+ Status + + {entry.status} + +
+ )} + {entry.owner && ( +
+ Owner + {entry.owner} +
+ )} + {entry.cadence && ( +
+ Cadence + {entry.cadence} +
+ )} +
+ Modified + {formatDate(entry.modifiedAt)} +
+
+ Words + {wordCount} +
+
+ +
+ ) +} + +export function Inspector({ collapsed, onToggle, entry, content, entries, onNavigate }: InspectorProps) { return (