From 711d49b3bbe50ead71afbbac2f00e76b35d5735f Mon Sep 17 00:00:00 2001 From: lucaronin Date: Thu, 16 Apr 2026 11:53:29 +0200 Subject: [PATCH] fix: align property panel rows --- src/components/DynamicPropertiesPanel.tsx | 156 ++++++++++++++------ src/components/Inspector.test.tsx | 108 +++++--------- src/components/TypeSelector.tsx | 28 +++- src/components/propertyPanelLayout.test.tsx | 31 ++++ src/components/propertyPanelLayout.ts | 8 + 5 files changed, 210 insertions(+), 121 deletions(-) diff --git a/src/components/DynamicPropertiesPanel.tsx b/src/components/DynamicPropertiesPanel.tsx index 40a4b523..950a0a07 100644 --- a/src/components/DynamicPropertiesPanel.tsx +++ b/src/components/DynamicPropertiesPanel.tsx @@ -1,4 +1,6 @@ +import { Plus } from '@phosphor-icons/react' import { useMemo, useCallback, useEffect, useState } from 'react' +import { Button } from '@/components/ui/button' import type { VaultEntry } from '../types' import type { FrontmatterValue } from './Inspector' import type { ParsedFrontmatter } from '../utils/frontmatter' @@ -9,7 +11,14 @@ 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_GRID_STYLE, PROPERTY_PANEL_ROW_STYLE } from './propertyPanelLayout' +import { + PROPERTY_PANEL_GRID_STYLE, + PROPERTY_PANEL_INTERACTIVE_ROW_CLASS_NAME, + PROPERTY_PANEL_LABEL_CLASS_NAME, + PROPERTY_PANEL_LABEL_ICON_SLOT_CLASS_NAME, + PROPERTY_PANEL_PLACEHOLDER_VALUE_CLASS_NAME, + PROPERTY_PANEL_ROW_STYLE, +} from './propertyPanelLayout' import { humanizePropertyKey } from '../utils/propertyLabels' import { canonicalSystemMetadataKey, hasSystemMetadataKey } from '../utils/systemMetadata' @@ -21,8 +30,6 @@ export function containsWikilinks(value: FrontmatterValue): boolean { } 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 @@ -45,7 +52,7 @@ function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultS return (
- + {humanizePropertyKey(propKey)} {onDelete && ( @@ -61,13 +68,27 @@ function PropertyRow({ propKey, value, editingKey, displayMode, autoMode, vaultS function AddPropertyButton({ onClick, disabled }: { onClick: () => void; disabled: boolean }) { return ( - + + + + Add property + +
- {showAddDialog - ? setShowAddDialog(false)} vaultStatuses={vaultStatuses} /> - : setShowAddDialog(true)} disabled={!onAddProperty} /> - } + {showAddDialog && ( + setShowAddDialog(false)} + vaultStatuses={vaultStatuses} + /> + )} ) } diff --git a/src/components/Inspector.test.tsx b/src/components/Inspector.test.tsx index 23e9b304..2a1eaf93 100644 --- a/src/components/Inspector.test.tsx +++ b/src/components/Inspector.test.tsx @@ -1,3 +1,4 @@ +import type { ComponentProps } from 'react' import { render, screen, fireEvent } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import { Inspector } from './Inspector' @@ -88,6 +89,20 @@ const defaultProps = { onNavigate: () => {}, } +type InspectorProps = ComponentProps + +function renderInspector(overrides: Partial = {}) { + return render() +} + +function renderSelectedInspector(overrides: Partial = {}) { + return renderInspector({ + entry: mockEntry, + content: mockContent, + ...overrides, + }) +} + describe('Inspector', () => { it('renders expanded state with "no note selected"', () => { render() @@ -133,7 +148,7 @@ describe('Inspector', () => { it('shows "Add property" button as disabled placeholder', () => { render() - const btn = screen.getByText('Add property') + const btn = screen.getByRole('button', { name: 'Add property' }) expect(btn).toBeDisabled() }) @@ -212,41 +227,22 @@ This is a test note with some words to count. }) it('hides backlinks section when no notes reference the current note', () => { - render( - - ) + renderSelectedInspector({ entries: [mockEntry] }) expect(screen.queryByText('Backlinks')).not.toBeInTheDocument() }) it('navigates when a backlink is clicked', () => { const onNavigate = vi.fn() - render( - - ) + renderSelectedInspector({ + entries: [mockEntry, referrerEntry], + onNavigate, + }) fireEvent.click(screen.getByText('Referrer Note')) expect(onNavigate).toHaveBeenCalledWith('Referrer Note') }) it('shows git history with commit hashes and messages', () => { - render( - - ) + renderSelectedInspector({ gitHistory: mockGitHistory }) expect(screen.getByText('History')).toBeInTheDocument() expect(screen.getByText('a1b2c3d')).toBeInTheDocument() expect(screen.getByText('e4f5g6h')).toBeInTheDocument() @@ -255,15 +251,10 @@ This is a test note with some words to count. it('renders commit entries as clickable buttons', () => { const onViewCommitDiff = vi.fn() - render( - - ) + renderSelectedInspector({ + gitHistory: mockGitHistory, + onViewCommitDiff, + }) const hashBtn = screen.getByText('a1b2c3d') const button = hashBtn.closest('button')! expect(button.tagName).toBe('BUTTON') @@ -272,25 +263,12 @@ This is a test note with some words to count. }) it('hides history section when no commits', () => { - render( - - ) + renderSelectedInspector({ gitHistory: [] }) expect(screen.queryByText('History')).not.toBeInTheDocument() }) it('shows separate Info section with read-only metadata', () => { - render( - - ) + renderSelectedInspector() expect(screen.getByText('Info')).toBeInTheDocument() expect(screen.getByText('Modified')).toBeInTheDocument() expect(screen.getByText('Created')).toBeInTheDocument() @@ -468,31 +446,23 @@ Status: Active }) it('hides referenced-by section when no entries reference the current note', () => { - render( - - ) + renderInspector({ + entry: targetEntry, + content: targetContent, + entries: [targetEntry], + }) expect(screen.queryByText('No references')).not.toBeInTheDocument() expect(screen.queryByText('Referenced by')).not.toBeInTheDocument() }) it('navigates when clicking a referenced-by entry', () => { const onNavigate = vi.fn() - render( - - ) + renderInspector({ + entry: targetEntry, + content: targetContent, + entries: [targetEntry, essayEntry], + onNavigate, + }) fireEvent.click(screen.getByText('On Writing Well')) expect(onNavigate).toHaveBeenCalledWith('On Writing Well') }) diff --git a/src/components/TypeSelector.tsx b/src/components/TypeSelector.tsx index 1db3db55..bbebe631 100644 --- a/src/components/TypeSelector.tsx +++ b/src/components/TypeSelector.tsx @@ -8,7 +8,11 @@ import { cn } from '@/lib/utils' import { getTypeColor, getTypeLightColor } from '../utils/typeColors' import { getTypeIcon } from './NoteItem' import { PROPERTY_CHIP_STYLE } from './propertyChipStyles' -import { PROPERTY_PANEL_ROW_STYLE } from './propertyPanelLayout' +import { + PROPERTY_PANEL_LABEL_CLASS_NAME, + PROPERTY_PANEL_LABEL_ICON_SLOT_CLASS_NAME, + PROPERTY_PANEL_ROW_STYLE, +} from './propertyPanelLayout' const TYPE_NONE = '__none__' const MIN_POPOVER_WIDTH = 220 @@ -94,9 +98,14 @@ function TypeSelectorValue({ function TypeRowLabel() { return ( - - - Type + + + + + Type ) } @@ -104,7 +113,10 @@ function TypeRowLabel() { function ReadOnlyType({ isA, customColorKey, onNavigate }: { isA?: string | null; customColorKey?: string | null; onNavigate?: (target: string) => void }) { if (!isA) return null return ( -
+
{onNavigate ? ( @@ -261,7 +273,11 @@ function EditableTypeSelector({ isA, customColorKey, availableTypes, typeColorKe } return ( -
+
diff --git a/src/components/propertyPanelLayout.test.tsx b/src/components/propertyPanelLayout.test.tsx index b2f12d85..b1445a3e 100644 --- a/src/components/propertyPanelLayout.test.tsx +++ b/src/components/propertyPanelLayout.test.tsx @@ -76,6 +76,37 @@ describe('property panel shared grid layout', () => { }) }) + it('keeps the add-property row on the shared grid', () => { + render( + + ) + + const row = screen.getByTestId('add-property-row') + expect(row.style.gridTemplateColumns).toBe('subgrid') + expect(row.style.gridColumn).toBe('1 / -1') + }) + + it('uses the same fixed icon slot size for type, suggested, and add-property rows', () => { + render( + + ) + + expect(screen.getByTestId('type-row-icon-slot')).toHaveClass('size-5') + screen.getAllByTestId('suggested-property-icon-slot').forEach((slot) => { + expect(slot).toHaveClass('size-5') + }) + expect(screen.getByTestId('add-property-icon-slot')).toHaveClass('size-5') + }) + it('renders plain text values flush with the shared value column', () => { render(