import { useMemo, useState, useCallback } from 'react' import type { VaultEntry } from '../types' import type { FrontmatterValue } from './Inspector' import type { ParsedFrontmatter } from '../utils/frontmatter' import { EditableValue, TagPillList } from './EditableValue' import { Button } from '@/components/ui/button' import { getTypeColor, getTypeLightColor } from '../utils/typeColors' 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', 'is_a', 'Is A']) // eslint-disable-next-line react-refresh/only-export-components -- utility co-located with component 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' }) } function coerceValue(raw: string): FrontmatterValue { if (raw.toLowerCase() === 'true') return true if (raw.toLowerCase() === 'false') return false if (!isNaN(Number(raw)) && raw.trim() !== '') return Number(raw) return raw } function parseNewValue(rawValue: string): FrontmatterValue { if (!rawValue.includes(',')) return rawValue.trim() || '' const items = rawValue.split(',').map(s => s.trim()).filter(s => s) return items.length === 1 ? items[0] : items } function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes} B` const kb = bytes / 1024 if (kb < 1024) return `${kb.toFixed(1)} KB` const mb = kb / 1024 return `${mb.toFixed(1)} MB` } function isStatusKey(key: string): boolean { return key === 'Status' || key.includes('Status') } function isDateKey(key: string): boolean { return key.includes('Created') || key.includes('Modified') || key.includes('time') || key.includes('Date') } function StatusValue({ propKey, value, isEditing, onSave, onStartEdit }: { propKey: string; value: FrontmatterValue; isEditing: boolean onSave: (key: string, value: string) => void; onStartEdit: (key: string | null) => void }) { const statusStr = String(value) const style = STATUS_STYLES[statusStr] ?? DEFAULT_STATUS_STYLE if (isEditing) { return ( { if (e.key === 'Enter') onSave(propKey, (e.target as HTMLInputElement).value) if (e.key === 'Escape') onStartEdit(null) }} onBlur={(e) => onSave(propKey, e.target.value)} autoFocus /> ) } return ( onStartEdit(propKey)} title={statusStr} > {statusStr} ) } function BooleanToggle({ value, onToggle }: { value: boolean; onToggle: () => void }) { return ( ) } function AddPropertyForm({ onAdd, onCancel }: { onAdd: (key: string, value: string) => void; onCancel: () => void }) { const [newKey, setNewKey] = useState('') const [newValue, setNewValue] = useState('') const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && newKey.trim()) onAdd(newKey, newValue) else if (e.key === 'Escape') onCancel() } return (
setNewKey(e.target.value)} onKeyDown={handleKeyDown} autoFocus /> setNewValue(e.target.value)} onKeyDown={handleKeyDown} />
) } function TypeRow({ isA, onNavigate }: { isA?: string | null; onNavigate?: (target: string) => void }) { if (!isA) return null return (
Type {onNavigate ? ( ) : ( {isA} )}
) } function PropertyRow({ propKey, value, editingKey, onStartEdit, onSave, onSaveList, onUpdate, onDelete }: { propKey: string; value: FrontmatterValue; editingKey: string | null onStartEdit: (key: string | null) => void; onSave: (key: string, value: string) => void onSaveList: (key: string, items: string[]) => void onUpdate?: (key: string, value: FrontmatterValue) => void; onDelete?: (key: string) => void }) { return (
{propKey} {onDelete && ( )}
) } function PropertyValueCell({ propKey, value, isEditing, onStartEdit, onSave, onSaveList, onUpdate }: { propKey: string; value: FrontmatterValue; isEditing: boolean onStartEdit: (key: string | null) => void; onSave: (key: string, value: string) => void onSaveList: (key: string, items: string[]) => void; onUpdate?: (key: string, value: FrontmatterValue) => void }) { const editProps = { value: String(value ?? ''), isEditing, onStartEdit: () => onStartEdit(propKey), onSave: (v: string) => onSave(propKey, v), onCancel: () => onStartEdit(null) } if (value === null || value === undefined) return if (isStatusKey(propKey)) return if (Array.isArray(value)) return onSaveList(propKey, items)} label={propKey} /> if (isDateKey(propKey)) return if (typeof value === 'boolean') return onUpdate?.(propKey, !value)} /> return } function InfoRow({ label, value }: { label: string; value: string }) { return (
{label} {value}
) } function AddPropertyButton({ onClick, disabled }: { onClick: () => void; disabled: boolean }) { return ( ) } export function DynamicPropertiesPanel({ entry, content, frontmatter, onUpdateProperty, onDeleteProperty, onAddProperty, onNavigate, }: { entry: VaultEntry content: string | null frontmatter: ParsedFrontmatter onUpdateProperty?: (key: string, value: FrontmatterValue) => void onDeleteProperty?: (key: string) => void onAddProperty?: (key: string, value: FrontmatterValue) => void onNavigate?: (target: string) => void }) { const [editingKey, setEditingKey] = useState(null) const [showAddDialog, setShowAddDialog] = useState(false) const wordCount = countWords(content) const propertyEntries = useMemo(() => { return Object.entries(frontmatter) .filter(([key, value]) => !SKIP_KEYS.has(key) && !RELATIONSHIP_KEYS.has(key) && !containsWikilinks(value)) }, [frontmatter]) const handleSaveValue = useCallback((key: string, newValue: string) => { setEditingKey(null) if (onUpdateProperty) onUpdateProperty(key, coerceValue(newValue)) }, [onUpdateProperty]) const handleSaveList = useCallback((key: string, newItems: string[]) => { if (!onUpdateProperty) return if (newItems.length === 0) onDeleteProperty?.(key) else if (newItems.length === 1) onUpdateProperty(key, newItems[0]) else onUpdateProperty(key, newItems) }, [onUpdateProperty, onDeleteProperty]) const handleAdd = useCallback((rawKey: string, rawValue: string) => { if (!rawKey.trim() || !onAddProperty) return onAddProperty(rawKey.trim(), parseNewValue(rawValue)) setShowAddDialog(false) }, [onAddProperty]) return (
{/* Editable properties section */}
{propertyEntries.map(([key, value]) => ( ))}
{showAddDialog ? setShowAddDialog(false)} /> : setShowAddDialog(true)} disabled={!onAddProperty} /> } {/* Read-only Info section */}

Info

) }