Skip to content

Commit 0658200

Browse files
committed
Indexing: Rich HTML size tooltips
- Show colored byte triads and human-readable sizes in tooltips (matching the size column colors) - Line breaks between size lines, file/folder counts, and stale warnings - "No files, no folders" instead of "0 files, 0 folders" - Selection summary tooltip: "Selected / Of total" with separate "On disk" section when sizes differ - Deduplicate `formatBytesHtml` by reusing `formatSizeTriads` - Document tooltip capabilities in `ui/CLAUDE.md`
1 parent 9c450cd commit 0658200

5 files changed

Lines changed: 209 additions & 82 deletions

File tree

apps/desktop/src/lib/file-explorer/selection/SelectionInfo.svelte

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
} from './selection-info-utils'
1313
import { measureDateColumnWidth } from '../views/full-list-utils'
1414
import { formatFileSize, formatDateTime, getSizeDisplayMode } from '$lib/settings/reactive-settings.svelte'
15-
import { getDisplaySize, buildFileSizeTooltip } from '../views/full-list-utils'
15+
import { getDisplaySize, buildFileSizeTooltip, buildSelectionSizeTooltip } from '../views/full-list-utils'
1616
import { isScanning } from '$lib/indexing/index-state.svelte'
1717
import { tooltip } from '$lib/tooltip/tooltip'
1818
import { Hourglass } from '@lucide/svelte'
@@ -232,12 +232,15 @@
232232
const totalSizeTriads = $derived(formatSizeTriads(totalSize))
233233
234234
// Tooltip shows human-readable sizes; includes both content and on-disk when they differ
235-
const selectionSizeTooltip = $derived.by(() => {
236-
if (totalLogicalSize <= 0) return undefined
237-
const selPart = buildFileSizeTooltip(selectedLogicalSize, selectedPhysicalSize, formatFileSize)
238-
const totPart = buildFileSizeTooltip(totalLogicalSize, totalPhysicalSize, formatFileSize)
239-
return `${selPart} of ${totPart}`
240-
})
235+
const selectionSizeTooltip = $derived(
236+
buildSelectionSizeTooltip(
237+
selectedLogicalSize,
238+
selectedPhysicalSize,
239+
totalLogicalSize,
240+
totalPhysicalSize,
241+
formatFileSize,
242+
),
243+
)
241244
</script>
242245

243246
<div class="selection-info" bind:this={containerElement}>

