import { useState, useCallback, useRef } from 'react' import { createPortal } from 'react-dom' import type { FrontmatterValue } from './Inspector' import { EditableValue, TagPillList, UrlValue } from './EditableValue' import { isUrlValue } from '../utils/url' import { Calendar } from '@/components/ui/calendar' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { XIcon } from 'lucide-react' import { isValidCssColor } from '../utils/colorUtils' import { type PropertyDisplayMode, formatDateValue, toISODate, DISPLAY_MODE_OPTIONS, DISPLAY_MODE_ICONS, } from '../utils/propertyTypes' import { StatusDropdown } from './StatusDropdown' import { getStatusStyle } from '../utils/statusStyles' import { TagsDropdown } from './TagsDropdown' import { getTagStyle } from '../utils/tagStyles' import { ColorEditableValue } from './ColorInput' function parseDateValue(value: string): Date | undefined { const iso = toISODate(value) const d = new Date(iso + 'T00:00:00') return isNaN(d.getTime()) ? undefined : d } function dateToISO(day: Date): string { const yyyy = day.getFullYear() const mm = String(day.getMonth() + 1).padStart(2, '0') const dd = String(day.getDate()).padStart(2, '0') return `${yyyy}-${mm}-${dd}` } function StatusValue({ propKey, value, isEditing, vaultStatuses, onSave, onStartEdit }: { propKey: string; value: FrontmatterValue; isEditing: boolean; vaultStatuses: string[] onSave: (key: string, value: string) => void; onStartEdit: (key: string | null) => void }) { const statusStr = String(value) const style = getStatusStyle(statusStr) return ( onStartEdit(propKey)} data-testid="status-badge" > {statusStr} {isEditing && ( onSave(propKey, newValue)} onCancel={() => onStartEdit(null)} /> )} ) } function TagsValue({ propKey, value, isEditing, vaultTags, onSave, onStartEdit }: { propKey: string; value: string[]; isEditing: boolean; vaultTags: string[] onSave: (key: string, items: string[]) => void; onStartEdit: (key: string | null) => void }) { const handleToggle = useCallback((tag: string) => { const idx = value.indexOf(tag) const next = idx >= 0 ? value.filter((_, i) => i !== idx) : [...value, tag] onSave(propKey, next) }, [propKey, value, onSave]) const handleRemove = useCallback((tag: string) => { onSave(propKey, value.filter(t => t !== tag)) }, [propKey, value, onSave]) return ( {value.map(tag => { const style = getTagStyle(tag) return ( {tag} ) })} {isEditing && ( onStartEdit(null)} /> )} ) } function BooleanToggle({ value, onToggle }: { value: boolean; onToggle: () => void }) { return ( ) } function DateValue({ value, onSave }: { value: string; onSave: (newValue: string) => void }) { const [open, setOpen] = useState(false) const formatted = formatDateValue(value) const selectedDate = parseDateValue(value) const handleSelect = (day: Date | undefined) => { if (day) onSave(dateToISO(day)) setOpen(false) } const handleClear = (e: React.MouseEvent) => { e.stopPropagation() onSave('') setOpen(false) } return ( {selectedDate && (
)}
) } export function DisplayModeSelector({ propKey, currentMode, autoMode, onSelect }: { propKey: string; currentMode: PropertyDisplayMode; autoMode: PropertyDisplayMode onSelect: (key: string, mode: PropertyDisplayMode | null) => void }) { const [open, setOpen] = useState(false) const triggerRef = useRef(null) const positionMenu = useCallback((node: HTMLDivElement | null) => { if (!node) return const el = triggerRef.current if (!el) return const rect = el.getBoundingClientRect() const menuW = 140 let left = rect.right - menuW if (left < 8) left = 8 node.style.top = `${rect.bottom + 4}px` node.style.left = `${left}px` }, []) const handleSelect = (mode: PropertyDisplayMode) => { if (mode === autoMode) { onSelect(propKey, null) } else { onSelect(propKey, mode) } setOpen(false) } return (
{open && createPortal( <>
setOpen(false)} />
{DISPLAY_MODE_OPTIONS.map(opt => { const OptIcon = DISPLAY_MODE_ICONS[opt.value] return ( ) })}
, document.body )}
) } function toBooleanValue(value: FrontmatterValue): boolean { if (typeof value === 'boolean') return value if (typeof value === 'string') return value.toLowerCase() === 'true' return false } function autoDetectFromValue(value: FrontmatterValue): PropertyDisplayMode { if (typeof value === 'boolean') return 'boolean' if (typeof value === 'string' && isUrlValue(value)) return 'url' if (typeof value === 'string' && isValidCssColor(value) && value.startsWith('#')) return 'color' return 'text' } type SmartCellProps = { propKey: string; value: FrontmatterValue; displayMode: PropertyDisplayMode; isEditing: boolean vaultStatuses: string[]; vaultTags: string[] onStartEdit: (key: string | null) => void; onSave: (key: string, value: string) => void onSaveList: (key: string, items: string[]) => void; onUpdate?: (key: string, value: FrontmatterValue) => void } function ScalarValueCell({ propKey, value, displayMode, isEditing, vaultStatuses, vaultTags, onStartEdit, onSave, onSaveList, onUpdate }: SmartCellProps) { const editProps = { value: String(value ?? ''), isEditing, onStartEdit: () => onStartEdit(propKey), onSave: (v: string) => onSave(propKey, v), onCancel: () => onStartEdit(null) } const resolvedMode = displayMode === 'text' ? autoDetectFromValue(value) : displayMode switch (resolvedMode) { case 'status': return case 'tags': return case 'date': return onSave(propKey, v)} /> case 'boolean': { const boolVal = toBooleanValue(value) return onUpdate?.(propKey, !boolVal)} /> } case 'url': return case 'color': return default: return } } export function SmartPropertyValueCell(props: SmartCellProps) { const { propKey, value, displayMode, isEditing, vaultTags, onSaveList, onStartEdit } = props if (Array.isArray(value)) { if (displayMode === 'tags') { return } return onSaveList(propKey, items)} label={propKey} /> } return }