| author | |
| committer | |
| log | ca2cc1339bbd1904b932c3a92d23a2c0ffe1d267 |
| tree | 315a8d16a8b0caf7c0895d36331168d16ab84fd9 |
| parent | e6286839369a29d829aee3c64dd64ce39c626d2f |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>15 files changed, 597 insertions(+), 110 deletions(-)
apps/web/src/components/AppSidebarLayout.tsx+7-2| ... | @@ -14,7 +14,11 @@ import { getLocalStorageItem, removeLocalStorageItem } from "../hooks/useLocalSt | ... | @@ -14,7 +14,11 @@ import { getLocalStorageItem, removeLocalStorageItem } from "../hooks/useLocalSt |
| 14 | import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; | 14 | import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; |
| 15 | import { cn, isMacPlatform } from "../lib/utils"; | 15 | import { cn, isMacPlatform } from "../lib/utils"; |
| 16 | import { primaryServerKeybindingsAtom } from "../state/server"; | 16 | import { primaryServerKeybindingsAtom } from "../state/server"; |
| 17 | import { useEnvironmentIdentificationMode, useLegacySidebarEnabled } from "../hooks/useSettings"; | 17 | import { |
| 18 | useCompactSidebarEnabled, | ||
| 19 | useEnvironmentIdentificationMode, | ||
| 20 | useLegacySidebarEnabled, | ||
| 21 | } from "../hooks/useSettings"; | ||
| 18 | import { | 22 | import { |
| 19 | PanelAnimationSuppressionProvider, | 23 | PanelAnimationSuppressionProvider, |
| 20 | usePanelAnimationSettings, | 24 | usePanelAnimationSettings, |
| ... | @@ -144,6 +148,7 @@ function ProjectProjectionRetention() { | ... | @@ -144,6 +148,7 @@ function ProjectProjectionRetention() { |
| 144 | export function AppSidebarLayout({ children }: { children: ReactNode }) { | 148 | export function AppSidebarLayout({ children }: { children: ReactNode }) { |
| 145 | const navigate = useNavigate(); | 149 | const navigate = useNavigate(); |
| 146 | const legacySidebarEnabled = useLegacySidebarEnabled(); | 150 | const legacySidebarEnabled = useLegacySidebarEnabled(); |
| 151 | const compactSidebarEnabled = useCompactSidebarEnabled(); | ||
| 147 | const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } = | 152 | const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } = |
| 148 | usePanelAnimationSettings(); | 153 | usePanelAnimationSettings(); |
| 149 | // Settings routes show the settings nav in place of whichever thread | 154 | // Settings routes show the settings nav in place of whichever thread |
| ... | @@ -229,7 +234,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { | ... | @@ -229,7 +234,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { |
| 229 | <ProjectProjectionRetention /> | 234 | <ProjectProjectionRetention /> |
| 230 | <Sidebar | 235 | <Sidebar |
| 231 | side="left" | 236 | side="left" |
| 232 | collapsible="offcanvas" | 237 | collapsible={compactSidebarEnabled ? "icon" : "offcanvas"} |
| 233 | data-app-sidebar="" | 238 | data-app-sidebar="" |
| 234 | className="border-r border-sidebar-border bg-sidebar text-sidebar-foreground" | 239 | className="border-r border-sidebar-border bg-sidebar text-sidebar-foreground" |
| 235 | resizable={{ | 240 | resizable={{ |
apps/web/src/components/LegacySidebar.tsx+52-25| ... | @@ -199,7 +199,11 @@ import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrom | ... | @@ -199,7 +199,11 @@ import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrom |
| 199 | import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; | 199 | import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; |
| 200 | import { useIsMobile } from "~/hooks/useMediaQuery"; | 200 | import { useIsMobile } from "~/hooks/useMediaQuery"; |
| 201 | import { CommandDialogTrigger } from "./ui/command"; | 201 | import { CommandDialogTrigger } from "./ui/command"; |
| 202 | import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"; | 202 | import { |
| 203 | useClientSettings, | ||
| 204 | useCompactSidebarEnabled, | ||
| 205 | useUpdateClientSettings, | ||
| 206 | } from "~/hooks/useSettings"; | ||
| 203 | import { primaryServerKeybindingsAtom } from "../state/server"; | 207 | import { primaryServerKeybindingsAtom } from "../state/server"; |
| 204 | import { | 208 | import { |
| 205 | derivePhysicalProjectKey, | 209 | derivePhysicalProjectKey, |
| ... | @@ -1194,7 +1198,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec | ... | @@ -1194,7 +1198,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 1194 | const router = useRouter(); | 1198 | const router = useRouter(); |
| 1195 | const queuePendingFileDrop = useSidebarPendingFileDropStore((s) => s.queuePendingFileDrop); | 1199 | const queuePendingFileDrop = useSidebarPendingFileDropStore((s) => s.queuePendingFileDrop); |
| 1196 | const clearPendingFileDrop = useSidebarPendingFileDropStore((s) => s.clearPendingFileDrop); | 1200 | const clearPendingFileDrop = useSidebarPendingFileDropStore((s) => s.clearPendingFileDrop); |
| 1197 | const { isMobile, setOpenMobile } = useSidebar(); | 1201 | const { isMobile, setOpenMobile, state, setOpen } = useSidebar(); |
| 1202 | const compactSidebarEnabled = useCompactSidebarEnabled(); | ||
| 1203 | const isCompact = compactSidebarEnabled && !isMobile && state === "collapsed"; | ||
| 1198 | const markThreadUnread = useUiStateStore((state) => state.markThreadUnread); | 1204 | const markThreadUnread = useUiStateStore((state) => state.markThreadUnread); |
| 1199 | const setProjectExpanded = useUiStateStore((state) => state.setProjectExpanded); | 1205 | const setProjectExpanded = useUiStateStore((state) => state.setProjectExpanded); |
| 1200 | const toggleThreadSelection = useThreadSelectionStore((state) => state.toggleThread); | 1206 | const toggleThreadSelection = useThreadSelectionStore((state) => state.toggleThread); |
| ... | @@ -1445,10 +1451,13 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec | ... | @@ -1445,10 +1451,13 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 1445 | if (useThreadSelectionStore.getState().hasSelection()) { | 1451 | if (useThreadSelectionStore.getState().hasSelection()) { |
| 1446 | clearSelection(); | 1452 | clearSelection(); |
| 1447 | } | 1453 | } |
| 1448 | setProjectExpanded(projectPreferenceKeys, !projectExpanded); | 1454 | setProjectExpanded(projectPreferenceKeys, isCompact || !projectExpanded); |
| 1455 | if (isCompact) setOpen(true); | ||
| 1449 | }, | 1456 | }, |
| 1450 | [ | 1457 | [ |
| 1451 | clearSelection, | 1458 | clearSelection, |
| 1459 | isCompact, | ||
| 1460 | setOpen, | ||
| 1452 | dragInProgressRef, | 1461 | dragInProgressRef, |
| 1453 | projectExpanded, | 1462 | projectExpanded, |
| 1454 | projectPreferenceKeys, | 1463 | projectPreferenceKeys, |
| ... | @@ -1465,9 +1474,17 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec | ... | @@ -1465,9 +1474,17 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 1465 | if (dragInProgressRef.current) { | 1474 | if (dragInProgressRef.current) { |
| 1466 | return; | 1475 | return; |
| 1467 | } | 1476 | } |
| 1468 | setProjectExpanded(projectPreferenceKeys, !projectExpanded); | 1477 | setProjectExpanded(projectPreferenceKeys, isCompact || !projectExpanded); |
| 1478 | if (isCompact) setOpen(true); | ||
| 1469 | }, | 1479 | }, |
| 1470 | [dragInProgressRef, projectExpanded, projectPreferenceKeys, setProjectExpanded], | 1480 | [ |
| 1481 | dragInProgressRef, | ||
| 1482 | isCompact, | ||
| 1483 | projectExpanded, | ||
| 1484 | projectPreferenceKeys, | ||
| 1485 | setOpen, | ||
| 1486 | setProjectExpanded, | ||
| 1487 | ], | ||
| 1471 | ); | 1488 | ); |
| 1472 | 1489 | ||
| 1473 | const handleProjectButtonPointerDownCapture = useCallback( | 1490 | const handleProjectButtonPointerDownCapture = useCallback( |
| ... | @@ -2359,8 +2376,10 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec | ... | @@ -2359,8 +2376,10 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 2359 | <> | 2376 | <> |
| 2360 | <div className="group/project-header relative"> | 2377 | <div className="group/project-header relative"> |
| 2361 | <SidebarMenuButton | 2378 | <SidebarMenuButton |
| 2379 | tooltip={project.displayName} | ||
| 2380 | aria-label={project.displayName} | ||
| 2362 | ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined} | 2381 | ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined} |
| 2363 | className={`pr-8 group-hover/project-header:bg-sidebar-row-hover group-hover/project-header:text-sidebar-foreground max-sm:pr-14 ${ | 2382 | className={`pr-8 group-hover/project-header:bg-sidebar-row-hover group-hover/project-header:text-sidebar-foreground max-sm:pr-14 group-data-[collapsible=icon]:justify-center ${ |
| 2364 | isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "" | 2383 | isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "" |
| 2365 | }`} | 2384 | }`} |
| 2366 | {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})} | 2385 | {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})} |
| ... | @@ -2370,7 +2389,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec | ... | @@ -2370,7 +2389,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 2370 | onKeyDown={handleProjectButtonKeyDown} | 2389 | onKeyDown={handleProjectButtonKeyDown} |
| 2371 | onContextMenu={handleProjectButtonContextMenu} | 2390 | onContextMenu={handleProjectButtonContextMenu} |
| 2372 | > | 2391 | > |
| 2373 | {!projectExpanded && projectStatus ? ( | 2392 | {isCompact ? null : !projectExpanded && projectStatus ? ( |
| 2374 | <Tooltip> | 2393 | <Tooltip> |
| 2375 | <TooltipTrigger | 2394 | <TooltipTrigger |
| 2376 | render={ | 2395 | render={ |
| ... | @@ -2401,7 +2420,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec | ... | @@ -2401,7 +2420,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 2401 | <span className="flex shrink-0"> | 2420 | <span className="flex shrink-0"> |
| 2402 | <ProjectFavicon project={project} /> | 2421 | <ProjectFavicon project={project} /> |
| 2403 | </span> | 2422 | </span> |
| 2404 | <span className="flex min-w-0 flex-1 items-center gap-2"> | 2423 | <span className="flex min-w-0 flex-1 items-center gap-2 group-data-[collapsible=icon]:hidden"> |
| 2405 | <span className="truncate text-sm font-medium text-sidebar-foreground/90"> | 2424 | <span className="truncate text-sm font-medium text-sidebar-foreground/90"> |
| 2406 | {project.displayName} | 2425 | {project.displayName} |
| 2407 | </span> | 2426 | </span> |
| ... | @@ -2415,7 +2434,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec | ... | @@ -2415,7 +2434,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 2415 | {/* Environment badge – visible by default, crossfades with the | 2434 | {/* Environment badge – visible by default, crossfades with the |
| 2416 | "new thread" button on hover using the same pointer-events + | 2435 | "new thread" button on hover using the same pointer-events + |
| 2417 | opacity pattern as the thread row archive/timestamp swap. */} | 2436 | opacity pattern as the thread row archive/timestamp swap. */} |
| 2418 | {project.environmentPresence === "remote-only" && ( | 2437 | {!isCompact && project.environmentPresence === "remote-only" && ( |
| 2419 | <Tooltip> | 2438 | <Tooltip> |
| 2420 | <TooltipTrigger | 2439 | <TooltipTrigger |
| 2421 | render={ | 2440 | render={ |
| ... | @@ -2441,7 +2460,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec | ... | @@ -2441,7 +2460,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 2441 | <Tooltip> | 2460 | <Tooltip> |
| 2442 | <TooltipTrigger | 2461 | <TooltipTrigger |
| 2443 | render={ | 2462 | render={ |
| 2444 | <div className="pointer-events-none absolute top-[calc(50%+1px)] right-0.5 -translate-y-1/2 opacity-0 transition-opacity duration-150 max-sm:pointer-events-auto max-sm:opacity-100 group-hover/project-header:pointer-events-auto group-hover/project-header:opacity-100 group-focus-within/project-header:pointer-events-auto group-focus-within/project-header:opacity-100"> | 2463 | <div className="pointer-events-none absolute top-[calc(50%+1px)] right-0.5 -translate-y-1/2 opacity-0 transition-opacity duration-150 max-sm:pointer-events-auto max-sm:opacity-100 group-hover/project-header:pointer-events-auto group-hover/project-header:opacity-100 group-focus-within/project-header:pointer-events-auto group-focus-within/project-header:opacity-100 group-data-[collapsible=icon]:hidden"> |
| 2445 | <button | 2464 | <button |
| 2446 | type="button" | 2465 | type="button" |
| 2447 | aria-label={`Create new thread in ${project.displayName}`} | 2466 | aria-label={`Create new thread in ${project.displayName}`} |
| ... | @@ -2674,7 +2693,7 @@ function LocalSecondaryStatus() { | ... | @@ -2674,7 +2693,7 @@ function LocalSecondaryStatus() { |
| 2674 | } | 2693 | } |
| 2675 | 2694 | ||
| 2676 | return ( | 2695 | return ( |
| 2677 | <SidebarGroup className="px-2 pt-2 pb-0"> | 2696 | <SidebarGroup className="px-2 pt-2 pb-0 group-data-[collapsible=icon]:hidden"> |
| 2678 | {connecting.length > 0 ? ( | 2697 | {connecting.length > 0 ? ( |
| 2679 | <Alert | 2698 | <Alert |
| 2680 | variant="default" | 2699 | variant="default" |
| ... | @@ -2978,14 +2997,16 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( | ... | @@ -2978,14 +2997,16 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( |
| 2978 | render={ | 2997 | render={ |
| 2979 | <SidebarMenuButton | 2998 | <SidebarMenuButton |
| 2980 | className="focus-visible:ring-0" | 2999 | className="focus-visible:ring-0" |
| 3000 | tooltip="Search" | ||
| 3001 | aria-label="Search" | ||
| 2981 | data-testid="command-palette-trigger" | 3002 | data-testid="command-palette-trigger" |
| 2982 | /> | 3003 | /> |
| 2983 | } | 3004 | } |
| 2984 | > | 3005 | > |
| 2985 | <SearchIcon /> | 3006 | <SearchIcon /> |
| 2986 | <span className="flex-1 truncate">Search</span> | 3007 | <span className="flex-1 truncate group-data-[collapsible=icon]:hidden">Search</span> |
| 2987 | {commandPaletteShortcutLabel ? ( | 3008 | {commandPaletteShortcutLabel ? ( |
| 2988 | <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px]"> | 3009 | <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px] group-data-[collapsible=icon]:hidden"> |
| 2989 | {commandPaletteShortcutLabel} | 3010 | {commandPaletteShortcutLabel} |
| 2990 | </Kbd> | 3011 | </Kbd> |
| 2991 | ) : null} | 3012 | ) : null} |
| ... | @@ -2996,7 +3017,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( | ... | @@ -2996,7 +3017,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( |
| 2996 | } | 3017 | } |
| 2997 | > | 3018 | > |
| 2998 | {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? ( | 3019 | {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? ( |
| 2999 | <SidebarGroup className="px-2 pt-2 pb-0"> | 3020 | <SidebarGroup className="px-2 pt-2 pb-0 group-data-[collapsible=icon]:hidden"> |
| 3000 | <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8"> | 3021 | <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8"> |
| 3001 | <TriangleAlertIcon /> | 3022 | <TriangleAlertIcon /> |
| 3002 | <AlertTitle>Intel build on Apple Silicon</AlertTitle> | 3023 | <AlertTitle>Intel build on Apple Silicon</AlertTitle> |
| ... | @@ -3020,17 +3041,21 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( | ... | @@ -3020,17 +3041,21 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( |
| 3020 | ) : null} | 3041 | ) : null} |
| 3021 | <LocalSecondaryStatus /> | 3042 | <LocalSecondaryStatus /> |
| 3022 | <SidebarGroup className="px-2 py-2"> | 3043 | <SidebarGroup className="px-2 py-2"> |
| 3023 | <div className="mb-1 flex items-center justify-between pl-2 pr-1.5"> | 3044 | <div className="mb-1 flex items-center justify-between pl-2 pr-1.5 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-0"> |
| 3024 | <span className="text-xs font-medium text-sidebar-muted-foreground/80">Projects</span> | 3045 | <span className="text-xs font-medium text-sidebar-muted-foreground/80 group-data-[collapsible=icon]:hidden"> |
| 3046 | Projects | ||
| 3047 | </span> | ||
| 3025 | <div className="flex items-center gap-1"> | 3048 | <div className="flex items-center gap-1"> |
| 3026 | <ProjectSortMenu | 3049 | <div className="group-data-[collapsible=icon]:hidden"> |
| 3027 | projectSortOrder={projectSortOrder} | 3050 | <ProjectSortMenu |
| 3028 | threadSortOrder={threadSortOrder} | 3051 | projectSortOrder={projectSortOrder} |
| 3029 | threadPreviewCount={threadPreviewCount} | 3052 | threadSortOrder={threadSortOrder} |
| 3030 | onProjectSortOrderChange={handleProjectSortOrderChange} | 3053 | threadPreviewCount={threadPreviewCount} |
| 3031 | onThreadSortOrderChange={handleThreadSortOrderChange} | 3054 | onProjectSortOrderChange={handleProjectSortOrderChange} |
| 3032 | onThreadPreviewCountChange={handleThreadPreviewCountChange} | 3055 | onThreadSortOrderChange={handleThreadSortOrderChange} |
| 3033 | /> | 3056 | onThreadPreviewCountChange={handleThreadPreviewCountChange} |
| 3057 | /> | ||
| 3058 | </div> | ||
| 3034 | <Tooltip> | 3059 | <Tooltip> |
| 3035 | <TooltipTrigger | 3060 | <TooltipTrigger |
| 3036 | render={ | 3061 | render={ |
| ... | @@ -3127,7 +3152,9 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( | ... | @@ -3127,7 +3152,9 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( |
| 3127 | )} | 3152 | )} |
| 3128 | 3153 | ||
| 3129 | {projectsLength === 0 && ( | 3154 | {projectsLength === 0 && ( |
| 3130 | <div className="px-2 pt-4 text-center text-secondary-label text-xs">No projects yet</div> | 3155 | <div className="px-2 pt-4 text-center text-secondary-label text-xs group-data-[collapsible=icon]:hidden"> |
| 3156 | No projects yet | ||
| 3157 | </div> | ||
| 3131 | )} | 3158 | )} |
| 3132 | </SidebarGroup> | 3159 | </SidebarGroup> |
| 3133 | </SidebarContent> | 3160 | </SidebarContent> |
apps/web/src/components/Sidebar.drag.ts+9-5| ... | @@ -103,6 +103,7 @@ export function createSidebarSortingStrategy(input: { | ... | @@ -103,6 +103,7 @@ export function createSidebarSortingStrategy(input: { |
| 103 | snoozedThreadCount?: number; | 103 | snoozedThreadCount?: number; |
| 104 | cardHeight?: number; | 104 | cardHeight?: number; |
| 105 | slimHeight?: number; | 105 | slimHeight?: number; |
| 106 | compact?: boolean; | ||
| 106 | /** Space each pinned boundary opens for its label while dragging. The | 107 | /** Space each pinned boundary opens for its label while dragging. The |
| 107 | * markers stay zero height at rest, so nothing is reserved until pickup. */ | 108 | * markers stay zero height at rest, so nothing is reserved until pickup. */ |
| 108 | boundaryLabelHeight?: number; | 109 | boundaryLabelHeight?: number; |
| ... | @@ -140,11 +141,14 @@ export function createSidebarSortingStrategy(input: { | ... | @@ -140,11 +141,14 @@ export function createSidebarSortingStrategy(input: { |
| 140 | else slimHeight ??= rects[index]?.height; | 141 | else slimHeight ??= rects[index]?.height; |
| 141 | if (item.key !== active.key) groups[item.section].push(item); | 142 | if (item.key !== active.key) groups[item.section].push(item); |
| 142 | } | 143 | } |
| 143 | // Cards are 4.875rem + 0.25rem padding; slim rows/placeholders are h-9. | 144 | // Compact icons use h-7; expanded cards include their vertical padding. |
| 144 | const scale = | 145 | const scale = input.compact |
| 145 | slimHeight !== undefined ? slimHeight / 36 : (headerScale ?? (cardHeight ?? 82) / 82); | 146 | ? (cardHeight ?? slimHeight ?? 28) / 28 |
| 146 | cardHeight ??= 82 * scale; | 147 | : slimHeight !== undefined |
| 147 | slimHeight ??= 36 * scale; | 148 | ? slimHeight / 36 |
| 149 | : (headerScale ?? (cardHeight ?? 82) / 82); | ||
| 150 | cardHeight ??= (input.compact ? 28 : 82) * scale; | ||
| 151 | slimHeight ??= (input.compact ? 28 : 36) * scale; | ||
| 148 | const labelHeight = (input.boundaryLabelHeight ?? 0) * scale; | 152 | const labelHeight = (input.boundaryLabelHeight ?? 0) * scale; |
| 149 | const group = groups[target.section]; | 153 | const group = groups[target.section]; |
| 150 | const order = | 154 | const order = |
apps/web/src/components/Sidebar.tsx+368-59| ... | @@ -1,10 +1,12 @@ | ... | @@ -1,10 +1,12 @@ |
| 1 | import { useSupportsMultiplePullRequests } from "~/hooks/useSupportsMultiplePullRequests"; | 1 | import { useSupportsMultiplePullRequests } from "~/hooks/useSupportsMultiplePullRequests"; |
| 2 | import { useCompactSidebarEnabled } from "../hooks/useSettings"; | ||
| 2 | import { resolveThreadCurrentPullRequestLink } from "@t3tools/shared/threadPullRequests"; | 3 | import { resolveThreadCurrentPullRequestLink } from "@t3tools/shared/threadPullRequests"; |
| 3 | import { useAtomValue } from "@effect/atom-react"; | 4 | import { useAtomValue } from "@effect/atom-react"; |
| 4 | import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences"; | 5 | import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences"; |
| 5 | import * as Schema from "effect/Schema"; | 6 | import * as Schema from "effect/Schema"; |
| 6 | import { | 7 | import { |
| 7 | DndContext, | 8 | DndContext, |
| 9 | DragOverlay, | ||
| 8 | useSensor, | 10 | useSensor, |
| 9 | useSensors, | 11 | useSensors, |
| 10 | type DragEndEvent, | 12 | type DragEndEvent, |
| ... | @@ -72,6 +74,7 @@ import { | ... | @@ -72,6 +74,7 @@ import { |
| 72 | type MouseEvent as ReactMouseEvent, | 74 | type MouseEvent as ReactMouseEvent, |
| 73 | type ReactNode, | 75 | type ReactNode, |
| 74 | } from "react"; | 76 | } from "react"; |
| 77 | import { createPortal } from "react-dom"; | ||
| 75 | import { useParams, useRouter } from "@tanstack/react-router"; | 78 | import { useParams, useRouter } from "@tanstack/react-router"; |
| 76 | 79 | ||
| 77 | import { useRightPanelStore } from "../rightPanelStore"; | 80 | import { useRightPanelStore } from "../rightPanelStore"; |
| ... | @@ -321,6 +324,7 @@ function SidebarThreadTooltip({ | ... | @@ -321,6 +324,7 @@ function SidebarThreadTooltip({ |
| 321 | branchMismatch, | 324 | branchMismatch, |
| 322 | terminalStatus, | 325 | terminalStatus, |
| 323 | terminalProcessCount, | 326 | terminalProcessCount, |
| 327 | compactStatus, | ||
| 324 | }: { | 328 | }: { |
| 325 | thread: SidebarThreadSummary; | 329 | thread: SidebarThreadSummary; |
| 326 | project: ProjectFaviconProject | null; | 330 | project: ProjectFaviconProject | null; |
| ... | @@ -337,6 +341,7 @@ function SidebarThreadTooltip({ | ... | @@ -337,6 +341,7 @@ function SidebarThreadTooltip({ |
| 337 | } | null; | 341 | } | null; |
| 338 | terminalStatus: TerminalStatusIndicator | null; | 342 | terminalStatus: TerminalStatusIndicator | null; |
| 339 | terminalProcessCount: number; | 343 | terminalProcessCount: number; |
| 344 | compactStatus?: string | undefined; | ||
| 340 | }) { | 345 | }) { |
| 341 | const driverKind = providerEntry?.driverKind ?? null; | 346 | const driverKind = providerEntry?.driverKind ?? null; |
| 342 | const supportsMultiplePullRequests = useSupportsMultiplePullRequests(thread.environmentId); | 347 | const supportsMultiplePullRequests = useSupportsMultiplePullRequests(thread.environmentId); |
| ... | @@ -353,6 +358,7 @@ function SidebarThreadTooltip({ | ... | @@ -353,6 +358,7 @@ function SidebarThreadTooltip({ |
| 353 | {thread.title} | 358 | {thread.title} |
| 354 | </div> | 359 | </div> |
| 355 | <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground"> | 360 | <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground"> |
| 361 | {compactStatus ? <div>{compactStatus}</div> : null} | ||
| 356 | {projectDisplayName ? ( | 362 | {projectDisplayName ? ( |
| 357 | <div className="flex min-w-0 items-center gap-2"> | 363 | <div className="flex min-w-0 items-center gap-2"> |
| 358 | {project ? <ProjectFavicon project={project} className="size-3 shrink-0" /> : null} | 364 | {project ? <ProjectFavicon project={project} className="size-3 shrink-0" /> : null} |
| ... | @@ -502,7 +508,7 @@ function SnoozePopoverButton(props: { | ... | @@ -502,7 +508,7 @@ function SnoozePopoverButton(props: { |
| 502 | type SortableThreadRowBag = Pick< | 508 | type SortableThreadRowBag = Pick< |
| 503 | ReturnType<typeof useSortable>, | 509 | ReturnType<typeof useSortable>, |
| 504 | "listeners" | "setNodeRef" | "transform" | "transition" | "isDragging" | 510 | "listeners" | "setNodeRef" | "transform" | "transition" | "isDragging" |
| 505 | >; | 511 | > & { hidden?: boolean }; |
| 506 | 512 | ||
| 507 | function SortableThreadRow(props: { | 513 | function SortableThreadRow(props: { |
| 508 | id: string; | 514 | id: string; |
| ... | @@ -583,7 +589,18 @@ function SidebarSectionPlaceholder(props: { | ... | @@ -583,7 +589,18 @@ function SidebarSectionPlaceholder(props: { |
| 583 | props.isDropTarget && "border-primary/40 bg-primary/5 text-primary", | 589 | props.isDropTarget && "border-primary/40 bg-primary/5 text-primary", |
| 584 | )} | 590 | )} |
| 585 | > | 591 | > |
| 586 | {props.label} | 592 | <span className="group-data-[collapsible=icon]:sr-only">{props.label}</span> |
| 593 | {props.marker === "settled-placeholder" ? ( | ||
| 594 | <CheckIcon | ||
| 595 | aria-hidden | ||
| 596 | className="hidden size-3.5 group-data-[collapsible=icon]:block" | ||
| 597 | /> | ||
| 598 | ) : ( | ||
| 599 | <Undo2Icon | ||
| 600 | aria-hidden | ||
| 601 | className="hidden size-3.5 group-data-[collapsible=icon]:block" | ||
| 602 | /> | ||
| 603 | )} | ||
| 587 | </div> | 604 | </div> |
| 588 | ) : null} | 605 | ) : null} |
| 589 | </SortableSidebarMarker> | 606 | </SortableSidebarMarker> |
| ... | @@ -607,19 +624,30 @@ function SidebarDragBoundary(props: { | ... | @@ -607,19 +624,30 @@ function SidebarDragBoundary(props: { |
| 607 | className="pointer-events-none relative mx-0.5 -mb-px h-0" | 624 | className="pointer-events-none relative mx-0.5 -mb-px h-0" |
| 608 | > | 625 | > |
| 609 | {props.visible ? ( | 626 | {props.visible ? ( |
| 610 | <div className="sidebar-drag-boundary-label absolute inset-x-2 top-1 flex h-4 items-center gap-2"> | 627 | <div className="sidebar-drag-boundary-label absolute inset-x-2 top-1 flex h-4 items-center gap-2 group-data-[collapsible=icon]:inset-x-0 group-data-[collapsible=icon]:justify-center"> |
| 611 | <span | 628 | <span |
| 612 | className={cn( | 629 | className={cn( |
| 613 | "shrink-0 text-xs font-medium", | 630 | "shrink-0 text-xs font-medium", |
| 614 | props.isDropTarget ? "text-primary" : "text-sidebar-foreground/80", | 631 | props.isDropTarget ? "text-primary" : "text-sidebar-foreground/80", |
| 615 | )} | 632 | )} |
| 616 | > | 633 | > |
| 617 | {props.label} | 634 | <span className="group-data-[collapsible=icon]:sr-only">{props.label}</span> |
| 635 | {props.marker === "pinned-header" ? ( | ||
| 636 | <PinIcon | ||
| 637 | aria-hidden | ||
| 638 | className="hidden size-3.5 group-data-[collapsible=icon]:block" | ||
| 639 | /> | ||
| 640 | ) : ( | ||
| 641 | <Undo2Icon | ||
| 642 | aria-hidden | ||
| 643 | className="hidden size-3.5 group-data-[collapsible=icon]:block" | ||
| 644 | /> | ||
| 645 | )} | ||
| 618 | </span> | 646 | </span> |
| 619 | <span | 647 | <span |
| 620 | aria-hidden | 648 | aria-hidden |
| 621 | className={cn( | 649 | className={cn( |
| 622 | "h-px flex-1", | 650 | "h-px flex-1 group-data-[collapsible=icon]:hidden", |
| 623 | props.isDropTarget ? "bg-primary/50" : "bg-sidebar-foreground/25", | 651 | props.isDropTarget ? "bg-primary/50" : "bg-sidebar-foreground/25", |
| 624 | )} | 652 | )} |
| 625 | /> | 653 | /> |
| ... | @@ -639,20 +667,32 @@ function SidebarSectionHeader(props: { | ... | @@ -639,20 +667,32 @@ function SidebarSectionHeader(props: { |
| 639 | isDropTarget?: boolean; | 667 | isDropTarget?: boolean; |
| 640 | toggle: { expanded: boolean; onToggle: () => void }; | 668 | toggle: { expanded: boolean; onToggle: () => void }; |
| 641 | }) { | 669 | }) { |
| 670 | const compactEnabled = useCompactSidebarEnabled(); | ||
| 671 | const { state, isMobile } = useSidebar(); | ||
| 672 | const compact = compactEnabled && state === "collapsed" && !isMobile; | ||
| 642 | const snoozed = props.marker === "snoozed-header"; | 673 | const snoozed = props.marker === "snoozed-header"; |
| 643 | const className = cn( | 674 | const className = cn( |
| 644 | "flex h-full w-full items-center gap-2 px-2 text-left text-xs font-medium", | 675 | "flex h-full w-full items-center gap-2 px-2 text-left text-xs font-medium", |
| 676 | compact && "justify-center px-0", | ||
| 645 | snoozed ? "text-blue-600 dark:text-blue-400" : "text-sidebar-muted-foreground/60", | 677 | snoozed ? "text-blue-600 dark:text-blue-400" : "text-sidebar-muted-foreground/60", |
| 646 | props.dragging && "text-sidebar-foreground/80", | 678 | props.dragging && "text-sidebar-foreground/80", |
| 647 | props.isDropTarget && "text-primary", | 679 | props.isDropTarget && "text-primary", |
| 648 | ); | 680 | ); |
| 649 | const content = ( | 681 | const content = ( |
| 650 | <> | 682 | <> |
| 651 | <span className="shrink-0">{props.label}</span> | 683 | <span className={compact ? "sr-only" : "shrink-0"}>{props.label}</span> |
| 684 | {compact ? ( | ||
| 685 | snoozed ? ( | ||
| 686 | <AlarmClockIcon aria-hidden className="size-3.5" /> | ||
| 687 | ) : ( | ||
| 688 | <CheckIcon aria-hidden className="size-3.5" /> | ||
| 689 | ) | ||
| 690 | ) : null} | ||
| 652 | <span | 691 | <span |
| 653 | aria-hidden | 692 | aria-hidden |
| 654 | className={cn( | 693 | className={cn( |
| 655 | "h-px min-w-2 flex-1", | 694 | "h-px min-w-2 flex-1", |
| 695 | compact && "hidden", | ||
| 656 | snoozed ? "bg-blue-500/20 dark:bg-blue-400/15" : "bg-sidebar-border/60", | 696 | snoozed ? "bg-blue-500/20 dark:bg-blue-400/15" : "bg-sidebar-border/60", |
| 657 | props.dragging && "bg-sidebar-foreground/25", | 697 | props.dragging && "bg-sidebar-foreground/25", |
| 658 | props.isDropTarget && "bg-primary/50", | 698 | props.isDropTarget && "bg-primary/50", |
| ... | @@ -662,6 +702,7 @@ function SidebarSectionHeader(props: { | ... | @@ -662,6 +702,7 @@ function SidebarSectionHeader(props: { |
| 662 | aria-hidden | 702 | aria-hidden |
| 663 | className={cn( | 703 | className={cn( |
| 664 | "size-3 shrink-0 transition-transform", | 704 | "size-3 shrink-0 transition-transform", |
| 705 | compact && "hidden", | ||
| 665 | props.toggle.expanded && "rotate-180", | 706 | props.toggle.expanded && "rotate-180", |
| 666 | )} | 707 | )} |
| 667 | /> | 708 | /> |
| ... | @@ -673,15 +714,22 @@ function SidebarSectionHeader(props: { | ... | @@ -673,15 +714,22 @@ function SidebarSectionHeader(props: { |
| 673 | data-testid={`sidebar-${props.marker}`} | 714 | data-testid={`sidebar-${props.marker}`} |
| 674 | className="mx-0.5 h-8" | 715 | className="mx-0.5 h-8" |
| 675 | > | 716 | > |
| 676 | <button | 717 | <Tooltip disabled={!compact}> |
| 677 | type="button" | 718 | <TooltipTrigger |
| 678 | onClick={props.toggle.onToggle} | 719 | render={ |
| 679 | aria-expanded={props.toggle.expanded} | 720 | <button |
| 680 | data-testid={`sidebar-${snoozed ? "snoozed" : "settled"}-shelf-toggle`} | 721 | type="button" |
| 681 | className={cn(className, "cursor-pointer")} | 722 | onClick={props.toggle.onToggle} |
| 682 | > | 723 | aria-expanded={props.toggle.expanded} |
| 683 | {content} | 724 | data-testid={`sidebar-${snoozed ? "snoozed" : "settled"}-shelf-toggle`} |
| 684 | </button> | 725 | className={cn(className, "cursor-pointer")} |
| 726 | /> | ||
| 727 | } | ||
| 728 | > | ||
| 729 | {content} | ||
| 730 | </TooltipTrigger> | ||
| 731 | <TooltipPopup side="right">{props.label}</TooltipPopup> | ||
| 732 | </Tooltip> | ||
| 685 | </SortableSidebarMarker> | 733 | </SortableSidebarMarker> |
| 686 | ); | 734 | ); |
| 687 | } | 735 | } |
| ... | @@ -704,6 +752,9 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { | ... | @@ -704,6 +752,9 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { |
| 704 | onDiscard: (draftId: DraftId) => void; | 752 | onDiscard: (draftId: DraftId) => void; |
| 705 | }) { | 753 | }) { |
| 706 | const { composer, draftId, onDiscard, onNavigate, session } = props; | 754 | const { composer, draftId, onDiscard, onNavigate, session } = props; |
| 755 | const compactEnabled = useCompactSidebarEnabled(); | ||
| 756 | const { state, isMobile } = useSidebar(); | ||
| 757 | const compact = compactEnabled && state === "collapsed" && !isMobile; | ||
| 707 | const promptPreview = | 758 | const promptPreview = |
| 708 | replaceComposerContextReferences(composer.prompt, (occurrence) => occurrence.label) | 759 | replaceComposerContextReferences(composer.prompt, (occurrence) => occurrence.label) |
| 709 | .trim() | 760 | .trim() |
| ... | @@ -742,6 +793,34 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { | ... | @@ -742,6 +793,34 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { |
| 742 | }, | 793 | }, |
| 743 | [draftId, onDiscard], | 794 | [draftId, onDiscard], |
| 744 | ); | 795 | ); |
| 796 | if (compact) { | ||
| 797 | return ( | ||
| 798 | <li className="list-none"> | ||
| 799 | <Tooltip> | ||
| 800 | <TooltipTrigger | ||
| 801 | render={ | ||
| 802 | <button | ||
| 803 | type="button" | ||
| 804 | data-testid="sidebar-draft-row" | ||
| 805 | aria-label={`Draft: ${preview}`} | ||
| 806 | onClick={handleActivate} | ||
| 807 | className={cn( | ||
| 808 | "relative flex h-7 w-full cursor-pointer items-center justify-center rounded-md outline-none hover:bg-sidebar-row-hover focus-visible:ring-2 focus-visible:ring-ring", | ||
| 809 | props.isActive ? "bg-sidebar-row-active" : draftSurfaceClassName, | ||
| 810 | )} | ||
| 811 | /> | ||
| 812 | } | ||
| 813 | > | ||
| 814 | <SquarePenIcon aria-hidden className={cn(draftPenClassName, "size-4")} /> | ||
| 815 | </TooltipTrigger> | ||
| 816 | <TooltipPopup side="right" className="max-w-80 whitespace-normal"> | ||
| 817 | <div className="font-medium">{props.projectDisplayName}</div> | ||
| 818 | <div>{preview}</div> | ||
| 819 | </TooltipPopup> | ||
| 820 | </Tooltip> | ||
| 821 | </li> | ||
| 822 | ); | ||
| 823 | } | ||
| 745 | return ( | 824 | return ( |
| 746 | <li className="list-none py-0.5"> | 825 | <li className="list-none py-0.5"> |
| 747 | <div | 826 | <div |
| ... | @@ -1031,6 +1110,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1031,6 +1110,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1031 | variant, | 1110 | variant, |
| 1032 | variantAction, | 1111 | variantAction, |
| 1033 | } = props; | 1112 | } = props; |
| 1113 | const compactEnabled = useCompactSidebarEnabled(); | ||
| 1114 | const { state, isMobile } = useSidebar(); | ||
| 1115 | const compact = compactEnabled && state === "collapsed" && !isMobile; | ||
| 1034 | const threadRef = useMemo( | 1116 | const threadRef = useMemo( |
| 1035 | () => scopeThreadRef(thread.environmentId, thread.id), | 1117 | () => scopeThreadRef(thread.environmentId, thread.id), |
| 1036 | [thread.environmentId, thread.id], | 1118 | [thread.environmentId, thread.id], |
| ... | @@ -1208,6 +1290,16 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1208,6 +1290,16 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1208 | branchMismatch={branchMismatch} | 1290 | branchMismatch={branchMismatch} |
| 1209 | terminalStatus={terminalStatus} | 1291 | terminalStatus={terminalStatus} |
| 1210 | terminalProcessCount={terminalProcessCount} | 1292 | terminalProcessCount={terminalProcessCount} |
| 1293 | compactStatus={ | ||
| 1294 | compact | ||
| 1295 | ? (topStatus?.label ?? | ||
| 1296 | (variantAction === "unsnooze" | ||
| 1297 | ? "Snoozed" | ||
| 1298 | : variantAction === "unsettle" | ||
| 1299 | ? "Settled" | ||
| 1300 | : "Ready")) | ||
| 1301 | : undefined | ||
| 1302 | } | ||
| 1211 | /> | 1303 | /> |
| 1212 | ); | 1304 | ); |
| 1213 | 1305 | ||
| ... | @@ -1254,6 +1346,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1254,6 +1346,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1254 | [isRenaming, onStartRename, thread.title, threadRef], | 1346 | [isRenaming, onStartRename, thread.title, threadRef], |
| 1255 | ); | 1347 | ); |
| 1256 | const [isFileDragOver, setIsFileDragOver] = useState(false); | 1348 | const [isFileDragOver, setIsFileDragOver] = useState(false); |
| 1349 | const [tooltipOpen, setTooltipOpen] = useState(false); | ||
| 1257 | const fileDropHandlers = useMemo( | 1350 | const fileDropHandlers = useMemo( |
| 1258 | () => | 1351 | () => |
| 1259 | onFileDropThreads | 1352 | onFileDropThreads |
| ... | @@ -1416,7 +1509,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1416,7 +1509,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1416 | // A zero-height boundary also makes dnd-kit scale the source to | 1509 | // A zero-height boundary also makes dnd-kit scale the source to |
| 1417 | // zero. Only projected peers use scaleY as a visibility sentinel. | 1510 | // zero. Only projected peers use scaleY as a visibility sentinel. |
| 1418 | visibility: | 1511 | visibility: |
| 1419 | !sortable.isDragging && sortable.transform?.scaleY === 0 | 1512 | sortable.hidden || (!sortable.isDragging && sortable.transform?.scaleY === 0) |
| 1420 | ? ("hidden" as const) | 1513 | ? ("hidden" as const) |
| 1421 | : undefined, | 1514 | : undefined, |
| 1422 | }, | 1515 | }, |
| ... | @@ -1557,6 +1650,77 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1557,6 +1650,77 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1557 | ) | 1650 | ) |
| 1558 | ) : null; | 1651 | ) : null; |
| 1559 | 1652 | ||
| 1653 | if (compact) { | ||
| 1654 | return ( | ||
| 1655 | <li | ||
| 1656 | data-thread-item | ||
| 1657 | {...sortableRootProps} | ||
| 1658 | {...(fileDropHandlers ?? {})} | ||
| 1659 | className={cn("list-none", sortable?.isDragging && "relative z-20")} | ||
| 1660 | > | ||
| 1661 | <Tooltip | ||
| 1662 | open={ | ||
| 1663 | sortable?.hidden ? false : sortable?.isDragging ? dragDestination !== null : tooltipOpen | ||
| 1664 | } | ||
| 1665 | onOpenChange={setTooltipOpen} | ||
| 1666 | > | ||
| 1667 | <TooltipTrigger | ||
| 1668 | render={ | ||
| 1669 | <div | ||
| 1670 | ref={rowRef} | ||
| 1671 | role="button" | ||
| 1672 | tabIndex={0} | ||
| 1673 | aria-label={`${thread.title}${topStatus ? `, ${topStatus.label}` : ""}`} | ||
| 1674 | aria-current={props.isActive ? "page" : undefined} | ||
| 1675 | aria-busy={isRegeneratingTitle || undefined} | ||
| 1676 | className={cn( | ||
| 1677 | rowSurfaceClassName, | ||
| 1678 | "flex h-7 items-center justify-center focus-visible:ring-2 focus-visible:ring-ring", | ||
| 1679 | )} | ||
| 1680 | onClick={handleClick} | ||
| 1681 | onDoubleClick={handleDoubleClick} | ||
| 1682 | onKeyDown={handleKeyDown} | ||
| 1683 | onContextMenu={handleContextMenu} | ||
| 1684 | /> | ||
| 1685 | } | ||
| 1686 | > | ||
| 1687 | {props.project ? ( | ||
| 1688 | <ProjectFavicon project={props.project} className="size-4 shrink-0" /> | ||
| 1689 | ) : driverKind ? ( | ||
| 1690 | <ProviderInstanceIcon | ||
| 1691 | driverKind={driverKind} | ||
| 1692 | displayName={providerEntry?.displayName ?? modelInstanceId} | ||
| 1693 | iconClassName="size-4" | ||
| 1694 | /> | ||
| 1695 | ) : ( | ||
| 1696 | <SquarePenIcon aria-hidden className="size-4" /> | ||
| 1697 | )} | ||
| 1698 | {topStatus ? ( | ||
| 1699 | <span | ||
| 1700 | aria-hidden | ||
| 1701 | className={cn( | ||
| 1702 | "absolute right-0.5 bottom-0.5 size-1.5 rounded-full bg-current", | ||
| 1703 | topStatus.className, | ||
| 1704 | )} | ||
| 1705 | /> | ||
| 1706 | ) : hasUnsentDraft ? ( | ||
| 1707 | <span | ||
| 1708 | aria-hidden | ||
| 1709 | className="absolute right-0.5 bottom-0.5 size-1.5 rounded-full bg-primary" | ||
| 1710 | /> | ||
| 1711 | ) : null} | ||
| 1712 | {props.jumpLabel ? <JumpHintBadge label={props.jumpLabel} /> : null} | ||
| 1713 | </TooltipTrigger> | ||
| 1714 | {sortable?.isDragging ? ( | ||
| 1715 | <TooltipPopup side="right">{dragDestination}</TooltipPopup> | ||
| 1716 | ) : ( | ||
| 1717 | detailsTooltip | ||
| 1718 | )} | ||
| 1719 | </Tooltip> | ||
| 1720 | </li> | ||
| 1721 | ); | ||
| 1722 | } | ||
| 1723 | |||
| 1560 | if (variant === "slim") { | 1724 | if (variant === "slim") { |
| 1561 | return ( | 1725 | return ( |
| 1562 | <li | 1726 | <li |
| ... | @@ -1569,7 +1733,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1569,7 +1733,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1569 | sortable?.isDragging && "relative z-20", | 1733 | sortable?.isDragging && "relative z-20", |
| 1570 | )} | 1734 | )} |
| 1571 | > | 1735 | > |
| 1572 | <Tooltip disabled={sortable?.isDragging}> | 1736 | <Tooltip |
| 1737 | open={!sortable?.isDragging && tooltipOpen} | ||
| 1738 | onOpenChange={setTooltipOpen} | ||
| 1739 | disabled={sortable?.isDragging} | ||
| 1740 | > | ||
| 1573 | <TooltipTrigger | 1741 | <TooltipTrigger |
| 1574 | render={ | 1742 | render={ |
| 1575 | <div | 1743 | <div |
| ... | @@ -1722,7 +1890,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -1722,7 +1890,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1722 | sortable?.isDragging && "relative z-20", | 1890 | sortable?.isDragging && "relative z-20", |
| 1723 | )} | 1891 | )} |
| 1724 | > | 1892 | > |
| 1725 | <Tooltip disabled={snoozeMenuOpen || sortable?.isDragging}> | 1893 | <Tooltip |
| 1894 | open={!snoozeMenuOpen && !sortable?.isDragging && tooltipOpen} | ||
| 1895 | onOpenChange={setTooltipOpen} | ||
| 1896 | disabled={snoozeMenuOpen || sortable?.isDragging} | ||
| 1897 | > | ||
| 1726 | <TooltipTrigger | 1898 | <TooltipTrigger |
| 1727 | render={ | 1899 | render={ |
| 1728 | <div | 1900 | <div |
| ... | @@ -1989,6 +2161,9 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { | ... | @@ -1989,6 +2161,9 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { |
| 1989 | onFileDropThreads: (threadRef: ScopedThreadRef, files: File[]) => void; | 2161 | onFileDropThreads: (threadRef: ScopedThreadRef, files: File[]) => void; |
| 1990 | }) { | 2162 | }) { |
| 1991 | const { thread } = props; | 2163 | const { thread } = props; |
| 2164 | const compactEnabled = useCompactSidebarEnabled(); | ||
| 2165 | const { state, isMobile } = useSidebar(); | ||
| 2166 | const compact = compactEnabled && state === "collapsed" && !isMobile; | ||
| 1992 | const threadRef = useMemo( | 2167 | const threadRef = useMemo( |
| 1993 | () => scopeThreadRef(thread.environmentId, thread.id), | 2168 | () => scopeThreadRef(thread.environmentId, thread.id), |
| 1994 | [thread.environmentId, thread.id], | 2169 | [thread.environmentId, thread.id], |
| ... | @@ -2074,6 +2249,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { | ... | @@ -2074,6 +2249,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { |
| 2074 | onClick={props.onSelect} | 2249 | onClick={props.onSelect} |
| 2075 | className={cn( | 2250 | className={cn( |
| 2076 | "flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-left text-sm outline-none", | 2251 | "flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-left text-sm outline-none", |
| 2252 | compact && "justify-center px-0", | ||
| 2077 | props.isHighlighted || props.isRouteActive | 2253 | props.isHighlighted || props.isRouteActive |
| 2078 | ? "bg-sidebar-row-active text-sidebar-foreground" | 2254 | ? "bg-sidebar-row-active text-sidebar-foreground" |
| 2079 | : "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground", | 2255 | : "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground", |
| ... | @@ -2085,9 +2261,15 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { | ... | @@ -2085,9 +2261,15 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { |
| 2085 | > | 2261 | > |
| 2086 | {props.project ? ( | 2262 | {props.project ? ( |
| 2087 | <ProjectFavicon project={props.project} className="size-4 shrink-0" /> | 2263 | <ProjectFavicon project={props.project} className="size-4 shrink-0" /> |
| 2264 | ) : compact ? ( | ||
| 2265 | <SquarePenIcon aria-hidden className="size-4" /> | ||
| 2088 | ) : null} | 2266 | ) : null} |
| 2089 | <span className="min-w-0 flex-1 truncate">{thread.title}</span> | 2267 | <span className={compact ? "sr-only" : "min-w-0 flex-1 truncate"}>{thread.title}</span> |
| 2090 | <span className="shrink-0 text-xs text-muted-foreground/55 tabular-nums"> | 2268 | <span |
| 2269 | className={ | ||
| 2270 | compact ? "sr-only" : "shrink-0 text-xs text-muted-foreground/55 tabular-nums" | ||
| 2271 | } | ||
| 2272 | > | ||
| 2091 | {threadTimeLabel(thread)} | 2273 | {threadTimeLabel(thread)} |
| 2092 | </span> | 2274 | </span> |
| 2093 | </TooltipTrigger> | 2275 | </TooltipTrigger> |
| ... | @@ -2115,7 +2297,10 @@ export default function Sidebar() { | ... | @@ -2115,7 +2297,10 @@ export default function Sidebar() { |
| 2115 | const projectOrder = useUiStateStore((store) => store.projectOrder); | 2297 | const projectOrder = useUiStateStore((store) => store.projectOrder); |
| 2116 | const threads = useThreadShells(); | 2298 | const threads = useThreadShells(); |
| 2117 | const router = useRouter(); | 2299 | const router = useRouter(); |
| 2118 | const { isMobile, setOpenMobile } = useSidebar(); | 2300 | const { isMobile, setOpenMobile, setOpen, state: sidebarState } = useSidebar(); |
| 2301 | const compactEnabled = useCompactSidebarEnabled(); | ||
| 2302 | const compact = compactEnabled && sidebarState === "collapsed" && !isMobile; | ||
| 2303 | const [snoozedFooter, setSnoozedFooter] = useState<HTMLUListElement | null>(null); | ||
| 2119 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); | 2304 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); |
| 2120 | const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete); | 2305 | const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete); |
| 2121 | const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive); | 2306 | const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive); |
| ... | @@ -2687,6 +2872,7 @@ export default function Sidebar() { | ... | @@ -2687,6 +2872,7 @@ export default function Sidebar() { |
| 2687 | [setSettledShelfExpanded], | 2872 | [setSettledShelfExpanded], |
| 2688 | ); | 2873 | ); |
| 2689 | const renderedSettledThreads = useMemo(() => { | 2874 | const renderedSettledThreads = useMemo(() => { |
| 2875 | if (compact) return EMPTY_THREADS; | ||
| 2690 | if (settledShelfExpanded) return visibleSettledThreads; | 2876 | if (settledShelfExpanded) return visibleSettledThreads; |
| 2691 | if (routeThreadKey === null) return EMPTY_THREADS; | 2877 | if (routeThreadKey === null) return EMPTY_THREADS; |
| 2692 | const routeThread = visibleSettledThreads.find( | 2878 | const routeThread = visibleSettledThreads.find( |
| ... | @@ -2694,7 +2880,7 @@ export default function Sidebar() { | ... | @@ -2694,7 +2880,7 @@ export default function Sidebar() { |
| 2694 | scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) === routeThreadKey, | 2880 | scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) === routeThreadKey, |
| 2695 | ); | 2881 | ); |
| 2696 | return routeThread === undefined ? EMPTY_THREADS : [routeThread]; | 2882 | return routeThread === undefined ? EMPTY_THREADS : [routeThread]; |
| 2697 | }, [routeThreadKey, settledShelfExpanded, visibleSettledThreads]); | 2883 | }, [compact, routeThreadKey, settledShelfExpanded, visibleSettledThreads]); |
| 2698 | 2884 | ||
| 2699 | // The snoozed shelf is collapsed by default: out of the way, never gone. | 2885 | // The snoozed shelf is collapsed by default: out of the way, never gone. |
| 2700 | // Collapsed threads don't render (and so don't participate in jump | 2886 | // Collapsed threads don't render (and so don't participate in jump |
| ... | @@ -2922,10 +3108,14 @@ export default function Sidebar() { | ... | @@ -2922,10 +3108,14 @@ export default function Sidebar() { |
| 2922 | 3108 | ||
| 2923 | const [renamingThreadKey, setRenamingThreadKey] = useState<string | null>(null); | 3109 | const [renamingThreadKey, setRenamingThreadKey] = useState<string | null>(null); |
| 2924 | const [renamingTitle, setRenamingTitle] = useState(""); | 3110 | const [renamingTitle, setRenamingTitle] = useState(""); |
| 2925 | const startThreadRename = useCallback((threadRef: ScopedThreadRef, title: string) => { | 3111 | const startThreadRename = useCallback( |
| 2926 | setRenamingThreadKey(scopedThreadKey(threadRef)); | 3112 | (threadRef: ScopedThreadRef, title: string) => { |
| 2927 | setRenamingTitle(title); | 3113 | if (compact) setOpen(true); |
| 2928 | }, []); | 3114 | setRenamingThreadKey(scopedThreadKey(threadRef)); |
| 3115 | setRenamingTitle(title); | ||
| 3116 | }, | ||
| 3117 | [compact, setOpen], | ||
| 3118 | ); | ||
| 2929 | const cancelThreadRename = useCallback(() => setRenamingThreadKey(null), []); | 3119 | const cancelThreadRename = useCallback(() => setRenamingThreadKey(null), []); |
| 2930 | const commitThreadRename = useCallback( | 3120 | const commitThreadRename = useCallback( |
| 2931 | (threadRef: ScopedThreadRef, title: string, originalTitle: string) => { | 3121 | (threadRef: ScopedThreadRef, title: string, originalTitle: string) => { |
| ... | @@ -3087,8 +3277,18 @@ export default function Sidebar() { | ... | @@ -3087,8 +3277,18 @@ export default function Sidebar() { |
| 3087 | const threadListRef = useRef<HTMLUListElement | null>(null); | 3277 | const threadListRef = useRef<HTMLUListElement | null>(null); |
| 3088 | const dragLabelOffsetRef = useRef(0); | 3278 | const dragLabelOffsetRef = useRef(0); |
| 3089 | const restrictBelowPins = useCallback<Modifier>( | 3279 | const restrictBelowPins = useCallback<Modifier>( |
| 3090 | (args) => restrictBelowSidebarLabel(args, dragLabelOffsetRef.current), | 3280 | (args) => |
| 3091 | [], | 3281 | restrictBelowSidebarLabel( |
| 3282 | { | ||
| 3283 | ...args, | ||
| 3284 | // The fixed snoozed shelf shares the main list's drag boundary. | ||
| 3285 | containerNodeRect: compact | ||
| 3286 | ? (threadListRef.current?.getBoundingClientRect() ?? args.containerNodeRect) | ||
| 3287 | : args.containerNodeRect, | ||
| 3288 | }, | ||
| 3289 | dragLabelOffsetRef.current, | ||
| 3290 | ), | ||
| 3291 | [compact], | ||
| 3092 | ); | 3292 | ); |
| 3093 | const listMotionRef = useRef<ReturnType<typeof createSidebarListMotion> | null>(null); | 3293 | const listMotionRef = useRef<ReturnType<typeof createSidebarListMotion> | null>(null); |
| 3094 | const attachListMotionRef = useCallback((node: HTMLUListElement | null) => { | 3294 | const attachListMotionRef = useCallback((node: HTMLUListElement | null) => { |
| ... | @@ -3310,7 +3510,7 @@ export default function Sidebar() { | ... | @@ -3310,7 +3510,7 @@ export default function Sidebar() { |
| 3310 | pinnedThreads.length + | 3510 | pinnedThreads.length + |
| 3311 | activeThreads.length + | 3511 | activeThreads.length + |
| 3312 | snoozedThreads.length + | 3512 | snoozedThreads.length + |
| 3313 | settledThreads.length === | 3513 | (compact ? 0 : settledThreads.length) === |
| 3314 | 0 | 3514 | 0 |
| 3315 | ) { | 3515 | ) { |
| 3316 | return []; | 3516 | return []; |
| ... | @@ -3326,13 +3526,15 @@ export default function Sidebar() { | ... | @@ -3326,13 +3526,15 @@ export default function Sidebar() { |
| 3326 | items.push({ kind: "marker", marker: "snoozed-header" }); | 3526 | items.push({ kind: "marker", marker: "snoozed-header" }); |
| 3327 | items.push(...rowsOf(visibleSnoozedThreads, "snoozed")); | 3527 | items.push(...rowsOf(visibleSnoozedThreads, "snoozed")); |
| 3328 | } | 3528 | } |
| 3329 | items.push({ kind: "marker", marker: "settled-header" }); | 3529 | if (!compact) { |
| 3330 | const settledRows = rowsOf(renderedSettledThreads, "settled"); | 3530 | items.push({ kind: "marker", marker: "settled-header" }); |
| 3331 | items.push({ kind: "marker", marker: "settled-placeholder" }); | 3531 | items.push({ kind: "marker", marker: "settled-placeholder" }); |
| 3332 | items.push(...settledRows); | 3532 | items.push(...rowsOf(renderedSettledThreads, "settled")); |
| 3533 | } | ||
| 3333 | return items; | 3534 | return items; |
| 3334 | }, [ | 3535 | }, [ |
| 3335 | activeThreads, | 3536 | activeThreads, |
| 3537 | compact, | ||
| 3336 | pinnedThreads, | 3538 | pinnedThreads, |
| 3337 | renderedSettledThreads, | 3539 | renderedSettledThreads, |
| 3338 | settledThreads.length, | 3540 | settledThreads.length, |
| ... | @@ -3402,6 +3604,7 @@ export default function Sidebar() { | ... | @@ -3402,6 +3604,7 @@ export default function Sidebar() { |
| 3402 | () => | 3604 | () => |
| 3403 | createSidebarSortingStrategy({ | 3605 | createSidebarSortingStrategy({ |
| 3404 | items: sidebarListItems, | 3606 | items: sidebarListItems, |
| 3607 | compact, | ||
| 3405 | boundaryLabelHeight: SIDEBAR_DRAG_LABEL_HEIGHT, | 3608 | boundaryLabelHeight: SIDEBAR_DRAG_LABEL_HEIGHT, |
| 3406 | settledOrder: draggedSettledOrder, | 3609 | settledOrder: draggedSettledOrder, |
| 3407 | settledExpanded: settledShelfExpanded, | 3610 | settledExpanded: settledShelfExpanded, |
| ... | @@ -3410,6 +3613,7 @@ export default function Sidebar() { | ... | @@ -3410,6 +3613,7 @@ export default function Sidebar() { |
| 3410 | snoozedThreadCount: snoozedThreads.length, | 3613 | snoozedThreadCount: snoozedThreads.length, |
| 3411 | }), | 3614 | }), |
| 3412 | [ | 3615 | [ |
| 3616 | compact, | ||
| 3413 | draggedSettledOrder, | 3617 | draggedSettledOrder, |
| 3414 | routeThreadKey, | 3618 | routeThreadKey, |
| 3415 | settledShelfExpanded, | 3619 | settledShelfExpanded, |
| ... | @@ -3418,6 +3622,22 @@ export default function Sidebar() { | ... | @@ -3418,6 +3622,22 @@ export default function Sidebar() { |
| 3418 | snoozedThreads.length, | 3622 | snoozedThreads.length, |
| 3419 | ], | 3623 | ], |
| 3420 | ); | 3624 | ); |
| 3625 | const draggingCompactSnoozed = compact && dragState?.activeSection === "snoozed"; | ||
| 3626 | const compactSnoozedDragThread = | ||
| 3627 | draggingCompactSnoozed && dragState ? threadByKey.get(dragState.activeKey) : undefined; | ||
| 3628 | const compactSidebarSortingStrategy = useCallback<typeof sidebarSortingStrategy>( | ||
| 3629 | (args) => { | ||
| 3630 | const item = sidebarListItems[args.index]; | ||
| 3631 | // Footer rows stay anchored while the main list previews a reorder. | ||
| 3632 | if ( | ||
| 3633 | item?.kind === "thread" ? item.section === "snoozed" : item?.marker === "snoozed-header" | ||
| 3634 | ) { | ||
| 3635 | return null; | ||
| 3636 | } | ||
| 3637 | return sidebarSortingStrategy(args); | ||
| 3638 | }, | ||
| 3639 | [sidebarListItems, sidebarSortingStrategy], | ||
| 3640 | ); | ||
| 3421 | // Hidden and filtered threads keep their keys. Reserve those slots without | 3641 | // Hidden and filtered threads keep their keys. Reserve those slots without |
| 3422 | // including the rows in the visible drop order or writing to them. | 3642 | // including the rows in the visible drop order or writing to them. |
| 3423 | const { pinnedKeysById, activeKeysById } = useMemo( | 3643 | const { pinnedKeysById, activeKeysById } = useMemo( |
| ... | @@ -4321,7 +4541,16 @@ export default function Sidebar() { | ... | @@ -4321,7 +4541,16 @@ export default function Sidebar() { |
| 4321 | <> | 4541 | <> |
| 4322 | <SidebarChromeHeader isElectron={isElectron} /> | 4542 | <SidebarChromeHeader isElectron={isElectron} /> |
| 4323 | <SidebarContent | 4543 | <SidebarContent |
| 4324 | className="gap-0" | 4544 | className={cn("gap-0", compact && "group-data-[collapsible=icon]:overflow-visible")} |
| 4545 | fixedFooter={ | ||
| 4546 | compact && !isSearchingThreads && snoozedThreads.length > 0 ? ( | ||
| 4547 | <ul | ||
| 4548 | ref={setSnoozedFooter} | ||
| 4549 | aria-label="Snoozed threads" | ||
| 4550 | className="relative flex max-h-[min(30vh,16rem)] flex-col gap-px overflow-y-auto px-[var(--sidebar-content-inset)] pb-1" | ||
| 4551 | /> | ||
| 4552 | ) : null | ||
| 4553 | } | ||
| 4325 | fixedHeader={ | 4554 | fixedHeader={ |
| 4326 | // Lifted above the stage backdrop, whose fade bleeds below the | 4555 | // Lifted above the stage backdrop, whose fade bleeds below the |
| 4327 | // header and would otherwise paint across the search row's outline. | 4556 | // header and would otherwise paint across the search row's outline. |
| ... | @@ -4381,8 +4610,12 @@ export default function Sidebar() { | ... | @@ -4381,8 +4610,12 @@ export default function Sidebar() { |
| 4381 | // popup opens under the field, is at least as wide as it, | 4610 | // popup opens under the field, is at least as wide as it, |
| 4382 | // and grows to fit project names up to a cap, past which | 4611 | // and grows to fit project names up to a cap, past which |
| 4383 | // the rows truncate. | 4612 | // the rows truncate. |
| 4384 | anchor={headerSearchRef} | 4613 | anchor={compact ? undefined : headerSearchRef} |
| 4385 | className="max-w-[min(18rem,var(--available-width))] overflow-hidden" | 4614 | side={compact ? "right" : "bottom"} |
| 4615 | className={cn( | ||
| 4616 | "max-w-[min(18rem,var(--available-width))] overflow-hidden", | ||
| 4617 | compact && "min-w-56", | ||
| 4618 | )} | ||
| 4386 | > | 4619 | > |
| 4387 | <ComboboxSearchInput | 4620 | <ComboboxSearchInput |
| 4388 | aria-label="Search projects" | 4621 | aria-label="Search projects" |
| ... | @@ -4537,7 +4770,10 @@ export default function Sidebar() { | ... | @@ -4537,7 +4770,10 @@ export default function Sidebar() { |
| 4537 | ) : ( | 4770 | ) : ( |
| 4538 | <p | 4771 | <p |
| 4539 | role="status" | 4772 | role="status" |
| 4540 | className="px-2 py-6 text-center text-xs text-sidebar-muted-foreground" | 4773 | className={cn( |
| 4774 | "px-2 py-6 text-center text-xs text-sidebar-muted-foreground", | ||
| 4775 | compact && "sr-only", | ||
| 4776 | )} | ||
| 4541 | > | 4777 | > |
| 4542 | No threads found | 4778 | No threads found |
| 4543 | </p> | 4779 | </p> |
| ... | @@ -4556,14 +4792,17 @@ export default function Sidebar() { | ... | @@ -4556,14 +4792,17 @@ export default function Sidebar() { |
| 4556 | modifiers={[ | 4792 | modifiers={[ |
| 4557 | restrictToVerticalAxis, | 4793 | restrictToVerticalAxis, |
| 4558 | restrictBelowPins, | 4794 | restrictBelowPins, |
| 4559 | restrictToFirstScrollableAncestor, | 4795 | ...(compact ? [] : [restrictToFirstScrollableAncestor]), |
| 4560 | ]} | 4796 | ]} |
| 4561 | onDragStart={handleThreadDragStart} | 4797 | onDragStart={handleThreadDragStart} |
| 4562 | onDragOver={handleThreadDragOver} | 4798 | onDragOver={handleThreadDragOver} |
| 4563 | onDragEnd={handleThreadDragEnd} | 4799 | onDragEnd={handleThreadDragEnd} |
| 4564 | > | 4800 | > |
| 4565 | <SidebarDragLifecycle onUnmount={cancelThreadDrag} /> | 4801 | <SidebarDragLifecycle onUnmount={cancelThreadDrag} /> |
| 4566 | <SortableContext items={sortableIds} strategy={sidebarSortingStrategy}> | 4802 | <SortableContext |
| 4803 | items={sortableIds} | ||
| 4804 | strategy={compact ? compactSidebarSortingStrategy : sidebarSortingStrategy} | ||
| 4805 | > | ||
| 4567 | <ul | 4806 | <ul |
| 4568 | ref={attachListMotionRef} | 4807 | ref={attachListMotionRef} |
| 4569 | role="list" | 4808 | role="list" |
| ... | @@ -4693,11 +4932,25 @@ export default function Sidebar() { | ... | @@ -4693,11 +4932,25 @@ export default function Sidebar() { |
| 4693 | !draggableThreadKeys.has(threadKey) || optimisticDrop !== null | 4932 | !draggableThreadKeys.has(threadKey) || optimisticDrop !== null |
| 4694 | } | 4933 | } |
| 4695 | > | 4934 | > |
| 4696 | {(bag) => renderThreadRowInner(thread, section, bag)} | 4935 | {(bag) => |
| 4936 | renderThreadRowInner( | ||
| 4937 | thread, | ||
| 4938 | section, | ||
| 4939 | draggingCompactSnoozed && bag.isDragging | ||
| 4940 | ? { ...bag, hidden: true } | ||
| 4941 | : bag, | ||
| 4942 | ) | ||
| 4943 | } | ||
| 4697 | </SortableThreadRow> | 4944 | </SortableThreadRow> |
| 4698 | ); | 4945 | ); |
| 4699 | }; | 4946 | }; |
| 4700 | const from = dragState?.activeSection ?? null; | 4947 | const from = dragState?.activeSection ?? null; |
| 4948 | const showDragLabels = | ||
| 4949 | from !== null && | ||
| 4950 | (!compact || | ||
| 4951 | dragTargetSection === "active" || | ||
| 4952 | dragTargetSection === "pinned"); | ||
| 4953 | const snoozedItems: ReactNode[] = []; | ||
| 4701 | const items: ReactNode[] = [ | 4954 | const items: ReactNode[] = [ |
| 4702 | <SidebarDraftBlock | 4955 | <SidebarDraftBlock |
| 4703 | key="draft-sessions" | 4956 | key="draft-sessions" |
| ... | @@ -4709,8 +4962,17 @@ export default function Sidebar() { | ... | @@ -4709,8 +4962,17 @@ export default function Sidebar() { |
| 4709 | />, | 4962 | />, |
| 4710 | ]; | 4963 | ]; |
| 4711 | for (const item of sidebarListItems) { | 4964 | for (const item of sidebarListItems) { |
| 4965 | const destination = | ||
| 4966 | compact && | ||
| 4967 | (item.kind === "thread" | ||
| 4968 | ? item.section === "snoozed" | ||
| 4969 | : item.marker === "snoozed-header") | ||
| 4970 | ? snoozedItems | ||
| 4971 | : items; | ||
| 4712 | if (item.kind === "thread") { | 4972 | if (item.kind === "thread") { |
| 4713 | items.push(renderThreadRow(threadByKey.get(item.key)!, item.section)); | 4973 | destination.push( |
| 4974 | renderThreadRow(threadByKey.get(item.key)!, item.section), | ||
| 4975 | ); | ||
| 4714 | continue; | 4976 | continue; |
| 4715 | } | 4977 | } |
| 4716 | switch (item.marker) { | 4978 | switch (item.marker) { |
| ... | @@ -4720,7 +4982,7 @@ export default function Sidebar() { | ... | @@ -4720,7 +4982,7 @@ export default function Sidebar() { |
| 4720 | key="pinned-header" | 4982 | key="pinned-header" |
| 4721 | marker="pinned-header" | 4983 | marker="pinned-header" |
| 4722 | label="Pinned" | 4984 | label="Pinned" |
| 4723 | visible={from !== null} | 4985 | visible={showDragLabels} |
| 4724 | isDropTarget={dragTargetSection === "pinned"} | 4986 | isDropTarget={dragTargetSection === "pinned"} |
| 4725 | />, | 4987 | />, |
| 4726 | ); | 4988 | ); |
| ... | @@ -4731,7 +4993,7 @@ export default function Sidebar() { | ... | @@ -4731,7 +4993,7 @@ export default function Sidebar() { |
| 4731 | key="pinned-divider" | 4993 | key="pinned-divider" |
| 4732 | marker="pinned-divider" | 4994 | marker="pinned-divider" |
| 4733 | label="Active" | 4995 | label="Active" |
| 4734 | visible={from !== null} | 4996 | visible={showDragLabels} |
| 4735 | isDropTarget={dragTargetSection === "active"} | 4997 | isDropTarget={dragTargetSection === "active"} |
| 4736 | />, | 4998 | />, |
| 4737 | ); | 4999 | ); |
| ... | @@ -4755,7 +5017,7 @@ export default function Sidebar() { | ... | @@ -4755,7 +5017,7 @@ export default function Sidebar() { |
| 4755 | ); | 5017 | ); |
| 4756 | break; | 5018 | break; |
| 4757 | case "snoozed-header": | 5019 | case "snoozed-header": |
| 4758 | items.push( | 5020 | destination.push( |
| 4759 | <SidebarSectionHeader | 5021 | <SidebarSectionHeader |
| 4760 | key="snoozed-shelf-header" | 5022 | key="snoozed-shelf-header" |
| 4761 | marker="snoozed-header" | 5023 | marker="snoozed-header" |
| ... | @@ -4810,18 +5072,56 @@ export default function Sidebar() { | ... | @@ -4810,18 +5072,56 @@ export default function Sidebar() { |
| 4810 | break; | 5072 | break; |
| 4811 | } | 5073 | } |
| 4812 | } | 5074 | } |
| 4813 | return items; | 5075 | // Keep the shelf inside this drag context while anchoring |
| 5076 | // its DOM outside the main thread scroller. | ||
| 5077 | return [ | ||
| 5078 | ...items, | ||
| 5079 | compact && snoozedFooter | ||
| 5080 | ? createPortal(snoozedItems, snoozedFooter, "snoozed-footer") | ||
| 5081 | : null, | ||
| 5082 | compactSnoozedDragThread | ||
| 5083 | ? createPortal( | ||
| 5084 | <DragOverlay dropAnimation={null}> | ||
| 5085 | <ul className="pointer-events-none"> | ||
| 5086 | {renderThreadRowInner(compactSnoozedDragThread, "snoozed", { | ||
| 5087 | isDragging: true, | ||
| 5088 | listeners: undefined, | ||
| 5089 | setNodeRef: () => {}, | ||
| 5090 | transform: null, | ||
| 5091 | transition: undefined, | ||
| 5092 | })} | ||
| 5093 | </ul> | ||
| 5094 | </DragOverlay>, | ||
| 5095 | document.body, | ||
| 5096 | "compact-snoozed-drag", | ||
| 5097 | ) | ||
| 5098 | : null, | ||
| 5099 | ]; | ||
| 4814 | })()} | 5100 | })()} |
| 4815 | {settledShelfExpanded && hiddenSettledCount > 0 ? ( | 5101 | {!compact && settledShelfExpanded && hiddenSettledCount > 0 ? ( |
| 4816 | <li className="list-none"> | 5102 | <li className="list-none"> |
| 4817 | <button | 5103 | <Tooltip disabled={!compact}> |
| 4818 | type="button" | 5104 | <TooltipTrigger |
| 4819 | onClick={showMoreSettled} | 5105 | render={ |
| 4820 | className="flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-left text-sm text-sidebar-muted-foreground/55 hover:bg-sidebar-row-hover hover:text-sidebar-foreground" | 5106 | <button |
| 4821 | > | 5107 | type="button" |
| 4822 | <PlusIcon aria-hidden className="size-4 shrink-0" /> | 5108 | onClick={showMoreSettled} |
| 4823 | Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more | 5109 | className={cn( |
| 4824 | </button> | 5110 | "flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-left text-sm text-sidebar-muted-foreground/55 hover:bg-sidebar-row-hover hover:text-sidebar-foreground", |
| 5111 | compact && "justify-center px-0", | ||
| 5112 | )} | ||
| 5113 | /> | ||
| 5114 | } | ||
| 5115 | > | ||
| 5116 | <PlusIcon aria-hidden className="size-4 shrink-0" /> | ||
| 5117 | <span className={compact ? "sr-only" : undefined}> | ||
| 5118 | Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more | ||
| 5119 | </span> | ||
| 5120 | </TooltipTrigger> | ||
| 5121 | <TooltipPopup side="right"> | ||
| 5122 | Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more | ||
| 5123 | </TooltipPopup> | ||
| 5124 | </Tooltip> | ||
| 4825 | </li> | 5125 | </li> |
| 4826 | ) : null} | 5126 | ) : null} |
| 4827 | </ul> | 5127 | </ul> |
| ... | @@ -4836,20 +5136,29 @@ export default function Sidebar() { | ... | @@ -4836,20 +5136,29 @@ export default function Sidebar() { |
| 4836 | snoozedThreads.length + | 5136 | snoozedThreads.length + |
| 4837 | settledThreads.length === | 5137 | settledThreads.length === |
| 4838 | 0 ? ( | 5138 | 0 ? ( |
| 4839 | <div className="flex flex-col items-center gap-2 px-2 py-6 text-center text-xs text-muted-foreground/60"> | 5139 | <div |
| 5140 | className={cn( | ||
| 5141 | "flex flex-col items-center gap-2 px-2 py-6 text-center text-xs text-muted-foreground/60", | ||
| 5142 | compact && "px-0", | ||
| 5143 | )} | ||
| 5144 | > | ||
| 4840 | {projects.length === 0 ? ( | 5145 | {projects.length === 0 ? ( |
| 4841 | <> | 5146 | <> |
| 4842 | <span>No projects yet</span> | 5147 | <span className={compact ? "sr-only" : undefined}>No projects yet</span> |
| 4843 | <button | 5148 | <button |
| 4844 | type="button" | 5149 | type="button" |
| 4845 | onClick={openAddProjectCommandPalette} | 5150 | onClick={openAddProjectCommandPalette} |
| 4846 | className="inline-flex cursor-pointer items-center gap-1.5 rounded-md border border-sidebar-border px-2.5 py-1 text-[11px] font-medium text-sidebar-muted-foreground transition-colors hover:bg-sidebar-row-hover hover:text-sidebar-foreground" | 5151 | aria-label="Add project" |
| 5152 | className={cn( | ||
| 5153 | "inline-flex cursor-pointer items-center gap-1.5 rounded-md border border-sidebar-border px-2.5 py-1 text-[11px] font-medium text-sidebar-muted-foreground transition-colors hover:bg-sidebar-row-hover hover:text-sidebar-foreground", | ||
| 5154 | compact && "justify-center px-1.5", | ||
| 5155 | )} | ||
| 4847 | > | 5156 | > |
| 4848 | <PlusIcon className="-mx-0.5 size-3" /> | 5157 | <PlusIcon className="-mx-0.5 size-3" /> |
| 4849 | Add project | 5158 | <span className={compact ? "sr-only" : undefined}>Add project</span> |
| 4850 | </button> | 5159 | </button> |
| 4851 | </> | 5160 | </> |
| 4852 | ) : scopedProjectGroup ? ( | 5161 | ) : compact ? null : scopedProjectGroup ? ( |
| 4853 | `No threads in ${scopedProjectGroup.displayName} yet` | 5162 | `No threads in ${scopedProjectGroup.displayName} yet` |
| 4854 | ) : ( | 5163 | ) : ( |
| 4855 | "No threads yet" | 5164 | "No threads yet" |
apps/web/src/components/settings/CompactSidebarPreview.tsx created+53| ... | @@ -0,0 +1,53 @@ | ||
| 1 | import { useState } from "react"; | ||
| 2 | |||
| 3 | import { cn } from "~/lib/utils"; | ||
| 4 | |||
| 5 | export function CompactSidebarPreview() { | ||
| 6 | const [collapsed, setCollapsed] = useState(false); | ||
| 7 | |||
| 8 | return ( | ||
| 9 | <button | ||
| 10 | type="button" | ||
| 11 | aria-label="Toggle compact sidebar preview" | ||
| 12 | aria-pressed={collapsed} | ||
| 13 | className="group/compact-preview relative flex h-10 w-full cursor-pointer overflow-hidden rounded-lg border border-border bg-background p-1 shadow-xs/5 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background" | ||
| 14 | onClick={() => setCollapsed((value) => !value)} | ||
| 15 | > | ||
| 16 | <span | ||
| 17 | aria-hidden | ||
| 18 | className={cn( | ||
| 19 | "flex h-full shrink-0 flex-col justify-center gap-1 overflow-hidden rounded-md bg-sidebar px-1 transition-[width] duration-250 ease-out motion-reduce:transition-none", | ||
| 20 | collapsed ? "w-3" : "w-9", | ||
| 21 | )} | ||
| 22 | > | ||
| 23 | {[0, 1, 2].map((row) => ( | ||
| 24 | <span key={row} className="flex h-1 shrink-0 items-center gap-1"> | ||
| 25 | <span className="size-1 shrink-0 rounded-[1px] bg-muted-foreground/50" /> | ||
| 26 | <span | ||
| 27 | className={cn( | ||
| 28 | "h-0.5 w-4 shrink-0 rounded-full bg-muted-foreground/25 transition-opacity duration-250 ease-out motion-reduce:transition-none", | ||
| 29 | collapsed && "opacity-0", | ||
| 30 | )} | ||
| 31 | /> | ||
| 32 | </span> | ||
| 33 | ))} | ||
| 34 | </span> | ||
| 35 | <span aria-hidden className="flex min-w-0 flex-1 flex-col gap-1 px-1 pt-1"> | ||
| 36 | <span className="h-0.5 w-full rounded-full bg-muted-foreground/25" /> | ||
| 37 | <span className="h-0.5 w-4/5 rounded-full bg-muted-foreground/20" /> | ||
| 38 | <span className="h-0.5 w-3/5 rounded-full bg-muted-foreground/15" /> | ||
| 39 | </span> | ||
| 40 | <span | ||
| 41 | aria-hidden | ||
| 42 | className={cn( | ||
| 43 | "pointer-events-none absolute top-3 left-5 flex w-9 flex-col gap-1 rounded-sm border border-border bg-popover p-1 opacity-0 shadow-xs transition-opacity duration-150 ease-out motion-reduce:transition-none", | ||
| 44 | collapsed && | ||
| 45 | "group-hover/compact-preview:opacity-100 group-focus-visible/compact-preview:opacity-100", | ||
| 46 | )} | ||
| 47 | > | ||
| 48 | <span className="h-0.5 w-full rounded-full bg-popover-foreground/45" /> | ||
| 49 | <span className="h-0.5 w-2/3 rounded-full bg-popover-foreground/25" /> | ||
| 50 | </span> | ||
| 51 | </button> | ||
| 52 | ); | ||
| 53 | } | ||
apps/web/src/components/settings/SettingsPanels.tsx+27| ... | @@ -160,6 +160,7 @@ import { | ... | @@ -160,6 +160,7 @@ import { |
| 160 | import { searchableSetting } from "./settingsSearch"; | 160 | import { searchableSetting } from "./settingsSearch"; |
| 161 | import { ProjectFavicon } from "../ProjectFavicon"; | 161 | import { ProjectFavicon } from "../ProjectFavicon"; |
| 162 | import { PanelAnimationsPreview } from "./PanelAnimationsPreview"; | 162 | import { PanelAnimationsPreview } from "./PanelAnimationsPreview"; |
| 163 | import { CompactSidebarPreview } from "./CompactSidebarPreview"; | ||
| 163 | 164 | ||
| 164 | const ENVIRONMENT_IDENTIFICATION_LABELS: Record<EnvironmentIdentificationMode, string> = { | 165 | const ENVIRONMENT_IDENTIFICATION_LABELS: Record<EnvironmentIdentificationMode, string> = { |
| 165 | artwork: "Artwork", | 166 | artwork: "Artwork", |
| ... | @@ -501,6 +502,9 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -501,6 +502,9 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 501 | ...(theme !== "system" ? ["Theme"] : []), | 502 | ...(theme !== "system" ? ["Theme"] : []), |
| 502 | ...(!followSystem ? ["Follow system"] : []), | 503 | ...(!followSystem ? ["Follow system"] : []), |
| 503 | ...(themeHalves !== null ? ["Theme mix"] : []), | 504 | ...(themeHalves !== null ? ["Theme mix"] : []), |
| 505 | ...(settings.compactSidebarEnabled !== DEFAULT_UNIFIED_SETTINGS.compactSidebarEnabled | ||
| 506 | ? ["Compact sidebar"] | ||
| 507 | : []), | ||
| 504 | ...(settings.appearanceContrast !== DEFAULT_UNIFIED_SETTINGS.appearanceContrast | 508 | ...(settings.appearanceContrast !== DEFAULT_UNIFIED_SETTINGS.appearanceContrast |
| 505 | ? ["Contrast"] | 509 | ? ["Contrast"] |
| 506 | : []), | 510 | : []), |
| ... | @@ -605,6 +609,7 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -605,6 +609,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 605 | settings.browserLinkTarget, | 609 | settings.browserLinkTarget, |
| 606 | settings.browserAutoShowFloatingPreview, | 610 | settings.browserAutoShowFloatingPreview, |
| 607 | settings.appearanceContrast, | 611 | settings.appearanceContrast, |
| 612 | settings.compactSidebarEnabled, | ||
| 608 | settings.diffColorScheme, | 613 | settings.diffColorScheme, |
| 609 | settings.enableAgentBrowserAccess, | 614 | settings.enableAgentBrowserAccess, |
| 610 | settings.confirmQuit, | 615 | settings.confirmQuit, |
| ... | @@ -712,6 +717,7 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -712,6 +717,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 712 | } | 717 | } |
| 713 | updateSettings({ | 718 | updateSettings({ |
| 714 | appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast, | 719 | appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast, |
| 720 | compactSidebarEnabled: DEFAULT_UNIFIED_SETTINGS.compactSidebarEnabled, | ||
| 715 | diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme, | 721 | diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme, |
| 716 | timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, | 722 | timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, |
| 717 | notificationMode: DEFAULT_UNIFIED_SETTINGS.notificationMode, | 723 | notificationMode: DEFAULT_UNIFIED_SETTINGS.notificationMode, |
| ... | @@ -1356,6 +1362,27 @@ export function AppearanceSettingsPanel() { | ... | @@ -1356,6 +1362,27 @@ export function AppearanceSettingsPanel() { |
| 1356 | /> | 1362 | /> |
| 1357 | </SettingsSection> | 1363 | </SettingsSection> |
| 1358 | 1364 | ||
| 1365 | <SettingsSection id="appearance-sidebar" title="Sidebar"> | ||
| 1366 | <SettingsRow | ||
| 1367 | {...searchableSetting("compact-sidebar")} | ||
| 1368 | description="Keep an icon rail when the sidebar is collapsed. Click the preview to try it." | ||
| 1369 | control={ | ||
| 1370 | <div className="grid w-full grid-cols-[5rem_auto] items-center justify-end gap-3 sm:w-auto sm:grid-cols-[7rem_auto] sm:gap-4"> | ||
| 1371 | <CompactSidebarPreview /> | ||
| 1372 | <div className="flex justify-end"> | ||
| 1373 | <Switch | ||
| 1374 | checked={settings.compactSidebarEnabled} | ||
| 1375 | onCheckedChange={(checked) => | ||
| 1376 | updateSettings({ compactSidebarEnabled: Boolean(checked) }) | ||
| 1377 | } | ||
| 1378 | aria-label="Compact sidebar" | ||
| 1379 | /> | ||
| 1380 | </div> | ||
| 1381 | </div> | ||
| 1382 | } | ||
| 1383 | /> | ||
| 1384 | </SettingsSection> | ||
| 1385 | |||
| 1359 | <TypographySection /> | 1386 | <TypographySection /> |
| 1360 | </SettingsPageContainer> | 1387 | </SettingsPageContainer> |
| 1361 | ); | 1388 | ); |
apps/web/src/components/settings/SettingsSidebarNav.tsx+26-7| ... | @@ -24,6 +24,7 @@ import { | ... | @@ -24,6 +24,7 @@ import { |
| 24 | XIcon, | 24 | XIcon, |
| 25 | } from "lucide-react"; | 25 | } from "lucide-react"; |
| 26 | import { useLocation, useNavigate } from "@tanstack/react-router"; | 26 | import { useLocation, useNavigate } from "@tanstack/react-router"; |
| 27 | import { useCompactSidebarEnabled } from "../../hooks/useSettings"; | ||
| 27 | 28 | ||
| 28 | import { Button } from "../ui/button"; | 29 | import { Button } from "../ui/button"; |
| 29 | import { Input } from "../ui/input"; | 30 | import { Input } from "../ui/input"; |
| ... | @@ -111,12 +112,13 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { | ... | @@ -111,12 +112,13 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { |
| 111 | (item) => item.to !== "/settings/projects" || isSettingsOverviewVisible(scopeSearch), | 112 | (item) => item.to !== "/settings/projects" || isSettingsOverviewVisible(scopeSearch), |
| 112 | ); | 113 | ); |
| 113 | const { isMobile, setOpenMobile, open, setOpen } = useSidebar(); | 114 | const { isMobile, setOpenMobile, open, setOpen } = useSidebar(); |
| 115 | const compactSidebarEnabled = useCompactSidebarEnabled(); | ||
| 114 | const searchInputRef = useRef<HTMLInputElement>(null); | 116 | const searchInputRef = useRef<HTMLInputElement>(null); |
| 115 | const [query, setQuery] = useState(""); | 117 | const [query, setQuery] = useState(""); |
| 116 | const [activeResultIndex, setActiveResultIndex] = useState(0); | 118 | const [activeResultIndex, setActiveResultIndex] = useState(0); |
| 117 | const searchableItems = useAvailableSettingsSearchItems(); | 119 | const searchableItems = useAvailableSettingsSearchItems(); |
| 118 | const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]); | 120 | const results = useMemo(() => searchSettings(query, searchableItems), [query, searchableItems]); |
| 119 | const isSearching = query.trim().length > 0; | 121 | const isSearching = query.trim().length > 0 && !(compactSidebarEnabled && !isMobile && !open); |
| 120 | const hasResults = results.length > 0; | 122 | const hasResults = results.length > 0; |
| 121 | 123 | ||
| 122 | useEffect(() => { | 124 | useEffect(() => { |
| ... | @@ -233,7 +235,18 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { | ... | @@ -233,7 +235,18 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { |
| 233 | <> | 235 | <> |
| 234 | <SidebarContent className="overflow-x-hidden"> | 236 | <SidebarContent className="overflow-x-hidden"> |
| 235 | <SidebarGroup className="gap-2 p-[var(--sidebar-content-inset)]"> | 237 | <SidebarGroup className="gap-2 p-[var(--sidebar-content-inset)]"> |
| 236 | <div className="flex h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground"> | 238 | <SidebarMenuButton |
| 239 | className="hidden group-data-[collapsible=icon]:flex" | ||
| 240 | aria-label="Search settings" | ||
| 241 | tooltip="Search settings" | ||
| 242 | onClick={() => { | ||
| 243 | setOpen(true); | ||
| 244 | requestAnimationFrame(() => searchInputRef.current?.focus()); | ||
| 245 | }} | ||
| 246 | > | ||
| 247 | <SearchIcon /> | ||
| 248 | </SidebarMenuButton> | ||
| 249 | <div className="flex h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground group-data-[collapsible=icon]:hidden"> | ||
| 237 | <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" /> | 250 | <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" /> |
| 238 | <Input | 251 | <Input |
| 239 | ref={searchInputRef} | 252 | ref={searchInputRef} |
| ... | @@ -330,10 +343,14 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { | ... | @@ -330,10 +343,14 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { |
| 330 | <SidebarMenuItem key={item.to}> | 343 | <SidebarMenuItem key={item.to}> |
| 331 | <SidebarMenuButton | 344 | <SidebarMenuButton |
| 332 | isActive={isActive} | 345 | isActive={isActive} |
| 346 | aria-label={item.label} | ||
| 347 | tooltip={item.label} | ||
| 333 | onClick={() => handleSectionClick(item.to)} | 348 | onClick={() => handleSectionClick(item.to)} |
| 334 | > | 349 | > |
| 335 | <Icon /> | 350 | <Icon /> |
| 336 | <span className="truncate">{item.label}</span> | 351 | <span className="truncate group-data-[collapsible=icon]:hidden"> |
| 352 | {item.label} | ||
| 353 | </span> | ||
| 337 | </SidebarMenuButton> | 354 | </SidebarMenuButton> |
| 338 | </SidebarMenuItem> | 355 | </SidebarMenuItem> |
| 339 | ); | 356 | ); |
| ... | @@ -343,10 +360,12 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { | ... | @@ -343,10 +360,12 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { |
| 343 | </SidebarGroup> | 360 | </SidebarGroup> |
| 344 | </SidebarContent> | 361 | </SidebarContent> |
| 345 | <SidebarFooter className="px-[var(--sidebar-content-inset)] py-1"> | 362 | <SidebarFooter className="px-[var(--sidebar-content-inset)] py-1"> |
| 346 | <Suspense fallback={null}> | 363 | <div className="contents group-data-[collapsible=icon]:hidden"> |
| 347 | <T3ConnectSidebarSignIn /> | 364 | <Suspense fallback={null}> |
| 348 | </Suspense> | 365 | <T3ConnectSidebarSignIn /> |
| 349 | <div className="flex items-center gap-1"> | 366 | </Suspense> |
| 367 | </div> | ||
| 368 | <div className="flex items-center gap-1 group-data-[collapsible=icon]:flex-col"> | ||
| 350 | <div className="min-w-0 flex-1"> | 369 | <div className="min-w-0 flex-1"> |
| 351 | <SidebarUtilityMenu /> | 370 | <SidebarUtilityMenu /> |
| 352 | </div> | 371 | </div> |
apps/web/src/components/settings/settingsSearch.ts+6| ... | @@ -159,6 +159,12 @@ export const SETTINGS_SEARCH_ITEMS = [ | ... | @@ -159,6 +159,12 @@ export const SETTINGS_SEARCH_ITEMS = [ |
| 159 | title: "Panel animations", | 159 | title: "Panel animations", |
| 160 | to: "/settings/appearance", | 160 | to: "/settings/appearance", |
| 161 | }, | 161 | }, |
| 162 | { | ||
| 163 | id: "compact-sidebar", | ||
| 164 | title: "Compact sidebar", | ||
| 165 | to: "/settings/appearance", | ||
| 166 | searchTerms: ["collapsed icons rail hover navigation preview"], | ||
| 167 | }, | ||
| 162 | { | 168 | { |
| 163 | id: "environment-identification", | 169 | id: "environment-identification", |
| 164 | title: "Environment identification", | 170 | title: "Environment identification", |
apps/web/src/components/sidebar/SidebarChrome.tsx+8-6| ... | @@ -86,7 +86,7 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { | ... | @@ -86,7 +86,7 @@ function SidebarBrand({ onBackdrop }: { onBackdrop: boolean }) { |
| 86 | <Link | 86 | <Link |
| 87 | aria-label="Go to threads" | 87 | aria-label="Go to threads" |
| 88 | className={cn( | 88 | className={cn( |
| 89 | "relative z-10 ml-[var(--workspace-titlebar-content-left)] hidden h-7 w-fit min-w-0 shrink-0 items-center overflow-hidden rounded-md outline-hidden ring-ring focus-visible:ring-2 md:flex", | 89 | "relative z-10 ml-[var(--workspace-titlebar-content-left)] hidden h-7 w-fit min-w-0 shrink-0 items-center overflow-hidden rounded-md outline-hidden ring-ring focus-visible:ring-2 md:flex group-data-[collapsible=icon]:hidden", |
| 90 | onBackdrop ? "text-white" : "text-foreground", | 90 | onBackdrop ? "text-white" : "text-foreground", |
| 91 | )} | 91 | )} |
| 92 | to="/" | 92 | to="/" |
| ... | @@ -187,12 +187,12 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { | ... | @@ -187,12 +187,12 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { |
| 187 | }, [canGoBack, closeMobileSidebar, navigate]); | 187 | }, [canGoBack, closeMobileSidebar, navigate]); |
| 188 | 188 | ||
| 189 | return ( | 189 | return ( |
| 190 | <SidebarMenu className="flex-row items-center"> | 190 | <SidebarMenu className="flex-row items-center group-data-[collapsible=icon]:flex-col"> |
| 191 | {currentFooterPage ? ( | 191 | {currentFooterPage ? ( |
| 192 | <SidebarMenuItem className="min-w-0 flex-1"> | 192 | <SidebarMenuItem className="min-w-0 flex-1"> |
| 193 | <SidebarMenuButton onClick={handleBackClick}> | 193 | <SidebarMenuButton onClick={handleBackClick} aria-label="Back" tooltip="Back"> |
| 194 | <ArrowLeftIcon /> | 194 | <ArrowLeftIcon /> |
| 195 | <span>Back</span> | 195 | <span className="group-data-[collapsible=icon]:hidden">Back</span> |
| 196 | </SidebarMenuButton> | 196 | </SidebarMenuButton> |
| 197 | </SidebarMenuItem> | 197 | </SidebarMenuItem> |
| 198 | ) : ( | 198 | ) : ( |
| ... | @@ -224,8 +224,10 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { | ... | @@ -224,8 +224,10 @@ export const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { |
| 224 | export const SidebarChromeFooter = memo(function SidebarChromeFooter() { | 224 | export const SidebarChromeFooter = memo(function SidebarChromeFooter() { |
| 225 | return ( | 225 | return ( |
| 226 | <SidebarFooter className="px-[var(--sidebar-content-inset)] py-1"> | 226 | <SidebarFooter className="px-[var(--sidebar-content-inset)] py-1"> |
| 227 | <SidebarProviderUpdatePill /> | 227 | <div className="contents group-data-[collapsible=icon]:hidden"> |
| 228 | <SidebarUpdateArchitectureWarning /> | 228 | <SidebarProviderUpdatePill /> |
| 229 | <SidebarUpdateArchitectureWarning /> | ||
| 230 | </div> | ||
| 229 | <SidebarUtilityMenu /> | 231 | <SidebarUtilityMenu /> |
| 230 | </SidebarFooter> | 232 | </SidebarFooter> |
| 231 | ); | 233 | ); |
apps/web/src/components/sidebar/SidebarThreadHeader.tsx+27-4| ... | @@ -20,9 +20,10 @@ import { | ... | @@ -20,9 +20,10 @@ import { |
| 20 | } from "react"; | 20 | } from "react"; |
| 21 | 21 | ||
| 22 | import { cn } from "~/lib/utils"; | 22 | import { cn } from "~/lib/utils"; |
| 23 | import { useCompactSidebarEnabled } from "../../hooks/useSettings"; | ||
| 23 | import { Button } from "../ui/button"; | 24 | import { Button } from "../ui/button"; |
| 24 | import { Input } from "../ui/input"; | 25 | import { Input } from "../ui/input"; |
| 25 | import { SidebarMenuButton } from "../ui/sidebar"; | 26 | import { SidebarMenuButton, useSidebar } from "../ui/sidebar"; |
| 26 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | 27 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 27 | 28 | ||
| 28 | export interface SidebarThreadHeaderProps { | 29 | export interface SidebarThreadHeaderProps { |
| ... | @@ -69,6 +70,9 @@ export function SidebarThreadHeader({ | ... | @@ -69,6 +70,9 @@ export function SidebarThreadHeader({ |
| 69 | activeSearchResultIndex, | 70 | activeSearchResultIndex, |
| 70 | onClearSearch, | 71 | onClearSearch, |
| 71 | }: SidebarThreadHeaderProps) { | 72 | }: SidebarThreadHeaderProps) { |
| 73 | const compactEnabled = useCompactSidebarEnabled(); | ||
| 74 | const { state, isMobile, setOpen } = useSidebar(); | ||
| 75 | const compact = compactEnabled && state === "collapsed" && !isMobile; | ||
| 72 | const resultsVisible = isSearching && searchResultCount > 0; | 76 | const resultsVisible = isSearching && searchResultCount > 0; |
| 73 | // Results shrink as the query narrows, so the active index can outrun the | 77 | // Results shrink as the query narrows, so the active index can outrun the |
| 74 | // list; pointing aria-activedescendant at a removed option strands the | 78 | // list; pointing aria-activedescendant at a removed option strands the |
| ... | @@ -79,10 +83,24 @@ export function SidebarThreadHeader({ | ... | @@ -79,10 +83,24 @@ export function SidebarThreadHeader({ |
| 79 | : "New thread"; | 83 | : "New thread"; |
| 80 | 84 | ||
| 81 | return ( | 85 | return ( |
| 82 | <div className="flex items-center gap-1"> | 86 | <div className={cn("flex items-center gap-1", compact && "flex-col")}> |
| 87 | {compact ? ( | ||
| 88 | <SidebarHeaderIconButton | ||
| 89 | label="Search threads" | ||
| 90 | onClick={() => { | ||
| 91 | setOpen(true); | ||
| 92 | requestAnimationFrame(() => searchInputRef.current?.focus()); | ||
| 93 | }} | ||
| 94 | > | ||
| 95 | <SearchIcon /> | ||
| 96 | </SidebarHeaderIconButton> | ||
| 97 | ) : null} | ||
| 83 | <div | 98 | <div |
| 84 | ref={searchFieldRef} | 99 | ref={searchFieldRef} |
| 85 | className="flex h-8 min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground" | 100 | className={cn( |
| 101 | "flex h-8 min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground", | ||
| 102 | compact && "hidden", | ||
| 103 | )} | ||
| 86 | > | 104 | > |
| 87 | <SearchIcon className="size-4 shrink-0 text-[var(--sidebar-icon-color)]" /> | 105 | <SearchIcon className="size-4 shrink-0 text-[var(--sidebar-icon-color)]" /> |
| 88 | <Input | 106 | <Input |
| ... | @@ -124,7 +142,12 @@ export function SidebarThreadHeader({ | ... | @@ -124,7 +142,12 @@ export function SidebarThreadHeader({ |
| 124 | </div> | 142 | </div> |
| 125 | {/* Segmented well: the icons read as one control instead of three loose | 143 | {/* Segmented well: the icons read as one control instead of three loose |
| 126 | buttons competing with the search field beside them. */} | 144 | buttons competing with the search field beside them. */} |
| 127 | <div className="flex shrink-0 items-center rounded-md bg-sidebar-control-surface/60 p-px"> | 145 | <div |
| 146 | className={cn( | ||
| 147 | "flex shrink-0 items-center rounded-md bg-sidebar-control-surface/60 p-px", | ||
| 148 | compact && "flex-col", | ||
| 149 | )} | ||
| 150 | > | ||
| 128 | {hasProjects ? ( | 151 | {hasProjects ? ( |
| 129 | <> | 152 | <> |
| 130 | {projectScope} | 153 | {projectScope} |
apps/web/src/components/sidebar/SidebarUpdatePill.tsx+1-1| ... | @@ -348,7 +348,7 @@ function SidebarUpdateControl() { | ... | @@ -348,7 +348,7 @@ function SidebarUpdateControl() { |
| 348 | ); | 348 | ); |
| 349 | 349 | ||
| 350 | return ( | 350 | return ( |
| 351 | <SidebarMenuItem className="ml-auto shrink-0"> | 351 | <SidebarMenuItem className="ml-auto shrink-0 group-data-[collapsible=icon]:hidden"> |
| 352 | <Popover | 352 | <Popover |
| 353 | handle={releaseNotesPopoverHandle} | 353 | handle={releaseNotesPopoverHandle} |
| 354 | onOpenChange={(open, details) => { | 354 | onOpenChange={(open, details) => { |
apps/web/src/components/ui/sidebar.tsx+3| ... | @@ -591,9 +591,11 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof S | ... | @@ -591,9 +591,11 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof S |
| 591 | function SidebarContent({ | 591 | function SidebarContent({ |
| 592 | className, | 592 | className, |
| 593 | fixedHeader, | 593 | fixedHeader, |
| 594 | fixedFooter, | ||
| 594 | ...props | 595 | ...props |
| 595 | }: React.ComponentProps<"div"> & { | 596 | }: React.ComponentProps<"div"> & { |
| 596 | fixedHeader?: React.ReactNode; | 597 | fixedHeader?: React.ReactNode; |
| 598 | fixedFooter?: React.ReactNode; | ||
| 597 | }) { | 599 | }) { |
| 598 | return ( | 600 | return ( |
| 599 | <> | 601 | <> |
| ... | @@ -617,6 +619,7 @@ function SidebarContent({ | ... | @@ -617,6 +619,7 @@ function SidebarContent({ |
| 617 | {...props} | 619 | {...props} |
| 618 | /> | 620 | /> |
| 619 | </ScrollArea> | 621 | </ScrollArea> |
| 622 | {fixedFooter ? <div className="w-full shrink-0">{fixedFooter}</div> : null} | ||
| 620 | </> | 623 | </> |
| 621 | ); | 624 | ); |
| 622 | } | 625 | } |
apps/web/src/hooks/useSettings.ts+7| ... | @@ -379,6 +379,13 @@ export function useLegacySidebarEnabled(): boolean { | ... | @@ -379,6 +379,13 @@ export function useLegacySidebarEnabled(): boolean { |
| 379 | return settingsHydrated && legacySidebarEnabled; | 379 | return settingsHydrated && legacySidebarEnabled; |
| 380 | } | 380 | } |
| 381 | 381 | ||
| 382 | /** Keep the default collapsed sidebar until persisted client settings hydrate. */ | ||
| 383 | export function useCompactSidebarEnabled(): boolean { | ||
| 384 | const settingsHydrated = useClientSettingsHydrated(); | ||
| 385 | const compactSidebarEnabled = useClientSettingsValue().compactSidebarEnabled; | ||
| 386 | return settingsHydrated && compactSidebarEnabled; | ||
| 387 | } | ||
| 388 | |||
| 382 | /** Read current settings for one environment, merged with client-local preferences. */ | 389 | /** Read current settings for one environment, merged with client-local preferences. */ |
| 383 | export function useEnvironmentSettings<T = UnifiedSettings>( | 390 | export function useEnvironmentSettings<T = UnifiedSettings>( |
| 384 | environmentId: EnvironmentId, | 391 | environmentId: EnvironmentId, |
apps/web/src/workspaceTitlebar.ts+1-1| ... | @@ -1,2 +1,2 @@ | ... | @@ -1,2 +1,2 @@ |
| 1 | export const COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS = | 1 | export const COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS = |
| 2 | "[[data-sidebar-state=collapsed]_&]:pl-[var(--workspace-titlebar-content-left)]"; | 2 | "[[data-sidebar-state=collapsed]_&]:pl-[var(--workspace-titlebar-content-left)] [[data-sidebar-state=collapsed]:has([data-side=left][data-collapsible=icon])_&]:pl-[max(calc(env(safe-area-inset-left)+1.25rem),calc(var(--workspace-titlebar-content-left)-var(--sidebar-width-icon)))]"; |
packages/contracts/src/settings.ts+2| ... | @@ -436,6 +436,7 @@ export const ClientSettingsSchema = Schema.Struct({ | ... | @@ -436,6 +436,7 @@ export const ClientSettingsSchema = Schema.Struct({ |
| 436 | // old keys, so everyone, including prior beta opt-outs, resets to the new | 436 | // old keys, so everyone, including prior beta opt-outs, resets to the new |
| 437 | // default sidebar. | 437 | // default sidebar. |
| 438 | legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), | 438 | legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 439 | compactSidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), | ||
| 439 | sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe( | 440 | sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe( |
| 440 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)), | 441 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)), |
| 441 | ), | 442 | ), |
| ... | @@ -1490,6 +1491,7 @@ export const ClientSettingsPatch = Schema.Struct({ | ... | @@ -1490,6 +1491,7 @@ export const ClientSettingsPatch = Schema.Struct({ |
| 1490 | proactivePanelsEnabled: Schema.optionalKey(Schema.Boolean), | 1491 | proactivePanelsEnabled: Schema.optionalKey(Schema.Boolean), |
| 1491 | showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean), | 1492 | showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean), |
| 1492 | legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), | 1493 | legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), |
| 1494 | compactSidebarEnabled: Schema.optionalKey(Schema.Boolean), | ||
| 1493 | sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), | 1495 | sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), |
| 1494 | sidebarProjectGroupingOverrides: Schema.optionalKey( | 1496 | sidebarProjectGroupingOverrides: Schema.optionalKey( |
| 1495 | Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode), | 1497 | Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode), |