feat(nav-rail): add expand/collapse toggle with project names and settings control (#511)
* feat(nav-rail): add expand/collapse toggle with project names and settings control The NavRail introduced in v1.7.5 only shows project icons without names, making it difficult to identify which project is active when multiple projects share similar icons or use letter avatars. Changes: - Add expandable NavRail that shows full project names alongside icons - Default to expanded when multiple projects are open - Auto-collapse to icon-only mode with a single project (nothing to differentiate) - Add toggle button at bottom of rail with Cmd+Shift+E keyboard shortcut - Highlight active project with interactive.selection background for clarity - Show keyboard shortcut hints in tooltip and expanded label - Add 'Expand project rail' checkbox in Settings > Appearance > Navigation - Persist expansion state across sessions Fixes: NavRail project icons are ambiguous without visible project names * feat: improve nav rail toggle and navigation settings - Make nav rail collapsed by default with smoother text fade behavior - Move nav rail and terminal quick keys controls into a dedicated Navigation section - Update keyboard shortcuts for logs/status and nav rail expand/collapse --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
a59bf010bb
commit
9a2af4c4d9
@@ -22,6 +22,8 @@ import {
|
||||
RiInformationLine,
|
||||
RiPencilLine,
|
||||
RiCloseLine,
|
||||
RiMenuFoldLine,
|
||||
RiMenuUnfoldLine,
|
||||
} from '@remixicon/react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -44,6 +46,7 @@ import { cn, formatDirectoryName, hasModifier } from '@/lib/utils';
|
||||
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP } from '@/lib/projectMeta';
|
||||
import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell } from '@/lib/desktop';
|
||||
import { useLongPress } from '@/hooks/useLongPress';
|
||||
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
||||
import { sessionEvents } from '@/lib/sessionEvents';
|
||||
import type { ProjectEntry } from '@/lib/api/types';
|
||||
|
||||
@@ -54,6 +57,10 @@ const normalize = (value: string): string => {
|
||||
};
|
||||
|
||||
const NAV_RAIL_WIDTH = 56;
|
||||
const NAV_RAIL_EXPANDED_WIDTH = 200;
|
||||
const NAV_RAIL_TEXT_FADE_MS = 180;
|
||||
const PROJECT_TEXT_FADE_IN_DELAY_MS = 24;
|
||||
const ACTION_TEXT_FADE_IN_DELAY_MS = 60;
|
||||
|
||||
/** Tinted background for project tiles — uses project color at low opacity, or neutral fallback */
|
||||
const TileBackground: React.FC<{ colorVar: string | null; children: React.ReactNode }> = ({
|
||||
@@ -129,10 +136,12 @@ const ProjectTile: React.FC<{
|
||||
hasStreaming: boolean;
|
||||
hasUnread: boolean;
|
||||
label: string;
|
||||
expanded: boolean;
|
||||
projectTextVisible: boolean;
|
||||
onClick: () => void;
|
||||
onEdit: () => void;
|
||||
onClose: () => void;
|
||||
}> = ({ project, isActive, hasStreaming, hasUnread, label, onClick, onEdit, onClose }) => {
|
||||
}> = ({ project, isActive, hasStreaming, hasUnread, label, expanded, projectTextVisible, onClick, onEdit, onClose }) => {
|
||||
const [menuOpen, setMenuOpen] = React.useState(false);
|
||||
const ProjectIcon = project.icon ? PROJECT_ICON_MAP[project.icon] : null;
|
||||
const projectColorVar = project.color ? (PROJECT_COLOR_MAP[project.color] ?? null) : null;
|
||||
@@ -144,94 +153,112 @@ const ProjectTile: React.FC<{
|
||||
onTap: onClick,
|
||||
});
|
||||
|
||||
const iconElement = (
|
||||
<TileBackground colorVar={projectColorVar}>
|
||||
<span className="relative h-full w-full leading-none">
|
||||
<span className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
{ProjectIcon ? (
|
||||
<ProjectIcon
|
||||
className="h-4 w-4 shrink-0"
|
||||
style={projectColorVar ? { color: projectColorVar } : { color: 'var(--surface-foreground)' }}
|
||||
/>
|
||||
) : (
|
||||
<LetterAvatar label={label} color={project.color} />
|
||||
)}
|
||||
</span>
|
||||
{showStreamingDots && (
|
||||
<span className="pointer-events-none absolute inset-x-0 top-[calc(50%+9px)] flex justify-center">
|
||||
<ProjectStatusDots color="var(--primary)" variant="streaming" />
|
||||
</span>
|
||||
)}
|
||||
{showAttentionDots && (
|
||||
<span className="pointer-events-none absolute inset-x-0 top-[calc(50%+9px)] flex justify-center">
|
||||
<ProjectStatusDots color="var(--status-info)" variant="attention" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</TileBackground>
|
||||
);
|
||||
|
||||
const tileButton = (
|
||||
<button
|
||||
type="button"
|
||||
{...longPressHandlers}
|
||||
className={cn(
|
||||
'group relative flex items-center rounded-lg overflow-hidden cursor-default',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)]',
|
||||
expanded ? 'h-9 w-full gap-2.5 pr-1.5 pl-[7px]' : 'h-9 w-9 justify-center',
|
||||
!expanded && (
|
||||
isActive
|
||||
? 'bg-transparent border border-[var(--surface-foreground)]'
|
||||
: 'bg-transparent border border-transparent hover:bg-[var(--interactive-hover)]/50 hover:border-[var(--interactive-border)]'
|
||||
),
|
||||
!expanded && menuOpen && !isActive && 'bg-[var(--interactive-hover)]/50 border-[var(--interactive-border)]',
|
||||
)}
|
||||
>
|
||||
{expanded && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-y-0 left-[6px] right-[5px] rounded-lg border transition-colors',
|
||||
isActive
|
||||
? 'bg-[var(--interactive-selection)] border-[var(--interactive-border)]'
|
||||
: 'bg-transparent border-transparent group-hover:bg-[var(--interactive-hover)]/50 group-hover:border-[var(--interactive-border)]',
|
||||
menuOpen && !isActive && 'bg-[var(--interactive-hover)]/50 border-[var(--interactive-border)]',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span className="flex size-[34px] basis-[34px] shrink-0 grow-0 items-center justify-center">
|
||||
{iconElement}
|
||||
</span>
|
||||
<span
|
||||
aria-hidden={!projectTextVisible}
|
||||
className={cn(
|
||||
'min-w-0 truncate text-left text-[13px] leading-tight transition-opacity duration-[180ms] ease-in-out',
|
||||
expanded ? 'flex-1' : 'w-0 flex-none',
|
||||
projectTextVisible ? 'opacity-100' : 'opacity-0',
|
||||
isActive && expanded ? 'font-medium text-[var(--interactive-selection-foreground)]' : 'text-[var(--surface-foreground)]',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="relative"
|
||||
onContextMenu={(e) => {
|
||||
// Only handle right-click (desktop), not long-tap (mobile)
|
||||
if (e.nativeEvent instanceof MouseEvent && e.nativeEvent.button === 2) {
|
||||
e.preventDefault();
|
||||
setMenuOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{hasStreaming ? (
|
||||
<button
|
||||
type="button"
|
||||
{...longPressHandlers}
|
||||
className={cn(
|
||||
'flex h-9 w-9 items-center justify-center rounded-lg overflow-hidden cursor-default',
|
||||
isActive
|
||||
? 'bg-transparent border border-[var(--surface-foreground)]'
|
||||
: 'bg-transparent border border-transparent hover:bg-[var(--interactive-hover)]/50 hover:border-[var(--interactive-border)]',
|
||||
menuOpen && !isActive && 'bg-[var(--interactive-hover)]/50 border-[var(--interactive-border)]',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)]',
|
||||
)}
|
||||
>
|
||||
<TileBackground colorVar={projectColorVar}>
|
||||
<span className="relative h-full w-full leading-none">
|
||||
<span className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
{ProjectIcon ? (
|
||||
<ProjectIcon
|
||||
className="h-4 w-4 shrink-0"
|
||||
style={projectColorVar ? { color: projectColorVar } : { color: 'var(--surface-foreground)' }}
|
||||
/>
|
||||
) : (
|
||||
<LetterAvatar label={label} color={project.color} />
|
||||
)}
|
||||
</span>
|
||||
{showStreamingDots && (
|
||||
<span className="pointer-events-none absolute inset-x-0 top-[calc(50%+9px)] flex justify-center">
|
||||
<ProjectStatusDots color="var(--primary)" variant="streaming" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</TileBackground>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
{...longPressHandlers}
|
||||
className={cn(
|
||||
'flex h-9 w-9 items-center justify-center rounded-lg overflow-hidden cursor-default',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)]',
|
||||
isActive
|
||||
? 'bg-transparent border border-[var(--surface-foreground)]'
|
||||
: 'bg-transparent border border-transparent hover:bg-[var(--interactive-hover)]/50 hover:border-[var(--interactive-border)]',
|
||||
menuOpen && !isActive && 'bg-[var(--interactive-hover)]/50 border-[var(--interactive-border)]',
|
||||
)}
|
||||
>
|
||||
<TileBackground colorVar={projectColorVar}>
|
||||
<span className="relative h-full w-full leading-none">
|
||||
<span className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
{ProjectIcon ? (
|
||||
<ProjectIcon
|
||||
className="h-4 w-4 shrink-0"
|
||||
style={projectColorVar ? { color: projectColorVar } : { color: 'var(--surface-foreground)' }}
|
||||
/>
|
||||
) : (
|
||||
<LetterAvatar label={label} color={project.color} />
|
||||
)}
|
||||
</span>
|
||||
{showAttentionDots && (
|
||||
<span className="pointer-events-none absolute inset-x-0 top-[calc(50%+9px)] flex justify-center">
|
||||
<ProjectStatusDots color="var(--status-info)" variant="attention" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</TileBackground>
|
||||
</button>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
{label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{expanded ? (
|
||||
<div
|
||||
className="relative w-full"
|
||||
onContextMenu={(e) => {
|
||||
if (e.nativeEvent instanceof MouseEvent && e.nativeEvent.button === 2) {
|
||||
e.preventDefault();
|
||||
setMenuOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tileButton}
|
||||
</div>
|
||||
) : (
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="relative"
|
||||
onContextMenu={(e) => {
|
||||
if (e.nativeEvent instanceof MouseEvent && e.nativeEvent.button === 2) {
|
||||
e.preventDefault();
|
||||
setMenuOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tileButton}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
{label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<span className="sr-only">Project options</span>
|
||||
@@ -308,6 +335,45 @@ export const NavRail: React.FC<NavRailProps> = ({ className, mobile }) => {
|
||||
const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen);
|
||||
const setAboutDialogOpen = useUIStore((s) => s.setAboutDialogOpen);
|
||||
const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog);
|
||||
const isNavRailExpanded = useUIStore((s) => s.isNavRailExpanded);
|
||||
const toggleNavRail = useUIStore((s) => s.toggleNavRail);
|
||||
const shortcutOverrides = useUIStore((s) => s.shortcutOverrides);
|
||||
const expanded = !mobile && isNavRailExpanded;
|
||||
const [showExpandedContent, setShowExpandedContent] = React.useState(expanded);
|
||||
const [projectTextVisible, setProjectTextVisible] = React.useState(expanded);
|
||||
const [actionTextVisible, setActionTextVisible] = React.useState(expanded);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (expanded) {
|
||||
setShowExpandedContent(true);
|
||||
setProjectTextVisible(false);
|
||||
setActionTextVisible(false);
|
||||
const projectTimer = window.setTimeout(() => {
|
||||
setProjectTextVisible(true);
|
||||
}, PROJECT_TEXT_FADE_IN_DELAY_MS);
|
||||
const actionTimer = window.setTimeout(() => {
|
||||
setActionTextVisible(true);
|
||||
}, ACTION_TEXT_FADE_IN_DELAY_MS);
|
||||
return () => {
|
||||
window.clearTimeout(projectTimer);
|
||||
window.clearTimeout(actionTimer);
|
||||
};
|
||||
}
|
||||
|
||||
setProjectTextVisible(false);
|
||||
setActionTextVisible(false);
|
||||
const timer = window.setTimeout(() => {
|
||||
setShowExpandedContent(false);
|
||||
}, NAV_RAIL_TEXT_FADE_MS);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [expanded]);
|
||||
|
||||
const shortcutLabel = React.useCallback((actionId: string) => {
|
||||
return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides));
|
||||
}, [shortcutOverrides]);
|
||||
|
||||
const sessionStatus = useSessionStore((s) => s.sessionStatus);
|
||||
const sessionAttentionStates = useSessionStore((s) => s.sessionAttentionStates);
|
||||
@@ -504,21 +570,80 @@ export const NavRail: React.FC<NavRailProps> = ({ className, mobile }) => {
|
||||
);
|
||||
|
||||
const navRailActionButtonClass = cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-lg',
|
||||
'text-foreground hover:bg-interactive-hover',
|
||||
'group relative flex h-8 items-center rounded-lg',
|
||||
showExpandedContent ? 'w-full justify-start gap-2.5 pr-2 pl-2' : 'w-8 justify-center',
|
||||
showExpandedContent
|
||||
? 'text-[var(--surface-mutedForeground)] hover:text-[var(--surface-foreground)]'
|
||||
: 'text-[var(--surface-mutedForeground)] hover:bg-[var(--interactive-hover)]/50 hover:text-[var(--surface-foreground)]',
|
||||
'transition-colors',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)]',
|
||||
);
|
||||
|
||||
const navRailActionIconClass = 'h-4.5 w-4.5';
|
||||
const navRailActionIconClass = 'h-4.5 w-4.5 shrink-0';
|
||||
|
||||
const ActionButton: React.FC<{
|
||||
onClick: () => void;
|
||||
ariaLabel: string;
|
||||
icon: React.ReactNode;
|
||||
tooltipLabel: string;
|
||||
shortcutHint?: string;
|
||||
showExpandedShortcutHint?: boolean;
|
||||
}> = ({ onClick, ariaLabel, icon, tooltipLabel, shortcutHint, showExpandedShortcutHint = true }) => {
|
||||
const btn = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={navRailActionButtonClass}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{showExpandedContent && (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-y-0 left-[6px] right-[5px] rounded-lg bg-transparent transition-colors group-hover:bg-[var(--interactive-hover)]/50"
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10 flex size-8 basis-8 shrink-0 grow-0 items-center justify-center">
|
||||
{icon}
|
||||
</span>
|
||||
<span
|
||||
aria-hidden={!actionTextVisible}
|
||||
className={cn(
|
||||
'relative z-10 min-w-0 flex items-center justify-between gap-1 overflow-hidden transition-opacity duration-[180ms] ease-in-out',
|
||||
showExpandedContent ? 'flex-1' : 'w-0 flex-none',
|
||||
actionTextVisible ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
>
|
||||
<span className="truncate text-left text-[13px]">{tooltipLabel}</span>
|
||||
{shortcutHint && showExpandedShortcutHint && (
|
||||
<span className="shrink-0 text-[10px] text-[var(--surface-mutedForeground)] opacity-70">
|
||||
{shortcutHint}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>{btn}</TooltipTrigger>
|
||||
{!showExpandedContent && (
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
<p>{shortcutHint ? `${tooltipLabel} (${shortcutHint})` : tooltipLabel}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav
|
||||
className={cn(
|
||||
'flex h-full w-14 shrink-0 flex-col items-center bg-[var(--surface-background)] overflow-hidden',
|
||||
'flex h-full shrink-0 flex-col bg-[var(--surface-background)] overflow-hidden',
|
||||
showExpandedContent ? 'items-stretch' : 'items-center',
|
||||
className,
|
||||
)}
|
||||
style={{ width: expanded ? NAV_RAIL_EXPANDED_WIDTH : NAV_RAIL_WIDTH }}
|
||||
aria-label="Project navigation"
|
||||
>
|
||||
{/* Projects list */}
|
||||
@@ -530,7 +655,7 @@ export const NavRail: React.FC<NavRailProps> = ({ className, mobile }) => {
|
||||
modifiers={[restrictToYAxis]}
|
||||
>
|
||||
<SortableContext items={projectIds} strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col items-center gap-3 px-1 pt-1 pb-3">
|
||||
<div className={cn('flex flex-col gap-3 pt-1 pb-3', showExpandedContent ? 'items-stretch px-1' : 'items-center px-1')}>
|
||||
{projects.map((project) => {
|
||||
const isActive = project.id === activeProjectId;
|
||||
const indicators = projectIndicators.get(project.id);
|
||||
@@ -542,6 +667,8 @@ export const NavRail: React.FC<NavRailProps> = ({ className, mobile }) => {
|
||||
hasStreaming={indicators?.hasStreaming ?? false}
|
||||
hasUnread={indicators?.hasUnread ?? false}
|
||||
label={formatLabel(project)}
|
||||
expanded={showExpandedContent}
|
||||
projectTextVisible={projectTextVisible}
|
||||
onClick={() => {
|
||||
if (project.id !== activeProjectId) {
|
||||
setActiveProjectIdOnly(project.id);
|
||||
@@ -559,100 +686,73 @@ export const NavRail: React.FC<NavRailProps> = ({ className, mobile }) => {
|
||||
</DndContext>
|
||||
|
||||
{/* Add project button */}
|
||||
<div className="flex flex-col items-center px-1 pb-3">
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddProject}
|
||||
className={navRailActionButtonClass}
|
||||
aria-label="Add project"
|
||||
>
|
||||
<RiFolderAddLine className={navRailActionIconClass} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
Add project
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className={cn('flex flex-col pb-3', showExpandedContent ? 'items-stretch px-1' : 'items-center px-1')}>
|
||||
<ActionButton
|
||||
onClick={handleAddProject}
|
||||
ariaLabel="Add project"
|
||||
icon={<RiFolderAddLine className={navRailActionIconClass} />}
|
||||
tooltipLabel="Add project"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom actions */}
|
||||
<div className="shrink-0 w-full pt-3 pb-4 flex flex-col items-center gap-2">
|
||||
<div className={cn(
|
||||
'shrink-0 w-full pt-3 pb-4 flex flex-col gap-1',
|
||||
showExpandedContent ? 'items-stretch px-1' : 'items-center',
|
||||
)}>
|
||||
{(updateAvailable || updateDownloaded) && (
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUpdateDialogOpen(true)}
|
||||
className={cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-lg',
|
||||
'bg-[var(--primary)]/10 text-[var(--primary)]',
|
||||
'hover:bg-[var(--primary)]/20 transition-colors',
|
||||
)}
|
||||
aria-label="Update available"
|
||||
>
|
||||
<RiDownloadLine className={navRailActionIconClass} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
Update available
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<ActionButton
|
||||
onClick={() => setUpdateDialogOpen(true)}
|
||||
ariaLabel="Update available"
|
||||
icon={<RiDownloadLine className={navRailActionIconClass} />}
|
||||
tooltipLabel="Update available"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isDesktopApp && !(updateAvailable || updateDownloaded) && (
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAboutDialogOpen(true)}
|
||||
className={navRailActionButtonClass}
|
||||
aria-label="About"
|
||||
>
|
||||
<RiInformationLine className={navRailActionIconClass} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
About OpenChamber
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<ActionButton
|
||||
onClick={() => setAboutDialogOpen(true)}
|
||||
ariaLabel="About"
|
||||
icon={<RiInformationLine className={navRailActionIconClass} />}
|
||||
tooltipLabel="About OpenChamber"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!mobile && (
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleHelpDialog}
|
||||
className={navRailActionButtonClass}
|
||||
aria-label="Keyboard shortcuts"
|
||||
>
|
||||
<RiQuestionLine className={navRailActionIconClass} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
Keyboard shortcuts
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<ActionButton
|
||||
onClick={toggleHelpDialog}
|
||||
ariaLabel="Keyboard shortcuts"
|
||||
icon={<RiQuestionLine className={navRailActionIconClass} />}
|
||||
tooltipLabel="Shortcuts"
|
||||
shortcutHint={shortcutLabel('open_help')}
|
||||
showExpandedShortcutHint={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSettingsDialogOpen(true)}
|
||||
className={navRailActionButtonClass}
|
||||
aria-label="Settings"
|
||||
>
|
||||
<RiSettings3Line className={navRailActionIconClass} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
Settings
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<ActionButton
|
||||
onClick={() => setSettingsDialogOpen(true)}
|
||||
ariaLabel="Settings"
|
||||
icon={<RiSettings3Line className={navRailActionIconClass} />}
|
||||
tooltipLabel="Settings"
|
||||
shortcutHint={shortcutLabel('open_settings')}
|
||||
showExpandedShortcutHint={false}
|
||||
/>
|
||||
|
||||
{/* Toggle expand/collapse (desktop only) */}
|
||||
{!mobile && (
|
||||
<ActionButton
|
||||
onClick={toggleNavRail}
|
||||
ariaLabel={expanded ? 'Collapse sidebar' : 'Expand sidebar'}
|
||||
icon={expanded
|
||||
? <RiMenuFoldLine className={navRailActionIconClass} />
|
||||
: <RiMenuUnfoldLine className={navRailActionIconClass} />
|
||||
}
|
||||
tooltipLabel={expanded ? 'Collapse' : 'Expand'}
|
||||
shortcutHint={shortcutLabel('toggle_nav_rail')}
|
||||
showExpandedShortcutHint={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -687,4 +787,4 @@ export const NavRail: React.FC<NavRailProps> = ({ className, mobile }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { NAV_RAIL_WIDTH };
|
||||
export { NAV_RAIL_WIDTH, NAV_RAIL_EXPANDED_WIDTH };
|
||||
|
||||
Reference in New Issue
Block a user