feat(ui): sidebar redesign — project zones, grouping modes, full-page surfaces (#2480)
* checkpoint: flatten sidebar core (flat sessions, zones, single mode, folders flat) * checkpoint: sidebar nav + scheduled/archive full-page surfaces, recent zone header * checkpoint: worktrees management surface via project menu * checkpoint: docs, i18n, validation for sidebar redesign * checkpoint: unified row/zone geometry, recent backfill, tooltips everywhere, primary spinner * checkpoint: branch icon marker, date moved to rich tooltip, recent back to pure time window * checkpoint: PR state on branch markers + tooltip, no reserved right space, aligned show-more, instant tooltips * checkpoint: reserve hover-action space so title text is not overlapped * checkpoint: tighten hover-action reserve * checkpoint: color-only unread emphasis to avoid title reflow * checkpoint: drop new-subfolder action, folder actions overlay on hover * checkpoint: fix sticky project headers (sticky on trigger div), stuck elevation * checkpoint: full-bleed semibold zone headers, no top scroll fade * checkpoint: headers without background tint (typography-only emphasis) * checkpoint: recent header flush with scroll top (no pre-stick bump) * checkpoint: shared tooltip provider with grouping (instant handoff between rows) * checkpoint: blur pointer-click focus so hover chrome hides on mouse-leave * checkpoint: tooltip closeDelay bridges inter-row gap * checkpoint: nav above controls, merged view dropdown, project-scoped bulk selection, cross-worktree folders * checkpoint: folder header tooltip with full path name * checkpoint: true page surfaces (hidden chat, header title, close-on-select), multirun page, run-now jump, folders on top, controls row polish * checkpoint: rename mode — dual-instance outside-click fix, no vertical shift * checkpoint: session grouping mode toggle (by-worktree default, flat option) * checkpoint: worktree header — hover padding reserve + delete worktree action * checkpoint: frosted sticky header backing under desktop vibrancy * checkpoint: align worktree sub-header with project header icon column * checkpoint: restore worktree group DnD reorder; dense vibrancy header tint (Chromium mask+backdrop-filter) * checkpoint: vibrancy — drop scroller mask so backdrop-filter samples rows (Chromium backdrop-root limitation) * checkpoint: vibrancy headers use opaque sidebar tone (Electron transparent-window backdrop-filter bug) * checkpoint: nav collapsed to one row (New session + surface icons), mirrors Add project row * checkpoint: raise sticky zone headers above row action layers (z-20) * checkpoint: New session as full-width CTA + single quiet toolbar row * checkpoint: New session row back to quiet text form above the toolbar * checkpoint: align toolbar left icon with New session icon column * checkpoint: hoverless flat header controls (color-only hover states) * checkpoint: sticky project headers toggle in view dropdown (default on) * checkpoint: full-page surfaces adapted — archive directory filter panel, scheduled master-detail, multirun without duplicate title bar * checkpoint: multirun joins mutually exclusive surface set * checkpoint: surfaces leave via navigation only (no close/cancel buttons), robust close-on-new-session, drop dead MultiRunWindow * checkpoint: no scheduled header description, aligned empty-worktree note, collapse/expand covers worktree groups * checkpoint: overlay scrollbar above sticky zone headers * checkpoint: archive delete icons reveal on hover with padding shift * checkpoint: new worktrees surface at top of the worktree list * checkpoint: no grab cursor on worktree headers * checkpoint: worktrees page list-only with inline action, no worktrees in edit dialog, drop menu ellipsis * checkpoint: worktrees page uses full content width * checkpoint: worktrees header — 'in' instead of em dash, no description * checkpoint: drop legacy OpenCode badge from worktree list, tooltip without OpenCode mention * review: bound pr summary cache
This commit is contained in:
committed by
GitHub
parent
5787ea5d49
commit
1291cde5c2
@@ -3,10 +3,17 @@ import { cn } from '@/lib/utils';
|
||||
import type { SessionFolder } from '@/stores/useSessionFoldersStore';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import type { SessionNodeChildRenderExtras, SessionNodeRenderExtras } from './sidebar/sessionNodeItemUtils';
|
||||
|
||||
interface SessionFolderItemProps<TSessionNode> {
|
||||
folder: SessionFolder;
|
||||
/**
|
||||
* Optional display label override. Flat folder rendering shows nested
|
||||
* folders at the top level with a "Parent / Child" path instead of
|
||||
* indentation.
|
||||
*/
|
||||
displayName?: string;
|
||||
sessions: TSessionNode[];
|
||||
/** Sub-folders that belong directly to this folder */
|
||||
subFolderItems?: React.ReactNode;
|
||||
@@ -46,8 +53,6 @@ interface SessionFolderItemProps<TSessionNode> {
|
||||
isDropTarget?: boolean;
|
||||
/** Create a new session scoped to this folder */
|
||||
onNewSession?: () => void;
|
||||
/** Create a new sub-folder inside this folder */
|
||||
onNewSubFolder?: () => void;
|
||||
/** Visual indent depth (0 = root folder, 1 = sub-folder) */
|
||||
depth?: number;
|
||||
/** Hide folder action buttons (rename/delete/new) */
|
||||
@@ -58,6 +63,7 @@ interface SessionFolderItemProps<TSessionNode> {
|
||||
|
||||
const SessionFolderItemBase = <TSessionNode,>({
|
||||
folder,
|
||||
displayName,
|
||||
sessions,
|
||||
subFolderItems,
|
||||
isCollapsed,
|
||||
@@ -78,7 +84,6 @@ const SessionFolderItemBase = <TSessionNode,>({
|
||||
droppableRef,
|
||||
isDropTarget = false,
|
||||
onNewSession,
|
||||
onNewSubFolder,
|
||||
depth = 0,
|
||||
hideActions = false,
|
||||
archivedBucket = false,
|
||||
@@ -145,10 +150,10 @@ const SessionFolderItemBase = <TSessionNode,>({
|
||||
}, [isRenaming]);
|
||||
|
||||
const folderIconName = isCollapsed ? 'folder' : 'folder-open';
|
||||
const isSubFolder = depth > 0;
|
||||
void depth;
|
||||
|
||||
return (
|
||||
<div className={cn('oc-folder', isSubFolder && 'ml-3')}>
|
||||
<div className="oc-folder">
|
||||
{/* Folder header – also acts as a drop zone when droppableRef is provided */}
|
||||
<div
|
||||
ref={droppableRef}
|
||||
@@ -157,7 +162,10 @@ const SessionFolderItemBase = <TSessionNode,>({
|
||||
'cursor-pointer',
|
||||
isDropTarget && 'bg-primary/10 ring-1 ring-inset ring-primary/30',
|
||||
)}
|
||||
onClick={renaming ? undefined : onToggle}
|
||||
onClick={renaming ? undefined : (event) => {
|
||||
(event.currentTarget as HTMLElement).blur();
|
||||
onToggle();
|
||||
}}
|
||||
role={renaming ? undefined : 'button'}
|
||||
tabIndex={renaming ? undefined : 0}
|
||||
onKeyDown={
|
||||
@@ -178,7 +186,10 @@ const SessionFolderItemBase = <TSessionNode,>({
|
||||
'min-w-0 flex items-center gap-1.5 pl-1.5 flex-1 transition-[padding]',
|
||||
archivedBucket
|
||||
? (alwaysShowActions ? 'pr-7' : 'group-hover/folder:pr-7 group-focus-within/folder:pr-7')
|
||||
: '',
|
||||
// Actions overlay on hover (new session, rename, delete = three
|
||||
// 24px buttons anchored at the right edge); reserve room only
|
||||
// while they are revealed, mirroring session-row behavior.
|
||||
: (alwaysShowActions ? 'pr-20' : 'group-hover/folder:pr-20 group-focus-within/folder:pr-20'),
|
||||
)}>
|
||||
<Icon name={folderIconName} className={cn('h-3.5 w-3.5 flex-shrink-0', isDropTarget ? 'text-primary' : 'text-muted-foreground')} />
|
||||
|
||||
@@ -238,9 +249,16 @@ const SessionFolderItemBase = <TSessionNode,>({
|
||||
</form>
|
||||
) : (
|
||||
<div className="min-w-0 flex items-center gap-1.5 flex-1">
|
||||
<span className={cn('typography-ui-label font-semibold truncate', isDropTarget ? 'text-primary' : 'text-muted-foreground')}>
|
||||
{folder.name}
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className={cn('typography-ui-label font-semibold truncate', isDropTarget ? 'text-primary' : 'text-muted-foreground')}>
|
||||
{displayName ?? folder.name}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8} className="max-w-xs">
|
||||
{displayName ?? folder.name}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="typography-micro text-muted-foreground/70 flex-shrink-0">
|
||||
• {sessions.length}
|
||||
</span>
|
||||
@@ -256,14 +274,15 @@ const SessionFolderItemBase = <TSessionNode,>({
|
||||
|
||||
{/* Action buttons */}
|
||||
{!renaming && (!hideActions || archivedBucket) ? (
|
||||
<div className="flex items-center gap-0.5 px-0.5">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-0.5 transition-opacity',
|
||||
alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/folder:opacity-100 group-focus-within/folder:opacity-100',
|
||||
archivedBucket && 'absolute right-0.5 top-1/2 z-10 -translate-y-1/2 px-0',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0.5 top-1/2 z-10 flex -translate-y-1/2 items-center gap-0.5 transition-opacity',
|
||||
alwaysShowActions
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 pointer-events-none group-hover/folder:opacity-100 group-hover/folder:pointer-events-auto group-focus-within/folder:opacity-100 group-focus-within/folder:pointer-events-auto',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-0.5">
|
||||
{!archivedBucket && onNewSession ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -278,21 +297,6 @@ const SessionFolderItemBase = <TSessionNode,>({
|
||||
<Icon name="add" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
{/* Only allow sub-folders at depth 0 (one level deep max) */}
|
||||
{!archivedBucket && onNewSubFolder && depth === 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onNewSubFolder();
|
||||
}}
|
||||
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
aria-label={t('sessions.sidebar.folderItem.newSubfolderAria', { folderName: folder.name })}
|
||||
title={t('sessions.sidebar.folderItem.newSubfolder')}
|
||||
>
|
||||
<Icon name="folder-add" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
{!archivedBucket ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -326,7 +330,7 @@ const SessionFolderItemBase = <TSessionNode,>({
|
||||
|
||||
{/* Folder body */}
|
||||
{!isCollapsed ? (
|
||||
<div className="pb-1 pl-2">
|
||||
<div className="pb-1">
|
||||
{/* Sub-folders first */}
|
||||
{subFolderItems}
|
||||
{/* Then sessions */}
|
||||
|
||||
Reference in New Issue
Block a user