fix: cap properties panel label column width
This commit is contained in:
@@ -9,6 +9,7 @@ import { TypeSelector } from './TypeSelector'
|
||||
import { AddPropertyForm } from './AddPropertyForm'
|
||||
import type { PropertyDisplayMode } from '../utils/propertyTypes'
|
||||
import { FOCUS_NOTE_ICON_PROPERTY_EVENT } from './noteIconPropertyEvents'
|
||||
import { PROPERTY_PANEL_COLUMN_STYLE } from './propertyPanelLayout'
|
||||
|
||||
function toSentenceCase(key: string): string {
|
||||
const spaced = key.replace(/[_-]/g, ' ')
|
||||
@@ -23,6 +24,10 @@ export function containsWikilinks(value: FrontmatterValue): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
const PROPERTY_ROW_CLASS_NAME = 'group/prop grid min-h-7 min-w-0 grid-cols-2 items-center gap-2 rounded px-1.5 outline-none transition-colors hover:bg-muted focus:bg-muted focus:ring-1 focus:ring-primary'
|
||||
const PROPERTY_LABEL_CLASS_NAME = 'flex max-w-full min-w-0 items-center gap-1 text-[12px] text-muted-foreground'
|
||||
const SUGGESTED_PROPERTY_SLOT_CLASS_NAME = 'grid min-h-7 min-w-0 grid-cols-2 items-center gap-2 rounded border-none bg-transparent px-1.5 text-left outline-none transition-colors hover:bg-muted focus:bg-muted focus:ring-1 focus:ring-primary cursor-pointer'
|
||||
|
||||
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
|
||||
@@ -40,9 +45,9 @@ function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultS
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group/prop grid min-h-7 min-w-0 grid-cols-2 items-center gap-2 rounded px-1.5 outline-none transition-colors hover:bg-muted focus:bg-muted focus:ring-1 focus:ring-primary" tabIndex={0} onKeyDown={handleKeyDown} data-testid="editable-property">
|
||||
<span className="flex min-w-0 items-center gap-1 text-[12px] text-muted-foreground">
|
||||
<span className="truncate">{toSentenceCase(propKey)}</span>
|
||||
<div className={PROPERTY_ROW_CLASS_NAME} style={PROPERTY_PANEL_COLUMN_STYLE} tabIndex={0} onKeyDown={handleKeyDown} data-testid="editable-property">
|
||||
<span className={PROPERTY_LABEL_CLASS_NAME}>
|
||||
<span className="min-w-0 flex-1 truncate">{toSentenceCase(propKey)}</span>
|
||||
{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>
|
||||
)}
|
||||
@@ -88,18 +93,63 @@ function getSuggestedDisplayMode(key: string): PropertyDisplayMode {
|
||||
function SuggestedPropertySlot({ label, onAdd }: { label: string; onAdd: () => void }) {
|
||||
return (
|
||||
<button
|
||||
className="grid min-h-7 min-w-0 grid-cols-2 items-center gap-2 rounded border-none bg-transparent px-1.5 text-left outline-none transition-colors hover:bg-muted focus:bg-muted focus:ring-1 focus:ring-primary cursor-pointer"
|
||||
className={SUGGESTED_PROPERTY_SLOT_CLASS_NAME}
|
||||
style={PROPERTY_PANEL_COLUMN_STYLE}
|
||||
tabIndex={0}
|
||||
onClick={onAdd}
|
||||
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onAdd() } }}
|
||||
data-testid="suggested-property"
|
||||
>
|
||||
<span className="min-w-0 truncate text-[12px] text-muted-foreground/50">{label}</span>
|
||||
<span className="min-w-0 max-w-full truncate text-[12px] text-muted-foreground/50">{label}</span>
|
||||
<span className="min-w-0 truncate text-[12px] text-muted-foreground/30">{'\u2014'}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function getExistingPropertyKeys(propertyEntries: [string, FrontmatterValue][], frontmatter: ParsedFrontmatter): Set<string> {
|
||||
const keys = new Set(propertyEntries.map(([key]) => key.toLowerCase()))
|
||||
for (const key of Object.keys(frontmatter)) keys.add(key.toLowerCase())
|
||||
return keys
|
||||
}
|
||||
|
||||
function getMissingSuggestedProperties(canAddProperty: boolean, existingKeys: Set<string>, pendingSuggestedKey: string | null) {
|
||||
if (!canAddProperty) return []
|
||||
|
||||
return SUGGESTED_PROPERTIES.filter(
|
||||
({ key }) => !existingKeys.has(key.toLowerCase()) && key !== pendingSuggestedKey,
|
||||
)
|
||||
}
|
||||
|
||||
function getIconPropertyKey(propertyEntries: [string, FrontmatterValue][]) {
|
||||
return propertyEntries.find(([key]) => key.toLowerCase() === 'icon')?.[0]
|
||||
}
|
||||
|
||||
function useFocusNoteIconProperty({
|
||||
onAddProperty,
|
||||
propertyEntries,
|
||||
setEditingKey,
|
||||
}: {
|
||||
onAddProperty?: (key: string, value: FrontmatterValue) => void
|
||||
propertyEntries: [string, FrontmatterValue][]
|
||||
setEditingKey: (key: string | null) => void
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const handleFocusNoteIcon = () => {
|
||||
const existingIconKey = getIconPropertyKey(propertyEntries)
|
||||
|
||||
if (!existingIconKey) {
|
||||
if (!onAddProperty) return
|
||||
onAddProperty('icon', '')
|
||||
}
|
||||
|
||||
setEditingKey(existingIconKey ?? 'icon')
|
||||
}
|
||||
|
||||
window.addEventListener(FOCUS_NOTE_ICON_PROPERTY_EVENT, handleFocusNoteIcon)
|
||||
return () => window.removeEventListener(FOCUS_NOTE_ICON_PROPERTY_EVENT, handleFocusNoteIcon)
|
||||
}, [onAddProperty, propertyEntries, setEditingKey])
|
||||
}
|
||||
|
||||
export function DynamicPropertiesPanel({
|
||||
entry, frontmatter, entries,
|
||||
onUpdateProperty, onDeleteProperty, onAddProperty, onNavigate,
|
||||
@@ -120,18 +170,11 @@ export function DynamicPropertiesPanel({
|
||||
} = usePropertyPanelState({ entries, entryIsA: entry.isA, frontmatter, onUpdateProperty, onDeleteProperty, onAddProperty })
|
||||
const [pendingSuggestedKey, setPendingSuggestedKey] = useState<string | null>(null)
|
||||
|
||||
const existingKeys = useMemo(() => {
|
||||
const keys = new Set(propertyEntries.map(([k]) => k.toLowerCase()))
|
||||
// Also check full frontmatter for relationship keys that are filtered out of propertyEntries
|
||||
for (const k of Object.keys(frontmatter)) keys.add(k.toLowerCase())
|
||||
return keys
|
||||
}, [propertyEntries, frontmatter])
|
||||
|
||||
const missingSuggested = onAddProperty
|
||||
? SUGGESTED_PROPERTIES.filter(
|
||||
p => !existingKeys.has(p.key.toLowerCase()) && p.key !== pendingSuggestedKey,
|
||||
)
|
||||
: []
|
||||
const existingKeys = useMemo(() => getExistingPropertyKeys(propertyEntries, frontmatter), [propertyEntries, frontmatter])
|
||||
const missingSuggested = useMemo(
|
||||
() => getMissingSuggestedProperties(Boolean(onAddProperty), existingKeys, pendingSuggestedKey),
|
||||
[existingKeys, onAddProperty, pendingSuggestedKey],
|
||||
)
|
||||
|
||||
const handleSuggestedAdd = useCallback((key: string) => {
|
||||
if (!onAddProperty) return
|
||||
@@ -157,21 +200,7 @@ export function DynamicPropertiesPanel({
|
||||
onAddProperty(key, trimmed)
|
||||
}, [onAddProperty, setEditingKey])
|
||||
|
||||
useEffect(() => {
|
||||
const handleFocusNoteIcon = () => {
|
||||
const existingIconKey = propertyEntries.find(([key]) => key.toLowerCase() === 'icon')?.[0]
|
||||
|
||||
if (!existingIconKey) {
|
||||
if (!onAddProperty) return
|
||||
onAddProperty('icon', '')
|
||||
}
|
||||
|
||||
setEditingKey(existingIconKey ?? 'icon')
|
||||
}
|
||||
|
||||
window.addEventListener(FOCUS_NOTE_ICON_PROPERTY_EVENT, handleFocusNoteIcon)
|
||||
return () => window.removeEventListener(FOCUS_NOTE_ICON_PROPERTY_EVENT, handleFocusNoteIcon)
|
||||
}, [onAddProperty, propertyEntries, setEditingKey])
|
||||
useFocusNoteIconProperty({ onAddProperty, propertyEntries, setEditingKey })
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
|
||||
@@ -2,6 +2,7 @@ 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'
|
||||
import { PROPERTY_PANEL_COLUMN_STYLE } from './propertyPanelLayout'
|
||||
|
||||
const TYPE_NONE = '__none__'
|
||||
|
||||
@@ -22,7 +23,7 @@ function TypeSelectorItem({ type, typeColorKeys, typeIconKeys }: {
|
||||
function ReadOnlyType({ isA, customColorKey, onNavigate }: { isA?: string | null; customColorKey?: string | null; onNavigate?: (target: string) => void }) {
|
||||
if (!isA) return null
|
||||
return (
|
||||
<div className="grid min-w-0 grid-cols-2 items-center gap-2 px-1.5">
|
||||
<div className="grid min-w-0 grid-cols-2 items-center gap-2 px-1.5" style={PROPERTY_PANEL_COLUMN_STYLE}>
|
||||
<span className="text-[12px] shrink-0 text-muted-foreground">Type</span>
|
||||
<div className="min-w-0">
|
||||
{onNavigate ? (
|
||||
@@ -57,7 +58,7 @@ export function TypeSelector({ isA, customColorKey, availableTypes, typeColorKey
|
||||
const typeLightColor = isA ? getTypeLightColor(isA, typeColorKeys[isA] ?? customColorKey) : undefined
|
||||
|
||||
return (
|
||||
<div className="grid min-w-0 grid-cols-2 items-center gap-2 px-1.5" data-testid="type-selector">
|
||||
<div className="grid min-w-0 grid-cols-2 items-center gap-2 px-1.5" style={PROPERTY_PANEL_COLUMN_STYLE} data-testid="type-selector">
|
||||
<span className="text-[12px] shrink-0 text-muted-foreground">Type</span>
|
||||
<div className="min-w-0">
|
||||
<Select value={currentValue} onValueChange={v => onUpdateProperty('type', v === TYPE_NONE ? null : v)}>
|
||||
|
||||
5
src/components/propertyPanelLayout.ts
Normal file
5
src/components/propertyPanelLayout.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
export const PROPERTY_PANEL_COLUMN_STYLE = {
|
||||
gridTemplateColumns: 'fit-content(50%) minmax(0, 1fr)',
|
||||
} satisfies CSSProperties
|
||||
Reference in New Issue
Block a user