import { useMemo, useState, useCallback } from 'react' import type { VaultEntry } from '../types' import type { FrontmatterValue } from './Inspector' import type { ParsedFrontmatter } from '../utils/frontmatter' import { EditableValue, EditableList } from './EditableValue' import { Button } from '@/components/ui/button' const STATUS_STYLES: Record = { Active: { bg: 'var(--accent-green-light)', color: 'var(--accent-green)' }, Done: { bg: 'var(--accent-blue-light)', color: 'var(--accent-blue)' }, Paused: { bg: 'var(--accent-yellow-light)', color: 'var(--accent-yellow)' }, Archived: { bg: 'var(--accent-blue-light)', color: 'var(--muted-foreground)' }, Dropped: { bg: 'var(--accent-red-light)', color: 'var(--accent-red)' }, Open: { bg: 'var(--accent-green-light)', color: 'var(--accent-green)' }, Closed: { bg: 'var(--accent-blue-light)', color: 'var(--muted-foreground)' }, 'Not started': { bg: 'var(--accent-blue-light)', color: 'var(--muted-foreground)' }, Draft: { bg: 'var(--accent-yellow-light)', color: 'var(--accent-yellow)' }, Mixed: { bg: 'var(--accent-yellow-light)', color: 'var(--accent-yellow)' }, } const DEFAULT_STATUS_STYLE = { bg: 'var(--accent-blue-light)', color: 'var(--muted-foreground)' } // Keys that are relationships (contain wikilinks) export const RELATIONSHIP_KEYS = new Set([ 'Belongs to', 'Related to', 'Events', 'Has Data', 'Owner', 'Advances', 'Parent', 'Children', 'Has', 'Notes', ]) // Keys to skip showing in Properties const SKIP_KEYS = new Set(['aliases', 'notion_id', 'workspace']) export function containsWikilinks(value: FrontmatterValue): boolean { if (typeof value === 'string') return /^\[\[.*\]\]$/.test(value) if (Array.isArray(value)) return value.some(v => typeof v === 'string' && /^\[\[.*\]\]$/.test(v)) return false } function countWords(content: string | null): number { if (!content) return 0 const stripped = content.replace(/^---[\s\S]*?---\n?/, '') const words = stripped.trim().split(/\s+/).filter((w) => w.length > 0) return words.length } function formatDate(timestamp: number | null): string { if (!timestamp) return '\u2014' const d = new Date(timestamp * 1000) return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) } export function DynamicPropertiesPanel({ entry, content, frontmatter, onUpdateProperty, onDeleteProperty, onAddProperty, }: { entry: VaultEntry content: string | null frontmatter: ParsedFrontmatter onUpdateProperty?: (key: string, value: FrontmatterValue) => void onDeleteProperty?: (key: string) => void onAddProperty?: (key: string, value: FrontmatterValue) => void }) { const [editingKey, setEditingKey] = useState(null) const [showAddDialog, setShowAddDialog] = useState(false) const [newKey, setNewKey] = useState('') const [newValue, setNewValue] = useState('') const wordCount = countWords(content) const propertyEntries = useMemo(() => { return Object.entries(frontmatter) .filter(([key, value]) => { if (SKIP_KEYS.has(key)) return false if (RELATIONSHIP_KEYS.has(key)) return false if (containsWikilinks(value)) return false return true }) }, [frontmatter]) const handleSaveValue = useCallback((key: string, newValue: string) => { setEditingKey(null) if (onUpdateProperty) { if (newValue.toLowerCase() === 'true') onUpdateProperty(key, true) else if (newValue.toLowerCase() === 'false') onUpdateProperty(key, false) else if (!isNaN(Number(newValue)) && newValue.trim() !== '') onUpdateProperty(key, Number(newValue)) else onUpdateProperty(key, newValue) } }, [onUpdateProperty]) const handleSaveList = useCallback((key: string, newItems: string[]) => { if (onUpdateProperty) { if (newItems.length === 0) onDeleteProperty?.(key) else if (newItems.length === 1) onUpdateProperty(key, newItems[0]) else onUpdateProperty(key, newItems) } }, [onUpdateProperty, onDeleteProperty]) const handleAddProperty = useCallback(() => { if (newKey.trim() && onAddProperty) { if (newValue.includes(',')) { const items = newValue.split(',').map(s => s.trim()).filter(s => s) onAddProperty(newKey.trim(), items.length === 1 ? items[0] : items) } else { onAddProperty(newKey.trim(), newValue.trim() || '') } setNewKey('') setNewValue('') setShowAddDialog(false) } }, [newKey, newValue, onAddProperty]) const handleAddKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') handleAddProperty() else if (e.key === 'Escape') { setShowAddDialog(false) setNewKey('') setNewValue('') } } const renderEditableValue = (key: string, value: FrontmatterValue) => { if (value === null || value === undefined) { return ( setEditingKey(key)} onSave={(v) => handleSaveValue(key, v)} onCancel={() => setEditingKey(null)} /> ) } if (key === 'Status' || key.includes('Status')) { const statusStr = String(value) const style = STATUS_STYLES[statusStr] ?? DEFAULT_STATUS_STYLE if (editingKey === key) { return ( { if (e.key === 'Enter') handleSaveValue(key, (e.target as HTMLInputElement).value) if (e.key === 'Escape') setEditingKey(null) }} onBlur={(e) => handleSaveValue(key, e.target.value)} autoFocus /> ) } return ( setEditingKey(key)} title="Click to edit" > {statusStr} ) } if (Array.isArray(value)) { return handleSaveList(key, items)} label={key} /> } if (key.includes('Created') || key.includes('Modified') || key.includes('time') || key.includes('Date')) { return ( setEditingKey(key)} onSave={(v) => handleSaveValue(key, v)} onCancel={() => setEditingKey(null)} /> ) } if (typeof value === 'boolean') { return ( ) } return ( setEditingKey(key)} onSave={(v) => handleSaveValue(key, v)} onCancel={() => setEditingKey(null)} /> ) } return (

Properties

{entry.isA && (
Type {entry.isA}
)} {propertyEntries.map(([key, value]) => (
{key} {onDeleteProperty && ( )} {renderEditableValue(key, value)}
))}
Modified {formatDate(entry.modifiedAt)}
Words {wordCount}
{showAddDialog ? (
setNewKey(e.target.value)} onKeyDown={handleAddKeyDown} autoFocus /> setNewValue(e.target.value)} onKeyDown={handleAddKeyDown} />
) : ( )}
) }