ui: rework selection/heap visuals, contextual shortcut hints, inline scan activity

- Selection now reads as a blue ring + tint with a springy scale-down,
  hover stays a subtle gray ring so keyboard-driven and mouse-driven
  states are tellable apart.
- Heap membership is signalled with a green tint only (no badge, no
  ring, no scale).
- Discard/restore is optimistic and non-yanking: photos stay in the
  grid greyed out until the next reload, X toggles based on the
  current state, and the same treatment applies in preview.
- Filmstrip mirrors the grid styling (selection blue, heap green,
  discarded grey).
- Preview close restores the LAST viewed photo as the focused/selected
  one in the grid.
- Right sidebar collapses on view change and re-opens when a photo is
  in focus; Esc clears active selection so the panel collapses too.
- Keyboard hints panel is context-aware (grid / preview / discarded
  section), collapsible with H, persisted, and rendered inside the
  preview column above the filmstrip.
- "Pick (P)" renamed to "Select (S)" everywhere.
- Needs review moved into the Flag pill dropdown.
- Fixed vertical videos overflowing the preview column (min-h-0).
- Replaced the bottom-right ScanProgress popover with an inline
  spinner next to the FOLDERS sidebar header (and on the specific
  folder row being scanned). ScanProgress is now a headless
  invalidator; useScanActivity exposes the live status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 23:45:36 +02:00
parent a6eb406052
commit 8529771122
17 changed files with 501 additions and 408 deletions

View File

@@ -50,6 +50,7 @@ import { UploadModal } from '../upload/UploadModal'
import { useSharedFoldersQuery } from '../../hooks/useSharingQueries'
import { useAuth } from '../../contexts/AuthContext'
import { useFeaturesQuery } from '../../hooks/useFeaturesQuery'
import { useScanActivity } from '../../hooks/useScanActivity'
interface TreeItem {
id: string
@@ -61,10 +62,15 @@ interface TreeItem {
/** For folder rows only: the user-set "hide from views" flag. Drives
* the muted styling + eye-off badge + menu item label. */
isHidden?: boolean
/** For folder rows only: filesystem path. Used to match against the
* scan-status `current_folder` so we can show an inline spinner on
* the row that's actively being scanned. */
path?: string
}
export function LeftSidebar() {
const { user, isAdmin, logout } = useAuth()
const scanActivity = useScanActivity()
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set(['library', 'folders', 'heaps']))
// Inline rename state for source-root rows. Stores the id being edited
// and the draft name. Double-click a folder row to start.
@@ -405,6 +411,7 @@ export function LeftSidebar() {
count: node.photo_count,
type: 'folder',
isHidden: node.is_hidden,
path: node.path,
children: node.children.length > 0
? node.children.map(folderNodeToTreeItem)
: undefined,
@@ -628,6 +635,26 @@ export function LeftSidebar() {
</span>
)}
{/* Activity spinner — shown on the FOLDERS section header
* whenever any background scan/processing is happening, and
* on a folder row whose path is the one currently being
* scanned. Replaces the old bottom-right ScanProgress popup. */}
{(() => {
const showOnFolders =
isSectionHeader && item.id === 'folders' && scanActivity.active
const showOnFolderRow =
!!item.path &&
!!scanActivity.currentFolder &&
scanActivity.currentFolder.startsWith(item.path)
if (!showOnFolders && !showOnFolderRow) return null
return (
<span
className="ml-1 inline-block h-2.5 w-2.5 flex-shrink-0 animate-spin rounded-full border border-primary/30 border-t-primary"
aria-label="Background activity in progress"
/>
)
})()}
{/* Count Badge — fixed-width slot so counts line up in a column
* across rows regardless of digit count. Section headers skip
* the badge entirely (they're labels, not navigable rows). */}