('text')
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && newKey.trim()) onAdd(newKey, newValue, displayMode)
else if (e.key === 'Escape') onCancel()
}
return (
setNewKey(e.target.value)} onKeyDown={handleKeyDown} autoFocus
/>
setNewValue(e.target.value)} onKeyDown={handleKeyDown}
/>
)
}
const TYPE_NONE = '__none__'
function ReadOnlyType({ isA, customColorKey, onNavigate }: { isA?: string | null; customColorKey?: string | null; onNavigate?: (target: string) => void }) {
if (!isA) return null
return (
Type
{onNavigate ? (
) : (
{isA}
)}
)
}
function TypeSelector({ isA, customColorKey, availableTypes, onUpdateProperty, onNavigate }: {
isA?: string | null; customColorKey?: string | null; availableTypes: string[]
onUpdateProperty?: (key: string, value: FrontmatterValue) => void
onNavigate?: (target: string) => void
}) {
if (!onUpdateProperty) return
const currentValue = isA || TYPE_NONE
const options = isA && !availableTypes.includes(isA)
? [...availableTypes, isA].sort((a, b) => a.localeCompare(b))
: availableTypes
return (
Type
)
}
function SmartPropertyValueCell({ propKey, value, displayMode, isEditing, vaultStatuses, onStartEdit, onSave, onSaveList, onUpdate }: {
propKey: string; value: FrontmatterValue; displayMode: PropertyDisplayMode; isEditing: boolean
vaultStatuses: string[]
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 (Array.isArray(value)) return onSaveList(propKey, items)} label={propKey} />
switch (displayMode) {
case 'status':
return
case 'date':
if (typeof value === 'string') {
return onSave(propKey, v)} />
}
return
case 'boolean':
if (typeof value === 'boolean') {
return onUpdate?.(propKey, !value)} />
}
return
case 'url':
if (typeof value === 'string' && isUrlValue(value)) {
return
}
return
default:
if (typeof value === 'boolean') {
return onUpdate?.(propKey, !value)} />
}
if (typeof value === 'string' && isUrlValue(value)) {
return
}
return
}
}
function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultStatuses, onStartEdit, onSave, onSaveList, onUpdate, onDelete, onDisplayModeChange }: {
propKey: string; value: FrontmatterValue; editingKey: string | null
displayMode: PropertyDisplayMode; autoMode: PropertyDisplayMode
vaultStatuses: string[]
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
onDisplayModeChange: (key: string, mode: PropertyDisplayMode | null) => void
}) {
return (
{propKey}
{onDelete && (
)}
)
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
{label}
{value}
)
}
function AddPropertyButton({ onClick, disabled }: { onClick: () => void; disabled: boolean }) {
return (
)
}
function NoteInfoSection({ entry, wordCount }: { entry: VaultEntry; wordCount: number }) {
return (
)
}
function reconcileListUpdate(
newItems: string[],
onUpdate: (key: string, value: FrontmatterValue) => void,
onDelete: ((key: string) => void) | undefined,
key: string,
) {
if (newItems.length === 0) onDelete?.(key)
else if (newItems.length === 1) onUpdate(key, newItems[0])
else onUpdate(key, newItems)
}
export function DynamicPropertiesPanel({
entry,
content,
frontmatter,
entries,
onUpdateProperty,
onDeleteProperty,
onAddProperty,
onNavigate,
}: {
entry: VaultEntry
content: string | null
frontmatter: ParsedFrontmatter
entries?: VaultEntry[]
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 [displayOverrides, setDisplayOverrides] = useState(() => loadDisplayModeOverrides())
const wordCount = countWords(content ?? '')
const { availableTypes, customColorKey } = useMemo(() => {
const typeEntries = (entries ?? []).filter(e => e.isA === 'Type')
return {
availableTypes: typeEntries.map(e => e.title).sort((a, b) => a.localeCompare(b)),
customColorKey: entry.isA ? (typeEntries.find(e => e.title === entry.isA)?.color ?? null) : null,
}
}, [entries, entry.isA])
const vaultStatuses = useMemo(() => {
const seen = new Set()
for (const e of entries ?? []) {
if (e.status) seen.add(e.status)
}
return Array.from(seen).sort((a, b) => a.localeCompare(b))
}, [entries])
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
reconcileListUpdate(newItems, onUpdateProperty, onDeleteProperty, key)
}, [onUpdateProperty, onDeleteProperty])
const handleAdd = useCallback((rawKey: string, rawValue: string, mode: PropertyDisplayMode) => {
if (!rawKey.trim() || !onAddProperty) return
onAddProperty(rawKey.trim(), parseNewValue(rawValue))
if (mode !== 'text') {
saveDisplayModeOverride(rawKey.trim(), mode)
setDisplayOverrides(loadDisplayModeOverrides())
}
setShowAddDialog(false)
}, [onAddProperty])
const handleDisplayModeChange = useCallback((key: string, mode: PropertyDisplayMode | null) => {
if (mode === null) {
removeDisplayModeOverride(key)
} else {
saveDisplayModeOverride(key, mode)
}
setDisplayOverrides(loadDisplayModeOverrides())
}, [])
return (
{/* Editable properties section */}
{propertyEntries.map(([key, value]) => {
const autoMode = detectPropertyType(key, value)
const effectiveMode = getEffectiveDisplayMode(key, value, displayOverrides)
return (
)
})}
{showAddDialog
?
setShowAddDialog(false)} />
: setShowAddDialog(true)} disabled={!onAddProperty} />
}
)
}