Skip to content

Commit 6defbf7

Browse files
committed
Indexing status: calm hourglass, details on hover
- Replace the top-right scan/replay overlays with one small `IndexingStatusIndicator` hourglass icon; the dynamic status message, progress bar, and ETA move into a rich tooltip on hover/focus - Size column and status bar: unindexed dirs show `<dir>`/`DIR` + a quiet hourglass (tooltip: "Sizes are usually ready after 3 minutes") instead of `Scanning...`; column measurement reserves the icon width via the shared `getDirSizeDisplayState` decider - Tooltip action gains generic `contentEl` support: live rich content via reparenting, singleton-safe, pinned by regression tests (incl. the hidden-host trap) - Extract pure ETA helpers into `lib/indexing/eta.ts` with unit tests; drop the replay overlay's 4s grace delay (an icon is unobtrusive, honesty wins) - Delete `ScanStatusOverlay`, `ReplayStatusOverlay`, `ProgressOverlay` + their a11y tests, gallery demo, and coverage-allowlist entries - Also: oxfmt reflow of two pre-existing `docs/specs/later/` drafts
1 parent b6fdd18 commit 6defbf7

30 files changed

Lines changed: 1732 additions & 1264 deletions

apps/desktop/coverage-allowlist.json

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,6 @@
189189
"tauri-commands/rename.ts": {
190190
"reason": "Tauri command wrappers, tested via integration"
191191
},
192-
"indexing/ScanStatusOverlay.svelte": {
193-
"reason": "UI overlay component, depends on Tauri event listeners"
194-
},
195192
"indexing/index-events.ts": {
196193
"reason": "Thin wrapper over Tauri listen API"
197194
},
@@ -252,12 +249,6 @@
252249
"tauri-commands/search.ts": {
253250
"reason": "Tauri command wrappers, tested via integration"
254251
},
255-
"indexing/ReplayStatusOverlay.svelte": {
256-
"reason": "UI overlay component, depends on Tauri event listeners"
257-
},
258-
"ui/ProgressOverlay.svelte": {
259-
"reason": "Pure UI component, no logic to test beyond rendering"
260-
},
261252
"crash-reporter/CrashReportDialog.svelte": {
262253
"reason": "UI dialog, depends on Tauri commands for send/dismiss"
263254
},

