feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
import { useMemo, useState, useCallback, useRef } from 'react'
|
2026-02-17 12:12:32 +01:00
|
|
|
import type { VaultEntry } from '../types'
|
|
|
|
|
import type { FrontmatterValue } from './Inspector'
|
|
|
|
|
import type { ParsedFrontmatter } from '../utils/frontmatter'
|
2026-02-24 14:46:18 +01:00
|
|
|
import { EditableValue, TagPillList, UrlValue } from './EditableValue'
|
|
|
|
|
import { isUrlValue } from '../utils/url'
|
2026-02-17 12:12:32 +01:00
|
|
|
import { Button } from '@/components/ui/button'
|
2026-02-26 19:17:40 +01:00
|
|
|
import { Calendar } from '@/components/ui/calendar'
|
|
|
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
2026-02-24 14:00:34 +01:00
|
|
|
import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from '@/components/ui/select'
|
2026-02-27 12:12:58 +01:00
|
|
|
import { CalendarIcon, XIcon, Check, X, Type, ToggleLeft, Circle, Link } from 'lucide-react'
|
2026-02-20 21:49:55 +01:00
|
|
|
import { getTypeColor, getTypeLightColor } from '../utils/typeColors'
|
2026-02-24 12:19:21 +01:00
|
|
|
import { countWords } from '../utils/wikilinks'
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
import {
|
|
|
|
|
type PropertyDisplayMode,
|
|
|
|
|
getEffectiveDisplayMode,
|
|
|
|
|
formatDateValue,
|
|
|
|
|
toISODate,
|
|
|
|
|
loadDisplayModeOverrides,
|
|
|
|
|
saveDisplayModeOverride,
|
|
|
|
|
removeDisplayModeOverride,
|
|
|
|
|
detectPropertyType,
|
|
|
|
|
} from '../utils/propertyTypes'
|
2026-02-26 13:40:27 +01:00
|
|
|
import { StatusPill, StatusDropdown } from './StatusDropdown'
|
2026-02-17 12:12:32 +01:00
|
|
|
|
|
|
|
|
// 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',
|
|
|
|
|
])
|
|
|
|
|
|
2026-02-23 15:24:57 +01:00
|
|
|
// Keys to skip showing in Properties (handled by dedicated UI or internal)
|
|
|
|
|
const SKIP_KEYS = new Set(['aliases', 'notion_id', 'workspace', 'title', 'type', 'is_a', 'Is A'])
|
2026-02-17 12:12:32 +01:00
|
|
|
|
2026-02-22 13:11:38 +01:00
|
|
|
// eslint-disable-next-line react-refresh/only-export-components -- utility co-located with component
|
2026-02-17 12:12:32 +01:00
|
|
|
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 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' })
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 10:55:45 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 15:19:57 +01:00
|
|
|
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`
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 13:40:27 +01:00
|
|
|
function StatusValue({ propKey, value, isEditing, vaultStatuses, onSave, onStartEdit }: {
|
|
|
|
|
propKey: string; value: FrontmatterValue; isEditing: boolean; vaultStatuses: string[]
|
2026-02-22 10:55:45 +01:00
|
|
|
onSave: (key: string, value: string) => void; onStartEdit: (key: string | null) => void
|
|
|
|
|
}) {
|
|
|
|
|
const statusStr = String(value)
|
|
|
|
|
return (
|
2026-02-26 13:40:27 +01:00
|
|
|
<span className="relative inline-flex min-w-0 items-center">
|
|
|
|
|
<span
|
|
|
|
|
className="cursor-pointer transition-opacity hover:opacity-80"
|
|
|
|
|
onClick={() => onStartEdit(propKey)}
|
|
|
|
|
data-testid="status-badge"
|
|
|
|
|
>
|
|
|
|
|
<StatusPill status={statusStr} />
|
|
|
|
|
</span>
|
|
|
|
|
{isEditing && (
|
|
|
|
|
<StatusDropdown
|
|
|
|
|
value={statusStr}
|
|
|
|
|
vaultStatuses={vaultStatuses}
|
|
|
|
|
onSave={(newValue) => onSave(propKey, newValue)}
|
|
|
|
|
onCancel={() => onStartEdit(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-02-22 10:55:45 +01:00
|
|
|
</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function BooleanToggle({ value, onToggle }: { value: boolean; onToggle: () => void }) {
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
className="rounded border border-border bg-transparent px-2 py-0.5 text-xs text-secondary-foreground transition-colors hover:bg-muted"
|
|
|
|
|
onClick={onToggle}
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
data-testid="boolean-toggle"
|
2026-02-22 10:55:45 +01:00
|
|
|
>
|
|
|
|
|
{value ? '\u2713 Yes' : '\u2717 No'}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 19:17:40 +01:00
|
|
|
function parseDateValue(value: string): Date | undefined {
|
|
|
|
|
const iso = toISODate(value)
|
|
|
|
|
const d = new Date(iso + 'T00:00:00')
|
|
|
|
|
return isNaN(d.getTime()) ? undefined : d
|
|
|
|
|
}
|
|
|
|
|
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
function DateValue({ value, onSave }: {
|
|
|
|
|
value: string; onSave: (newValue: string) => void
|
|
|
|
|
}) {
|
2026-02-26 19:17:40 +01:00
|
|
|
const [open, setOpen] = useState(false)
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
const formatted = formatDateValue(value)
|
2026-02-26 19:17:40 +01:00
|
|
|
const selectedDate = parseDateValue(value)
|
|
|
|
|
|
|
|
|
|
const handleSelect = (day: Date | undefined) => {
|
|
|
|
|
if (day) {
|
|
|
|
|
const yyyy = day.getFullYear()
|
|
|
|
|
const mm = String(day.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
const dd = String(day.getDate()).padStart(2, '0')
|
|
|
|
|
onSave(`${yyyy}-${mm}-${dd}`)
|
|
|
|
|
}
|
|
|
|
|
setOpen(false)
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-26 19:17:40 +01:00
|
|
|
const handleClear = (e: React.MouseEvent) => {
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
onSave('')
|
|
|
|
|
setOpen(false)
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-26 19:17:40 +01:00
|
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button
|
|
|
|
|
className="inline-flex min-w-0 cursor-pointer items-center gap-1 rounded border-none bg-transparent px-1 py-0.5 text-right text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
|
|
|
|
title={value}
|
|
|
|
|
data-testid="date-display"
|
|
|
|
|
>
|
|
|
|
|
<CalendarIcon className="size-3 shrink-0 text-muted-foreground" />
|
|
|
|
|
<span className="min-w-0 truncate">{formatted}</span>
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-auto p-0" align="end" data-testid="date-picker-popover">
|
|
|
|
|
<Calendar
|
|
|
|
|
mode="single"
|
|
|
|
|
selected={selectedDate}
|
|
|
|
|
onSelect={handleSelect}
|
|
|
|
|
defaultMonth={selectedDate}
|
|
|
|
|
data-testid="date-picker-calendar"
|
|
|
|
|
/>
|
|
|
|
|
{selectedDate && (
|
|
|
|
|
<div className="border-t px-3 py-2">
|
|
|
|
|
<button
|
|
|
|
|
className="inline-flex items-center gap-1 border-none bg-transparent text-xs text-muted-foreground transition-colors hover:text-foreground"
|
|
|
|
|
onClick={handleClear}
|
|
|
|
|
data-testid="date-picker-clear"
|
|
|
|
|
>
|
|
|
|
|
<XIcon className="size-3" />
|
|
|
|
|
Clear date
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
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 containerRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
const handleSelect = (mode: PropertyDisplayMode) => {
|
|
|
|
|
if (mode === autoMode) {
|
|
|
|
|
onSelect(propKey, null)
|
|
|
|
|
} else {
|
|
|
|
|
onSelect(propKey, mode)
|
|
|
|
|
}
|
|
|
|
|
setOpen(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div ref={containerRef} className="relative">
|
|
|
|
|
<button
|
|
|
|
|
className="flex h-4 w-4 items-center justify-center rounded border-none bg-transparent p-0 text-[10px] leading-none text-muted-foreground opacity-0 transition-all hover:bg-muted hover:text-foreground group-hover/prop:opacity-100"
|
|
|
|
|
onClick={() => setOpen(!open)}
|
|
|
|
|
title="Change display mode"
|
|
|
|
|
data-testid="display-mode-trigger"
|
|
|
|
|
>
|
|
|
|
|
{'\u25BE'}
|
|
|
|
|
</button>
|
|
|
|
|
{open && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
|
|
|
|
<div
|
|
|
|
|
className="absolute right-0 top-full z-50 mt-1 min-w-[130px] rounded-md border border-border bg-background py-1 shadow-md"
|
|
|
|
|
data-testid="display-mode-menu"
|
|
|
|
|
>
|
|
|
|
|
{DISPLAY_MODE_OPTIONS.map(opt => (
|
|
|
|
|
<button
|
|
|
|
|
key={opt.value}
|
|
|
|
|
className="flex w-full items-center gap-2 border-none bg-transparent px-3 py-1.5 text-left text-[12px] text-foreground transition-colors hover:bg-muted"
|
|
|
|
|
onClick={() => handleSelect(opt.value)}
|
|
|
|
|
data-testid={`display-mode-option-${opt.value}`}
|
|
|
|
|
>
|
|
|
|
|
<span className="w-3 text-center text-[10px]">
|
|
|
|
|
{currentMode === opt.value ? '\u2713' : ''}
|
|
|
|
|
</span>
|
|
|
|
|
{opt.label}
|
|
|
|
|
{opt.value === autoMode && (
|
|
|
|
|
<span className="ml-auto text-[10px] text-muted-foreground">auto</span>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 14:31:31 +01:00
|
|
|
const DISPLAY_MODE_ICONS: Record<PropertyDisplayMode, typeof Type> = {
|
2026-02-27 12:12:58 +01:00
|
|
|
text: Type, date: CalendarIcon, boolean: ToggleLeft, status: Circle, url: Link,
|
2026-02-26 14:31:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AddPropertyForm({ onAdd, onCancel }: {
|
|
|
|
|
onAdd: (key: string, value: string, displayMode: PropertyDisplayMode) => void; onCancel: () => void
|
|
|
|
|
}) {
|
2026-02-22 10:55:45 +01:00
|
|
|
const [newKey, setNewKey] = useState('')
|
|
|
|
|
const [newValue, setNewValue] = useState('')
|
2026-02-26 14:31:31 +01:00
|
|
|
const [displayMode, setDisplayMode] = useState<PropertyDisplayMode>('text')
|
|
|
|
|
|
2026-02-22 10:55:45 +01:00
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
2026-02-26 14:31:31 +01:00
|
|
|
if (e.key === 'Enter' && newKey.trim()) onAdd(newKey, newValue, displayMode)
|
2026-02-22 10:55:45 +01:00
|
|
|
else if (e.key === 'Escape') onCancel()
|
|
|
|
|
}
|
2026-02-26 14:31:31 +01:00
|
|
|
|
2026-02-22 10:55:45 +01:00
|
|
|
return (
|
2026-02-26 14:31:31 +01:00
|
|
|
<div className="mt-1 flex items-center gap-1.5 rounded px-1.5 py-1" data-testid="add-property-form">
|
2026-02-22 10:55:45 +01:00
|
|
|
<input
|
2026-02-26 14:31:31 +01:00
|
|
|
className="h-[26px] w-[90px] shrink-0 rounded border border-border bg-muted px-1.5 text-[12px] text-foreground outline-none focus:border-primary"
|
2026-02-22 10:55:45 +01:00
|
|
|
type="text" placeholder="Property name" value={newKey}
|
|
|
|
|
onChange={(e) => setNewKey(e.target.value)} onKeyDown={handleKeyDown} autoFocus
|
|
|
|
|
/>
|
2026-02-26 14:31:31 +01:00
|
|
|
<Select value={displayMode} onValueChange={(v) => setDisplayMode(v as PropertyDisplayMode)}>
|
|
|
|
|
<SelectTrigger
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-[26px] w-[82px] shrink-0 gap-1 border-border bg-muted px-1.5 py-0 shadow-none"
|
|
|
|
|
style={{ fontSize: 12, borderRadius: 4 }}
|
|
|
|
|
data-testid="add-property-type-trigger"
|
|
|
|
|
>
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{DISPLAY_MODE_OPTIONS.map(opt => {
|
|
|
|
|
const OptIcon = DISPLAY_MODE_ICONS[opt.value]
|
|
|
|
|
return (
|
|
|
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
|
|
|
<OptIcon className="size-3.5 text-muted-foreground" />
|
|
|
|
|
{opt.label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2026-02-22 10:55:45 +01:00
|
|
|
<input
|
2026-02-26 14:31:31 +01:00
|
|
|
className="h-[26px] min-w-0 flex-1 rounded border border-border bg-muted px-1.5 text-[12px] text-foreground outline-none focus:border-primary"
|
2026-02-22 10:55:45 +01:00
|
|
|
type="text" placeholder="Value" value={newValue}
|
|
|
|
|
onChange={(e) => setNewValue(e.target.value)} onKeyDown={handleKeyDown}
|
|
|
|
|
/>
|
2026-02-26 14:31:31 +01:00
|
|
|
<Button
|
|
|
|
|
size="icon-xs" onClick={() => onAdd(newKey, newValue, displayMode)}
|
|
|
|
|
disabled={!newKey.trim()} title="Add property"
|
|
|
|
|
data-testid="add-property-confirm"
|
|
|
|
|
>
|
|
|
|
|
<Check className="size-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button size="icon-xs" variant="outline" onClick={onCancel} title="Cancel" data-testid="add-property-cancel">
|
|
|
|
|
<X className="size-3.5" />
|
|
|
|
|
</Button>
|
2026-02-22 10:55:45 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 14:00:34 +01:00
|
|
|
const TYPE_NONE = '__none__'
|
|
|
|
|
|
2026-02-25 15:02:33 +01:00
|
|
|
function ReadOnlyType({ isA, customColorKey, onNavigate }: { isA?: string | null; customColorKey?: string | null; onNavigate?: (target: string) => void }) {
|
2026-02-22 10:55:45 +01:00
|
|
|
if (!isA) return null
|
|
|
|
|
return (
|
2026-02-24 22:37:37 +01:00
|
|
|
<div className="flex items-center justify-between px-1.5">
|
2026-02-22 10:55:45 +01:00
|
|
|
<span className="font-mono-overline shrink-0 text-muted-foreground">Type</span>
|
|
|
|
|
{onNavigate ? (
|
|
|
|
|
<button
|
|
|
|
|
className="min-w-0 truncate border-none text-right cursor-pointer hover:opacity-80"
|
2026-02-25 15:02:33 +01:00
|
|
|
style={{ background: getTypeLightColor(isA, customColorKey), color: getTypeColor(isA, customColorKey), borderRadius: 6, padding: '2px 8px', fontSize: 12, fontWeight: 500 }}
|
2026-02-22 10:55:45 +01:00
|
|
|
onClick={() => onNavigate(`type/${isA.toLowerCase()}`)} title={isA}
|
|
|
|
|
>{isA}</button>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-right text-[12px] text-secondary-foreground">{isA}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 15:02:33 +01:00
|
|
|
function TypeSelector({ isA, customColorKey, availableTypes, onUpdateProperty, onNavigate }: {
|
|
|
|
|
isA?: string | null; customColorKey?: string | null; availableTypes: string[]
|
2026-02-24 14:00:34 +01:00
|
|
|
onUpdateProperty?: (key: string, value: FrontmatterValue) => void
|
|
|
|
|
onNavigate?: (target: string) => void
|
|
|
|
|
}) {
|
2026-02-25 15:02:33 +01:00
|
|
|
if (!onUpdateProperty) return <ReadOnlyType isA={isA} customColorKey={customColorKey} onNavigate={onNavigate} />
|
2026-02-24 14:00:34 +01:00
|
|
|
|
|
|
|
|
const currentValue = isA || TYPE_NONE
|
|
|
|
|
const options = isA && !availableTypes.includes(isA)
|
|
|
|
|
? [...availableTypes, isA].sort((a, b) => a.localeCompare(b))
|
|
|
|
|
: availableTypes
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-24 22:37:37 +01:00
|
|
|
<div className="flex items-center justify-between px-1.5" data-testid="type-selector">
|
2026-02-24 14:00:34 +01:00
|
|
|
<span className="font-mono-overline shrink-0 text-muted-foreground">Type</span>
|
|
|
|
|
<Select value={currentValue} onValueChange={v => onUpdateProperty('type', v === TYPE_NONE ? null : v)}>
|
|
|
|
|
<SelectTrigger
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-auto min-h-0 gap-1 border-none px-2 py-0.5 shadow-none"
|
|
|
|
|
style={isA ? {
|
2026-02-25 15:02:33 +01:00
|
|
|
background: getTypeLightColor(isA, customColorKey),
|
|
|
|
|
color: getTypeColor(isA, customColorKey),
|
2026-02-24 14:00:34 +01:00
|
|
|
fontSize: 12,
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
borderRadius: 6,
|
|
|
|
|
} : { fontSize: 12, borderRadius: 6 }}
|
|
|
|
|
>
|
|
|
|
|
<SelectValue placeholder="None" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value={TYPE_NONE}>None</SelectItem>
|
|
|
|
|
<SelectSeparator />
|
|
|
|
|
{options.map(type => (
|
|
|
|
|
<SelectItem key={type} value={type}>{type}</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 13:40:27 +01:00
|
|
|
function SmartPropertyValueCell({ propKey, value, displayMode, isEditing, vaultStatuses, onStartEdit, onSave, onSaveList, onUpdate }: {
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
propKey: string; value: FrontmatterValue; displayMode: PropertyDisplayMode; isEditing: boolean
|
2026-02-26 13:40:27 +01:00
|
|
|
vaultStatuses: string[]
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
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 <EditableValue {...editProps} />
|
|
|
|
|
if (Array.isArray(value)) return <TagPillList items={value.map(String)} onSave={(items) => onSaveList(propKey, items)} label={propKey} />
|
|
|
|
|
|
|
|
|
|
switch (displayMode) {
|
|
|
|
|
case 'status':
|
2026-02-26 13:40:27 +01:00
|
|
|
return <StatusValue propKey={propKey} value={value} isEditing={isEditing} vaultStatuses={vaultStatuses} onSave={onSave} onStartEdit={onStartEdit} />
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
case 'date':
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
return <DateValue value={value} onSave={(v) => onSave(propKey, v)} />
|
|
|
|
|
}
|
|
|
|
|
return <EditableValue {...editProps} />
|
|
|
|
|
case 'boolean':
|
|
|
|
|
if (typeof value === 'boolean') {
|
|
|
|
|
return <BooleanToggle value={value} onToggle={() => onUpdate?.(propKey, !value)} />
|
|
|
|
|
}
|
|
|
|
|
return <EditableValue {...editProps} />
|
|
|
|
|
case 'url':
|
|
|
|
|
if (typeof value === 'string' && isUrlValue(value)) {
|
|
|
|
|
return <UrlValue {...editProps} />
|
|
|
|
|
}
|
|
|
|
|
return <EditableValue {...editProps} />
|
|
|
|
|
default:
|
|
|
|
|
if (typeof value === 'boolean') {
|
|
|
|
|
return <BooleanToggle value={value} onToggle={() => onUpdate?.(propKey, !value)} />
|
|
|
|
|
}
|
|
|
|
|
if (typeof value === 'string' && isUrlValue(value)) {
|
|
|
|
|
return <UrlValue {...editProps} />
|
|
|
|
|
}
|
|
|
|
|
return <EditableValue {...editProps} />
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 13:40:27 +01:00
|
|
|
function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultStatuses, onStartEdit, onSave, onSaveList, onUpdate, onDelete, onDisplayModeChange }: {
|
2026-02-22 10:55:45 +01:00
|
|
|
propKey: string; value: FrontmatterValue; editingKey: string | null
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
displayMode: PropertyDisplayMode; autoMode: PropertyDisplayMode
|
2026-02-26 13:40:27 +01:00
|
|
|
vaultStatuses: string[]
|
2026-02-22 10:55:45 +01:00
|
|
|
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
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
onDisplayModeChange: (key: string, mode: PropertyDisplayMode | null) => void
|
2026-02-22 10:55:45 +01:00
|
|
|
}) {
|
|
|
|
|
return (
|
2026-02-23 15:19:57 +01:00
|
|
|
<div className="group/prop flex items-center justify-between rounded px-1.5 py-0.5 transition-colors hover:bg-muted" data-testid="editable-property">
|
2026-02-22 10:55:45 +01:00
|
|
|
<span className="font-mono-overline flex shrink-0 items-center gap-1 text-muted-foreground">
|
|
|
|
|
{propKey}
|
|
|
|
|
{onDelete && (
|
|
|
|
|
<button className="border-none bg-transparent p-0 text-sm leading-none text-muted-foreground opacity-0 transition-all hover:text-destructive group-hover/prop:opacity-100" onClick={() => onDelete(propKey)} title="Delete property">×</button>
|
|
|
|
|
)}
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
<DisplayModeSelector propKey={propKey} currentMode={displayMode} autoMode={autoMode} onSelect={onDisplayModeChange} />
|
2026-02-22 10:55:45 +01:00
|
|
|
</span>
|
2026-02-26 13:40:27 +01:00
|
|
|
<SmartPropertyValueCell propKey={propKey} value={value} displayMode={displayMode} isEditing={editingKey === propKey} vaultStatuses={vaultStatuses} onStartEdit={onStartEdit} onSave={onSave} onSaveList={onSaveList} onUpdate={onUpdate} />
|
2026-02-22 10:55:45 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 15:19:57 +01:00
|
|
|
function InfoRow({ label, value }: { label: string; value: string }) {
|
2026-02-22 10:55:45 +01:00
|
|
|
return (
|
2026-02-23 15:19:57 +01:00
|
|
|
<div className="flex items-center justify-between px-1.5" data-testid="readonly-property">
|
|
|
|
|
<span className="font-mono-overline shrink-0" style={{ color: 'var(--text-muted)' }}>{label}</span>
|
|
|
|
|
<span className="text-right text-[12px]" style={{ color: 'var(--text-muted)' }}>{value}</span>
|
2026-02-22 10:55:45 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AddPropertyButton({ onClick, disabled }: { onClick: () => void; disabled: boolean }) {
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
className="mt-3 w-full cursor-pointer border border-border bg-transparent text-center text-muted-foreground transition-colors hover:border-primary hover:text-primary disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
style={{ borderRadius: 6, padding: '6px 12px', fontSize: 12 }}
|
|
|
|
|
onClick={onClick} disabled={disabled}
|
|
|
|
|
>+ Add property</button>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 15:02:33 +01:00
|
|
|
function NoteInfoSection({ entry, wordCount }: { entry: VaultEntry; wordCount: number }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="border-t border-border pt-3">
|
|
|
|
|
<h4 className="font-mono-overline mb-2 text-muted-foreground">Info</h4>
|
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
|
|
|
<InfoRow label="Modified" value={formatDate(entry.modifiedAt)} />
|
|
|
|
|
<InfoRow label="Created" value={formatDate(entry.createdAt)} />
|
|
|
|
|
<InfoRow label="Words" value={String(wordCount)} />
|
|
|
|
|
<InfoRow label="Size" value={formatFileSize(entry.fileSize)} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 12:12:32 +01:00
|
|
|
export function DynamicPropertiesPanel({
|
|
|
|
|
entry,
|
|
|
|
|
content,
|
|
|
|
|
frontmatter,
|
2026-02-24 14:00:34 +01:00
|
|
|
entries,
|
2026-02-17 12:12:32 +01:00
|
|
|
onUpdateProperty,
|
|
|
|
|
onDeleteProperty,
|
|
|
|
|
onAddProperty,
|
2026-02-20 21:49:55 +01:00
|
|
|
onNavigate,
|
2026-02-17 12:12:32 +01:00
|
|
|
}: {
|
|
|
|
|
entry: VaultEntry
|
|
|
|
|
content: string | null
|
|
|
|
|
frontmatter: ParsedFrontmatter
|
2026-02-24 14:00:34 +01:00
|
|
|
entries?: VaultEntry[]
|
2026-02-17 12:12:32 +01:00
|
|
|
onUpdateProperty?: (key: string, value: FrontmatterValue) => void
|
|
|
|
|
onDeleteProperty?: (key: string) => void
|
|
|
|
|
onAddProperty?: (key: string, value: FrontmatterValue) => void
|
2026-02-20 21:49:55 +01:00
|
|
|
onNavigate?: (target: string) => void
|
2026-02-17 12:12:32 +01:00
|
|
|
}) {
|
|
|
|
|
const [editingKey, setEditingKey] = useState<string | null>(null)
|
|
|
|
|
const [showAddDialog, setShowAddDialog] = useState(false)
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
const [displayOverrides, setDisplayOverrides] = useState(() => loadDisplayModeOverrides())
|
2026-02-17 12:12:32 +01:00
|
|
|
|
2026-02-24 12:19:21 +01:00
|
|
|
const wordCount = countWords(content ?? '')
|
2026-02-17 12:12:32 +01:00
|
|
|
|
2026-02-25 15:02:33 +01:00
|
|
|
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])
|
2026-02-24 14:00:34 +01:00
|
|
|
|
2026-02-26 13:40:27 +01:00
|
|
|
const vaultStatuses = useMemo(() => {
|
|
|
|
|
const seen = new Set<string>()
|
|
|
|
|
for (const e of entries ?? []) {
|
|
|
|
|
if (e.status) seen.add(e.status)
|
|
|
|
|
}
|
|
|
|
|
return Array.from(seen).sort((a, b) => a.localeCompare(b))
|
|
|
|
|
}, [entries])
|
|
|
|
|
|
2026-02-17 12:12:32 +01:00
|
|
|
const propertyEntries = useMemo(() => {
|
|
|
|
|
return Object.entries(frontmatter)
|
2026-02-22 10:55:45 +01:00
|
|
|
.filter(([key, value]) => !SKIP_KEYS.has(key) && !RELATIONSHIP_KEYS.has(key) && !containsWikilinks(value))
|
2026-02-17 12:12:32 +01:00
|
|
|
}, [frontmatter])
|
|
|
|
|
|
|
|
|
|
const handleSaveValue = useCallback((key: string, newValue: string) => {
|
|
|
|
|
setEditingKey(null)
|
2026-02-22 10:55:45 +01:00
|
|
|
if (onUpdateProperty) onUpdateProperty(key, coerceValue(newValue))
|
2026-02-17 12:12:32 +01:00
|
|
|
}, [onUpdateProperty])
|
|
|
|
|
|
|
|
|
|
const handleSaveList = useCallback((key: string, newItems: string[]) => {
|
2026-02-22 10:55:45 +01:00
|
|
|
if (!onUpdateProperty) return
|
2026-02-25 15:02:33 +01:00
|
|
|
reconcileListUpdate(newItems, onUpdateProperty, onDeleteProperty, key)
|
2026-02-17 12:12:32 +01:00
|
|
|
}, [onUpdateProperty, onDeleteProperty])
|
|
|
|
|
|
2026-02-26 14:31:31 +01:00
|
|
|
const handleAdd = useCallback((rawKey: string, rawValue: string, mode: PropertyDisplayMode) => {
|
2026-02-22 10:55:45 +01:00
|
|
|
if (!rawKey.trim() || !onAddProperty) return
|
|
|
|
|
onAddProperty(rawKey.trim(), parseNewValue(rawValue))
|
2026-02-26 14:31:31 +01:00
|
|
|
if (mode !== 'text') {
|
|
|
|
|
saveDisplayModeOverride(rawKey.trim(), mode)
|
|
|
|
|
setDisplayOverrides(loadDisplayModeOverrides())
|
|
|
|
|
}
|
2026-02-22 10:55:45 +01:00
|
|
|
setShowAddDialog(false)
|
|
|
|
|
}, [onAddProperty])
|
2026-02-17 12:12:32 +01:00
|
|
|
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
const handleDisplayModeChange = useCallback((key: string, mode: PropertyDisplayMode | null) => {
|
|
|
|
|
if (mode === null) {
|
|
|
|
|
removeDisplayModeOverride(key)
|
|
|
|
|
} else {
|
|
|
|
|
saveDisplayModeOverride(key, mode)
|
|
|
|
|
}
|
|
|
|
|
setDisplayOverrides(loadDisplayModeOverrides())
|
|
|
|
|
}, [])
|
|
|
|
|
|
2026-02-17 12:12:32 +01:00
|
|
|
return (
|
2026-02-23 15:19:57 +01:00
|
|
|
<div className="flex flex-col gap-3">
|
|
|
|
|
{/* Editable properties section */}
|
2026-02-17 12:12:32 +01:00
|
|
|
<div className="flex flex-col gap-2">
|
2026-02-25 15:02:33 +01:00
|
|
|
<TypeSelector isA={entry.isA} customColorKey={customColorKey} availableTypes={availableTypes} onUpdateProperty={onUpdateProperty} onNavigate={onNavigate} />
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
{propertyEntries.map(([key, value]) => {
|
|
|
|
|
const autoMode = detectPropertyType(key, value)
|
|
|
|
|
const effectiveMode = getEffectiveDisplayMode(key, value, displayOverrides)
|
|
|
|
|
return (
|
|
|
|
|
<PropertyRow
|
|
|
|
|
key={key} propKey={key} value={value}
|
|
|
|
|
editingKey={editingKey} displayMode={effectiveMode} autoMode={autoMode}
|
2026-02-26 13:40:27 +01:00
|
|
|
vaultStatuses={vaultStatuses}
|
feat: smart property display — type-aware rendering with display mode override (#83)
* design: add smart property display wireframes
Frames: date picker, boolean toggle, status badge, display mode override dropdown.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: smart property display — type-aware rendering with display mode override
- Add property type auto-detection (date, boolean, status, url, text) based on value content and property name
- Date properties (ISO strings) show as friendly format (e.g. "Mar 31, 2026") with native date picker on click
- Boolean properties show as toggle buttons
- Status properties auto-detected from key name or known status values, rendered as colored badges
- Each property gets a display mode override dropdown (visible on hover) to force a specific display type
- Display mode overrides persist across sessions via localStorage
- Add mock data with date/boolean fields for testing
- 36 new tests covering type detection, date formatting, localStorage persistence, and UI rendering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 01:12:40 +01:00
|
|
|
onStartEdit={setEditingKey} onSave={handleSaveValue}
|
|
|
|
|
onSaveList={handleSaveList} onUpdate={onUpdateProperty}
|
|
|
|
|
onDelete={onDeleteProperty}
|
|
|
|
|
onDisplayModeChange={handleDisplayModeChange}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
})}
|
2026-02-17 12:12:32 +01:00
|
|
|
</div>
|
2026-02-22 10:55:45 +01:00
|
|
|
{showAddDialog
|
|
|
|
|
? <AddPropertyForm onAdd={handleAdd} onCancel={() => setShowAddDialog(false)} />
|
|
|
|
|
: <AddPropertyButton onClick={() => setShowAddDialog(true)} disabled={!onAddProperty} />
|
|
|
|
|
}
|
2026-02-25 15:02:33 +01:00
|
|
|
<NoteInfoSection entry={entry} wordCount={wordCount} />
|
2026-02-17 12:12:32 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|