From 730b56ecbd9ca52e4f98c41fefd635eba2012b6d Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 29 Mar 2026 16:04:28 +0200 Subject: [PATCH] feat: separate Backlinks and History with horizontal dividers in Inspector Add visual separation between properties, Backlinks, and History sections using Separator components. Restyle Backlinks with arrow-up-right icon header and blue clickable links. Restyle History with counter-clockwise icon header, combined hash+message blue links, and muted date below. Both sections now hide entirely when empty instead of showing placeholder text. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/Inspector.test.tsx | 40 ++++---------- src/components/Inspector.tsx | 5 +- src/components/InspectorPanels.test.tsx | 50 +++++------------ src/components/inspector/BacklinksPanel.tsx | 58 +++++++------------- src/components/inspector/GitHistoryPanel.tsx | 41 ++++++++------ 5 files changed, 71 insertions(+), 123 deletions(-) diff --git a/src/components/Inspector.test.tsx b/src/components/Inspector.test.tsx index 9810107b..dad07093 100644 --- a/src/components/Inspector.test.tsx +++ b/src/components/Inspector.test.tsx @@ -188,10 +188,7 @@ This is a test note with some words to count. entries={[mockEntry, referrerEntry]} /> ) - // Backlinks section is collapsed by default, but header with count is visible - expect(screen.getByText('Backlinks (1)')).toBeInTheDocument() - // Expand to see the backlink entry - fireEvent.click(screen.getByTestId('backlinks-toggle')) + expect(screen.getByText('Backlinks')).toBeInTheDocument() expect(screen.getByText('Referrer Note')).toBeInTheDocument() }) @@ -204,10 +201,8 @@ This is a test note with some words to count. entries={[mockEntry, { ...referrerEntry, outgoingLinks: [] }]} /> ) - // Initially no backlinks — section is hidden entirely - expect(screen.queryByTestId('backlinks-toggle')).not.toBeInTheDocument() + expect(screen.queryByText('Backlinks')).not.toBeInTheDocument() - // Rerender with updated outgoingLinks (simulates adding [[Test Project]] to referrer) rerender( ) - expect(screen.getByText('Backlinks (1)')).toBeInTheDocument() - fireEvent.click(screen.getByTestId('backlinks-toggle')) + expect(screen.getByText('Backlinks')).toBeInTheDocument() expect(screen.getByText('Referrer Note')).toBeInTheDocument() }) @@ -230,7 +224,7 @@ This is a test note with some words to count. entries={[mockEntry]} /> ) - expect(screen.queryByTestId('backlinks-toggle')).not.toBeInTheDocument() + expect(screen.queryByText('Backlinks')).not.toBeInTheDocument() }) it('navigates when a backlink is clicked', () => { @@ -244,7 +238,6 @@ This is a test note with some words to count. onNavigate={onNavigate} /> ) - fireEvent.click(screen.getByTestId('backlinks-toggle')) fireEvent.click(screen.getByText('Referrer Note')) expect(onNavigate).toHaveBeenCalledWith('Referrer Note') }) @@ -260,12 +253,11 @@ This is a test note with some words to count. ) expect(screen.getByText('History')).toBeInTheDocument() expect(screen.getByText('a1b2c3d')).toBeInTheDocument() - expect(screen.getByText('Update test with latest changes')).toBeInTheDocument() expect(screen.getByText('e4f5g6h')).toBeInTheDocument() expect(screen.getByText('i7j8k9l')).toBeInTheDocument() }) - it('renders commit hashes as clickable buttons', () => { + it('renders commit entries as clickable buttons', () => { const onViewCommitDiff = vi.fn() render( ) const hashBtn = screen.getByText('a1b2c3d') - expect(hashBtn.tagName).toBe('BUTTON') - hashBtn.click() + const button = hashBtn.closest('button')! + expect(button.tagName).toBe('BUTTON') + button.click() expect(onViewCommitDiff).toHaveBeenCalledWith('a1b2c3d4e5f6a7b8') }) - it('shows author name in commit rows', () => { - render( - - ) - const authors = screen.getAllByText('Luca Rossi') - expect(authors.length).toBeGreaterThan(0) - }) - - it('shows "No revision history" when no commits', () => { + it('hides history section when no commits', () => { render( ) - expect(screen.getByText('No revision history')).toBeInTheDocument() + expect(screen.queryByText('History')).not.toBeInTheDocument() }) it('shows separate Info section with read-only metadata', () => { diff --git a/src/components/Inspector.tsx b/src/components/Inspector.tsx index 2b397fcb..c92dfd03 100644 --- a/src/components/Inspector.tsx +++ b/src/components/Inspector.tsx @@ -3,6 +3,7 @@ import { useDragRegion } from '../hooks/useDragRegion' import type { VaultEntry, GitCommit } from '../types' import { cn } from '@/lib/utils' import { SlidersHorizontal, X } from '@phosphor-icons/react' +import { Separator } from './ui/separator' import { parseFrontmatter } from '../utils/frontmatter' import { DynamicPropertiesPanel } from './DynamicPropertiesPanel' import { DynamicRelationshipsPanel, BacklinksPanel, ReferencedByPanel, GitHistoryPanel, InstancesPanel } from './InspectorPanels' @@ -162,7 +163,9 @@ export function Inspector({ /> - + {backlinks.length > 0 && } + + {gitHistory.length > 0 && } ) : ( diff --git a/src/components/InspectorPanels.test.tsx b/src/components/InspectorPanels.test.tsx index 4cf5ceb2..c4402bde 100644 --- a/src/components/InspectorPanels.test.tsx +++ b/src/components/InspectorPanels.test.tsx @@ -632,7 +632,7 @@ describe('BacklinksPanel', () => { }) it('renders nothing when empty', () => { - const { container } = render() + const { container } = render() expect(container.innerHTML).toBe('') }) @@ -641,23 +641,16 @@ describe('BacklinksPanel', () => { { entry: makeEntry({ title: 'Another Note', isA: 'Project', path: '/vault/project/another.md' }), context: null }, ] - it('renders collapsed by default with count badge', () => { - render() - expect(screen.getByText('Backlinks (2)')).toBeInTheDocument() - expect(screen.queryByText('Referencing Note')).not.toBeInTheDocument() - }) - - it('expands to show backlink entries when toggle clicked', () => { - render() - fireEvent.click(screen.getByTestId('backlinks-toggle')) + it('renders header and all backlinks immediately (no collapse)', () => { + render() + expect(screen.getByText('Backlinks')).toBeInTheDocument() expect(screen.getByText('Referencing Note')).toBeInTheDocument() expect(screen.getByText('Another Note')).toBeInTheDocument() }) it('navigates when clicking backlink', () => { const backlinks = [{ entry: makeEntry({ title: 'Reference' }), context: null }] - render() - fireEvent.click(screen.getByTestId('backlinks-toggle')) + render() fireEvent.click(screen.getByText('Reference')) expect(onNavigate).toHaveBeenCalledWith('Reference') }) @@ -666,39 +659,26 @@ describe('BacklinksPanel', () => { const backlinks = [ { entry: makeEntry({ title: 'Referencing Note' }), context: 'This references [[My Note]] in context.' }, ] - render() - fireEvent.click(screen.getByTestId('backlinks-toggle')) + render() expect(screen.getByText('This references [[My Note]] in context.')).toBeInTheDocument() }) - it('collapses when toggle clicked twice', () => { - const backlinks = [{ entry: makeEntry({ title: 'Note A' }), context: null }] - render() - fireEvent.click(screen.getByTestId('backlinks-toggle')) - expect(screen.getByText('Note A')).toBeInTheDocument() - fireEvent.click(screen.getByTestId('backlinks-toggle')) - expect(screen.queryByText('Note A')).not.toBeInTheDocument() - }) - it('shows emoji icon before backlink title when entry has an emoji', () => { const backlinks = [{ entry: makeEntry({ title: 'Starred Note', icon: '⭐' }), context: null, }] - render() - fireEvent.click(screen.getByTestId('backlinks-toggle')) + render() expect(screen.getByText('⭐')).toBeInTheDocument() expect(screen.getByText('Starred Note')).toBeInTheDocument() }) it('does not show emoji when backlink entry has no icon', () => { const backlinks = [{ entry: makeEntry({ title: 'Plain Note' }), context: null }] - render() - fireEvent.click(screen.getByTestId('backlinks-toggle')) + render() expect(screen.getByText('Plain Note')).toBeInTheDocument() const btn = screen.getByText('Plain Note').closest('button') const spans = btn?.querySelectorAll('span.shrink-0') - // No emoji span should exist (only possible shrink-0 spans are trash icon, not emoji) const emojiSpans = Array.from(spans ?? []).filter(s => /[\p{Emoji_Presentation}]/u.test(s.textContent ?? '')) expect(emojiSpans).toHaveLength(0) }) @@ -764,12 +744,12 @@ describe('GitHistoryPanel', () => { vi.clearAllMocks() }) - it('shows "No revision history" when empty', () => { - render() - expect(screen.getByText('No revision history')).toBeInTheDocument() + it('renders nothing when empty', () => { + const { container } = render() + expect(container.innerHTML).toBe('') }) - it('renders commit entries', () => { + it('renders commit entries with hash and message', () => { const commits: GitCommit[] = [ { hash: 'abc1234567890', shortHash: 'abc1234', message: 'Initial commit', author: 'luca', date: Math.floor(Date.now() / 1000) - 3600 }, { hash: 'def4567890123', shortHash: 'def4567', message: 'Fix bug', author: 'jane', date: Math.floor(Date.now() / 1000) - 86400 * 2 }, @@ -777,13 +757,9 @@ describe('GitHistoryPanel', () => { render() expect(screen.getByText('abc1234')).toBeInTheDocument() expect(screen.getByText('def4567')).toBeInTheDocument() - expect(screen.getByText('Initial commit')).toBeInTheDocument() - expect(screen.getByText('Fix bug')).toBeInTheDocument() - expect(screen.getByText('luca')).toBeInTheDocument() - expect(screen.getByText('jane')).toBeInTheDocument() }) - it('calls onViewCommitDiff when clicking commit hash', () => { + it('calls onViewCommitDiff when clicking commit entry', () => { const commits: GitCommit[] = [ { hash: 'abc1234567890', shortHash: 'abc1234', message: 'test', author: '', date: Math.floor(Date.now() / 1000) }, ] diff --git a/src/components/inspector/BacklinksPanel.tsx b/src/components/inspector/BacklinksPanel.tsx index cdb397d0..555c03d4 100644 --- a/src/components/inspector/BacklinksPanel.tsx +++ b/src/components/inspector/BacklinksPanel.tsx @@ -1,7 +1,5 @@ -import { useState } from 'react' import type { VaultEntry } from '../../types' -import { CaretRight, Trash } from '@phosphor-icons/react' -import { getTypeColor } from '../../utils/typeColors' +import { ArrowUpRight, Trash } from '@phosphor-icons/react' import { isEmoji } from '../../utils/emoji' import { entryStatusTitle } from './shared' import { StatusSuffix } from './LinkButton' @@ -11,23 +9,21 @@ export interface BacklinkItem { context: string | null } -function BacklinkEntry({ entry, context, typeEntryMap, onNavigate }: { +function BacklinkEntry({ entry, context, onNavigate }: { entry: VaultEntry context: string | null - typeEntryMap: Record onNavigate: (target: string) => void }) { - const te = typeEntryMap[entry.isA ?? ''] const isDimmed = entry.archived || entry.trashed return ( - {expanded && ( -
- {backlinks.map(({ entry, context }) => ( - - ))} -
- )} +

+ + Backlinks +

+
+ {backlinks.map(({ entry, context }) => ( + + ))} +
) } diff --git a/src/components/inspector/GitHistoryPanel.tsx b/src/components/inspector/GitHistoryPanel.tsx index 8ecf23ce..8ded1326 100644 --- a/src/components/inspector/GitHistoryPanel.tsx +++ b/src/components/inspector/GitHistoryPanel.tsx @@ -1,3 +1,4 @@ +import { ArrowCounterClockwise } from '@phosphor-icons/react' import type { GitCommit } from '../../types' function formatRelativeDate(timestamp: number): string { @@ -11,26 +12,32 @@ function formatRelativeDate(timestamp: number): string { } export function GitHistoryPanel({ commits, onViewCommitDiff }: { commits: GitCommit[]; onViewCommitDiff?: (commitHash: string) => void }) { + if (commits.length === 0) return null + return (
-

History

- {commits.length === 0 - ?

No revision history

- : ( -
- {commits.map((c) => ( -
-
- - {formatRelativeDate(c.date)} -
-
{c.message}
- {c.author &&
{c.author}
} -
- ))} +

+ + History +

+
+ {commits.map((c) => ( +
+ +
+ {formatRelativeDate(c.date)} +
- ) - } + ))} +
) }