diff --git a/src/App.tsx b/src/App.tsx index 0b272853..d616289f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -263,7 +263,7 @@ function App() {
- setShowCreateDialog(true)} /> + setShowCreateDialog(true)} />
diff --git a/src/components/NoteList.css b/src/components/NoteList.css index ac4249b4..f73e9856 100644 --- a/src/components/NoteList.css +++ b/src/components/NoteList.css @@ -8,7 +8,7 @@ } .note-list__header { - padding: 16px; + padding: 14px 16px; border-bottom: 1px solid #2a2a4a; display: flex; align-items: center; @@ -28,8 +28,8 @@ } .note-list__count { - font-size: 12px; - color: #888; + font-size: 11px; + color: #777; background: #2a2a4a; padding: 2px 8px; border-radius: 10px; @@ -65,7 +65,7 @@ .note-list__search-input { width: 100%; - padding: 6px 10px; + padding: 7px 10px; background: #1e1e3a; border: 1px solid #2a2a4a; border-radius: 6px; @@ -73,14 +73,15 @@ font-size: 13px; outline: none; box-sizing: border-box; + transition: border-color 0.15s; } .note-list__search-input::placeholder { - color: #666; + color: #555; } .note-list__search-input:focus { - border-color: #4a4a7a; + border-color: #4a9eff; } /* Type filter pills */ @@ -98,72 +99,157 @@ border: 1px solid #2a2a4a; border-radius: 12px; background: transparent; - color: #888; + color: #777; cursor: pointer; white-space: nowrap; + transition: all 0.15s; } .note-list__pill:hover { background: #2a2a4a; - color: #e0e0e0; + color: #ccc; } .note-list__pill--active { - background: #3a3a5a; + background: #4a9eff; color: #ffffff; - border-color: #4a4a7a; + border-color: #4a9eff; } .note-list__items { flex: 1; + overflow-y: auto; } .note-list__empty { - padding: 24px 16px; - color: #666; + padding: 32px 16px; + color: #555; font-size: 13px; text-align: center; } +/* Note items */ .note-list__item { padding: 10px 16px; - border-bottom: 1px solid #1e1e3a; + border-bottom: 1px solid rgba(42, 42, 74, 0.5); cursor: pointer; + transition: background 0.1s; } .note-list__item:hover { background: #1e1e3a; } +.note-list__item-top { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; +} + .note-list__title { - font-size: 14px; - font-weight: 500; - margin-bottom: 4px; + font-size: 13px; + font-weight: 600; + color: #e0e0e0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; +} + +.note-list__date { + font-size: 11px; + color: #555; + flex-shrink: 0; + white-space: nowrap; +} + +.note-list__snippet { + font-size: 12px; + color: #666; + margin-top: 3px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.4; } .note-list__meta { display: flex; - gap: 8px; + gap: 6px; font-size: 11px; color: #888; + margin-top: 5px; } +/* Type badges with distinct colors */ .note-list__type { - background: #2a2a4a; - padding: 1px 6px; + padding: 1px 7px; border-radius: 3px; + font-weight: 500; + font-size: 10px; + letter-spacing: 0.02em; +} + +.note-list__type--project { + background: rgba(74, 158, 255, 0.15); + color: #4a9eff; +} + +.note-list__type--responsibility { + background: rgba(156, 114, 255, 0.15); + color: #9c72ff; +} + +.note-list__type--procedure { + background: rgba(255, 152, 0, 0.15); + color: #ff9800; +} + +.note-list__type--experiment { + background: rgba(0, 200, 150, 0.15); + color: #00c896; +} + +.note-list__type--note { + background: rgba(200, 200, 200, 0.1); + color: #999; +} + +.note-list__type--person { + background: rgba(255, 100, 130, 0.15); + color: #ff6482; +} + +.note-list__type--event { + background: rgba(255, 200, 50, 0.15); + color: #e6b800; +} + +.note-list__type--topic { + background: rgba(100, 220, 200, 0.15); + color: #64dcc8; } .note-list__status { - color: #6a6; + color: #4caf50; + font-size: 10px; } +/* Selected & pinned states */ .note-list__item--selected { - background: #2a2a4a; + background: #24244a; + border-left: 3px solid #4a9eff; + padding-left: 13px; +} + +.note-list__item--selected:hover { + background: #28284e; } .note-list__item--pinned { background: #1e1e3a; - border-left: 3px solid #6a6; + border-left: 3px solid #4caf50; padding-left: 13px; } diff --git a/src/components/NoteList.test.tsx b/src/components/NoteList.test.tsx index 265ca7e4..9bb2e037 100644 --- a/src/components/NoteList.test.tsx +++ b/src/components/NoteList.test.tsx @@ -81,13 +81,13 @@ const mockEntries: VaultEntry[] = [ describe('NoteList', () => { it('shows empty state when no entries', () => { - render() + render() expect(screen.getByText('No notes found')).toBeInTheDocument() expect(screen.getByText('0')).toBeInTheDocument() }) it('renders all entries with All Notes filter', () => { - render() + render() expect(screen.getByText('Build Laputa App')).toBeInTheDocument() expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument() expect(screen.getByText('Matteo Cellini')).toBeInTheDocument() @@ -95,20 +95,20 @@ describe('NoteList', () => { }) it('filters by People', () => { - render() + render() expect(screen.getByText('Matteo Cellini')).toBeInTheDocument() expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() expect(screen.getByText('1')).toBeInTheDocument() }) it('filters by Events', () => { - render() + render() expect(screen.getByText('Kickoff Meeting')).toBeInTheDocument() expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() }) it('filters by section group type', () => { - render() + render() expect(screen.getByText('Build Laputa App')).toBeInTheDocument() expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument() expect(screen.getByText('1')).toBeInTheDocument() @@ -116,7 +116,7 @@ describe('NoteList', () => { it('shows entity pinned at top with children', () => { render( - + ) // Pinned entity + child (Facebook Ads Strategy belongsTo this project) expect(screen.getByText('Build Laputa App')).toBeInTheDocument() @@ -127,7 +127,7 @@ describe('NoteList', () => { it('filters by topic (relatedTo references)', () => { render( - + ) // Build Laputa App has relatedTo: [[topic/software-development]] expect(screen.getByText('Build Laputa App')).toBeInTheDocument() @@ -135,12 +135,12 @@ describe('NoteList', () => { }) it('renders a search bar', () => { - render() + render() expect(screen.getByPlaceholderText('Search notes...')).toBeInTheDocument() }) it('filters by search query (case-insensitive substring)', () => { - render() + render() const input = screen.getByPlaceholderText('Search notes...') fireEvent.change(input, { target: { value: 'facebook' } }) expect(screen.getByText('Facebook Ads Strategy')).toBeInTheDocument() @@ -154,14 +154,14 @@ describe('NoteList', () => { { ...mockEntries[1], modifiedAt: 3000, title: 'Newest', path: '/p2' }, { ...mockEntries[2], modifiedAt: 2000, title: 'Middle', path: '/p3' }, ] - render() + render() const titles = screen.getAllByText(/Oldest|Newest|Middle/) const titleTexts = titles.map((el) => el.textContent) expect(titleTexts).toEqual(['Newest', 'Middle', 'Oldest']) }) it('renders type filter pills', () => { - const { container } = render() + const { container } = render() const pills = container.querySelectorAll('.note-list__pill') const labels = Array.from(pills).map((p) => p.textContent) expect(labels).toContain('All') @@ -172,7 +172,7 @@ describe('NoteList', () => { }) it('filters by type pill', () => { - render() + render() fireEvent.click(screen.getByText('Projects')) expect(screen.getByText('Build Laputa App')).toBeInTheDocument() expect(screen.queryByText('Matteo Cellini')).not.toBeInTheDocument() @@ -180,7 +180,7 @@ describe('NoteList', () => { }) it('clicking All pill resets type filter', () => { - render() + render() fireEvent.click(screen.getByText('People')) expect(screen.queryByText('Build Laputa App')).not.toBeInTheDocument() fireEvent.click(screen.getByText('All')) diff --git a/src/components/NoteList.tsx b/src/components/NoteList.tsx index c5c0896a..4431bdf5 100644 --- a/src/components/NoteList.tsx +++ b/src/components/NoteList.tsx @@ -6,10 +6,39 @@ interface NoteListProps { entries: VaultEntry[] selection: SidebarSelection selectedNote: VaultEntry | null + allContent: Record onSelectNote: (entry: VaultEntry) => void onCreateNote: () => void } +/** Extract first ~80 chars of content after the title heading */ +function getSnippet(content: string | undefined): string { + if (!content) return '' + // Remove frontmatter + const withoutFm = content.replace(/^---[\s\S]*?---\s*/, '') + // Remove the first heading + const withoutH1 = withoutFm.replace(/^#\s+.*\n+/, '') + // Clean markdown syntax and collapse whitespace + const clean = withoutH1 + .replace(/[#*_`\[\]]/g, '') + .replace(/\n+/g, ' ') + .trim() + return clean.slice(0, 80) + (clean.length > 80 ? '...' : '') +} + +/** Format a relative date string */ +function relativeDate(ts: number | null): string { + if (!ts) return '' + const now = Math.floor(Date.now() / 1000) + const diff = now - ts + if (diff < 60) return 'just now' + if (diff < 3600) return `${Math.floor(diff / 60)}m ago` + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago` + if (diff < 604800) return `${Math.floor(diff / 86400)}d ago` + const date = new Date(ts * 1000) + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) +} + /** Check if a wikilink array (e.g. belongsTo) references a given entry by path stem */ function refsMatch(refs: string[], entry: VaultEntry): boolean { // Extract the path stem: /Users/luca/Laputa/project/26q1-laputa-app.md → project/26q1-laputa-app @@ -69,7 +98,7 @@ const TYPE_PILLS = [ { label: 'Responsibilities', type: 'Responsibility' }, ] as const -export function NoteList({ entries, selection, selectedNote, onSelectNote, onCreateNote }: NoteListProps) { +export function NoteList({ entries, selection, selectedNote, allContent, onSelectNote, onCreateNote }: NoteListProps) { const [search, setSearch] = useState('') const [typeFilter, setTypeFilter] = useState(null) @@ -138,9 +167,13 @@ export function NoteList({ entries, selection, selectedNote, onSelectNote, onCre }${selectedNote?.path === entry.path ? ' note-list__item--selected' : ''}`} onClick={() => onSelectNote(entry)} > -
{entry.title}
+
+
{entry.title}
+ {relativeDate(entry.modifiedAt)} +
+
{getSnippet(allContent[entry.path])}
- {entry.isA && {entry.isA}} + {entry.isA && {entry.isA}} {entry.status && {entry.status}}
diff --git a/src/components/Sidebar.css b/src/components/Sidebar.css index c5aa70a1..f1b7f55a 100644 --- a/src/components/Sidebar.css +++ b/src/components/Sidebar.css @@ -8,16 +8,17 @@ } .sidebar__header { - padding: 16px; + padding: 20px 16px 16px; border-bottom: 1px solid #2a2a4a; flex-shrink: 0; } .sidebar__header h2 { margin: 0; - font-size: 18px; - font-weight: 600; + font-size: 17px; + font-weight: 700; color: #ffffff; + letter-spacing: -0.01em; } .sidebar__nav { @@ -37,17 +38,21 @@ padding: 6px 16px; cursor: pointer; font-size: 13px; - border-radius: 4px; - margin: 1px 4px; + border-radius: 6px; + margin: 1px 6px; + transition: all 0.12s; + color: #aaa; } .sidebar__filter-item:hover { - background: #2a2a4a; + background: #252545; + color: #ddd; } .sidebar__filter-item--active { - background: #2a2a4a; - color: #ffffff; + background: #4a9eff18; + color: #4a9eff; + font-weight: 500; } /* Section Groups */ @@ -132,20 +137,24 @@ padding: 5px 16px 5px 28px; cursor: pointer; font-size: 13px; - border-radius: 4px; - margin: 1px 4px; + border-radius: 6px; + margin: 1px 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + color: #bbb; + transition: all 0.12s; } .sidebar__item:hover { - background: #2a2a4a; + background: #252545; + color: #ddd; } .sidebar__item--active { - background: #2a2a4a; - color: #ffffff; + background: #4a9eff18; + color: #4a9eff; + font-weight: 500; } /* Topics */