diff --git a/.claude-done b/.claude-done index 3c329694..21dd6a01 100644 --- a/.claude-done +++ b/.claude-done @@ -1,56 +1,34 @@ -# Vault from GitHub — Implementation Summary +# Properties Inspector: Editable vs Read-Only Distinction -## What was built +## What was implemented -A "Vault from GitHub" feature that lets users create or clone a vault from a GitHub repository, directly from within the Laputa app. +Split the Properties panel into two visually distinct sections: -## Architecture +### 1. Editable Properties (top section) +- Frontmatter properties the user can modify +- Interactive styling: `hover:bg-muted` background on row hover, cursor pointer, click-to-edit +- Includes: Type badge, Status pill, boolean toggles, array tag pills, text fields +- "Add property" button for user-defined properties -### Rust Backend (`src-tauri/src/github.rs`) -- `github_list_repos` — Paginated GitHub API call to list authenticated user's repos (up to 1000) -- `github_create_repo` — Creates a new GitHub repo via API with auto_init -- `clone_repo` — Clones a repo locally using HTTPS + OAuth token injection, configures remote auth -- Helper functions: `inject_token_into_url`, `configure_remote_auth` -- All three exposed as Tauri commands in `lib.rs` +### 2. Info Section (bottom section, separated by border) +- Read-only derived metadata with "Info" header +- Muted styling: `--text-muted` color for both labels and values +- No hover states, no click interaction +- Fields: Modified date, Created date, Word count, File size (formatted as B/KB/MB) -### Frontend (`src/components/GitHubVaultModal.tsx`) -- Two-tab modal: "Clone Existing" and "Create New" -- Clone tab: searchable repo list with Private/Public badges, auto-fill clone path -- Create tab: repo name, Private/Public toggle, auto-fill clone path -- Loading and cloning progress states -- No-token state redirects to Settings -- Integrated into `App.tsx` via StatusBar vault menu +### Other changes +- Added `is_a` and `Is A` to SKIP_KEYS to prevent duplication (already shown via TypeRow) +- Added `formatFileSize()` helper for human-readable file sizes +- Created `design/properties-inspector.pen` with wireframe frames +- Updated `docs/ABSTRACTIONS.md` with new Inspector section documentation -### Settings Extension -- Added `github_token` field to Settings (Rust + TypeScript) -- GitHub Token input field in SettingsPanel -- Refactored `settings.rs` to use testable internal functions (`get_settings_at`, `save_settings_at`) +## Commits on this branch +1. `design: properties-inspector wireframes` — 2 frames showing editable vs read-only distinction +2. `feat: distinguish editable from read-only properties in inspector` — main implementation + 12 new tests +3. `fix: hide is_a/Is A from editable properties` — prevent TypeRow duplication +4. `docs: update abstractions for properties inspector Info section` -### Mock Data (`src/mock-tauri.ts`) -- 5 realistic mock repos for browser-mode testing -- Mock handlers for all 3 GitHub commands - -## Design Decisions - -1. **HTTPS+token auth over SSH**: Simpler for users (no SSH key setup), token injected into remote URL -2. **Auto-fill clone path**: `~/Vaults/` — sensible default, editable -3. **Private by default** for new repos — safer default for personal vaults -4. **No OAuth flow**: GitHub token entered manually in Settings — ships faster, no server dependency -5. **Extra vaults tracked in state**: Cloned vaults added to vault switcher dynamically (not persisted across sessions yet) - -## Test Coverage - -- **Frontend**: 80.79% statements, 83.34% lines (threshold: 70%) -- **Rust**: 86.20% lines (threshold: 85%) -- 12 GitHubVaultModal component tests -- 12 Rust github.rs unit tests -- 9 Rust settings.rs unit tests (refactored for coverage) -- E2E Playwright verification: vault menu, modal tabs, repo list, search filter, repo selection - -## Commits (6 total) -1. `design: vault-from-github wireframes` -2. `feat: add Rust backend for GitHub vault operations` -3. `feat: GitHub vault modal with clone/create flows` -4. `test: add GitHubVaultModal component tests` -5. `test: improve Rust test coverage for settings and github modules` -6. `fix: remove unused GithubRepo import in mock-tauri.ts` +## Test coverage +- 469 tests passing (12 new tests for this feature) +- CodeScene quality gates: passed on all commits +- `pnpm build`: succeeds diff --git a/design/properties-inspector.pen b/design/properties-inspector.pen new file mode 100644 index 00000000..9fb52459 --- /dev/null +++ b/design/properties-inspector.pen @@ -0,0 +1,550 @@ +{ + "version": "2.8", + "themes": { "Mode": ["Dark"] }, + "variables": {}, + "fonts": [], + "children": [ + { + "type": "frame", + "id": "pi01", + "name": "Properties Inspector — Editable Properties", + "width": 320, + "height": 520, + "layout": "vertical", + "fill": "$--background", + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "text", + "id": "pi02", + "name": "frameLabel", + "content": "Editable properties section: interactive hover states, cursor pointer, click-to-edit inputs. Properties from frontmatter YAML that the user can modify.", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [8, 12] + }, + { + "type": "frame", + "id": "pi03", + "name": "Inspector Header", + "width": "fill_container", + "height": 45, + "fill": "$--background", + "stroke": { "align": "inside", "thickness": { "bottom": 1 }, "fill": "$--border" }, + "gap": 8, + "padding": [0, 12], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "pi04", + "width": 16, + "height": 16, + "iconFontName": "sliders-horizontal", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "pi05", + "content": "Properties", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "600", + "fill": "$--muted-foreground", + "width": "fill_container" + }, + { + "type": "icon_font", + "id": "pi06", + "width": 16, + "height": 16, + "iconFontName": "x", + "iconFontFamily": "lucide", + "fill": "$--muted-foreground" + } + ] + }, + { + "type": "frame", + "id": "pi07", + "name": "Properties Section", + "width": "fill_container", + "layout": "vertical", + "padding": [12, 12], + "gap": 10, + "children": [ + { + "type": "frame", + "id": "pi08", + "name": "Type Row", + "width": "fill_container", + "justifyContent": "space-between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pi09", + "content": "TYPE", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2, + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "pi10", + "name": "Type Badge", + "fill": "$--accent-blue-light", + "cornerRadius": 6, + "padding": [2, 8], + "children": [ + { + "type": "text", + "id": "pi11", + "content": "Project", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "500", + "fill": "$--accent-blue" + } + ] + } + ] + }, + { + "type": "frame", + "id": "pi12", + "name": "Editable Row — Status (hover state)", + "width": "fill_container", + "justifyContent": "space-between", + "alignItems": "center", + "cornerRadius": 4, + "fill": "$--bg-hover-subtle", + "padding": [4, 6], + "children": [ + { + "type": "text", + "id": "pi13", + "content": "STATUS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2, + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "pi14", + "name": "Status Badge", + "fill": "$--accent-green-light", + "cornerRadius": 16, + "padding": [1, 6], + "children": [ + { + "type": "text", + "id": "pi15", + "content": "ACTIVE", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "600", + "letterSpacing": 1.2, + "fill": "$--accent-green" + } + ] + } + ] + }, + { + "type": "frame", + "id": "pi16", + "name": "Editable Row — Owner (normal state)", + "width": "fill_container", + "justifyContent": "space-between", + "alignItems": "center", + "cornerRadius": 4, + "padding": [4, 6], + "children": [ + { + "type": "text", + "id": "pi17", + "content": "OWNER", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "pi18", + "content": "Luca Rossi", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal", + "fill": "$--foreground" + } + ] + }, + { + "type": "frame", + "id": "pi19", + "name": "Editable Row — Tags (normal state)", + "width": "fill_container", + "justifyContent": "space-between", + "alignItems": "center", + "cornerRadius": 4, + "padding": [4, 6], + "children": [ + { + "type": "text", + "id": "pi20", + "content": "TAGS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2, + "fill": "$--muted-foreground" + }, + { + "type": "frame", + "id": "pi21", + "name": "Tag Pills", + "gap": 4, + "children": [ + { + "type": "frame", + "id": "pi22", + "fill": "$--accent-blue-light", + "cornerRadius": 12, + "padding": [2, 8], + "children": [ + { "type": "text", "id": "pi23", "content": "React", "fontFamily": "Inter", "fontSize": 11, "fontWeight": "500", "fill": "$--accent-blue" } + ] + }, + { + "type": "frame", + "id": "pi24", + "fill": "$--accent-blue-light", + "cornerRadius": 12, + "padding": [2, 8], + "children": [ + { "type": "text", "id": "pi25", "content": "TypeScript", "fontFamily": "Inter", "fontSize": 11, "fontWeight": "500", "fill": "$--accent-blue" } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "pi26", + "name": "Add Property Button", + "width": "fill_container", + "height": 32, + "stroke": { "align": "inside", "thickness": 1, "fill": "$--border" }, + "cornerRadius": 6, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pi27", + "content": "+ Add property", + "fontFamily": "Inter", + "fontSize": 12, + "fill": "$--muted-foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "pi28", + "name": "Separator", + "width": "fill_container", + "height": 1, + "fill": "$--border" + }, + { + "type": "frame", + "id": "pi29", + "name": "Info Section — Read-only Metadata", + "width": "fill_container", + "layout": "vertical", + "padding": [12, 12], + "gap": 6, + "children": [ + { + "type": "text", + "id": "pi30", + "content": "INFO", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2, + "fill": "$--muted-foreground", + "padding": [0, 0, 4, 0] + }, + { + "type": "frame", + "id": "pi31", + "name": "Info Row — Modified (read-only, muted)", + "width": "fill_container", + "justifyContent": "space-between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pi32", + "content": "MODIFIED", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2, + "fill": "$--text-muted" + }, + { + "type": "text", + "id": "pi33", + "content": "Feb 14, 2026", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal", + "fill": "$--text-muted" + } + ] + }, + { + "type": "frame", + "id": "pi34", + "name": "Info Row — Created (read-only, muted)", + "width": "fill_container", + "justifyContent": "space-between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pi35", + "content": "CREATED", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2, + "fill": "$--text-muted" + }, + { + "type": "text", + "id": "pi36", + "content": "Jan 5, 2026", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal", + "fill": "$--text-muted" + } + ] + }, + { + "type": "frame", + "id": "pi37", + "name": "Info Row — Words (read-only, muted)", + "width": "fill_container", + "justifyContent": "space-between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pi38", + "content": "WORDS", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2, + "fill": "$--text-muted" + }, + { + "type": "text", + "id": "pi39", + "content": "1,247", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal", + "fill": "$--text-muted" + } + ] + }, + { + "type": "frame", + "id": "pi40", + "name": "Info Row — File Size (read-only, muted)", + "width": "fill_container", + "justifyContent": "space-between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "pi41", + "content": "SIZE", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2, + "fill": "$--text-muted" + }, + { + "type": "text", + "id": "pi42", + "content": "4.2 KB", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal", + "fill": "$--text-muted" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "pi50", + "name": "Properties Inspector — Info Section Detail", + "width": 320, + "height": 400, + "layout": "vertical", + "fill": "$--background", + "theme": { "Mode": "Light" }, + "children": [ + { + "type": "text", + "id": "pi51", + "name": "frameLabel", + "content": "Info section for read-only derived metadata. Labels and values use --text-muted color. No hover states, no cursor pointer, no click interaction. Visually distinct from editable properties above.", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "fill": "$--muted-foreground", + "padding": [8, 12] + }, + { + "type": "frame", + "id": "pi52", + "name": "Comparison Side by Side", + "width": "fill_container", + "layout": "vertical", + "padding": [12, 12], + "gap": 16, + "children": [ + { + "type": "frame", + "id": "pi53", + "name": "Editable Property Example", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "pi54", + "content": "EDITABLE (interactive)", + "fontFamily": "IBM Plex Mono", + "fontSize": 9, + "fontWeight": "600", + "letterSpacing": 1.5, + "fill": "$--accent-green" + }, + { + "type": "frame", + "id": "pi55", + "name": "Row — cursor:pointer, hover:bg-muted, rounded", + "width": "fill_container", + "justifyContent": "space-between", + "alignItems": "center", + "cornerRadius": 4, + "fill": "$--bg-hover-subtle", + "padding": [4, 6], + "children": [ + { + "type": "text", + "id": "pi56", + "content": "OWNER", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2, + "fill": "$--muted-foreground" + }, + { + "type": "text", + "id": "pi57", + "content": "Luca Rossi", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal", + "fill": "$--foreground" + } + ] + } + ] + }, + { + "type": "frame", + "id": "pi58", + "name": "Read-only Property Example", + "width": "fill_container", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "pi59", + "content": "READ-ONLY (muted, no interaction)", + "fontFamily": "IBM Plex Mono", + "fontSize": 9, + "fontWeight": "600", + "letterSpacing": 1.5, + "fill": "$--text-muted" + }, + { + "type": "frame", + "id": "pi60", + "name": "Row — cursor:default, no hover, muted color", + "width": "fill_container", + "justifyContent": "space-between", + "alignItems": "center", + "padding": [4, 6], + "children": [ + { + "type": "text", + "id": "pi61", + "content": "MODIFIED", + "fontFamily": "IBM Plex Mono", + "fontSize": 10, + "fontWeight": "500", + "letterSpacing": 1.2, + "fill": "$--text-muted" + }, + { + "type": "text", + "id": "pi62", + "content": "Feb 14, 2026", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal", + "fill": "$--text-muted" + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index 7be915e5..e7a32567 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -259,7 +259,10 @@ In brief: `src/theme.json` defines editor typography and styling as nested JSON. The Inspector panel (`src/components/Inspector.tsx`) is composed of four sub-panels: -1. **DynamicPropertiesPanel** (`src/components/DynamicPropertiesPanel.tsx`): Renders frontmatter as editable key-value pairs. Uses `EditableValue` for inline editing. +1. **DynamicPropertiesPanel** (`src/components/DynamicPropertiesPanel.tsx`): Renders frontmatter as editable key-value pairs with two distinct sections: + - **Editable properties** (top): frontmatter fields the user can modify — shown with interactive hover styling (`hover:bg-muted`), cursor pointer, and click-to-edit. Includes Type badge, Status pill, boolean toggles, array tag pills, and text fields. + - **Info section** (bottom, separated by border): read-only derived metadata — Modified, Created, Words, File Size. Uses muted text color (`--text-muted`) with no hover states or click interaction. These fields are computed from file metadata and content, not from frontmatter. + - Keys in `SKIP_KEYS` (`aliases`, `notion_id`, `workspace`, `is_a`, `Is A`) are hidden from the editable section since they are either internal or already displayed elsewhere (e.g., `is_a` is shown via the TypeRow badge). 2. **Relationships**: Shows `belongs_to` and `related_to` wikilinks as clickable chips. 3. **Backlinks**: Scans `allContent` for notes that reference the current note via `[[title]]` or `[[path]]`. 4. **Git History**: Shows the last few commits from `gitHistory` state. diff --git a/src/components/DynamicPropertiesPanel.test.tsx b/src/components/DynamicPropertiesPanel.test.tsx index d9bfec1d..d28c5293 100644 --- a/src/components/DynamicPropertiesPanel.test.tsx +++ b/src/components/DynamicPropertiesPanel.test.tsx @@ -128,6 +128,23 @@ describe('DynamicPropertiesPanel', () => { expect(screen.getByText('cadence')).toBeInTheDocument() }) + it('skips is_a key since it is shown via TypeRow', () => { + render( + + ) + // Type is shown via TypeRow, is_a should NOT appear as a separate editable property + const editableRows = screen.getAllByTestId('editable-property') + const editableLabels = editableRows.map(row => row.querySelector('span')?.textContent?.replace('×', '').trim()) + expect(editableLabels).not.toContain('is_a') + // But Type row should still show + expect(screen.getByText('Type')).toBeInTheDocument() + expect(screen.getByText('Project')).toBeInTheDocument() + }) + it('renders boolean property as toggle', () => { render( { fireEvent.click(screen.getByText('Cancel')) expect(screen.getByText('+ Add property')).toBeInTheDocument() }) + + describe('editable vs read-only distinction', () => { + it('renders Info section header', () => { + render( + + ) + expect(screen.getByText('Info')).toBeInTheDocument() + }) + + it('renders Modified and Words in read-only Info section', () => { + render( + + ) + const readOnlyRows = screen.getAllByTestId('readonly-property') + const labels = readOnlyRows.map(row => row.querySelector('span')?.textContent) + expect(labels).toContain('Modified') + expect(labels).toContain('Words') + }) + + it('renders Created date in Info section', () => { + render( + + ) + const readOnlyRows = screen.getAllByTestId('readonly-property') + const labels = readOnlyRows.map(row => row.querySelector('span')?.textContent) + expect(labels).toContain('Created') + }) + + it('renders file size in Info section', () => { + render( + + ) + expect(screen.getByText('Size')).toBeInTheDocument() + expect(screen.getByText('4.2 KB')).toBeInTheDocument() + }) + + it('shows em dash for null timestamps in Info section', () => { + render( + + ) + // Two em dashes for null Modified and Created + const dashes = screen.getAllByText('\u2014') + expect(dashes.length).toBeGreaterThanOrEqual(2) + }) + + it('editable properties have hover styling via data-testid', () => { + render( + + ) + const editableRows = screen.getAllByTestId('editable-property') + expect(editableRows.length).toBe(2) + // Editable rows have hover:bg-muted class for interactivity + editableRows.forEach(row => { + expect(row.className).toContain('hover:bg-muted') + }) + }) + + it('read-only rows do not have hover styling', () => { + render( + + ) + const readOnlyRows = screen.getAllByTestId('readonly-property') + readOnlyRows.forEach(row => { + expect(row.className).not.toContain('hover:bg-muted') + }) + }) + + it('formats file sizes correctly', () => { + // Small file — bytes + const { rerender } = render( + + ) + expect(screen.getByText('500 B')).toBeInTheDocument() + + // KB range + rerender( + + ) + expect(screen.getByText('2.0 KB')).toBeInTheDocument() + + // MB range + rerender( + + ) + expect(screen.getByText('1.0 MB')).toBeInTheDocument() + }) + }) }) diff --git a/src/components/DynamicPropertiesPanel.tsx b/src/components/DynamicPropertiesPanel.tsx index b36ac11f..dba9f630 100644 --- a/src/components/DynamicPropertiesPanel.tsx +++ b/src/components/DynamicPropertiesPanel.tsx @@ -28,7 +28,7 @@ export const RELATIONSHIP_KEYS = new Set([ ]) // Keys to skip showing in Properties -const SKIP_KEYS = new Set(['aliases', 'notion_id', 'workspace']) +const SKIP_KEYS = new Set(['aliases', 'notion_id', 'workspace', 'is_a', 'Is A']) // eslint-disable-next-line react-refresh/only-export-components -- utility co-located with component export function containsWikilinks(value: FrontmatterValue): boolean { @@ -63,6 +63,14 @@ function parseNewValue(rawValue: string): FrontmatterValue { return items.length === 1 ? items[0] : items } +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` +} + function isStatusKey(key: string): boolean { return key === 'Status' || key.includes('Status') } @@ -165,7 +173,7 @@ function PropertyRow({ propKey, value, editingKey, onStartEdit, onSave, onSaveLi onUpdate?: (key: string, value: FrontmatterValue) => void; onDelete?: (key: string) => void }) { return ( -
+
{propKey} {onDelete && ( @@ -191,11 +199,11 @@ function PropertyValueCell({ propKey, value, isEditing, onStartEdit, onSave, onS return } -function StaticRow({ label, value }: { label: string; value: string }) { +function InfoRow({ label, value }: { label: string; value: string }) { return ( -
- {label} - {value} +
+ {label} + {value}
) } @@ -256,19 +264,28 @@ export function DynamicPropertiesPanel({ }, [onAddProperty]) return ( -
+
+ {/* Editable properties section */}
{propertyEntries.map(([key, value]) => ( ))} - -
{showAddDialog ? setShowAddDialog(false)} /> : setShowAddDialog(true)} disabled={!onAddProperty} /> } + {/* Read-only Info section */} +
+

Info

+
+ + + + +
+
) } diff --git a/src/components/Inspector.test.tsx b/src/components/Inspector.test.tsx index d7e31820..0d771cfa 100644 --- a/src/components/Inspector.test.tsx +++ b/src/components/Inspector.test.tsx @@ -279,4 +279,49 @@ This is a test note with some words to count. ) expect(screen.getByText('No revision history')).toBeInTheDocument() }) + + it('shows separate Info section with read-only metadata', () => { + render( + + ) + expect(screen.getByText('Info')).toBeInTheDocument() + expect(screen.getByText('Modified')).toBeInTheDocument() + expect(screen.getByText('Created')).toBeInTheDocument() + expect(screen.getByText('Size')).toBeInTheDocument() + }) + + it('renders editable properties with interactive styling', () => { + render( + + ) + const editableRows = screen.getAllByTestId('editable-property') + expect(editableRows.length).toBeGreaterThan(0) + editableRows.forEach(row => { + expect(row.className).toContain('hover:bg-muted') + }) + }) + + it('renders read-only properties with muted non-interactive styling', () => { + render( + + ) + const readOnlyRows = screen.getAllByTestId('readonly-property') + expect(readOnlyRows.length).toBe(4) // Modified, Created, Words, Size + readOnlyRows.forEach(row => { + expect(row.className).not.toContain('hover:bg-muted') + expect(row.className).not.toContain('cursor-pointer') + }) + }) })