Move expand/collapse to header bar, dim tree lines, increase gap

- Lift expandedVersionIds state to DeletedFilesContext (shared between
  DeletedFilesList and FileExplorerHeader)
- Move 'Expand all' / 'Collapse all' buttons from the table header into
  the FileExplorerHeader top bar with outline button variant
- Change tree-connector line color from bg-foreground-muted to
  bg-border-stronger for a subtler appearance
- Increase gap between horizontal tree branch and version title text
  (pl-[22px] → pl-[36px], w-[11px] → w-[8px])
- Apply same tree line changes to standalone Trash page (TrashList)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiUEvmC84bRsteqsWXHY2p
This commit is contained in:
Claude committed 2026-08-07 14:59:12 +00:00
1 parent 889146ee06
commit c20faa7ff7
4 files changed
+69 -43

No files matched your search

@@ -1,5 +1,13 @@
import { parseAsBoolean, useQueryState } from 'nuqs'
import { createContext, useCallback, useContext, useState, type ReactNode } from 'react'
import {
createContext,
useCallback,
useContext,
useState,
type Dispatch,
type ReactNode,
type SetStateAction,
} from 'react'
import {
type DeletedObjectVersion,
@@ -24,6 +32,8 @@ interface DeletedFilesContextValue {
lastToggledId: string | null
setLastToggledId: (id: string | null) => void
clearDeletedSelection: () => void
expandedVersionIds: Set<string>
setExpandedVersionIds: Dispatch<SetStateAction<Set<string>>>
}
const DeletedFilesContext = createContext<DeletedFilesContextValue>({
@@ -39,6 +49,8 @@ const DeletedFilesContext = createContext<DeletedFilesContextValue>({
lastToggledId: null,
setLastToggledId: () => {},
clearDeletedSelection: () => {},
expandedVersionIds: new Set(),
setExpandedVersionIds: () => {},
})
export const useDeletedFilesContext = () => useContext(DeletedFilesContext)
@@ -59,6 +71,7 @@ export const DeletedFilesProvider = ({ enabled, children }: DeletedFilesProvider
>()
const [selectedDeletedIds, setSelectedDeletedIds] = useState<string[]>([])
const [lastToggledId, setLastToggledId] = useState<string | null>(null)
const [expandedVersionIds, setExpandedVersionIds] = useState<Set<string>>(new Set())
const clearDeletedSelection = useCallback(() => {
setSelectedDeletedIds([])
@@ -80,6 +93,8 @@ export const DeletedFilesProvider = ({ enabled, children }: DeletedFilesProvider
lastToggledId,
setLastToggledId,
clearDeletedSelection,
expandedVersionIds,
setExpandedVersionIds,
}}
>
{children}
@@ -59,6 +59,8 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
setSelectedDeletedIds,
lastToggledId,
setLastToggledId,
expandedVersionIds,
setExpandedVersionIds,
} = useDeletedFilesContext()
const { can: canUpdateFiles } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*')
@@ -67,8 +69,6 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
parentObject: TrashObject
version: DeletedObjectVersion
}>()
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
const {
data: objects,
isPending,
@@ -173,7 +173,7 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
const orderedIds = filtered.flatMap((o) => {
const ids = [o.id]
if (expandedIds.has(o.id) && o.noncurrentVersions) {
if (expandedVersionIds.has(o.id) && o.noncurrentVersions) {
for (const v of o.noncurrentVersions) {
ids.push(versionKey(o.id, v.versionId))
}
@@ -223,15 +223,8 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
setVersionToDelete({ parentObject: parent, version })
}
// ── Expand / collapse all ───────────────────────────────────────────
const expandableIds = filtered
.filter((o) => o.noncurrentVersions && o.noncurrentVersions.length > 0)
.map((o) => o.id)
const isAllExpanded = expandableIds.length > 0 && expandableIds.every((id) => expandedIds.has(id))
const toggleExpanded = (id: string) => {
setExpandedIds((prev) => {
setExpandedVersionIds((prev) => {
const next = new Set(prev)
if (next.has(id)) {
next.delete(id)
@@ -242,9 +235,6 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
})
}
const expandAll = () => setExpandedIds(new Set(expandableIds))
const collapseAll = () => setExpandedIds(new Set())
const totalVersionCount = (object: TrashObject) => {
return 1 + (object.noncurrentVersions?.length ?? 0)
}
@@ -278,25 +268,7 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
<TableHead>Original location</TableHead>
<TableHead>Deleted</TableHead>
<TableHead className="text-right">Size</TableHead>
<TableHead className="text-right">
{expandableIds.length > 0 && (
<div className="flex items-center justify-end gap-x-2 text-xs font-normal">
<button
className="text-foreground-lighter hover:text-foreground transition-colors"
onClick={expandAll}
>
Expand all
</button>
<span className="text-border-stronger">·</span>
<button
className="text-foreground-lighter hover:text-foreground transition-colors"
onClick={collapseAll}
>
Collapse all
</button>
</div>
)}
</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
@@ -306,7 +278,7 @@ export const DeletedFilesList = ({ bucketId, searchString }: DeletedFilesListPro
selectedDeletedFile?.id === object.id && selectedDeletedVersion === undefined
const hasVersions =
object.noncurrentVersions !== undefined && object.noncurrentVersions.length > 0
const isExpanded = expandedIds.has(object.id)
const isExpanded = expandedVersionIds.has(object.id)
return (
<Fragment key={object.id}>
@@ -540,14 +512,14 @@ const NoncurrentVersionRow = ({
{/* Tree connector: vertical line */}
<div
className={cn(
'absolute left-[23px] w-px bg-foreground-muted pointer-events-none',
'absolute left-[23px] w-px bg-border-stronger pointer-events-none',
isLast ? 'top-0 h-1/2' : 'inset-y-0'
)}
/>
{/* Tree connector: horizontal branch */}
<div className="absolute left-[23px] top-1/2 h-px w-[11px] -translate-y-px bg-foreground-muted pointer-events-none" />
{/* Content aligned with parent row text */}
<div className="flex items-center gap-x-2 pl-[22px]">
<div className="absolute left-[23px] top-1/2 h-px w-[8px] -translate-y-px bg-border-stronger pointer-events-none" />
{/* Content aligned after horizontal branch with gap */}
<div className="flex items-center gap-x-2 pl-[36px]">
<span className="text-foreground-lighter font-mono text-xs">{shortId}</span>
<span className="text-foreground-muted text-xs">({version.action})</span>
</div>
@@ -3,6 +3,8 @@ import { compact, isEqual, noop } from 'lodash'
import {
ArrowLeft,
Check,
ChevronsDownUp,
ChevronsUpDown,
Columns,
Edit2,
FolderPlus,
@@ -50,6 +52,8 @@ import { useTrack } from '@/lib/telemetry/track'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useStorageExplorerStateSnapshot } from '@/state/storage-explorer'
import { useBucketTrashQuery } from '@/data/storage/protection/bucket-trash-query'
import { useSelectedBucket } from '../FilesBuckets/useSelectedBucket'
import { STORAGE_SORT_BY, STORAGE_SORT_BY_ORDER, STORAGE_VIEWS } from '../Storage.constants'
import { hasVersioningHistory } from '../StorageProtection.constants'
@@ -293,7 +297,19 @@ export const FileExplorerHeader = ({
const { data: bucket } = useSelectedBucket()
const isVersioned = hasVersioningHistory(bucket?.id)
const { isShowingDeleted, setIsShowingDeleted } = useDeletedFilesContext()
const { isShowingDeleted, setIsShowingDeleted, expandedVersionIds, setExpandedVersionIds } =
useDeletedFilesContext()
const { data: trashObjects } = useBucketTrashQuery({
projectRef: projectRef ?? undefined,
bucketId: bucket?.id,
})
const expandableIds = (trashObjects ?? [])
.filter((o) => o.noncurrentVersions && o.noncurrentVersions.length > 0)
.map((o) => o.id)
const hasExpandableVersions = expandableIds.length > 0
const isAllExpanded =
hasExpandableVersions && expandableIds.every((id) => expandedVersionIds.has(id))
const refreshData = async () => {
await refreshAll()
@@ -454,6 +470,29 @@ export const FileExplorerHeader = ({
)}
</div>
{isShowingDeleted && hasExpandableVersions && (
<div className="flex shrink-0 items-center gap-1">
<Button
size="tiny"
variant="outline"
icon={<ChevronsUpDown size={14} />}
onClick={() => setExpandedVersionIds(new Set(expandableIds))}
disabled={isAllExpanded}
>
Expand all
</Button>
<Button
size="tiny"
variant="outline"
icon={<ChevronsDownUp size={14} />}
onClick={() => setExpandedVersionIds(new Set())}
disabled={expandedVersionIds.size === 0}
>
Collapse all
</Button>
</div>
)}
{!isShowingDeleted && (
<div className="flex shrink-0 items-center gap-1">
<div className="hidden">
@@ -231,14 +231,14 @@ const NoncurrentVersionRow = ({
{/* Tree connector: vertical line */}
<div
className={cn(
'absolute left-[23px] w-px bg-foreground-muted pointer-events-none',
'absolute left-[23px] w-px bg-border-stronger pointer-events-none',
isLast ? 'top-0 h-1/2' : 'inset-y-0'
)}
/>
{/* Tree connector: horizontal branch */}
<div className="absolute left-[23px] top-1/2 h-px w-[11px] -translate-y-px bg-foreground-muted pointer-events-none" />
<div className="absolute left-[23px] top-1/2 h-px w-[8px] -translate-y-px bg-border-stronger pointer-events-none" />
{/* Content aligned with parent row text */}
<div className="flex items-center gap-x-2 pl-[22px]">
<div className="flex items-center gap-x-2 pl-[36px]">
<span className="text-foreground-lighter font-mono text-xs">{shortId}</span>
<span className="text-foreground-muted text-xs">({version.action})</span>
</div>