Property values (dates, URLs, text) were inheriting the 14px root font size instead of using the 12px size already applied in InfoRow and other inspector sections. Standardize all property value and editing input text to 12px for visual consistency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
253 lines
7.1 KiB
TypeScript
253 lines
7.1 KiB
TypeScript
import { useState, useCallback } from 'react'
|
|
import { normalizeUrl } from '../utils/url'
|
|
|
|
export function UrlValue({
|
|
value,
|
|
onSave,
|
|
onCancel,
|
|
isEditing,
|
|
onStartEdit,
|
|
}: {
|
|
value: string
|
|
onSave: (newValue: string) => void
|
|
onCancel: () => void
|
|
isEditing: boolean
|
|
onStartEdit: () => void
|
|
}) {
|
|
const [editValue, setEditValue] = useState(value)
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
onSave(editValue)
|
|
} else if (e.key === 'Escape') {
|
|
setEditValue(value)
|
|
onCancel()
|
|
}
|
|
}
|
|
|
|
const handleOpen = useCallback(() => {
|
|
const normalized = normalizeUrl(value)
|
|
try {
|
|
new URL(normalized)
|
|
window.open(normalized, '_blank')
|
|
} catch {
|
|
// malformed URL — do nothing
|
|
}
|
|
}, [value])
|
|
|
|
if (isEditing) {
|
|
return (
|
|
<input
|
|
className="w-full rounded border border-ring bg-muted px-2 py-1 text-[13px] text-foreground outline-none focus:border-primary"
|
|
type="text"
|
|
value={editValue}
|
|
onChange={(e) => setEditValue(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
onBlur={() => onSave(editValue)}
|
|
autoFocus
|
|
/>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<span className="group/url inline-flex min-w-0 items-center gap-1">
|
|
<span
|
|
className="min-w-0 cursor-pointer truncate rounded px-1 py-0.5 text-right text-secondary-foreground transition-colors hover:text-primary hover:underline"
|
|
onClick={handleOpen}
|
|
title={value}
|
|
data-testid="url-link"
|
|
>
|
|
{value || '\u2014'}
|
|
</span>
|
|
<button
|
|
className="shrink-0 border-none bg-transparent p-0 text-[12px] leading-none text-muted-foreground opacity-0 transition-all hover:text-foreground group-hover/url:opacity-100"
|
|
onClick={onStartEdit}
|
|
title="Edit URL"
|
|
data-testid="url-edit-btn"
|
|
>
|
|
✎
|
|
</button>
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export function EditableValue({
|
|
value,
|
|
onSave,
|
|
onCancel,
|
|
isEditing,
|
|
onStartEdit
|
|
}: {
|
|
value: string
|
|
onSave: (newValue: string) => void
|
|
onCancel: () => void
|
|
isEditing: boolean
|
|
onStartEdit: () => void
|
|
}) {
|
|
const [editValue, setEditValue] = useState(value)
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
onSave(editValue)
|
|
} else if (e.key === 'Escape') {
|
|
setEditValue(value)
|
|
onCancel()
|
|
}
|
|
}
|
|
|
|
if (isEditing) {
|
|
return (
|
|
<input
|
|
className="w-full rounded border border-ring bg-muted px-2 py-1 text-[12px] text-foreground outline-none focus:border-primary"
|
|
type="text"
|
|
value={editValue}
|
|
onChange={(e) => setEditValue(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
onBlur={() => onSave(editValue)}
|
|
autoFocus
|
|
/>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<span
|
|
className="min-w-0 cursor-pointer truncate rounded px-1 py-0.5 text-right text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
|
onClick={onStartEdit}
|
|
title={value || 'Click to edit'}
|
|
>
|
|
{value || '\u2014'}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export function TagPillList({
|
|
items,
|
|
onSave,
|
|
label,
|
|
}: {
|
|
items: string[]
|
|
onSave: (newItems: string[]) => void
|
|
label: string
|
|
}) {
|
|
const [editingIndex, setEditingIndex] = useState<number | null>(null)
|
|
const [editValue, setEditValue] = useState('')
|
|
const [isAddingNew, setIsAddingNew] = useState(false)
|
|
const [newValue, setNewValue] = useState('')
|
|
|
|
const handleStartEdit = (index: number) => {
|
|
setEditingIndex(index)
|
|
setEditValue(items[index])
|
|
}
|
|
|
|
const handleSaveEdit = () => {
|
|
if (editingIndex !== null) {
|
|
const newItems = [...items]
|
|
if (editValue.trim()) {
|
|
newItems[editingIndex] = editValue.trim()
|
|
} else {
|
|
newItems.splice(editingIndex, 1)
|
|
}
|
|
onSave(newItems)
|
|
setEditingIndex(null)
|
|
}
|
|
}
|
|
|
|
const handleDeleteItem = (index: number) => {
|
|
const newItems = items.filter((_, i) => i !== index)
|
|
onSave(newItems)
|
|
}
|
|
|
|
const handleAddNew = () => {
|
|
if (newValue.trim()) {
|
|
onSave([...items, newValue.trim()])
|
|
setNewValue('')
|
|
setIsAddingNew(false)
|
|
}
|
|
}
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent, action: 'edit' | 'add') => {
|
|
if (e.key === 'Enter') {
|
|
if (action === 'edit') handleSaveEdit()
|
|
else handleAddNew()
|
|
} else if (e.key === 'Escape') {
|
|
if (action === 'edit') {
|
|
setEditingIndex(null)
|
|
setEditValue('')
|
|
} else {
|
|
setIsAddingNew(false)
|
|
setNewValue('')
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-wrap items-center gap-1">
|
|
{items.map((item, idx) =>
|
|
editingIndex === idx ? (
|
|
<input
|
|
key={idx}
|
|
className="rounded-full border border-ring bg-muted px-2 py-0.5 text-[11px] text-foreground outline-none focus:border-primary"
|
|
style={{ width: Math.max(60, editValue.length * 7 + 16) }}
|
|
type="text"
|
|
value={editValue}
|
|
onChange={(e) => setEditValue(e.target.value)}
|
|
onKeyDown={(e) => handleKeyDown(e, 'edit')}
|
|
onBlur={handleSaveEdit}
|
|
autoFocus
|
|
/>
|
|
) : (
|
|
<span
|
|
key={idx}
|
|
className="group/pill inline-flex cursor-pointer items-center gap-0.5 rounded-full py-0.5 pl-2 pr-1 transition-colors"
|
|
style={{
|
|
backgroundColor: 'var(--accent-blue-light)',
|
|
color: 'var(--accent-blue)',
|
|
fontSize: 11,
|
|
fontWeight: 500,
|
|
}}
|
|
onClick={() => handleStartEdit(idx)}
|
|
title="Click to edit"
|
|
>
|
|
{item}
|
|
<button
|
|
className="ml-0.5 inline-flex h-3.5 w-3.5 items-center justify-center rounded-full border-none bg-transparent p-0 text-[10px] leading-none opacity-0 transition-all hover:bg-[var(--accent-red-light)] hover:text-[var(--accent-red)] group-hover/pill:opacity-100"
|
|
style={{ color: 'var(--accent-blue)' }}
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
handleDeleteItem(idx)
|
|
}}
|
|
title="Remove"
|
|
>
|
|
×
|
|
</button>
|
|
</span>
|
|
)
|
|
)}
|
|
{isAddingNew ? (
|
|
<input
|
|
className="rounded-full border border-ring bg-muted px-2 py-0.5 text-[11px] text-foreground outline-none focus:border-primary"
|
|
style={{ width: Math.max(60, newValue.length * 7 + 16) }}
|
|
type="text"
|
|
value={newValue}
|
|
onChange={(e) => setNewValue(e.target.value)}
|
|
onKeyDown={(e) => handleKeyDown(e, 'add')}
|
|
onBlur={() => {
|
|
if (newValue.trim()) handleAddNew()
|
|
else { setIsAddingNew(false); setNewValue('') }
|
|
}}
|
|
placeholder={`${label}...`}
|
|
autoFocus
|
|
/>
|
|
) : (
|
|
<button
|
|
className="inline-flex h-5 w-5 items-center justify-center rounded-full border border-dashed border-[var(--accent-blue)] bg-transparent p-0 text-[12px] leading-none text-[var(--accent-blue)] transition-colors hover:bg-[var(--accent-blue-light)]"
|
|
onClick={() => setIsAddingNew(true)}
|
|
title={`Add ${label.toLowerCase()}`}
|
|
>
|
|
+
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|