feat(web): sidebar root folder + folder counts, drill-in colors/tags/ratings, click semantics rework
Sidebar
- New `/` root-folder entry at the top of the Folders group. Active
when the timeline is scoped to root; the photo grid post-filters to
`Path === ''` because PhotoPrism's `path:` operator can't express an
exact-root match. Collapsible chevron, persisted to its own
localStorage key, and a kebab carrying just "New subfolder".
- Per-folder count badges. `/api/v1/photos?q=path:X&count=1000` per
folder in parallel via `listFolderCounts`; root count derived from
`config.count.all − Σ subfolder counts`.
- Folder tree starts at depth=1 under the root so nested rows indent
visually relative to `/`.
- Footer matches the Toolbar / action-bar h-9 height.
Timeline interaction
- Single click on a tile selects only that tile (clears others); the
preview now lives on dblclick. Modifier clicks still go through
`gridKeyNav`'s document handler (shift = range, cmd/ctrl = toggle).
- `x` (archive) now actually archives — PhotoPrism's photo PUT
silently drops the Archived field, so we route through
/batch/photos/{archive,restore} the same way the BulkActionBar
already did. Mirror for `u`.
- Preview close restores the timeline focus + scrolls the last-shown
photo into view via `forcedExpand`+`scrollTileIntoView` so it
actually mounts (selection ring would otherwise stay invisible when
the user navigated far in preview).
- `applyFolderScope` only narrows the timeline to root when the active
view is a folder view (no heap / search / non-default section), so
label clicks / heap views / favorites no longer drop subfolder
photos.
Action bar
- Inline `h-9` row at the bottom of the main column (not `fixed`),
matching the Toolbar's visual language. Right sidebar stays full
height — the bar only spans the timeline width.
- Approve action wired for the review pile.
Colors / Tags / Ratings drill-ins
- New shared `PhotoGrid` component owning tile rendering, selection
styling, single-click-selects + dblclick-previews, and `setOrder`
for arrow-key nav.
- Each route's drill-in `<main>` carries `use:gridKeyNav` and a
trailing `<BulkActionBar />` so shift/cmd/ctrl click, arrow keys,
and the keyboard shortcuts work the same as the timeline.
- Tags switches from `goto('/?q=label:…')` to an in-place drill-in
with a back button, mirroring `/colors`'s flow.
- Category cards + drill-in photo cards honour the global
`view.thumbnailSize` (XS–XL) so the timeline's size selector now
reaches into all four grids.
Settings
- General-settings dialog merges Appearance into UI and switches free
text inputs to selects for the PhotoPrism theme / language / start
page / map style (the value-from-server prepends if it's outside
the curated list so we never silently rewrite a custom value). Time
zone uses `<datalist>` with `Intl.supportedValuesOf('timeZone')`.
Sidecar
- Heap convert runs reindex synchronously per source path so the
client's invalidate-and-refetch sees the moved files.
Inbox
- New /inbox route stub for the upcoming import workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,11 @@
|
||||
* `filters.folderPath` matches (the sidebar nav case); the picker
|
||||
* passes its own selection so the dialog has independent state. */
|
||||
selectedPath?: string | null;
|
||||
/** Optional per-path photo count. When provided, each row renders a
|
||||
* compact badge with the count — matching the heaps section's
|
||||
* "{n} photos" affordance. Undefined keeps the badge off entirely
|
||||
* (the picker dialog doesn't need it). */
|
||||
counts?: Record<string, number>;
|
||||
}
|
||||
let {
|
||||
nodes,
|
||||
@@ -69,7 +74,8 @@
|
||||
onDelete,
|
||||
onCreateChild,
|
||||
readonly = false,
|
||||
selectedPath
|
||||
selectedPath,
|
||||
counts
|
||||
}: Props = $props();
|
||||
|
||||
// Auto-expanded folders, persisted to localStorage so the tree state
|
||||
@@ -114,7 +120,7 @@
|
||||
with the px-2 of Views/Heaps rows; +12px per nested level.
|
||||
-->
|
||||
<div
|
||||
class="group flex h-[24px] items-center rounded text-[12px] leading-tight hover:bg-accent"
|
||||
class="group flex h-[24px] items-center rounded pr-2 text-[12px] leading-tight hover:bg-accent"
|
||||
class:bg-primary={active}
|
||||
class:text-primary-foreground={active}
|
||||
class:hover:bg-primary={active}
|
||||
@@ -137,7 +143,7 @@
|
||||
<span class="inline-block h-[18px] w-4" aria-hidden="true"></span>
|
||||
{/if}
|
||||
<button
|
||||
class="flex flex-1 items-center truncate text-left"
|
||||
class="flex min-w-0 flex-1 items-center truncate text-left"
|
||||
class:px-1={hasChildren || depth > 0}
|
||||
onclick={() => onPick(node.path)}
|
||||
ondblclick={readonly ? undefined : () => onRename?.(node.path)}
|
||||
@@ -145,11 +151,23 @@
|
||||
>
|
||||
<span class="truncate">{node.name}</span>
|
||||
</button>
|
||||
{#if counts && counts[node.path] !== undefined}
|
||||
{@const n = counts[node.path]}
|
||||
<span
|
||||
class="ml-auto shrink-0 rounded px-1 text-[10px] tabular-nums {active
|
||||
? 'bg-primary-foreground/15 text-primary-foreground'
|
||||
: 'bg-secondary text-muted-foreground'}"
|
||||
>
|
||||
{n >= 1000 ? '1000+' : n}
|
||||
</span>
|
||||
{/if}
|
||||
{#if !readonly}
|
||||
<!-- Hover-revealed kebab. Reserves zero width when idle so the
|
||||
row stays compact; expands on hover and stays visible while
|
||||
the menu is open. Suppressed in readonly mode (picker). -->
|
||||
<div class="mr-1">
|
||||
<!-- Hover-revealed kebab. `display: none` until row hover
|
||||
(or while the menu is open via has-[[data-state=open]])
|
||||
so the count holds the row's right edge by default
|
||||
and the kebab pushes it left when it appears.
|
||||
Suppressed in readonly mode (picker). -->
|
||||
<div class="ml-1 hidden group-hover:block has-[[data-state=open]]:block">
|
||||
<KebabMenu label="Folder actions">
|
||||
<Item
|
||||
class="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-[12px] outline-none hover:bg-accent focus:bg-accent"
|
||||
@@ -187,6 +205,7 @@
|
||||
{onCreateChild}
|
||||
{readonly}
|
||||
{selectedPath}
|
||||
{counts}
|
||||
/>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user