(null)
const positionMenu = useCallback((node: HTMLDivElement | null) => {
if (!node) return
const el = triggerRef.current
if (!el) return
const rect = el.getBoundingClientRect()
const menuW = 140
const left = Math.max(rect.right - menuW, 8)
node.style.top = `${rect.bottom + 4}px`
node.style.left = `${left}px`
}, [triggerRef])
useEffect(() => {
const backdrop = backdropRef.current
if (!backdrop) return
backdrop.addEventListener('click', onClose)
return () => backdrop.removeEventListener('click', onClose)
}, [onClose])
return createPortal(
<>
{DISPLAY_MODE_OPTIONS.map((opt) => (
))}
>,
document.body,
)
}
function DisplayModeOption({
autoMode,
currentMode,
onSelect,
option,
}: {
autoMode: PropertyDisplayMode
currentMode: PropertyDisplayMode
onSelect: (mode: PropertyDisplayMode) => void
option: typeof DISPLAY_MODE_OPTIONS[number]
}) {
const OptIcon = DISPLAY_MODE_ICONS[option.value]
return (
)
}
function toBooleanValue(value: FrontmatterValue): boolean {
if (typeof value === 'boolean') return value
if (typeof value === 'string') return value.toLowerCase() === 'true'
return false
}
function autoDetectFromValue(propKey: string, value: FrontmatterValue): PropertyDisplayMode {
if (canonicalSystemMetadataKey(propKey) === '_icon') return 'text'
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
locale?: AppLocale
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
}
interface ScalarEditProps {
value: string
isEditing: boolean
onStartEdit: () => void
onSave: (nextValue: string) => void
onCancel: () => void
}
function createScalarEditProps({
propKey,
value,
isEditing,
onStartEdit,
onSave,
}: {
propKey: string
value: FrontmatterValue
isEditing: boolean
onStartEdit: (key: string | null) => void
onSave: (key: string, value: string) => void
}): ScalarEditProps {
return {
value: String(value ?? ''),
isEditing,
onStartEdit: () => onStartEdit(propKey),
onSave: (nextValue: string) => onSave(propKey, nextValue),
onCancel: () => onStartEdit(null),
}
}
type ScalarRendererProps = SmartCellProps & {
editProps: ScalarEditProps
}
type ScalarDisplayRenderer = (props: ScalarRendererProps & { resolvedMode: PropertyDisplayMode }) => ReactNode
const SCALAR_DISPLAY_RENDERERS: readonly [PropertyDisplayMode, ScalarDisplayRenderer][] = [
['status', (props) => (
)],
['tags', (props) => (
)],
['date', (props) => (
props.onSave(props.propKey, nextValue)}
autoOpen={props.isEditing}
onCancel={() => props.onStartEdit(null)}
/>
)],
['number', (props) => ],
['boolean', (props) => {
const boolVal = toBooleanValue(props.value)
return props.onUpdate?.(props.propKey, !boolVal)} />
}],
['url', (props) => ],
['color', (props) => ],
]
function renderScalarDisplayMode(props: ScalarRendererProps & { resolvedMode: PropertyDisplayMode }) {
const renderer = SCALAR_DISPLAY_RENDERERS.find(([mode]) => mode === props.resolvedMode)?.[1]
return renderer ? renderer(props) :
}
function ScalarValueCell(props: SmartCellProps) {
const { propKey, value, displayMode, isEditing, onStartEdit, onSave } = props
const editProps = createScalarEditProps({
propKey,
value,
isEditing,
onStartEdit,
onSave,
})
if (canonicalSystemMetadataKey(propKey) === '_icon') {
return
}
const resolvedMode = displayMode === 'text' ? autoDetectFromValue(propKey, value) : displayMode
return renderScalarDisplayMode({ ...props, resolvedMode, editProps })
}
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
}