fix: align Properties panel visual style to design (pills, truncation, labels)
Sentence-case labels, colored tag pills, date chips, checkbox booleans, blue URL links, status dot indicator, subtle Add Property button. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,7 +113,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
/>
|
||||
)
|
||||
// Status rendered with CSS text-transform: uppercase, DOM text is still "Active"
|
||||
expect(screen.getByTitle('Active')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('status-badge')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders properties from frontmatter', () => {
|
||||
@@ -124,9 +124,9 @@ describe('DynamicPropertiesPanel', () => {
|
||||
frontmatter={{ cadence: 'Weekly', owner: 'Luca' }}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('cadence')).toBeInTheDocument()
|
||||
expect(screen.getByText('Cadence')).toBeInTheDocument()
|
||||
expect(screen.getByText('Weekly')).toBeInTheDocument()
|
||||
expect(screen.getByText('owner')).toBeInTheDocument()
|
||||
expect(screen.getByText('Owner')).toBeInTheDocument()
|
||||
expect(screen.getByText('Luca')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -162,7 +162,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
frontmatter={{ notion_id: 'abc-123-def' }}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('notion_id')).toBeInTheDocument()
|
||||
expect(screen.getByText('Notion id')).toBeInTheDocument()
|
||||
expect(screen.getByText('abc-123-def')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
// aliases skipped (in SKIP_KEYS); 'Belongs to' skipped (has wikilinks)
|
||||
expect(screen.queryByText('aliases')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Belongs to')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('cadence')).toBeInTheDocument()
|
||||
expect(screen.getByText('Cadence')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows former relationship key with plain text value in Properties', () => {
|
||||
@@ -219,7 +219,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
const typeLabels = screen.getAllByText('Type')
|
||||
// Only the TypeRow label should exist, not a property row
|
||||
expect(typeLabels).toHaveLength(1)
|
||||
expect(screen.getByTitle('Active')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('status-badge')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders boolean property as toggle', () => {
|
||||
@@ -232,7 +232,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
/>
|
||||
)
|
||||
// Boolean should show as Yes/No toggle
|
||||
const toggleBtn = screen.getByText('\u2717 No')
|
||||
const toggleBtn = screen.getByText('No')
|
||||
fireEvent.click(toggleBtn)
|
||||
expect(onUpdateProperty).toHaveBeenCalledWith('published', true)
|
||||
})
|
||||
@@ -260,7 +260,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('+ Add property')).toBeInTheDocument()
|
||||
expect(screen.getByText('Add property')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens add property form when button clicked', () => {
|
||||
@@ -272,7 +272,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
expect(screen.getByPlaceholderText('Property name')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('Value')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('add-property-type-trigger')).toBeInTheDocument()
|
||||
@@ -287,7 +287,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
const keyInput = screen.getByPlaceholderText('Property name')
|
||||
const valueInput = screen.getByPlaceholderText('Value')
|
||||
fireEvent.change(keyInput, { target: { value: 'priority' } })
|
||||
@@ -412,7 +412,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
/>
|
||||
)
|
||||
// Click status pill to open dropdown
|
||||
fireEvent.click(screen.getByTitle('Active'))
|
||||
fireEvent.click(screen.getByTestId('status-badge'))
|
||||
// Should show dropdown with search input
|
||||
expect(screen.getByTestId('status-dropdown')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('status-search-input')).toBeInTheDocument()
|
||||
@@ -478,11 +478,11 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
const keyInput = screen.getByPlaceholderText('Property name')
|
||||
fireEvent.keyDown(keyInput, { key: 'Escape' })
|
||||
// Form should be hidden, button should reappear
|
||||
expect(screen.getByText('+ Add property')).toBeInTheDocument()
|
||||
expect(screen.getByText('Add property')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('adds property on Enter in form', () => {
|
||||
@@ -494,7 +494,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
const keyInput = screen.getByPlaceholderText('Property name')
|
||||
const valueInput = screen.getByPlaceholderText('Value')
|
||||
fireEvent.change(keyInput, { target: { value: 'key' } })
|
||||
@@ -512,7 +512,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
const keyInput = screen.getByPlaceholderText('Property name')
|
||||
const valueInput = screen.getByPlaceholderText('Value')
|
||||
fireEvent.change(keyInput, { target: { value: 'tags' } })
|
||||
@@ -530,9 +530,9 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
fireEvent.click(screen.getByTestId('add-property-cancel'))
|
||||
expect(screen.getByText('+ Add property')).toBeInTheDocument()
|
||||
expect(screen.getByText('Add property')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('editable vs read-only distinction', () => {
|
||||
@@ -775,7 +775,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
expect(screen.getByText('Mar 31, 2026')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders calendar icon in date trigger button', () => {
|
||||
it('renders date trigger button', () => {
|
||||
render(
|
||||
<DynamicPropertiesPanel
|
||||
entry={makeEntry()}
|
||||
@@ -786,7 +786,6 @@ describe('DynamicPropertiesPanel', () => {
|
||||
)
|
||||
const trigger = screen.getByTestId('date-display')
|
||||
expect(trigger.tagName).toBe('BUTTON')
|
||||
expect(trigger.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens calendar popover when date button clicked', () => {
|
||||
@@ -841,7 +840,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Active'))
|
||||
fireEvent.click(screen.getByTestId('status-badge'))
|
||||
expect(screen.getByTestId('status-dropdown')).toBeInTheDocument()
|
||||
fireEvent.keyDown(screen.getByTestId('status-search-input'), { key: 'Escape' })
|
||||
expect(screen.queryByTestId('status-dropdown')).not.toBeInTheDocument()
|
||||
@@ -857,7 +856,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Active'))
|
||||
fireEvent.click(screen.getByTestId('status-badge'))
|
||||
fireEvent.click(screen.getByTestId('status-dropdown-backdrop'))
|
||||
expect(screen.queryByTestId('status-dropdown')).not.toBeInTheDocument()
|
||||
expect(onUpdateProperty).not.toHaveBeenCalled()
|
||||
@@ -872,7 +871,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Active'))
|
||||
fireEvent.click(screen.getByTestId('status-badge'))
|
||||
const input = screen.getByTestId('status-search-input')
|
||||
fireEvent.change(input, { target: { value: 'Needs Review' } })
|
||||
fireEvent.keyDown(input, { key: 'Enter' })
|
||||
@@ -893,7 +892,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByTitle('Active'))
|
||||
fireEvent.click(screen.getByTestId('status-badge'))
|
||||
expect(screen.getByTestId('status-option-Reviewing')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('status-option-Shipped')).toBeInTheDocument()
|
||||
})
|
||||
@@ -910,7 +909,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
/>
|
||||
)
|
||||
expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument()
|
||||
expect(screen.getByText('\u2713 Yes')).toBeInTheDocument()
|
||||
expect(screen.getByText('Yes')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders boolean toggle for false values', () => {
|
||||
@@ -923,7 +922,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
/>
|
||||
)
|
||||
expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument()
|
||||
expect(screen.getByText('\u2717 No')).toBeInTheDocument()
|
||||
expect(screen.getByText('No')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -937,13 +936,13 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
/>
|
||||
)
|
||||
expect(screen.queryByText('trashed')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('trashed_at')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('archived')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('archived_at')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('icon')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Trashed')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Trashed at')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Archived')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Archived at')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Icon')).not.toBeInTheDocument()
|
||||
// Custom property still visible
|
||||
expect(screen.getByText('cadence')).toBeInTheDocument()
|
||||
expect(screen.getByText('Cadence')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('filters system properties case-insensitively', () => {
|
||||
@@ -958,7 +957,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
expect(screen.queryByText('Trashed')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Archived')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Icon')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('cadence')).toBeInTheDocument()
|
||||
expect(screen.getByText('Cadence')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not filter similar but non-matching property names', () => {
|
||||
@@ -971,7 +970,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('Is Trashed')).toBeInTheDocument()
|
||||
expect(screen.getByText('archive_date')).toBeInTheDocument()
|
||||
expect(screen.getByText('Archive date')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1055,7 +1054,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
/>
|
||||
)
|
||||
expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument()
|
||||
expect(screen.getByText('\u2713 Yes')).toBeInTheDocument()
|
||||
expect(screen.getByText('Yes')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders boolean toggle for string "false" when boolean mode overridden', () => {
|
||||
@@ -1069,7 +1068,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
/>
|
||||
)
|
||||
expect(screen.getByTestId('boolean-toggle')).toBeInTheDocument()
|
||||
expect(screen.getByText('\u2717 No')).toBeInTheDocument()
|
||||
expect(screen.getByText('No')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('toggles string boolean from false to true', () => {
|
||||
@@ -1124,7 +1123,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
// Switch type to boolean
|
||||
fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' })
|
||||
fireEvent.click(screen.getByRole('option', { name: /Boolean/ }))
|
||||
@@ -1141,7 +1140,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' })
|
||||
fireEvent.click(screen.getByRole('option', { name: /Boolean/ }))
|
||||
// Toggle from No to Yes
|
||||
@@ -1158,7 +1157,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
const keyInput = screen.getByPlaceholderText('Property name')
|
||||
fireEvent.change(keyInput, { target: { value: 'published' } })
|
||||
// Switch to boolean type
|
||||
@@ -1180,7 +1179,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' })
|
||||
fireEvent.click(screen.getByRole('option', { name: /Date/ }))
|
||||
expect(screen.getByTestId('add-property-date-trigger')).toBeInTheDocument()
|
||||
@@ -1196,7 +1195,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
fireEvent.pointerDown(screen.getByTestId('add-property-type-trigger'), { button: 0, pointerType: 'mouse' })
|
||||
fireEvent.click(screen.getByRole('option', { name: /Status/ }))
|
||||
expect(screen.getByTestId('add-property-status-trigger')).toBeInTheDocument()
|
||||
@@ -1211,7 +1210,7 @@ describe('DynamicPropertiesPanel', () => {
|
||||
onAddProperty={onAddProperty}
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByText('+ Add property'))
|
||||
fireEvent.click(screen.getByText('Add property'))
|
||||
// Default mode is text
|
||||
expect(screen.getByPlaceholderText('Value')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -9,6 +9,12 @@ import { AddPropertyForm } from './AddPropertyForm'
|
||||
import { countWords } from '../utils/wikilinks'
|
||||
import type { PropertyDisplayMode } from '../utils/propertyTypes'
|
||||
|
||||
function toSentenceCase(key: string): string {
|
||||
const spaced = key.replace(/[_-]/g, ' ')
|
||||
if (!spaced) return spaced
|
||||
return spaced.charAt(0).toUpperCase() + spaced.slice(1)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- utility co-located with component
|
||||
export function containsWikilinks(value: FrontmatterValue): boolean {
|
||||
if (typeof value === 'string') return /^\[\[.*\]\]$/.test(value)
|
||||
@@ -48,8 +54,8 @@ function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultS
|
||||
|
||||
return (
|
||||
<div className="group/prop grid 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="font-mono-overline flex min-w-0 items-center gap-1 text-muted-foreground">
|
||||
<span className="truncate">{propKey}</span>
|
||||
<span className="flex min-w-0 items-center gap-1 text-[12px] text-muted-foreground">
|
||||
<span className="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>
|
||||
)}
|
||||
@@ -65,7 +71,7 @@ function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultS
|
||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="grid min-w-0 grid-cols-2 items-center gap-2 px-1.5" data-testid="readonly-property">
|
||||
<span className="font-mono-overline min-w-0 truncate" style={{ color: 'var(--text-muted)' }}>{label}</span>
|
||||
<span className="min-w-0 truncate text-[12px] text-muted-foreground">{label}</span>
|
||||
<span className="min-w-0 truncate text-right text-[12px]" style={{ color: 'var(--text-muted)' }}>{value}</span>
|
||||
</div>
|
||||
)
|
||||
@@ -74,10 +80,12 @@ function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
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 }}
|
||||
className="mt-1 flex w-full cursor-pointer items-center gap-1 border-none bg-transparent px-1.5 text-[12px] text-muted-foreground opacity-50 transition-opacity hover:opacity-100 disabled:cursor-not-allowed"
|
||||
onClick={onClick} disabled={disabled}
|
||||
>+ Add property</button>
|
||||
>
|
||||
<span className="text-[12px] leading-none">+</span>
|
||||
Add property
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export function UrlValue({
|
||||
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-[12px] text-secondary-foreground transition-colors hover:text-primary hover:underline"
|
||||
className="min-w-0 cursor-pointer truncate rounded px-1 py-0.5 text-right text-[12px] text-[var(--accent-blue)] underline decoration-[var(--accent-blue)]/40 transition-colors hover:decoration-[var(--accent-blue)]"
|
||||
onClick={handleOpen}
|
||||
title={value}
|
||||
data-testid="url-link"
|
||||
|
||||
@@ -122,11 +122,11 @@ describe('Inspector', () => {
|
||||
expect(screen.getByText('Words')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders status as a colored pill', () => {
|
||||
it('renders status as a colored badge with dot indicator', () => {
|
||||
render(<Inspector {...defaultProps} entry={mockEntry} content={mockContent} />)
|
||||
const pill = screen.getByText('Active')
|
||||
// Status is rendered as an inline pill with CSS variable-based styles
|
||||
expect(pill).toHaveStyle({ borderRadius: '16px' })
|
||||
const badge = screen.getByTestId('status-badge')
|
||||
expect(badge).toHaveTextContent('Active')
|
||||
expect(badge.className).toContain('rounded')
|
||||
})
|
||||
|
||||
it('computes word count from content minus frontmatter and title', () => {
|
||||
@@ -137,7 +137,7 @@ describe('Inspector', () => {
|
||||
|
||||
it('shows "Add property" button as disabled placeholder', () => {
|
||||
render(<Inspector {...defaultProps} entry={mockEntry} content={mockContent} />)
|
||||
const btn = screen.getByText('+ Add property')
|
||||
const btn = screen.getByText('Add property')
|
||||
expect(btn).toBeDisabled()
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 { XIcon } from 'lucide-react'
|
||||
import { isValidCssColor } from '../utils/colorUtils'
|
||||
import {
|
||||
type PropertyDisplayMode,
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
DISPLAY_MODE_OPTIONS,
|
||||
DISPLAY_MODE_ICONS,
|
||||
} from '../utils/propertyTypes'
|
||||
import { StatusPill, StatusDropdown } from './StatusDropdown'
|
||||
import { StatusDropdown } from './StatusDropdown'
|
||||
import { getStatusStyle } from '../utils/statusStyles'
|
||||
import { TagsDropdown } from './TagsDropdown'
|
||||
import { getTagStyle } from '../utils/tagStyles'
|
||||
import { ColorEditableValue } from './ColorInput'
|
||||
@@ -37,14 +38,17 @@ function StatusValue({ propKey, value, isEditing, vaultStatuses, onSave, onStart
|
||||
onSave: (key: string, value: string) => void; onStartEdit: (key: string | null) => void
|
||||
}) {
|
||||
const statusStr = String(value)
|
||||
const style = getStatusStyle(statusStr)
|
||||
return (
|
||||
<span className="relative inline-flex min-w-0 items-center">
|
||||
<span
|
||||
className="cursor-pointer transition-opacity hover:opacity-80"
|
||||
className="inline-flex cursor-pointer items-center gap-1.5 rounded px-2 py-0.5 text-[12px] font-medium transition-opacity hover:opacity-80"
|
||||
style={{ backgroundColor: style.bg, color: style.color }}
|
||||
onClick={() => onStartEdit(propKey)}
|
||||
data-testid="status-badge"
|
||||
>
|
||||
<StatusPill status={statusStr} />
|
||||
<span className="inline-block size-1.5 shrink-0 rounded-full" style={{ backgroundColor: style.color }} />
|
||||
{statusStr}
|
||||
</span>
|
||||
{isEditing && (
|
||||
<StatusDropdown
|
||||
@@ -86,11 +90,8 @@ function TagsValue({ propKey, value, isEditing, vaultTags, onSave, onStartEdit }
|
||||
className="transition-[max-width] duration-150 group-hover/tag:[mask-image:linear-gradient(to_right,black_60%,transparent_100%)]"
|
||||
style={{
|
||||
color: style.color,
|
||||
fontFamily: "'IBM Plex Mono', monospace",
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0',
|
||||
textTransform: 'uppercase' as const,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap' as const,
|
||||
}}
|
||||
@@ -99,7 +100,7 @@ function TagsValue({ propKey, value, isEditing, vaultTags, onSave, onStartEdit }
|
||||
</span>
|
||||
<button
|
||||
className="ml-0.5 max-w-0 overflow-hidden border-none bg-transparent p-0 leading-none opacity-0 transition-all duration-150 group-hover/tag:max-w-[14px] group-hover/tag:opacity-100"
|
||||
style={{ color: style.color, fontSize: 10, flexShrink: 0 }}
|
||||
style={{ color: style.color, fontSize: 11, flexShrink: 0 }}
|
||||
onClick={() => handleRemove(tag)}
|
||||
title={`Remove ${tag}`}
|
||||
>
|
||||
@@ -128,13 +129,15 @@ function TagsValue({ propKey, value, isEditing, vaultTags, onSave, onStartEdit }
|
||||
|
||||
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}
|
||||
data-testid="boolean-toggle"
|
||||
>
|
||||
{value ? '\u2713 Yes' : '\u2717 No'}
|
||||
</button>
|
||||
<label className="inline-flex cursor-pointer items-center gap-1.5" data-testid="boolean-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value}
|
||||
onChange={onToggle}
|
||||
className="size-3.5 cursor-pointer accent-primary"
|
||||
/>
|
||||
<span className="text-[12px] text-secondary-foreground">{value ? 'Yes' : 'No'}</span>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -160,11 +163,10 @@ function DateValue({ value, onSave }: {
|
||||
<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"
|
||||
className={`inline-flex min-w-0 cursor-pointer items-center gap-1 border-none px-2 py-0.5 text-right text-[12px] font-medium transition-colors hover:opacity-80${formatted ? ' rounded bg-muted text-accent-foreground' : ' bg-transparent text-muted-foreground'}`}
|
||||
title={value}
|
||||
data-testid="date-display"
|
||||
>
|
||||
<CalendarIcon className="size-3 shrink-0 text-muted-foreground" />
|
||||
<span className={`min-w-0 truncate${!formatted ? ' text-muted-foreground' : ''}`}>{formatted || 'Pick a date\u2026'}</span>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
|
||||
Reference in New Issue
Block a user