apps/desktop/src/lib/file-explorer/selection/CLAUDE.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,11 @@ Stale indicator (Lucide hourglass icon via `~icons/lucide/hourglass`, rendered i
7373
`getDirSizeDisplayState(displaySize, indexing, recursiveSizePending)` — the same decider FullList uses, so Brief's
7474
status bar matches Full's size column. Here `indexing = isScanning() || isAggregating()` (the aggregation phase
7575
matters too), and `recursiveSizePending` lights the hourglass during a live delete/copy even with no full scan. An
76-
unindexed dir shows `Scanning...` while indexing, else `DIR`. The per-folder `recursiveSizePending` flag lives only on
77-
`DirStats` (not `get_file_range`), so `FilePane.fetchEntryUnderCursor` overlays it onto the cursor entry via
78-
`updateIndexSizesInPlace([entry])` (skipping `..`, whose entry path is the parent folder), and re-runs on
79-
`index-dir-updated` so the hourglass tracks a storm live.
76+
unindexed dir always shows `DIR`; while indexing it adds a "Size not ready yet" hourglass (tooltip: "Sizes are usually
77+
ready after 3 minutes"), the same de-emphasized treatment Full's size column gives its `scanning` state. The
78+
per-folder `recursiveSizePending` flag lives only on `DirStats` (not `get_file_range`), so
79+
`FilePane.fetchEntryUnderCursor` overlays it onto the cursor entry via `updateIndexSizesInPlace([entry])` (skipping
80+
`..`, whose entry path is the parent folder), and re-runs on `index-dir-updated` so the hourglass tracks a storm live.
8081

8182
Symlink hint (Lucide info icon via `~icons/lucide/info`, rendered in tertiary text color) appears next to a directory's
8283
size in `file-info` mode when `entry.recursiveHasSymlinks === true`. The tooltip reads: "This folder contains symlinks.

apps/desktop/src/lib/file-explorer/selection/SelectionInfo.dir-size-state.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,12 @@ describe('SelectionInfo Brief file-info dir size state', () => {
105105
expect(t.querySelector('.stale-indicator')).toBeNull()
106106
})
107107

108-
it('shows "Scanning" for an unindexed dir while indexing', async () => {
108+
it('shows the dir placeholder with the not-ready hourglass for an unindexed dir while indexing', async () => {
109109
idx.scanning = true
110110
const t = mountFileInfo(makeDir({ recursiveSize: undefined, recursivePhysicalSize: undefined }))
111111
await tick()
112-
expect(t.textContent).toMatch(/Scanning/i)
112+
expect(t.textContent).toMatch(/DIR/)
113+
expect(t.querySelector('.stale-indicator')).not.toBeNull()
113114
})
114115

115116
it('shows the dir placeholder for an unindexed dir when idle', async () => {

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,17 @@
237237
<span class="name" use:tooltip={displayName} use:useShortenMiddle={{ text: displayName, preferBreakAt: '.', startRatio: 0.7 }}></span>
238238
<span class="size" use:tooltip={sizeTooltip}>
239239
{#if sizeDisplay === 'DIR'}
240-
{#if dirSizeState === 'scanning'}Scanning...{:else}DIR{/if}
240+
DIR
241+
{#if dirSizeState === 'scanning'}
242+
<span
243+
class="stale-indicator stale-icon"
244+
role="img"
245+
aria-label="Size not ready yet"
246+
use:tooltip={'Sizes are usually ready after 3 minutes'}
247+
>
248+
<IconHourglass width="12" height="12" />
249+
</span>
250+
{/if}
241251
{:else if sizeDisplay}
242252
{#each sizeDisplay as triad, i (i)}
243253
<span class={triad.tierClass}>{triad.value}</span>

apps/desktop/src/lib/file-explorer/views/CLAUDE.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,17 @@ without DOM performance issues.
3636
delegates to triads in bytes mode, a dynamic friendliest-unit string in dynamic mode, and a forced single-unit string
3737
in `kB`/`MB`/`GB` mode. `measure-column-widths.ts` accepts the same options so the size column shrink-wraps the
3838
actually-rendered cell text. Uses Lucide icons (via `unplugin-icons`): `~icons/lucide/circle-alert` for size mismatch
39-
warnings and `~icons/lucide/hourglass` for stale index indicators. The hourglass shows when the global `indexing` flag
39+
warnings and `~icons/lucide/hourglass` for the index indicators. The hourglass shows when the global `indexing` flag
4040
is set (full scan/aggregation, every size in flux) OR the row's own `recursiveSizePending` is set (live delete/copy in
41-
flight for that dir, even with no scan running). The per-dir flag rides `DirStats.recursiveSizePending`, copied onto
42-
entries by `updateIndexSizesInPlace` / `createParentEntry` (backend: `indexing/pending_sizes.rs`). Also renders an
43-
optional Git status column between Name and Ext when `gitRepoRoot` is set and `showGitColumn` is true (gated by the
44-
`fileExplorer.git.showStatusColumn` setting in `FilePane`); fetches `fetchStatusMap` and refreshes on
45-
`git-state-changed` for the active repo
41+
flight for that dir, even with no scan running). Two flavors share the glyph: the `size-stale` state pairs it with a
42+
visible recursive size (tooltip "Updating index: size may change."), while the `scanning` state (no size yet) renders
43+
the `<dir>` placeholder plus the hourglass with tooltip "Sizes are usually ready after 3 minutes" so a fresh install
44+
reads as quietly working rather than `Scanning...` on every row. `measure-column-widths.ts` reserves `SIZE_ICON_WIDTH`
45+
for both states so the shrink-wrapped column never clips the glyph. The per-dir flag rides
46+
`DirStats.recursiveSizePending`, copied onto entries by `updateIndexSizesInPlace` / `createParentEntry` (backend:
47+
`indexing/pending_sizes.rs`). Also renders an optional Git status column between Name and Ext when `gitRepoRoot` is
48+
set and `showGitColumn` is true (gated by the `fileExplorer.git.showStatusColumn` setting in `FilePane`); fetches
49+
`fetchStatusMap` and refreshes on `git-state-changed` for the active repo
4650
- **dir-size-display.test.ts** – Tests for `getDirSizeDisplayState` / `buildDirSizeTooltip` (functions in
4751
`full-list-utils.ts`)
4852
- **view-modes.test.ts** – Integration tests for hidden-file filtering and directory listing structure (uses

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,14 @@
962962
</span>
963963
{/if}
964964
{:else if dirSizeState === 'scanning'}
965-
<span class="size-scanning">Scanning...</span>
965+
<span class="size-dir">&lt;dir&gt;</span>
966+
<span
967+
class="size-stale icon-indicator"
968+
role="img"
969+
aria-label="Size not ready yet"
970+
use:tooltip={'Sizes are usually ready after 3 minutes'}
971+
><IconHourglass width="12" height="12" /></span
972+
>
966973
{:else}
967974
<span class="size-dir">&lt;dir&gt;</span>
968975
{/if}
@@ -1314,12 +1321,6 @@
13141321
cursor: help;
13151322
}
13161323
1317-
.size-scanning {
1318-
color: var(--color-text-secondary);
1319-
font-size: var(--font-size-xs);
1320-
white-space: nowrap;
1321-
}
1322-
13231324
.col-date {
13241325
overflow: hidden;
13251326
text-overflow: ellipsis;

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,10 @@ describe('buildDirSizeTooltip', () => {
9292
expect(buildDirSizeTooltip(undefined, undefined, 0, 0, false, formatSize, formatNum, plural)).toBe('')
9393
})
9494

95-
it('returns "Scanning..." when no data and scanning is active', () => {
96-
expect(buildDirSizeTooltip(undefined, undefined, 0, 0, true, formatSize, formatNum, plural)).toBe('Scanning...')
95+
it('returns the size-readiness hint when no data and scanning is active', () => {
96+
expect(buildDirSizeTooltip(undefined, undefined, 0, 0, true, formatSize, formatNum, plural)).toBe(
97+
'Sizes are usually ready after 3 minutes',
98+
)
9799
})
98100

99101
it('returns HTML tooltip with size and counts when recursive size is available', () => {

apps/desktop/src/lib/file-explorer/views/full-list-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ export type DirSizeDisplayState = 'dir' | 'scanning' | 'size' | 'size-stale'
324324
* Rules (where "active" = global `indexing` OR this dir's own `pending` flag):
325325
* - Has recursiveSize + active -> 'size-stale' (show size with hourglass)
326326
* - Has recursiveSize + not active -> 'size' (show formatted size)
327-
* - No recursiveSize + active -> 'scanning' (show spinner)
327+
* - No recursiveSize + active -> 'scanning' (show <dir> placeholder with hourglass)
328328
* - No recursiveSize + not active -> 'dir' (show <dir> placeholder)
329329
*
330330
* Global `indexing` means a full scan/aggregation is running (every size in
@@ -395,5 +395,5 @@ export function buildDirSizeTooltip(
395395

396396
return { html: lines.join('<br>') }
397397
}
398-
return scanning ? 'Scanning...' : ''
398+
return scanning ? 'Sizes are usually ready after 3 minutes' : ''
399399
}

apps/desktop/src/lib/file-explorer/views/measure-column-widths.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,22 @@ describe('computeFullListColumnWidths', () => {
206206
expect(pending.size).toBeGreaterThanOrEqual(idle.size)
207207
})
208208

209+
it('reserves icon width for a scanning directory with no size yet', () => {
210+
_setMeasureForTests(fakeMeasure)
211+
const idle = computeFullListColumnWidths({
212+
...baseArgs,
213+
entries: [entry({ name: 'd', isDirectory: true, recursiveSize: undefined })],
214+
})
215+
const scanning = computeFullListColumnWidths({
216+
...baseArgs,
217+
indexing: true,
218+
entries: [entry({ name: 'd', isDirectory: true, recursiveSize: undefined })],
219+
})
220+
// The `<dir>` placeholder text is the same in both, but the scanning row also
221+
// draws the hourglass, so its column must reserve the extra icon width.
222+
expect(scanning.size).toBeGreaterThan(idle.size)
223+
})
224+
209225
it('includes parentDirStats size when provided', () => {
210226
_setMeasureForTests(fakeMeasure)
211227
const without = computeFullListColumnWidths({

apps/desktop/src/lib/file-explorer/views/measure-column-widths.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ function sizeCellText(bytes: number, opts: SizeFormatOpts): string {
174174
function sizeTextForEntry(
175175
entry: FileEntry,
176176
sizeDisplayMode: 'smart' | 'logical' | 'physical',
177-
indexing: boolean,
178177
sizeFormatOpts: SizeFormatOpts,
179178
isRestricted: boolean,
180179
): string {
@@ -191,9 +190,10 @@ function sizeTextForEntry(
191190
if (entry.isDirectory) {
192191
const s = getDisplaySize(entry.recursiveSize, entry.recursivePhysicalSize, sizeDisplayMode)
193192
if (s !== undefined) return sizeCellText(s, sizeFormatOpts)
194-
// Mirror FullList's render decision (same `getDirSizeDisplayState`) so the
195-
// measured text matches what's drawn for the no-size case.
196-
return getDirSizeDisplayState(s, indexing, entry.recursiveSizePending) === 'scanning' ? 'Scanning...' : '<dir>'
193+
// Mirror FullList's render decision (same `getDirSizeDisplayState`): both the
194+
// scanning and dir states render the `<dir>` placeholder text (the scanning
195+
// state adds an hourglass on top, reserved separately in `sizeIconSuffixForEntry`).
196+
return '<dir>'
197197
}
198198
const s = getDisplaySize(entry.size, entry.physicalSize, sizeDisplayMode)
199199
return s !== undefined ? sizeCellText(s, sizeFormatOpts) : ''
@@ -227,10 +227,23 @@ function foldDate(current: DateMaxima, formatted: FormattedDate, measure: (text:
227227
}
228228

229229
/** Pixel width of the size-column icons that follow the text for this row. */
230-
function sizeIconSuffixForEntry(entry: FileEntry, indexing: boolean, showSizeMismatchWarning: boolean): number {
230+
function sizeIconSuffixForEntry(
231+
entry: FileEntry,
232+
sizeDisplayMode: 'smart' | 'logical' | 'physical',
233+
indexing: boolean,
234+
showSizeMismatchWarning: boolean,
235+
): number {
231236
let suffix = 0
232-
if (entry.isDirectory && (indexing || entry.recursiveSizePending) && entry.recursiveSize != null)
233-
suffix += SIZE_ICON_WIDTH
237+
if (entry.isDirectory) {
238+
// FullList draws the hourglass for both the `size-stale` state (a settled
239+
// size that may still change) and the `scanning` state (`<dir>` placeholder
240+
// with no size yet). Both reserve the icon width here so the shrink-wrapped
241+
// column doesn't clip the glyph. Mirror the same `getDirSizeDisplayState`
242+
// decision the renderer uses.
243+
const s = getDisplaySize(entry.recursiveSize, entry.recursivePhysicalSize, sizeDisplayMode)
244+
const state = getDirSizeDisplayState(s, indexing, entry.recursiveSizePending)
245+
if (state === 'size-stale' || state === 'scanning') suffix += SIZE_ICON_WIDTH
246+
}
234247
if (showSizeMismatchWarning) {
235248
const logical = entry.isDirectory ? entry.recursiveSize : entry.size
236249
const physical = entry.isDirectory ? entry.recursivePhysicalSize : entry.physicalSize
@@ -377,11 +390,10 @@ function foldEntries(
377390
const sizeText = sizeTextForEntry(
378391
entry,
379392
ctx.sizeDisplayMode,
380-
ctx.indexing,
381393
ctx.sizeFormatOpts,
382394
ctx.isRestricted?.(entry.path) ?? false,
383395
)
384-
const iconSuffix = sizeIconSuffixForEntry(entry, ctx.indexing, ctx.showSizeMismatchWarning)
396+
const iconSuffix = sizeIconSuffixForEntry(entry, ctx.sizeDisplayMode, ctx.indexing, ctx.showSizeMismatchWarning)
385397
const rowSize = (sizeText ? ctx.measure(sizeText) : 0) + iconSuffix
386398
if (rowSize > sizeMax) sizeMax = rowSize
387399
if (iconSuffix > sizeIconSuffixMax) sizeIconSuffixMax = iconSuffix

0 commit comments

Comments
 (0)