apps/desktop/src/lib/file-explorer/views/BriefList.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@
433433
})
434434
435435
/** Build tooltip for a directory entry showing recursive size info. */
436-
function buildDirTooltip(file: FileEntry): string | undefined {
436+
function buildDirTooltip(file: FileEntry): string | { html: string } | undefined {
437437
if (!file.isDirectory) return undefined
438438
return (
439439
buildDirSizeTooltip(

apps/desktop/src/lib/file-explorer/views/dir-size-display.test.ts

Lines changed: 98 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,19 @@ import {
1414

1515
// Mock settings store (required by full-list-utils)
1616
vi.mock('$lib/settings/settings-store', () => ({
17-
getSetting: vi.fn().mockReturnValue(20),
17+
getSetting: vi.fn(() => 20),
1818
}))
1919

20+
// Test helpers
21+
const formatSize = (bytes: number): string => `${String(bytes)} bytes`
22+
const formatNum = (n: number): string => String(n)
23+
const plural = (count: number, singular: string, pluralForm: string): string => (count === 1 ? singular : pluralForm)
24+
25+
/** Extracts the html from a tooltip result, or returns the string as-is */
26+
function tooltipHtml(result: string | { html: string }): string {
27+
return typeof result === 'object' ? result.html : result
28+
}
29+
2030
// ============================================================================
2131
// getDirSizeDisplayState
2232
// ============================================================================
@@ -30,36 +40,32 @@ describe('getDirSizeDisplayState', () => {
3040
expect(getDirSizeDisplayState(undefined, true)).toBe('scanning')
3141
})
3242

33-
it('returns "size" when recursive size is available and not scanning', () => {
34-
expect(getDirSizeDisplayState(1234567, false)).toBe('size')
43+
it('returns "size" when data available and not scanning', () => {
44+
expect(getDirSizeDisplayState(1234, false)).toBe('size')
3545
})
3646

37-
it('returns "size-stale" when recursive size is available and scanning is active', () => {
38-
expect(getDirSizeDisplayState(1234567, true)).toBe('size-stale')
47+
it('returns "size-stale" when data available and scanning', () => {
48+
expect(getDirSizeDisplayState(1234, true)).toBe('size-stale')
3949
})
4050

41-
it('returns "size" for zero-size directory when not scanning', () => {
51+
it('returns "size" for zero size when not scanning', () => {
4252
expect(getDirSizeDisplayState(0, false)).toBe('size')
4353
})
4454

45-
it('returns "size-stale" for zero-size directory when scanning', () => {
55+
it('returns "size-stale" for zero size when scanning', () => {
4656
expect(getDirSizeDisplayState(0, true)).toBe('size-stale')
4757
})
4858

49-
it('returns "size" for very large sizes', () => {
50-
expect(getDirSizeDisplayState(1_000_000_000_000, false)).toBe('size')
59+
it('handles undefined recursiveSize correctly regardless of scanning state', () => {
60+
expect(getDirSizeDisplayState(undefined, false)).toBe('dir')
61+
expect(getDirSizeDisplayState(undefined, true)).toBe('scanning')
5162
})
5263
})
5364

5465
// ============================================================================
5566
// buildDirSizeTooltip
5667
// ============================================================================
5768

58-
// Simple formatters for testing (mirrors the real ones but deterministic)
59-
const formatSize = (bytes: number): string => `${String(bytes)} bytes`
60-
const formatNum = (n: number): string => String(n)
61-
const plural = (count: number, singular: string, pluralForm: string): string => (count === 1 ? singular : pluralForm)
62-
6369
describe('buildDirSizeTooltip', () => {
6470
it('returns empty string when no data and not scanning', () => {
6571
expect(buildDirSizeTooltip(undefined, undefined, 0, 0, false, formatSize, formatNum, plural)).toBe('')
@@ -69,70 +75,90 @@ describe('buildDirSizeTooltip', () => {
6975
expect(buildDirSizeTooltip(undefined, undefined, 0, 0, true, formatSize, formatNum, plural)).toBe('Scanning...')
7076
})
7177

72-
it('returns formatted size info when recursive size is available', () => {
73-
const tooltip = buildDirSizeTooltip(1234, undefined, 10, 3, false, formatSize, formatNum, plural)
74-
expect(tooltip).toBe('1234 bytes \u00B7 10 files \u00B7 3 folders')
78+
it('returns HTML tooltip with size and counts when recursive size is available', () => {
79+
const result = buildDirSizeTooltip(1234, undefined, 10, 3, false, formatSize, formatNum, plural)
80+
const html = tooltipHtml(result)
81+
expect(html).toContain('1234 bytes')
82+
expect(html).toContain('10 files')
83+
expect(html).toContain('3 folders')
84+
expect(html).toContain('<br>')
7585
})
7686

7787
it('appends stale warning when scanning with existing data', () => {
78-
const tooltip = buildDirSizeTooltip(1234, undefined, 10, 3, true, formatSize, formatNum, plural)
79-
expect(tooltip).toContain('1234 bytes')
80-
expect(tooltip).toContain('10 files')
81-
expect(tooltip).toContain('3 folders')
82-
expect(tooltip).toContain('Updating index — size may change.')
88+
const result = buildDirSizeTooltip(1234, undefined, 10, 3, true, formatSize, formatNum, plural)
89+
const html = tooltipHtml(result)
90+
expect(html).toContain('1234 bytes')
91+
expect(html).toContain('10 files')
92+
expect(html).toContain('Updating index')
8393
})
8494

8595
it('uses singular form for 1 file', () => {
86-
const tooltip = buildDirSizeTooltip(100, undefined, 1, 5, false, formatSize, formatNum, plural)
87-
expect(tooltip).toContain('1 file')
88-
expect(tooltip).not.toContain('1 files')
96+
const html = tooltipHtml(buildDirSizeTooltip(100, undefined, 1, 5, false, formatSize, formatNum, plural))
97+
expect(html).toContain('1 file')
98+
expect(html).not.toContain('1 files')
8999
})
90100

91101
it('uses singular form for 1 folder', () => {
92-
const tooltip = buildDirSizeTooltip(100, undefined, 5, 1, false, formatSize, formatNum, plural)
93-
expect(tooltip).toContain('1 folder')
94-
expect(tooltip).not.toContain('1 folders')
102+
const html = tooltipHtml(buildDirSizeTooltip(100, undefined, 5, 1, false, formatSize, formatNum, plural))
103+
expect(html).toContain('1 folder')
104+
expect(html).not.toContain('1 folders')
95105
})
96106

97-
it('uses plural form for 0 files', () => {
98-
const tooltip = buildDirSizeTooltip(100, undefined, 0, 0, false, formatSize, formatNum, plural)
99-
expect(tooltip).toContain('0 files')
100-
expect(tooltip).toContain('0 folders')
107+
it('uses "No files" and "no folders" for zero counts', () => {
108+
const html = tooltipHtml(buildDirSizeTooltip(100, undefined, 0, 0, false, formatSize, formatNum, plural))
109+
expect(html).toContain('No files')
110+
expect(html).toContain('no folders')
101111
})
102112

103113
it('handles zero-size directory correctly', () => {
104-
const tooltip = buildDirSizeTooltip(0, undefined, 0, 0, false, formatSize, formatNum, plural)
105-
expect(tooltip).toBe('0 bytes \u00B7 0 files \u00B7 0 folders')
114+
const html = tooltipHtml(buildDirSizeTooltip(0, undefined, 0, 0, false, formatSize, formatNum, plural))
115+
expect(html).toContain('0 bytes')
116+
expect(html).toContain('No files')
117+
expect(html).toContain('no folders')
106118
})
107119

108120
it('handles zero-size directory while scanning', () => {
109-
const tooltip = buildDirSizeTooltip(0, undefined, 0, 0, true, formatSize, formatNum, plural)
110-
expect(tooltip).toContain('0 bytes')
111-
expect(tooltip).toContain('Updating index')
121+
const html = tooltipHtml(buildDirSizeTooltip(0, undefined, 0, 0, true, formatSize, formatNum, plural))
122+
expect(html).toContain('0 bytes')
123+
expect(html).toContain('Updating index')
112124
})
113125

114126
it('handles large file and folder counts', () => {
115-
const tooltip = buildDirSizeTooltip(1_000_000_000, undefined, 50000, 1200, false, formatSize, formatNum, plural)
116-
expect(tooltip).toContain('1000000000 bytes')
117-
expect(tooltip).toContain('50000 files')
118-
expect(tooltip).toContain('1200 folders')
127+
const html = tooltipHtml(
128+
buildDirSizeTooltip(1_000_000_000, undefined, 50000, 1200, false, formatSize, formatNum, plural),
129+
)
130+
expect(html).toContain('1000000000 bytes')
131+
expect(html).toContain('50000 files')
132+
expect(html).toContain('1200 folders')
119133
})
120134

121135
it('uses provided formatSize function', () => {
122136
const customFormat = (bytes: number): string => `${(bytes / 1024).toFixed(1)} KB`
123-
const tooltip = buildDirSizeTooltip(2048, undefined, 3, 1, false, customFormat, formatNum, plural)
124-
expect(tooltip).toContain('2.0 KB')
137+
const html = tooltipHtml(buildDirSizeTooltip(2048, undefined, 3, 1, false, customFormat, formatNum, plural))
138+
expect(html).toContain('2.0 KB')
125139
})
126140

127-
it('shows both sizes when physical differs significantly', () => {
128-
const tooltip = buildDirSizeTooltip(1000000, 800000, 10, 3, false, formatSize, formatNum, plural)
129-
expect(tooltip).toContain('Content: 1000000 bytes')
130-
expect(tooltip).toContain('On disk: 800000 bytes')
141+
it('shows both sizes on separate lines when physical differs significantly', () => {
142+
const result = buildDirSizeTooltip(1000000, 800000, 10, 3, false, formatSize, formatNum, plural)
143+
const html = tooltipHtml(result)
144+
expect(html).toContain('Content:')
145+
expect(html).toContain('1000000 bytes')
146+
expect(html).toContain('On disk:')
147+
expect(html).toContain('800000 bytes')
148+
// Both size lines should be on separate lines from counts
149+
expect(html).toContain('<br>')
131150
})
132151

133152
it('shows single size when physical is similar', () => {
134-
const tooltip = buildDirSizeTooltip(1000000, 1000005, 10, 3, false, formatSize, formatNum, plural)
135-
expect(tooltip).toBe('1000000 bytes \u00B7 10 files \u00B7 3 folders')
153+
const html = tooltipHtml(buildDirSizeTooltip(1000000, 1000005, 10, 3, false, formatSize, formatNum, plural))
154+
expect(html).toContain('1000000 bytes')
155+
expect(html).not.toContain('Content:')
156+
expect(html).not.toContain('On disk:')
157+
})
158+
159+
it('includes colored triad spans in HTML output', () => {
160+
const html = tooltipHtml(buildDirSizeTooltip(1234567, undefined, 5, 2, false, formatSize, formatNum, plural))
161+
expect(html).toContain('class="size-')
136162
})
137163
})
138164

@@ -216,25 +242,38 @@ describe('buildFileSizeTooltip', () => {
216242
expect(buildFileSizeTooltip(undefined, undefined, formatSize)).toBe('')
217243
})
218244

219-
it('returns single size when only logical is available', () => {
220-
expect(buildFileSizeTooltip(1024, undefined, formatSize)).toBe('1024 bytes')
245+
it('returns HTML tooltip when only logical is available', () => {
246+
const result = buildFileSizeTooltip(1024, undefined, formatSize)
247+
const html = tooltipHtml(result)
248+
expect(html).toContain('1024 bytes')
249+
expect(html).toContain('class="size-')
221250
})
222251

223-
it('returns single size when only physical is available', () => {
224-
expect(buildFileSizeTooltip(undefined, 2048, formatSize)).toBe('2048 bytes')
252+
it('returns HTML tooltip when only physical is available', () => {
253+
const result = buildFileSizeTooltip(undefined, 2048, formatSize)
254+
const html = tooltipHtml(result)
255+
expect(html).toContain('2048 bytes')
225256
})
226257

227-
it('shows both when sizes differ significantly', () => {
228-
const tooltip = buildFileSizeTooltip(1000000, 800000, formatSize)
229-
expect(tooltip).toBe('Content: 1000000 bytes \u00B7 On disk: 800000 bytes')
258+
it('shows both sizes on separate lines when they differ significantly', () => {
259+
const result = buildFileSizeTooltip(1000000, 800000, formatSize)
260+
const html = tooltipHtml(result)
261+
expect(html).toContain('Content:')
262+
expect(html).toContain('1000000 bytes')
263+
expect(html).toContain('On disk:')
264+
expect(html).toContain('800000 bytes')
265+
expect(html).toContain('<br>')
230266
})
231267

232268
it('shows single size when sizes are similar', () => {
233-
const tooltip = buildFileSizeTooltip(1000000, 1000005, formatSize)
234-
expect(tooltip).toBe('1000000 bytes')
269+
const html = tooltipHtml(buildFileSizeTooltip(1000000, 1000005, formatSize))
270+
expect(html).toContain('1000000 bytes')
271+
expect(html).not.toContain('Content:')
235272
})
236273

237274
it('shows single size when sizes are equal', () => {
238-
expect(buildFileSizeTooltip(500, 500, formatSize)).toBe('500 bytes')
275+
const html = tooltipHtml(buildFileSizeTooltip(500, 500, formatSize))
276+
expect(html).toContain('500 bytes')
277+
expect(html).not.toContain('Content:')
239278
})
240279
})

0 commit comments

Comments
 (0)