feat: heap row kebab menu with rename + duplicate

The heap row used to fan out three small icon buttons (set active, convert
to folder, delete) on hover, which crowded the row and didn't leave room
for new actions. Collapse the destructive / occasional ones into a kebab
menu and add the missing operations.

- Right-aligned action cluster: active indicator → count badge → target
  toggle (when not active) → kebab menu, all flex-shrink-0 so the name
  truncates first.
- Kebab menu items: Rename, Duplicate, Move to folder…, Delete. Outside
  click and Escape close the popover; the trigger has aria-haspopup +
  aria-expanded. Delete still confirms via window.confirm.
- Inline rename: double-click a heap row OR pick Rename from the menu
  to edit the name in place. Enter commits, Escape cancels. Mirrors the
  folder rename pattern in LeftSidebar.
- backend: new POST /heaps/{id}/duplicate creates a copy with the same
  membership ("{name} (copy)") via INSERT...SELECT on heap_photos.
  Never marks the new heap as active so duplicating doesn't quietly
  steal the user's T-key destination.
- api.ts: heaps.duplicate wrapper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 20:36:00 +02:00
parent fc63b1f69d
commit 696477eefd
3 changed files with 260 additions and 54 deletions

View File

@@ -229,6 +229,13 @@ export const heaps = {
await api.delete(`/heaps/${heapId}`)
},
/** Duplicate a heap, copying its membership but never marking the new
* one as active. The new heap is named "{name} (copy)". */
duplicate: async (heapId: string): Promise<Heap> => {
const response = await api.post(`/heaps/${heapId}/duplicate`)
return response.data
},
/** Lightweight: just the photo ids in a heap, for client-side membership
* lookups (the basket affordance on thumbnails). */
photoIds: async (heapId: string): Promise<string[]> => {