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() {
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 */