From 3fb7cf6a5663edaa8932ca8e6bd2fbf48e4a14db Mon Sep 17 00:00:00 2001 From: Luca Rossi Date: Mon, 16 Mar 2026 07:25:40 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20split=20DynamicPropertiesPanel=20in?= =?UTF-8?q?to=20focused=20sub-components=20=E2=80=94=20reduce=20699-line?= =?UTF-8?q?=20catch-all=20(#197)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract PropertyValueCells, TypeSelector, and AddPropertyForm into dedicated files. Move shared display-mode constants to utils/propertyTypes to satisfy react-refresh lint rule. All tests pass, no behavior change. Co-authored-by: Test Co-authored-by: Claude Opus 4.6 (1M context) --- src/components/AddPropertyForm.tsx | 175 +++++++ src/components/DynamicPropertiesPanel.tsx | 568 +--------------------- src/components/PropertyValueCells.tsx | 324 ++++++++++++ src/components/TypeSelector.tsx | 77 +++ src/utils/propertyTypes.ts | 15 + 5 files changed, 596 insertions(+), 563 deletions(-) create mode 100644 src/components/AddPropertyForm.tsx create mode 100644 src/components/PropertyValueCells.tsx create mode 100644 src/components/TypeSelector.tsx diff --git a/src/components/AddPropertyForm.tsx b/src/components/AddPropertyForm.tsx new file mode 100644 index 00000000..67d9a1ef --- /dev/null +++ b/src/components/AddPropertyForm.tsx @@ -0,0 +1,175 @@ +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Calendar } from '@/components/ui/calendar' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { CalendarIcon, Check, X } from 'lucide-react' +import { + type PropertyDisplayMode, + formatDateValue, + toISODate, +} from '../utils/propertyTypes' +import { StatusPill, StatusDropdown } from './StatusDropdown' +import { DISPLAY_MODE_OPTIONS, DISPLAY_MODE_ICONS } from '../utils/propertyTypes' + +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}` +} + +const ADD_INPUT_CLASS = "h-[26px] min-w-[60px] flex-1 rounded border border-border bg-muted px-1.5 text-[12px] text-foreground outline-none focus:border-primary" + +function AddBooleanInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { + const boolVal = value.toLowerCase() === 'true' + return ( + + ) +} + +function AddDateInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { + const selectedDate = value ? parseDateValue(value) : undefined + const formatted = value ? formatDateValue(value) : '' + return ( + + + + + + { if (day) onChange(dateToISO(day)) }} + defaultMonth={selectedDate} + /> + + + ) +} + +function AddStatusInput({ value, onChange, vaultStatuses }: { value: string; onChange: (v: string) => void; vaultStatuses: string[] }) { + const [showDropdown, setShowDropdown] = useState(false) + return ( + + + {showDropdown && ( + { onChange(v); setShowDropdown(false) }} + onCancel={() => setShowDropdown(false)} + /> + )} + + ) +} + +function AddPropertyValueInput({ displayMode, value, onChange, onKeyDown, vaultStatuses }: { + displayMode: PropertyDisplayMode; value: string; onChange: (v: string) => void + onKeyDown: (e: React.KeyboardEvent) => void; vaultStatuses: string[] +}) { + switch (displayMode) { + case 'boolean': return + case 'date': return + case 'status': return + case 'tags': return ( + onChange(e.target.value)} onKeyDown={onKeyDown} + /> + ) + default: return ( + onChange(e.target.value)} onKeyDown={onKeyDown} + /> + ) + } +} + +export function AddPropertyForm({ onAdd, onCancel, vaultStatuses }: { + onAdd: (key: string, value: string, displayMode: PropertyDisplayMode) => void; onCancel: () => void + vaultStatuses: string[] +}) { + const [newKey, setNewKey] = useState('') + const [newValue, setNewValue] = useState('') + const [displayMode, setDisplayMode] = useState('text') + + const handleModeChange = (mode: PropertyDisplayMode) => { + setDisplayMode(mode) + if (mode === 'boolean') setNewValue('false') + else if (mode !== displayMode) setNewValue('') + } + + 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 + /> + + + + +
+ ) +} diff --git a/src/components/DynamicPropertiesPanel.tsx b/src/components/DynamicPropertiesPanel.tsx index b2ef076e..1a6f9956 100644 --- a/src/components/DynamicPropertiesPanel.tsx +++ b/src/components/DynamicPropertiesPanel.tsx @@ -1,31 +1,13 @@ -import { useState, useCallback, useRef } from 'react' -import { createPortal } from 'react-dom' import type { VaultEntry } from '../types' import type { FrontmatterValue } from './Inspector' import type { ParsedFrontmatter } from '../utils/frontmatter' -import { EditableValue, TagPillList, UrlValue } from './EditableValue' -import { isUrlValue } from '../utils/url' import { usePropertyPanelState } from '../hooks/usePropertyPanelState' -import { Button } from '@/components/ui/button' -import { Calendar } from '@/components/ui/calendar' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from '@/components/ui/select' -import { CalendarIcon, XIcon, Check, X, Type, ToggleLeft, Circle, Link, Tag, Palette } from 'lucide-react' -import { getTypeColor, getTypeLightColor } from '../utils/typeColors' -import { isValidCssColor } from '../utils/colorUtils' -import { getTypeIcon } from './NoteItem' +import { getEffectiveDisplayMode, detectPropertyType } from '../utils/propertyTypes' +import { SmartPropertyValueCell, DisplayModeSelector } from './PropertyValueCells' +import { TypeSelector } from './TypeSelector' +import { AddPropertyForm } from './AddPropertyForm' import { countWords } from '../utils/wikilinks' -import { - type PropertyDisplayMode, - getEffectiveDisplayMode, - formatDateValue, - toISODate, - detectPropertyType, -} from '../utils/propertyTypes' -import { StatusPill, StatusDropdown } from './StatusDropdown' -import { TagsDropdown } from './TagsDropdown' -import { getTagStyle } from '../utils/tagStyles' -import { ColorEditableValue } from './ColorInput' +import type { PropertyDisplayMode } from '../utils/propertyTypes' // eslint-disable-next-line react-refresh/only-export-components -- utility co-located with component export function containsWikilinks(value: FrontmatterValue): boolean { @@ -34,7 +16,6 @@ export function containsWikilinks(value: FrontmatterValue): boolean { return false } - function formatDate(timestamp: number | null): string { if (!timestamp) return '\u2014' const d = new Date(timestamp * 1000) @@ -49,545 +30,6 @@ function formatFileSize(bytes: number): string { return `${mb.toFixed(1)} MB` } -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) - return ( - - onStartEdit(propKey)} - data-testid="status-badge" - > - - - {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 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 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 && ( -
- -
- )} -
-
- ) -} - -const DISPLAY_MODE_OPTIONS: { value: PropertyDisplayMode; label: string }[] = [ - { value: 'text', label: 'Text' }, - { value: 'date', label: 'Date' }, - { value: 'boolean', label: 'Boolean' }, - { value: 'status', label: 'Status' }, - { value: 'url', label: 'URL' }, - { value: 'tags', label: 'Tags' }, - { value: 'color', label: 'Color' }, -] - -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 - )} -
- ) -} - -const DISPLAY_MODE_ICONS: Record = { - text: Type, date: CalendarIcon, boolean: ToggleLeft, status: Circle, url: Link, tags: Tag, color: Palette, -} - -const ADD_INPUT_CLASS = "h-[26px] min-w-[60px] flex-1 rounded border border-border bg-muted px-1.5 text-[12px] text-foreground outline-none focus:border-primary" - -function AddBooleanInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { - const boolVal = value.toLowerCase() === 'true' - return ( - - ) -} - -function AddDateInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { - const selectedDate = value ? parseDateValue(value) : undefined - const formatted = value ? formatDateValue(value) : '' - return ( - - - - - - { if (day) onChange(dateToISO(day)) }} - defaultMonth={selectedDate} - /> - - - ) -} - -function AddStatusInput({ value, onChange, vaultStatuses }: { value: string; onChange: (v: string) => void; vaultStatuses: string[] }) { - const [showDropdown, setShowDropdown] = useState(false) - return ( - - - {showDropdown && ( - { onChange(v); setShowDropdown(false) }} - onCancel={() => setShowDropdown(false)} - /> - )} - - ) -} - -function AddPropertyValueInput({ displayMode, value, onChange, onKeyDown, vaultStatuses }: { - displayMode: PropertyDisplayMode; value: string; onChange: (v: string) => void - onKeyDown: (e: React.KeyboardEvent) => void; vaultStatuses: string[] -}) { - switch (displayMode) { - case 'boolean': return - case 'date': return - case 'status': return - case 'tags': return ( - onChange(e.target.value)} onKeyDown={onKeyDown} - /> - ) - default: return ( - onChange(e.target.value)} onKeyDown={onKeyDown} - /> - ) - } -} - -function AddPropertyForm({ onAdd, onCancel, vaultStatuses }: { - onAdd: (key: string, value: string, displayMode: PropertyDisplayMode) => void; onCancel: () => void - vaultStatuses: string[] -}) { - const [newKey, setNewKey] = useState('') - const [newValue, setNewValue] = useState('') - const [displayMode, setDisplayMode] = useState('text') - - const handleModeChange = (mode: PropertyDisplayMode) => { - setDisplayMode(mode) - if (mode === 'boolean') setNewValue('false') - else if (mode !== displayMode) setNewValue('') - } - - 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 - /> - - - - -
- ) -} - -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 TypeSelectorItem({ type, typeColorKeys, typeIconKeys }: { - type: string; typeColorKeys: Record; typeIconKeys: Record -}) { - const Icon = getTypeIcon(type, typeIconKeys[type]) - const color = getTypeColor(type, typeColorKeys[type]) - return ( - <> - {/* eslint-disable-next-line react-hooks/static-components -- icon from static map lookup */} - - {type} - - ) -} - -function TypeSelector({ isA, customColorKey, availableTypes, typeColorKeys, typeIconKeys, onUpdateProperty, onNavigate }: { - isA?: string | null; customColorKey?: string | null; availableTypes: string[] - typeColorKeys: Record - typeIconKeys: Record - 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 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 - } -} - -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 -} - function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultStatuses, vaultTags, onStartEdit, onSave, onSaveList, onUpdate, onDelete, onDisplayModeChange }: { propKey: string; value: FrontmatterValue; editingKey: string | null displayMode: PropertyDisplayMode; autoMode: PropertyDisplayMode diff --git a/src/components/PropertyValueCells.tsx b/src/components/PropertyValueCells.tsx new file mode 100644 index 00000000..eb0084e4 --- /dev/null +++ b/src/components/PropertyValueCells.tsx @@ -0,0 +1,324 @@ +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 { CalendarIcon, XIcon } from 'lucide-react' +import { isValidCssColor } from '../utils/colorUtils' +import { + type PropertyDisplayMode, + formatDateValue, + toISODate, + DISPLAY_MODE_OPTIONS, + DISPLAY_MODE_ICONS, +} from '../utils/propertyTypes' +import { StatusPill, StatusDropdown } from './StatusDropdown' +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) + return ( + + onStartEdit(propKey)} + data-testid="status-badge" + > + + + {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 +} + diff --git a/src/components/TypeSelector.tsx b/src/components/TypeSelector.tsx new file mode 100644 index 00000000..52af9434 --- /dev/null +++ b/src/components/TypeSelector.tsx @@ -0,0 +1,77 @@ +import type { FrontmatterValue } from './Inspector' +import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from '@/components/ui/select' +import { getTypeColor, getTypeLightColor } from '../utils/typeColors' +import { getTypeIcon } from './NoteItem' + +const TYPE_NONE = '__none__' + +function TypeSelectorItem({ type, typeColorKeys, typeIconKeys }: { + type: string; typeColorKeys: Record; typeIconKeys: Record +}) { + const Icon = getTypeIcon(type, typeIconKeys[type]) + const color = getTypeColor(type, typeColorKeys[type]) + return ( + <> + {/* eslint-disable-next-line react-hooks/static-components -- icon from static map lookup */} + + {type} + + ) +} + +function ReadOnlyType({ isA, customColorKey, onNavigate }: { isA?: string | null; customColorKey?: string | null; onNavigate?: (target: string) => void }) { + if (!isA) return null + return ( +
+ Type + {onNavigate ? ( + + ) : ( + {isA} + )} +
+ ) +} + +export function TypeSelector({ isA, customColorKey, availableTypes, typeColorKeys, typeIconKeys, onUpdateProperty, onNavigate }: { + isA?: string | null; customColorKey?: string | null; availableTypes: string[] + typeColorKeys: Record + typeIconKeys: Record + 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 + +
+ ) +} diff --git a/src/utils/propertyTypes.ts b/src/utils/propertyTypes.ts index d888cc2a..a1b68698 100644 --- a/src/utils/propertyTypes.ts +++ b/src/utils/propertyTypes.ts @@ -1,6 +1,7 @@ import type { FrontmatterValue } from '../components/Inspector' import { isValidCssColor, isColorKeyName } from './colorUtils' import { updateVaultConfigField } from './vaultConfigStore' +import { CalendarIcon, Type, ToggleLeft, Circle, Link, Tag, Palette } from 'lucide-react' export type PropertyDisplayMode = 'text' | 'date' | 'boolean' | 'status' | 'url' | 'tags' | 'color' @@ -112,3 +113,17 @@ export function toISODate(value: string): string { } return value } + +export const DISPLAY_MODE_ICONS: Record = { + text: Type, date: CalendarIcon, boolean: ToggleLeft, status: Circle, url: Link, tags: Tag, color: Palette, +} + +export const DISPLAY_MODE_OPTIONS: { value: PropertyDisplayMode; label: string }[] = [ + { value: 'text', label: 'Text' }, + { value: 'date', label: 'Date' }, + { value: 'boolean', label: 'Boolean' }, + { value: 'status', label: 'Status' }, + { value: 'url', label: 'URL' }, + { value: 'tags', label: 'Tags' }, + { value: 'color', label: 'Color' }, +]