From c3415e3ce02a1edb7d4e6d1a209d3000bba88b30 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 22 Feb 2026 10:22:26 +0100 Subject: [PATCH] refactor: extract Inspector sub-panels into InspectorPanels.tsx Split Inspector.tsx (score 7.78) into: - Inspector.tsx: slim shell with header, layout, prop wiring (score 9.6) - InspectorPanels.tsx: DynamicRelationshipsPanel, BacklinksPanel, GitHistoryPanel, LinkButton, RelationshipGroup (score 8.87) Extracted LinkButton as reusable component for both relationship refs and backlinks. Moved resolveRefProps to simplify RelationshipGroup. Extracted AddRelationshipForm and extractRelationshipRefs as focused units. Co-Authored-By: Claude Opus 4.6 --- src/components/Inspector.tsx | 348 +++-------------------------- src/components/InspectorPanels.tsx | 242 ++++++++++++++++++++ 2 files changed, 270 insertions(+), 320 deletions(-) create mode 100644 src/components/InspectorPanels.tsx diff --git a/src/components/Inspector.tsx b/src/components/Inspector.tsx index 26724061..edfe5693 100644 --- a/src/components/Inspector.tsx +++ b/src/components/Inspector.tsx @@ -1,29 +1,12 @@ -import { useMemo, useCallback, useState, useRef } from 'react' -import type { ComponentType, SVGAttributes } from 'react' +import { useMemo, useCallback } from 'react' import type { VaultEntry, GitCommit } from '../types' import { cn } from '@/lib/utils' -import { - SlidersHorizontal, X, Wrench, Flask, Target, ArrowsClockwise, - Users, CalendarBlank, Tag, FileText, StackSimple, Trash, -} from '@phosphor-icons/react' -import { parseFrontmatter, type ParsedFrontmatter } from '../utils/frontmatter' -import { DynamicPropertiesPanel, RELATIONSHIP_KEYS, containsWikilinks } from './DynamicPropertiesPanel' -import { getTypeColor, getTypeLightColor } from '../utils/typeColors' +import { SlidersHorizontal, X } from '@phosphor-icons/react' +import { parseFrontmatter } from '../utils/frontmatter' +import { DynamicPropertiesPanel } from './DynamicPropertiesPanel' +import { DynamicRelationshipsPanel, BacklinksPanel, GitHistoryPanel } from './InspectorPanels' -const TYPE_ICON_MAP: Record>> = { - Project: Wrench, - Experiment: Flask, - Responsibility: Target, - Procedure: ArrowsClockwise, - Person: Users, - Event: CalendarBlank, - Topic: Tag, - Type: StackSimple, -} - -function getTypeIcon(isA: string | undefined): ComponentType> { - return (isA && TYPE_ICON_MAP[isA]) || FileText -} +export type FrontmatterValue = string | number | boolean | string[] | null interface InspectorProps { collapsed: boolean @@ -40,292 +23,38 @@ interface InspectorProps { onAddProperty?: (path: string, key: string, value: FrontmatterValue) => Promise } -export type FrontmatterValue = string | number | boolean | string[] | null - -/** Check if a string is a wikilink */ -function isWikilink(value: string): boolean { - return /^\[\[.*\]\]$/.test(value) -} - -/** Extract display name from a wikilink */ -function wikilinkDisplay(ref: string): string { - const inner = ref.replace(/^\[\[|\]\]$/g, '') - const pipeIdx = inner.indexOf('|') - if (pipeIdx !== -1) return inner.slice(pipeIdx + 1) - const last = inner.split('/').pop() ?? inner - return last.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) -} - -/** Extract the target path for navigation from a wikilink ref */ -function wikilinkTarget(ref: string): string { - const inner = ref.replace(/^\[\[|\]\]$/g, '') - const pipeIdx = inner.indexOf('|') - return pipeIdx !== -1 ? inner.slice(0, pipeIdx) : inner -} - -function resolveRef(ref: string, entries: VaultEntry[]): VaultEntry | undefined { - const target = wikilinkTarget(ref) - return entries.find((e) => { - const stem = e.path.replace(/^.*\/Laputa\//, '').replace(/\.md$/, '') - if (stem === target) return true - const fileStem = e.filename.replace(/\.md$/, '') - if (fileStem === target.split('/').pop()) return true - return false - }) -} - - -function RelationshipGroup({ label, refs, entries, onNavigate }: { label: string; refs: string[]; entries: VaultEntry[]; onNavigate: (target: string) => void }) { - if (refs.length === 0) return null - return ( -
- {label} -
- {refs.map((ref, idx) => { - const resolved = resolveRef(ref, entries) - const refType = resolved?.isA ?? undefined - const isArchived = resolved?.archived ?? false - const isTrashed = resolved?.trashed ?? false - const isDimmed = isArchived || isTrashed - const color = refType ? getTypeColor(refType) : 'var(--accent-blue)' - const bgColor = refType ? getTypeLightColor(refType) : 'var(--accent-blue-light)' - const TypeIcon = getTypeIcon(refType) - return ( - - ) - })} -
-
- ) -} - -function DynamicRelationshipsPanel({ - frontmatter, entries, onNavigate, onAddProperty, -}: { - frontmatter: ParsedFrontmatter - entries: VaultEntry[] - onNavigate: (target: string) => void - onAddProperty?: (key: string, value: FrontmatterValue) => void -}) { - const [showForm, setShowForm] = useState(false) - const [relKey, setRelKey] = useState('') - const [relTarget, setRelTarget] = useState('') - const keyInputRef = useRef(null) - - const relationshipEntries = useMemo(() => { - return Object.entries(frontmatter) - .filter(([key, value]) => key !== 'Type' && (RELATIONSHIP_KEYS.has(key) || containsWikilinks(value))) - .map(([key, value]) => { - const refs: string[] = [] - if (typeof value === 'string' && isWikilink(value)) refs.push(value) - else if (Array.isArray(value)) value.forEach(v => { if (typeof v === 'string' && isWikilink(v)) refs.push(v) }) - return { key, refs } - }) - .filter(({ refs }) => refs.length > 0) - }, [frontmatter]) - - // All note titles for datalist autocomplete - const noteTitles = useMemo(() => entries.map(e => e.title), [entries]) - - const handleAdd = useCallback(() => { - const key = relKey.trim() - const target = relTarget.trim() - if (!key || !target || !onAddProperty) return - onAddProperty(key, `[[${target}]]`) - setRelKey('') - setRelTarget('') - setShowForm(false) - }, [relKey, relTarget, onAddProperty]) - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') handleAdd() - else if (e.key === 'Escape') { setShowForm(false); setRelKey(''); setRelTarget('') } - } - - return ( -
- {relationshipEntries.length === 0 ? ( -

No relationships

- ) : ( - relationshipEntries.map(({ key, refs }) => ( - - )) - )} - - {showForm ? ( -
- - {noteTitles.map(t => - setRelKey(e.target.value)} - /> - setRelTarget(e.target.value)} - /> -
- - -
-
- ) : ( - - )} -
- ) -} - function useBacklinks(entry: VaultEntry | null, entries: VaultEntry[], allContent: Record): VaultEntry[] { return useMemo(() => { if (!entry) return [] - const title = entry.title + const targets = [entry.title, ...entry.aliases] const stem = entry.filename.replace(/\.md$/, '') - const targets = [title, ...entry.aliases] const pathStem = entry.path.replace(/^.*\/Laputa\//, '').replace(/\.md$/, '') return entries.filter((e) => { if (e.path === entry.path) return false - const content = allContent[e.path] - if (!content) return false - for (const t of targets) { - if (content.includes(`[[${t}]]`)) return true - } - if (content.includes(`[[${stem}]]`)) return true - if (content.includes(`[[${pathStem}]]`)) return true - if (content.includes(`[[${pathStem}|`)) return true - return false + const c = allContent[e.path] + if (!c) return false + for (const t of targets) { if (c.includes(`[[${t}]]`)) return true } + return c.includes(`[[${stem}]]`) || c.includes(`[[${pathStem}]]`) || c.includes(`[[${pathStem}|`) }) }, [entry, entries, allContent]) } -function BacklinksPanel({ backlinks, onNavigate }: { backlinks: VaultEntry[]; onNavigate: (target: string) => void }) { +function InspectorHeader({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) { return ( -
-

- Backlinks {backlinks.length > 0 && {backlinks.length}} -

- {backlinks.length === 0 ? ( -

No backlinks

+
+ {collapsed ? ( + ) : ( -
- {backlinks.map((e) => { - const color = e.isA ? getTypeColor(e.isA) : 'var(--accent-blue)' - const TypeIcon = getTypeIcon(e.isA ?? undefined) - const isDimmed = e.archived || e.trashed - return ( - - ) - })} -
- )} -
- ) -} - -function formatRelativeDate(timestamp: number): string { - const now = Math.floor(Date.now() / 1000) - const diff = now - timestamp - if (diff < 86400) return 'today' - const days = Math.floor(diff / 86400) - if (days === 1) return 'yesterday' - if (days < 30) return `${days}d ago` - const months = Math.floor(days / 30) - if (months === 1) return '1mo ago' - return `${months}mo ago` -} - -function GitHistoryPanel({ commits, onViewCommitDiff }: { commits: GitCommit[]; onViewCommitDiff?: (commitHash: string) => void }) { - return ( -
-

History

- {commits.length === 0 ? ( -

No revision history

- ) : ( -
- {commits.map((c) => ( -
-
- - {formatRelativeDate(c.date)} -
-
{c.message}
- {c.author && ( -
{c.author}
- )} -
- ))} -
+ <> + + Properties + + )}
) @@ -334,12 +63,8 @@ function GitHistoryPanel({ commits, onViewCommitDiff }: { commits: GitCommit[]; function EmptyInspector() { return ( <> -
-

No note selected

-
-
-

No relationships

-
+

No note selected

+

No relationships

Backlinks

No backlinks

@@ -372,25 +97,8 @@ export function Inspector({ }, [entry, onAddProperty]) return ( -