fix: show tablet actions without hover
Makes hover-only controls visible on tablets Preserves existing mobile and desktop layouts Covers chat, session sidebar, file, tab, and picker actions
This commit is contained in:
@@ -55,6 +55,7 @@ type Props = {
|
||||
lastRepoStatus: boolean;
|
||||
toggleGroupSessionLimit: (groupKey: string) => void;
|
||||
mobileVariant: boolean;
|
||||
alwaysShowActions: boolean;
|
||||
activeProjectId: string | null;
|
||||
setActiveProjectIdOnly: (id: string) => void;
|
||||
setActiveMainTab: (tab: MainTab) => void;
|
||||
@@ -120,6 +121,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
||||
lastRepoStatus,
|
||||
toggleGroupSessionLimit,
|
||||
mobileVariant,
|
||||
alwaysShowActions,
|
||||
activeProjectId,
|
||||
setActiveProjectIdOnly,
|
||||
setActiveMainTab,
|
||||
@@ -405,6 +407,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
||||
groupDirectory={group.directory}
|
||||
projectId={projectId}
|
||||
mobileVariant={mobileVariant}
|
||||
alwaysShowActions={alwaysShowActions}
|
||||
isRenaming={renamingFolderId === folder.id}
|
||||
renameDraft={renamingFolderId === folder.id ? renameFolderDraft : undefined}
|
||||
onRenameDraftChange={(value) => setRenameFolderDraft(value)}
|
||||
@@ -443,7 +446,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
||||
|
||||
const renderFolderItems = () => rootFolders.map(({ folder, nodes }) => renderOneFolderItem(folder, nodes, 0));
|
||||
const hasWorktreeDeleteAction = Boolean(!group.isMain && group.worktree);
|
||||
const groupHeaderRightPadding = mobileVariant
|
||||
const groupHeaderRightPadding = alwaysShowActions
|
||||
? (hasWorktreeDeleteAction ? 'pr-14' : 'pr-7')
|
||||
: isMinimalMode
|
||||
? (hasWorktreeDeleteAction
|
||||
@@ -534,10 +537,13 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
||||
<span className="inline-flex shrink-0 items-center gap-1 leading-none align-middle">
|
||||
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
||||
<RiGitBranchLine
|
||||
className="h-3.5 w-3.5 shrink-0 group-hover/gh:hidden"
|
||||
className={cn('h-3.5 w-3.5 shrink-0', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')}
|
||||
style={branchIconColor ? { color: branchIconColor } : undefined}
|
||||
/>
|
||||
<span className="hidden text-muted-foreground group-hover/gh:inline-flex h-3.5 w-3.5 items-center justify-center">
|
||||
<span className={cn(
|
||||
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
|
||||
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
|
||||
)}>
|
||||
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
||||
</span>
|
||||
</span>
|
||||
@@ -587,8 +593,11 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
||||
) : group.isArchivedBucket ? (
|
||||
<span className="inline-flex min-w-0 max-w-full items-center gap-1">
|
||||
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
||||
<RiArchiveLine className="h-3.5 w-3.5 shrink-0 text-muted-foreground group-hover/gh:hidden" />
|
||||
<span className="hidden text-muted-foreground group-hover/gh:inline-flex h-3.5 w-3.5 items-center justify-center">
|
||||
<RiArchiveLine className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')} />
|
||||
<span className={cn(
|
||||
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
|
||||
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
|
||||
)}>
|
||||
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
||||
</span>
|
||||
</span>
|
||||
@@ -598,10 +607,13 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
||||
<span className="inline-flex min-w-0 max-w-full items-center gap-1">
|
||||
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center">
|
||||
<RiGitBranchLine
|
||||
className="h-3.5 w-3.5 shrink-0 text-muted-foreground group-hover/gh:hidden"
|
||||
className={cn('h-3.5 w-3.5 shrink-0 text-muted-foreground', alwaysShowActions ? 'hidden' : 'group-hover/gh:hidden')}
|
||||
style={branchIconColor ? { color: branchIconColor } : undefined}
|
||||
/>
|
||||
<span className="hidden text-muted-foreground group-hover/gh:inline-flex h-3.5 w-3.5 items-center justify-center">
|
||||
<span className={cn(
|
||||
'text-muted-foreground h-3.5 w-3.5 items-center justify-center',
|
||||
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/gh:inline-flex',
|
||||
)}>
|
||||
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
||||
</span>
|
||||
</span>
|
||||
@@ -671,7 +683,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
||||
</div>
|
||||
</div>
|
||||
{group.isArchivedBucket && allGroupSessions.length > 0 ? (
|
||||
<div className={cn('absolute right-0.5 top-1/2 -translate-y-1/2 z-10 transition-opacity', mobileVariant ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
||||
<div className={cn('absolute right-0.5 top-1/2 -translate-y-1/2 z-10 transition-opacity', alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
@@ -694,7 +706,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
||||
</div>
|
||||
) : null}
|
||||
{group.directory && !group.isMain && group.worktree ? (
|
||||
<div className={cn('absolute right-7 top-1/2 -translate-y-1/2 z-10 transition-opacity', mobileVariant ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
||||
<div className={cn('absolute right-7 top-1/2 -translate-y-1/2 z-10 transition-opacity', alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
@@ -718,7 +730,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
||||
</div>
|
||||
) : null}
|
||||
{group.directory ? (
|
||||
<div className={cn('absolute right-0.5 top-1/2 -translate-y-1/2 z-10 transition-opacity', mobileVariant ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
||||
<div className={cn('absolute right-0.5 top-1/2 -translate-y-1/2 z-10 transition-opacity', alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/gh:opacity-100 group-focus-within/gh:opacity-100')}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
|
||||
@@ -95,6 +95,7 @@ type Props = {
|
||||
openContextPanelTab: (directory: string, options: { mode: 'chat'; dedupeKey: string; label: string }) => void;
|
||||
handleDeleteSession: (session: Session, source?: { archivedBucket?: boolean }) => void;
|
||||
mobileVariant: boolean;
|
||||
alwaysShowActions: boolean;
|
||||
renderSessionNode: (node: SessionNode, depth?: number, groupDirectory?: string | null, projectId?: string | null, archivedBucket?: boolean, secondaryMeta?: SecondaryMeta | null, renderContext?: 'project' | 'recent') => React.ReactNode;
|
||||
secondaryMeta?: SecondaryMeta | null;
|
||||
renderContext?: 'project' | 'recent';
|
||||
@@ -207,6 +208,7 @@ const areEqual = (prev: Props, next: Props): boolean => {
|
||||
if ((prev.secondaryMeta?.projectLabel ?? null) !== (next.secondaryMeta?.projectLabel ?? null)) return false;
|
||||
if ((prev.secondaryMeta?.branchLabel ?? null) !== (next.secondaryMeta?.branchLabel ?? null)) return false;
|
||||
if (prev.mobileVariant !== next.mobileVariant) return false;
|
||||
if (prev.alwaysShowActions !== next.alwaysShowActions) return false;
|
||||
if ((prev.renderContext ?? 'project') !== (next.renderContext ?? 'project')) return false;
|
||||
if (prev.renamingFolderId !== next.renamingFolderId) return false;
|
||||
|
||||
@@ -253,6 +255,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
openContextPanelTab,
|
||||
handleDeleteSession,
|
||||
mobileVariant,
|
||||
alwaysShowActions,
|
||||
renderSessionNode,
|
||||
secondaryMeta,
|
||||
renderContext = 'project',
|
||||
@@ -500,7 +503,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
'pointer-events-none absolute inline-flex h-3.5 items-center justify-center gap-0.5 transition-opacity',
|
||||
isMinimalMode ? 'top-1/2 -translate-y-1/2' : 'top-[14.5px] -translate-y-1/2',
|
||||
showStatusMarker && isPinnedSession ? 'left-[-18px] w-6' : 'left-[-10px] w-3.5',
|
||||
hasChildren ? 'opacity-100 group-hover:opacity-0 group-focus-within:opacity-0' : '',
|
||||
hasChildren && !alwaysShowActions ? 'opacity-100 group-hover:opacity-0 group-focus-within:opacity-0' : '',
|
||||
)}
|
||||
>
|
||||
{showStatusMarker ? statusMarkerContent : null}
|
||||
@@ -525,7 +528,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
className={cn(
|
||||
'absolute left-[-10px] inline-flex h-3.5 w-3.5 items-center justify-center rounded-md text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 transition-opacity',
|
||||
isMinimalMode ? 'top-1/2 -translate-y-1/2' : 'top-[14.5px] -translate-y-1/2',
|
||||
isMinimalMode && showStatusMarker
|
||||
isMinimalMode && showStatusMarker && !alwaysShowActions
|
||||
? 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto'
|
||||
: '',
|
||||
)}
|
||||
@@ -741,15 +744,15 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
className={cn(
|
||||
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
|
||||
isTouchPressed && 'bg-interactive-hover/70',
|
||||
mobileVariant
|
||||
alwaysShowActions
|
||||
? (isVSCode ? revealPaddingClass : 'pr-7')
|
||||
: revealPaddingClass,
|
||||
)}
|
||||
>
|
||||
<div className={cn('flex w-full items-center min-w-0 flex-1 overflow-hidden', isMinimalMode ? 'gap-1' : 'gap-1')}>
|
||||
<div className={cn('block min-w-0 flex-1 truncate typography-ui-label font-normal', isActive ? 'text-primary' : 'text-foreground')}>{renderHighlightedText(sessionTitle, normalizedSessionSearchQuery)}</div>
|
||||
{mobileVariant ? <span className="ml-2 flex-shrink-0 text-[0.72rem] text-muted-foreground/75">{sessionCompactUpdatedLabel}</span> : null}
|
||||
{!mobileVariant ? (
|
||||
{alwaysShowActions ? <span className="ml-2 flex-shrink-0 text-[0.72rem] text-muted-foreground/75">{sessionCompactUpdatedLabel}</span> : null}
|
||||
{!alwaysShowActions ? (
|
||||
<div className="relative ml-1 flex h-4 min-w-4 flex-shrink-0 items-center justify-end">
|
||||
<span className={cn(
|
||||
'whitespace-nowrap text-right text-[0.72rem] text-muted-foreground/75 transition-opacity duration-150',
|
||||
@@ -805,7 +808,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
className={cn(
|
||||
'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]',
|
||||
isTouchPressed && 'bg-interactive-hover/70',
|
||||
mobileVariant
|
||||
alwaysShowActions
|
||||
? (isVSCode ? revealPaddingClass : 'pr-7')
|
||||
: revealPaddingClass
|
||||
)}
|
||||
@@ -844,7 +847,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
'absolute right-0 top-1/2 z-10 -translate-y-1/2 transition-opacity',
|
||||
isMenuOpen
|
||||
? 'opacity-100'
|
||||
: (mobileVariant && !isVSCode)
|
||||
: (alwaysShowActions && !isVSCode)
|
||||
? 'opacity-100'
|
||||
: cn('opacity-0', revealOnHoverClass),
|
||||
)}>
|
||||
@@ -854,7 +857,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
type="button"
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 transition-opacity',
|
||||
isMinimalMode && !mobileVariant
|
||||
isMinimalMode && !alwaysShowActions
|
||||
? (isMenuOpen
|
||||
? 'h-4 w-4 opacity-100'
|
||||
: cn('h-4 w-4 opacity-0', revealOnHoverClass))
|
||||
@@ -866,7 +869,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
onClick={handleMenuTriggerClick}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<RiMore2Line className={cn(isMinimalMode && !mobileVariant ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
|
||||
<RiMore2Line className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
{sessionMenuContent}
|
||||
|
||||
@@ -47,6 +47,7 @@ type Props = {
|
||||
isDesktopShellRuntime: boolean;
|
||||
stuckProjectHeaders: Set<string>;
|
||||
mobileVariant: boolean;
|
||||
alwaysShowActions: boolean;
|
||||
toggleProject: (id: string) => void;
|
||||
setActiveProjectIdOnly: (id: string) => void;
|
||||
setActiveMainTab: (tab: 'chat' | 'plan' | 'git' | 'diff' | 'terminal' | 'files') => void;
|
||||
@@ -168,6 +169,7 @@ export function SidebarProjectsList(props: Props): React.ReactNode {
|
||||
isStuck={props.stuckProjectHeaders.has(projectKey)}
|
||||
hideDirectoryControls={props.hideDirectoryControls}
|
||||
mobileVariant={props.mobileVariant}
|
||||
alwaysShowActions={props.alwaysShowActions}
|
||||
onToggle={() => props.toggleProject(projectKey)}
|
||||
onNewSession={() => {
|
||||
if (projectKey !== props.activeProjectId) props.setActiveProjectIdOnly(projectKey);
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface SortableProjectItemProps {
|
||||
isStuck: boolean;
|
||||
hideDirectoryControls: boolean;
|
||||
mobileVariant: boolean;
|
||||
alwaysShowActions: boolean;
|
||||
onToggle: () => void;
|
||||
onNewSession: () => void;
|
||||
onNewWorktreeSession?: () => void;
|
||||
@@ -70,7 +71,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
isDesktopShell,
|
||||
isStuck,
|
||||
hideDirectoryControls,
|
||||
mobileVariant,
|
||||
alwaysShowActions,
|
||||
onToggle,
|
||||
onNewSession,
|
||||
onNewWorktreeSession,
|
||||
@@ -176,17 +177,23 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
className={cn(
|
||||
'flex-1 min-w-0 flex items-center gap-1.5 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 rounded-md cursor-grab active:cursor-grabbing transition-[padding]',
|
||||
isRepo && !hideDirectoryControls
|
||||
? (mobileVariant ? 'pr-20' : 'pr-7 group-hover/project:pr-20 group-focus-within/project:pr-20')
|
||||
: (mobileVariant ? 'pr-14' : 'pr-7 group-hover/project:pr-14 group-focus-within/project:pr-14'),
|
||||
? (alwaysShowActions ? 'pr-20' : 'pr-7 group-hover/project:pr-20 group-focus-within/project:pr-20')
|
||||
: (alwaysShowActions ? 'pr-14' : 'pr-7 group-hover/project:pr-14 group-focus-within/project:pr-14'),
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
||||
<span className="hidden h-3.5 w-3.5 items-center justify-center text-muted-foreground group-hover/project:inline-flex group-focus-within/project:inline-flex">
|
||||
<span className={cn(
|
||||
'h-3.5 w-3.5 items-center justify-center text-muted-foreground',
|
||||
alwaysShowActions ? 'inline-flex' : 'hidden group-hover/project:inline-flex group-focus-within/project:inline-flex',
|
||||
)}>
|
||||
{isCollapsed ? <RiArrowRightSLine className="h-3.5 w-3.5" /> : <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
||||
</span>
|
||||
{imageUrl ? (
|
||||
<span
|
||||
className="inline-flex h-3.5 w-3.5 items-center justify-center overflow-hidden rounded-[3px] group-hover/project:hidden group-focus-within/project:hidden"
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 items-center justify-center overflow-hidden rounded-[3px]',
|
||||
alwaysShowActions ? 'hidden' : 'inline-flex group-hover/project:hidden group-focus-within/project:hidden',
|
||||
)}
|
||||
style={projectIconBackground ? { backgroundColor: projectIconBackground } : undefined}
|
||||
>
|
||||
<img
|
||||
@@ -198,9 +205,9 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
/>
|
||||
</span>
|
||||
) : ProjectIcon ? (
|
||||
<ProjectIcon className="h-3.5 w-3.5 group-hover/project:hidden group-focus-within/project:hidden" style={iconColor ? { color: iconColor } : undefined} />
|
||||
<ProjectIcon className={cn('h-3.5 w-3.5', alwaysShowActions ? 'hidden' : 'group-hover/project:hidden group-focus-within/project:hidden')} style={iconColor ? { color: iconColor } : undefined} />
|
||||
) : (
|
||||
<RiFolderLine className="h-3.5 w-3.5 text-muted-foreground/80 group-hover/project:hidden group-focus-within/project:hidden" style={iconColor ? { color: iconColor } : undefined} />
|
||||
<RiFolderLine className={cn('h-3.5 w-3.5 text-muted-foreground/80', alwaysShowActions ? 'hidden' : 'group-hover/project:hidden group-focus-within/project:hidden')} style={iconColor ? { color: iconColor } : undefined} />
|
||||
)}
|
||||
</span>
|
||||
<span className={cn(
|
||||
@@ -231,7 +238,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
}}
|
||||
className={cn(
|
||||
'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground transition-opacity',
|
||||
mobileVariant ? 'opacity-100' : 'opacity-0 pointer-events-none group-hover/project:opacity-100 group-hover/project:pointer-events-auto group-focus-within/project:opacity-100 group-focus-within/project:pointer-events-auto',
|
||||
alwaysShowActions ? 'opacity-100' : 'opacity-0 pointer-events-none group-hover/project:opacity-100 group-hover/project:pointer-events-auto group-focus-within/project:opacity-100 group-focus-within/project:pointer-events-auto',
|
||||
)}
|
||||
aria-label={t('sessions.sidebar.project.actions.newWorktree')}
|
||||
>
|
||||
@@ -255,7 +262,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground',
|
||||
isMenuOpen
|
||||
? 'opacity-100 pointer-events-auto'
|
||||
: mobileVariant
|
||||
: alwaysShowActions
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 pointer-events-none group-hover/project:opacity-100 group-hover/project:pointer-events-auto group-focus-within/project:opacity-100 group-focus-within/project:pointer-events-auto',
|
||||
)}
|
||||
@@ -301,7 +308,7 @@ export const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
}}
|
||||
className={cn(
|
||||
'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 transition-opacity',
|
||||
mobileVariant ? 'opacity-100' : 'opacity-0 pointer-events-none group-hover/project:opacity-100 group-hover/project:pointer-events-auto group-focus-within/project:opacity-100 group-focus-within/project:pointer-events-auto',
|
||||
alwaysShowActions ? 'opacity-100' : 'opacity-0 pointer-events-none group-hover/project:opacity-100 group-hover/project:pointer-events-auto group-focus-within/project:opacity-100 group-focus-within/project:pointer-events-auto',
|
||||
)}
|
||||
aria-label={isRepo
|
||||
? t('sessions.sidebar.project.actions.newDraftSession')
|
||||
|
||||
Reference in New Issue
Block a user