feat: surface active heap on photos + sidebar row, drop topbar pill
- PhotoThumbnail: the bottom-right basket badge now expands into a name chip when the active heap name is supplied. Truncated to a 120px max-width so it doesn't eat the thumbnail. - Timeline: read activeHeap.name from useActiveHeapMembersQuery and pipe it down to PhotoThumbnail. - TopBar: drop the leftover heap pill next to "Mulimago" — the active state now lives where the user navigates to it (the heaps row). - HeapsPanel: the active heap row gets a soft bg-pick/10 wash when not also filtered, the basket icon turns text-pick, and a small "ACTIVE" pill renders next to the name. Together they make it obvious which heap Pick / T target without needing the topbar pill. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -266,6 +266,10 @@ export function HeapsPanel() {
|
||||
className={clsx(
|
||||
'group relative flex cursor-pointer items-center gap-1 rounded px-2 py-1 text-[13px]',
|
||||
isFiltered ? 'bg-primary/20 text-primary' : 'text-text hover:bg-surface-2',
|
||||
// Active-heap row gets a left accent stripe so the
|
||||
// user always knows where Pick / T will land, even
|
||||
// when navigating to a different section.
|
||||
isActive && !isFiltered && 'bg-pick/10 text-text',
|
||||
isDropTarget && 'ring-2 ring-primary bg-primary/10'
|
||||
)}
|
||||
style={{ paddingLeft: '32px' }}
|
||||
@@ -308,7 +312,11 @@ export function HeapsPanel() {
|
||||
<ShoppingBasket
|
||||
className={clsx(
|
||||
'h-4 w-4 flex-shrink-0',
|
||||
isFiltered ? 'text-primary' : 'text-text-muted'
|
||||
isActive
|
||||
? 'text-pick'
|
||||
: isFiltered
|
||||
? 'text-primary'
|
||||
: 'text-text-muted'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -330,12 +338,23 @@ export function HeapsPanel() {
|
||||
className="flex-1 rounded border border-border bg-bg px-1 py-0 text-[13px] text-text focus:border-primary focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={clsx('flex-1 truncate', isActive && 'font-semibold')}
|
||||
title={heap.name}
|
||||
>
|
||||
{heap.name}
|
||||
</span>
|
||||
<>
|
||||
<span
|
||||
className={clsx('truncate', isActive && 'font-semibold')}
|
||||
title={heap.name}
|
||||
>
|
||||
{heap.name}
|
||||
</span>
|
||||
{isActive && (
|
||||
<span
|
||||
className="ml-1 flex-shrink-0 rounded-full bg-pick/25 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-pick"
|
||||
title="Active heap — Pick (P) and the basket badge on photos point here"
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-1" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Right cluster. Count is the rightmost element in the
|
||||
|
||||
@@ -1,33 +1,17 @@
|
||||
import { ShoppingBasket } from 'lucide-react'
|
||||
import { useHeapsQuery } from '../../hooks/useHeapsQuery'
|
||||
import muliLogo from '../../assets/muli-logo.png'
|
||||
|
||||
/**
|
||||
* Slim top bar — just the logo and the active-heap pill. The search input
|
||||
* lives in the FilterBar now (next to the rest of the filter controls).
|
||||
* Slim top bar — just the logo. The active heap badge moved into the
|
||||
* Heaps panel in the left sidebar (where it actually relates to the
|
||||
* heap rows the user navigates to).
|
||||
*/
|
||||
export function TopBar() {
|
||||
const { data: heapsList = [] } = useHeapsQuery()
|
||||
const activeHeap = heapsList.find((h) => h.is_active)
|
||||
|
||||
return (
|
||||
<header className="flex h-12 items-center justify-between border-b border-border bg-surface px-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={muliLogo} alt="Mulimago" className="h-7 w-7 object-contain" />
|
||||
<h1 className="text-lg font-semibold text-text">Mulimago</h1>
|
||||
</div>
|
||||
{activeHeap && (
|
||||
<span
|
||||
className="flex items-center gap-1 rounded bg-primary/20 px-2 py-0.5 text-xs text-primary"
|
||||
title="Active heap — press P to add selected photos here"
|
||||
>
|
||||
<ShoppingBasket className="h-3 w-3" />
|
||||
{activeHeap.name}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={muliLogo} alt="Mulimago" className="h-7 w-7 object-contain" />
|
||||
<h1 className="text-lg font-semibold text-text">Mulimago</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2" />
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -18,6 +18,9 @@ interface PhotoThumbnailProps {
|
||||
isSelected: boolean
|
||||
/** True when the photo belongs to the currently active heap. */
|
||||
isInActiveHeap?: boolean
|
||||
/** Name of the active heap. When set + isInActiveHeap, the basket
|
||||
* badge expands into a name chip so the user knows which heap. */
|
||||
activeHeapName?: string | null
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
onDoubleClick?: (e: React.MouseEvent) => void
|
||||
}
|
||||
@@ -27,6 +30,7 @@ export function PhotoThumbnail({
|
||||
size,
|
||||
isSelected,
|
||||
isInActiveHeap = false,
|
||||
activeHeapName = null,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
}: PhotoThumbnailProps) {
|
||||
@@ -210,10 +214,15 @@ export function PhotoThumbnail({
|
||||
<div className="absolute bottom-1 right-1 flex items-center gap-1">
|
||||
{isInActiveHeap && (
|
||||
<div
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full bg-pick text-white shadow-md ring-2 ring-white/90"
|
||||
title="In active heap"
|
||||
className="flex h-5 max-w-[120px] items-center gap-1 rounded-full bg-pick px-1.5 text-white shadow-md ring-2 ring-white/90"
|
||||
title={activeHeapName ? `In heap: ${activeHeapName}` : 'In active heap'}
|
||||
>
|
||||
<ShoppingBasket className="h-3 w-3" strokeWidth={2.5} />
|
||||
<ShoppingBasket className="h-3 w-3 flex-shrink-0" strokeWidth={2.5} />
|
||||
{activeHeapName && (
|
||||
<span className="truncate text-[10px] font-medium leading-none">
|
||||
{activeHeapName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{photo.is_duplicate && (
|
||||
|
||||
@@ -196,7 +196,8 @@ export function Timeline() {
|
||||
// Membership in the active heap (for the basket affordance). Subscribed
|
||||
// once at this level so we don't have hundreds of thumbnails each
|
||||
// subscribing to the same query.
|
||||
const { memberIds: activeHeapMembers } = useActiveHeapMembers()
|
||||
const { memberIds: activeHeapMembers, activeHeap } = useActiveHeapMembers()
|
||||
const activeHeapName = activeHeap?.name ?? null
|
||||
|
||||
// Build the flat virtualizer items: a mix of group headers and rows of
|
||||
// photos. Date headers appear when sorted by a date field; tag headers
|
||||
@@ -510,6 +511,7 @@ export function Timeline() {
|
||||
size={THUMBNAIL_SIZE}
|
||||
isSelected={selectedPhotos.includes(photo.id)}
|
||||
isInActiveHeap={activeHeapMembers.has(photo.id)}
|
||||
activeHeapName={activeHeapName}
|
||||
onClick={(e) => {
|
||||
if (e.shiftKey) {
|
||||
selectRange(photo.id)
|
||||
|
||||
Reference in New Issue
Block a user