Unified · Split
| @@ -41,7 +41,7 @@ export function CommandPaletteResults(props: CommandPaletteResultsProps) { · 40 unmodified lines | ||
| 41 | 41 | <CommandList> |
| 42 | 42 | {props.groups.map((group) => ( |
| 43 | 43 | <CommandGroup items={group.items} key={group.value}> |
| 44 | − <CommandGroupLabel>{group.label}</CommandGroupLabel> | |
| 44 | + <CommandGroupLabel className="ps-[9px]">{group.label}</CommandGroupLabel> | |
| 45 | 45 | <CommandCollection> |
| 46 | 46 | {(item) => |
| 47 | 47 | item.disabled ? ( |
| @@ -133,13 +133,13 @@ function CommandPaletteResultRow(props: { · 85 unmodified lines | ||
| 133 | 133 | )} |
| 134 | 134 | {props.item.titleTrailingContent} |
| 135 | 135 | {props.item.timestamp ? ( |
| 136 | − <span className="min-w-12 shrink-0 text-right text-[10px] tabular-nums text-muted-foreground/70"> | |
| 136 | + <span className="min-w-12 shrink-0 text-right text-xs tabular-nums text-muted-foreground/70"> | |
| 137 | 137 | {props.item.timestamp} |
| 138 | 138 | </span> |
| 139 | 139 | ) : null} |
| 140 | 140 | {shortcutLabel ? <CommandShortcut>{shortcutLabel}</CommandShortcut> : null} |
| 141 | 141 | {props.item.kind === "submenu" ? ( |
| 142 | − <ChevronRightIcon className="ml-auto size-4 shrink-0 text-muted-foreground/50" /> | |
| 142 | + <ChevronRightIcon className="-me-0.5 ms-auto size-4 shrink-0 text-muted-foreground/70" /> | |
| 143 | 143 | ) : null} |
| 144 | 144 | </CommandItem> |
| 145 | 145 | ); |
| @@ -4,6 +4,7 @@ import { FolderIcon } from "lucide-react"; · 3 unmodified lines | ||
| 4 | 4 | import type { ComponentType } from "react"; |
| 5 | 5 | import { useState } from "react"; |
| 6 | 6 | import { useAssetUrl } from "../assets/assetUrls"; |
| 7 | +import { cn } from "~/lib/utils"; | |
| 7 | 8 | |
| 8 | 9 | const loadedProjectFaviconSrcs = new Set<string>(); |
| 9 | 10 | |
| @@ -40,7 +41,7 @@ function ProjectFaviconFallback({ · 30 unmodified lines | ||
| 40 | 41 | readonly className?: string | undefined; |
| 41 | 42 | readonly icon: ComponentType<{ className?: string }>; |
| 42 | 43 | }) { |
| 43 | − return <Icon className={`size-3.5 shrink-0 text-muted-foreground/50 ${className ?? ""}`} />; | |
| 44 | + return <Icon className={cn("size-3.5 shrink-0 text-muted-foreground/50", className)} />; | |
| 44 | 45 | } |
| 45 | 46 | |
| 46 | 47 | function ProjectFaviconImage({ |
| @@ -64,7 +65,11 @@ function ProjectFaviconImage({ · 17 unmodified lines | ||
| 64 | 65 | <img |
| 65 | 66 | src={src} |
| 66 | 67 | alt="" |
| 67 | − className={`size-3.5 shrink-0 rounded-sm object-contain ${status === "loaded" ? "" : "hidden"} ${className ?? ""}`} | |
| 68 | + className={cn( | |
| 69 | + "size-3.5 shrink-0 rounded-sm object-contain", | |
| 70 | + status !== "loaded" && "hidden", | |
| 71 | + className, | |
| 72 | + )} | |
| 68 | 73 | onLoad={() => { |
| 69 | 74 | loadedProjectFaviconSrcs.add(src); |
| 70 | 75 | setStatus("loaded"); |
| @@ -2218,9 +2218,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec · 2217 unmodified lines | ||
| 2218 | 2218 | <div className="group/project-header relative"> |
| 2219 | 2219 | <SidebarMenuButton |
| 2220 | 2220 | ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined} |
| 2221 | − size="sm" | |
| 2222 | − className={`h-8 gap-2 rounded-md px-2 py-1.5 pr-8 text-left hover:bg-sidebar-row-hover group-hover/project-header:bg-sidebar-row-hover group-hover/project-header:text-sidebar-foreground max-sm:pr-14 ${ | |
| 2223 | − isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "cursor-pointer" | |
| 2221 | + className={`pr-8 group-hover/project-header:bg-sidebar-row-hover group-hover/project-header:text-sidebar-foreground max-sm:pr-14 ${ | |
| 2222 | + isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "" | |
| 2224 | 2223 | }`} |
| 2225 | 2224 | {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})} |
| 2226 | 2225 | {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.listeners : {})} |
| @@ -2822,30 +2821,33 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( · 595 unmodified lines | ||
| 2822 | 2821 | ); |
| 2823 | 2822 | |
| 2824 | 2823 | return ( |
| 2825 | − <SidebarContent className="gap-0"> | |
| 2826 | − <SidebarGroup className="px-2 pt-2 pb-1"> | |
| 2827 | − <SidebarMenu> | |
| 2828 | − <SidebarMenuItem> | |
| 2829 | − <CommandDialogTrigger | |
| 2830 | − render={ | |
| 2831 | − <SidebarMenuButton | |
| 2832 | − size="sm" | |
| 2833 | − className="h-8 gap-2 rounded-md px-2 py-1.5 text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-0" | |
| 2834 | − data-testid="command-palette-trigger" | |
| 2835 | − /> | |
| 2836 | − } | |
| 2837 | − > | |
| 2838 | − <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" /> | |
| 2839 | − <span className="flex-1 truncate text-left text-sm font-medium">Search</span> | |
| 2840 | − {commandPaletteShortcutLabel ? ( | |
| 2841 | − <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px]"> | |
| 2842 | − {commandPaletteShortcutLabel} | |
| 2843 | − </Kbd> | |
| 2844 | − ) : null} | |
| 2845 | − </CommandDialogTrigger> | |
| 2846 | − </SidebarMenuItem> | |
| 2847 | − </SidebarMenu> | |
| 2848 | − </SidebarGroup> | |
| 2824 | + <SidebarContent | |
| 2825 | + className="gap-0" | |
| 2826 | + fixedHeader={ | |
| 2827 | + <SidebarGroup className="px-2 pt-2 pb-1"> | |
| 2828 | + <SidebarMenu> | |
| 2829 | + <SidebarMenuItem> | |
| 2830 | + <CommandDialogTrigger | |
| 2831 | + render={ | |
| 2832 | + <SidebarMenuButton | |
| 2833 | + className="focus-visible:ring-0" | |
| 2834 | + data-testid="command-palette-trigger" | |
| 2835 | + /> | |
| 2836 | + } | |
| 2837 | + > | |
| 2838 | + <SearchIcon /> | |
| 2839 | + <span className="flex-1 truncate">Search</span> | |
| 2840 | + {commandPaletteShortcutLabel ? ( | |
| 2841 | + <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px]"> | |
| 2842 | + {commandPaletteShortcutLabel} | |
| 2843 | + </Kbd> | |
| 2844 | + ) : null} | |
| 2845 | + </CommandDialogTrigger> | |
| 2846 | + </SidebarMenuItem> | |
| 2847 | + </SidebarMenu> | |
| 2848 | + </SidebarGroup> | |
| 2849 | + } | |
| 2850 | + > | |
| 2849 | 2851 | {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? ( |
| 2850 | 2852 | <SidebarGroup className="px-2 pt-2 pb-0"> |
| 2851 | 2853 | <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8"> |
| @@ -242,38 +242,40 @@ function SidebarV2ThreadTooltip({ · 241 unmodified lines | ||
| 242 | 242 | <TooltipPopup |
| 243 | 243 | side="right" |
| 244 | 244 | align="start" |
| 245 | − sideOffset={8} | |
| 245 | + sideOffset={4} | |
| 246 | 246 | variant="glass" |
| 247 | 247 | className="max-w-80 text-left whitespace-normal" |
| 248 | 248 | > |
| 249 | − <div className="flex max-w-80 flex-col gap-2 p-2"> | |
| 250 | − <div className="whitespace-nowrap text-sm font-medium text-foreground">{thread.title}</div> | |
| 251 | − <div className="grid gap-1.5 text-xs text-muted-foreground"> | |
| 249 | + <div className="flex min-w-0 max-w-80 flex-col gap-2 px-0.5 py-1.5"> | |
| 250 | + <div className="min-w-0 truncate text-xs leading-none font-medium text-foreground"> | |
| 251 | + {thread.title} | |
| 252 | + </div> | |
| 253 | + <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground"> | |
| 252 | 254 | {projectTitle ? ( |
| 253 | 255 | <div className="flex min-w-0 items-center gap-2"> |
| 254 | 256 | <ProjectFavicon |
| 255 | 257 | environmentId={thread.environmentId} |
| 256 | 258 | cwd={projectCwd ?? ""} |
| 257 | − className="size-4 shrink-0 stroke-muted-foreground" | |
| 259 | + className="size-3 shrink-0 stroke-muted-foreground" | |
| 258 | 260 | /> |
| 259 | − <div className="min-w-0 wrap-break-word text-foreground/90">{projectTitle}</div> | |
| 261 | + <div className="min-w-0 truncate text-foreground/75">{projectTitle}</div> | |
| 260 | 262 | </div> |
| 261 | 263 | ) : null} |
| 262 | 264 | {environmentLabel ? ( |
| 263 | 265 | <div className="flex min-w-0 items-center gap-2"> |
| 264 | − <ServerIcon className="size-4 shrink-0 stroke-muted-foreground" /> | |
| 265 | − <div className="min-w-0 wrap-break-word text-foreground/90">{environmentLabel}</div> | |
| 266 | + <ServerIcon className="size-3 shrink-0 stroke-muted-foreground" /> | |
| 267 | + <div className="min-w-0 truncate text-foreground/75">{environmentLabel}</div> | |
| 266 | 268 | </div> |
| 267 | 269 | ) : null} |
| 268 | 270 | {thread.branch ? ( |
| 269 | 271 | <div className="flex min-w-0 items-center gap-2"> |
| 270 | − <GitBranchIcon className="size-4 shrink-0 stroke-muted-foreground" /> | |
| 271 | − <div className="min-w-0 wrap-break-word text-foreground/90">{thread.branch}</div> | |
| 272 | + <GitBranchIcon className="size-3 shrink-0 stroke-muted-foreground" /> | |
| 273 | + <div className="min-w-0 truncate text-foreground/75">{thread.branch}</div> | |
| 272 | 274 | </div> |
| 273 | 275 | ) : null} |
| 274 | 276 | {branchMismatch ? ( |
| 275 | 277 | <div className="flex min-w-0 items-start gap-2 text-warning"> |
| 276 | − <CircleAlertIcon aria-hidden className="mt-0.5 size-4 shrink-0 stroke-current" /> | |
| 278 | + <CircleAlertIcon aria-hidden className="mt-0.5 size-3 shrink-0 stroke-current" /> | |
| 277 | 279 | <div className="min-w-0 flex-1 wrap-break-word leading-5"> |
| 278 | 280 | You're currently checked out on another branch. |
| 279 | 281 | </div> |
| @@ -284,15 +286,15 @@ function SidebarV2ThreadTooltip({ · 4 unmodified lines | ||
| 284 | 286 | <ProviderInstanceIcon |
| 285 | 287 | driverKind={driverKind} |
| 286 | 288 | displayName={thread.session?.providerName ?? modelInstanceId} |
| 287 | − iconClassName="size-4 shrink-0" | |
| 289 | + iconClassName="size-3 shrink-0 grayscale opacity-60" | |
| 288 | 290 | /> |
| 289 | − <div className="min-w-0 wrap-break-word text-foreground/90">{modelLabel}</div> | |
| 291 | + <div className="min-w-0 truncate text-foreground/75">{modelLabel}</div> | |
| 290 | 292 | </div> |
| 291 | 293 | ) : null} |
| 292 | 294 | {thread.session?.lastError ? ( |
| 293 | 295 | <div className="flex min-w-0 items-center gap-2 text-red-600 dark:text-red-400"> |
| 294 | − <CircleAlertIcon className="size-4 shrink-0 stroke-current" /> | |
| 295 | − <div className="min-w-0 wrap-break-word">{thread.session.lastError}</div> | |
| 296 | + <CircleAlertIcon className="size-3 shrink-0 stroke-current" /> | |
| 297 | + <div className="min-w-0 truncate">Error occurred</div> | |
| 296 | 298 | </div> |
| 297 | 299 | ) : null} |
| 298 | 300 | </div> |
| @@ -815,9 +817,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { · 516 unmodified lines | ||
| 815 | 817 | type="button" |
| 816 | 818 | aria-label="Un-settle thread" |
| 817 | 819 | onClick={handleUnsettleClick} |
| 818 | − className="absolute inset-y-0 right-0 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-2 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100" | |
| 820 | + className="absolute inset-y-0 right-0 -mr-1 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-1.5 text-xs text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover/v2-row:opacity-100" | |
| 819 | 821 | > |
| 820 | − <Undo2Icon className="size-3" /> | |
| 822 | + <Undo2Icon className="mb-px size-3.5" /> | |
| 821 | 823 | </button> |
| 822 | 824 | ) : ( |
| 823 | 825 | <button |
| @@ -917,7 +919,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { · 93 unmodified lines | ||
| 917 | 919 | {props.settlementSupported || showSnoozeButton ? ( |
| 918 | 920 | <span |
| 919 | 921 | className={cn( |
| 920 | − "absolute inset-y-0 right-0 flex items-stretch gap-0.5 opacity-0 transition-opacity focus-within:opacity-100 group-hover/v2-row:opacity-100", | |
| 922 | + "absolute inset-y-0 right-0 flex items-stretch opacity-0 transition-opacity focus-within:opacity-100 group-hover/v2-row:opacity-100", | |
| 921 | 923 | snoozeMenuOpen && "opacity-100", |
| 922 | 924 | )} |
| 923 | 925 | > |
| @@ -933,9 +935,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { · 9 unmodified lines | ||
| 933 | 935 | type="button" |
| 934 | 936 | aria-label="Settle thread" |
| 935 | 937 | onClick={handleSettleClick} |
| 936 | − className="inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-2 text-xs text-muted-foreground hover:text-foreground" | |
| 938 | + className="-mr-1 inline-flex cursor-pointer items-center gap-1 rounded-md bg-transparent px-1.5 text-xs text-muted-foreground hover:text-foreground" | |
| 937 | 939 | > |
| 938 | − <CheckIcon className="size-3" /> | |
| 940 | + <CheckIcon className="size-3.5" /> | |
| 939 | 941 | Settle |
| 940 | 942 | </button> |
| 941 | 943 | ) : null} |
| @@ -2220,151 +2222,158 @@ export default function SidebarV2() { · 1278 unmodified lines | ||
| 2220 | 2222 | return ( |
| 2221 | 2223 | <> |
| 2222 | 2224 | <SidebarChromeHeader isElectron={isElectron} /> |
| 2223 | − <SidebarContent className="gap-0"> | |
| 2224 | − <SidebarGroup className="px-2 pb-2 pt-3"> | |
| 2225 | − <div className="flex items-center gap-1"> | |
| 2226 | − <div className="min-w-0 flex-1"> | |
| 2227 | − <CommandDialogTrigger | |
| 2228 | − render={ | |
| 2229 | − <SidebarMenuButton | |
| 2230 | − size="sm" | |
| 2231 | − type="button" | |
| 2232 | − aria-label="Search threads and commands" | |
| 2233 | − className="h-8 gap-2 rounded-md border-0 bg-transparent px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" | |
| 2234 | − data-testid="command-palette-trigger" | |
| 2235 | − /> | |
| 2236 | − } | |
| 2237 | − > | |
| 2238 | − <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" /> | |
| 2239 | − <div className="flex-1 truncate text-left">Search</div> | |
| 2240 | − {commandPaletteShortcutLabel ? ( | |
| 2241 | − <Kbd className="h-4 min-w-0 rounded-sm bg-sidebar-control-surface px-1.5 text-[10px] text-sidebar-muted-foreground ring-1 ring-sidebar-border"> | |
| 2242 | − {commandPaletteShortcutLabel} | |
| 2243 | − </Kbd> | |
| 2244 | − ) : null} | |
| 2245 | − </CommandDialogTrigger> | |
| 2246 | − </div> | |
| 2247 | − <div className="shrink-0"> | |
| 2248 | − <Tooltip> | |
| 2249 | − <TooltipTrigger | |
| 2225 | + <SidebarContent | |
| 2226 | + className="gap-0" | |
| 2227 | + fixedHeader={ | |
| 2228 | + <SidebarGroup className="gap-1 p-2"> | |
| 2229 | + <div className="flex items-center gap-1"> | |
| 2230 | + <div className="min-w-0 flex-1"> | |
| 2231 | + <CommandDialogTrigger | |
| 2250 | 2232 | render={ |
| 2251 | 2233 | <SidebarMenuButton |
| 2252 | − size="sm" | |
| 2253 | 2234 | type="button" |
| 2254 | − className="relative size-8 justify-center rounded-md border-0 bg-transparent p-0 text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" | |
| 2255 | − onClick={handleNewThreadClick} | |
| 2256 | − disabled={projects.length === 0} | |
| 2257 | − aria-label="New thread" | |
| 2235 | + aria-label="Search threads and commands" | |
| 2236 | + className="focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" | |
| 2237 | + data-testid="command-palette-trigger" | |
| 2258 | 2238 | /> |
| 2259 | 2239 | } |
| 2260 | 2240 | > |
| 2261 | − <SquarePenIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" /> | |
| 2262 | − <span | |
| 2263 | − className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden" | |
| 2264 | − aria-hidden="true" | |
| 2265 | − /> | |
| 2266 | − </TooltipTrigger> | |
| 2267 | − <TooltipPopup side="right"> | |
| 2268 | − {newThreadShortcutLabel ? `New thread (${newThreadShortcutLabel})` : "New thread"} | |
| 2269 | − </TooltipPopup> | |
| 2270 | − </Tooltip> | |
| 2271 | − </div> | |
| 2272 | − </div> | |
| 2273 | − </SidebarGroup> | |
| 2274 | − {projectGroups.length > 0 ? ( | |
| 2275 | − <SidebarGroup className="px-2 pb-2 pt-0"> | |
| 2276 | − <div className="flex items-center gap-1"> | |
| 2277 | − <Menu open={projectScopeMenuOpen} onOpenChange={setProjectScopeMenuOpen}> | |
| 2278 | − <MenuTrigger | |
| 2279 | − aria-label="Filter threads by project" | |
| 2280 | − className="flex h-8 min-w-0 flex-1 cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm font-medium text-sidebar-muted-foreground outline-none hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" | |
| 2281 | − > | |
| 2282 | − {scopedProjectGroup ? ( | |
| 2283 | − <ProjectFavicon | |
| 2284 | − environmentId={scopedProjectGroup.environmentId} | |
| 2285 | − cwd={scopedProjectGroup.workspaceRoot} | |
| 2286 | − className="size-4 shrink-0" | |
| 2241 | + <SearchIcon /> | |
| 2242 | + <div className="flex-1 truncate text-left">Search</div> | |
| 2243 | + {commandPaletteShortcutLabel ? ( | |
| 2244 | + <Kbd className="mr-px h-4 min-w-0 rounded-sm bg-sidebar-control-surface px-1.5 text-[10px] text-sidebar-muted-foreground ring-1 ring-sidebar-border"> | |
| 2245 | + {commandPaletteShortcutLabel} | |
| 2246 | + </Kbd> | |
| 2247 | + ) : null} | |
| 2248 | + </CommandDialogTrigger> | |
| 2249 | + </div> | |
| 2250 | + <div className="shrink-0"> | |
| 2251 | + <Tooltip> | |
| 2252 | + <TooltipTrigger | |
| 2253 | + render={ | |
| 2254 | + <SidebarMenuButton | |
| 2255 | + size="icon" | |
| 2256 | + type="button" | |
| 2257 | + className="relative focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" | |
| 2258 | + onClick={handleNewThreadClick} | |
| 2259 | + disabled={projects.length === 0} | |
| 2260 | + aria-label="New thread" | |
| 2261 | + /> | |
| 2262 | + } | |
| 2263 | + > | |
| 2264 | + <SquarePenIcon /> | |
| 2265 | + <span | |
| 2266 | + className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden" | |
| 2267 | + aria-hidden="true" | |
| 2287 | 2268 | /> |
| 2288 | − ) : ( | |
| 2289 | − <FolderIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" /> | |
| 2290 | − )} | |
| 2291 | − <span className="min-w-0 flex-1 truncate"> | |
| 2292 | − {scopedProjectGroup?.displayName ?? "All projects"} | |
| 2293 | − </span> | |
| 2294 | − <ChevronDownIcon className="size-4 shrink-0 text-sidebar-muted-foreground/70" /> | |
| 2295 | − </MenuTrigger> | |
| 2296 | − <MenuPopup align="start" className="w-(--anchor-width)"> | |
| 2297 | − <MenuRadioGroup | |
| 2298 | − value={projectScopeKey ?? "all"} | |
| 2299 | − onValueChange={(value) => | |
| 2300 | − setProjectScopeKey(value === "all" ? null : (value as string)) | |
| 2269 | + </TooltipTrigger> | |
| 2270 | + <TooltipPopup side="right"> | |
| 2271 | + {newThreadShortcutLabel | |
| 2272 | + ? `New thread (${newThreadShortcutLabel})` | |
| 2273 | + : "New thread"} | |
| 2274 | + </TooltipPopup> | |
| 2275 | + </Tooltip> | |
| 2276 | + </div> | |
| 2277 | + </div> | |
| 2278 | + {projectGroups.length > 0 ? ( | |
| 2279 | + <div className="flex items-center gap-1"> | |
| 2280 | + <Menu open={projectScopeMenuOpen} onOpenChange={setProjectScopeMenuOpen}> | |
| 2281 | + <MenuTrigger | |
| 2282 | + render={ | |
| 2283 | + <SidebarMenuButton | |
| 2284 | + aria-label="Filter threads by project" | |
| 2285 | + className="min-w-0 flex-1 focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" | |
| 2286 | + /> | |
| 2301 | 2287 | } |
| 2302 | 2288 | > |
| 2303 | − <MenuRadioItem | |
| 2304 | − value="all" | |
| 2305 | − closeOnClick | |
| 2306 | − className="h-8 min-h-8 px-1 py-0 text-sm font-medium [&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2" | |
| 2307 | − > | |
| 2289 | + {scopedProjectGroup ? ( | |
| 2290 | + <ProjectFavicon | |
| 2291 | + environmentId={scopedProjectGroup.environmentId} | |
| 2292 | + cwd={scopedProjectGroup.workspaceRoot} | |
| 2293 | + className="size-4 shrink-0" | |
| 2294 | + /> | |
| 2295 | + ) : ( | |
| 2308 | 2296 | <FolderIcon className="size-4 shrink-0" /> |
| 2309 | − <span className="min-w-0 truncate text-sm">All projects</span> | |
| 2310 | − </MenuRadioItem> | |
| 2311 | − {projectGroups.map((project) => { | |
| 2312 | − const scopeKey = project.projectKey; | |
| 2313 | − return ( | |
| 2314 | − <MenuRadioItem | |
| 2315 | − key={scopeKey} | |
| 2316 | − value={scopeKey} | |
| 2317 | − closeOnClick | |
| 2318 | − className="h-8 min-h-8 px-1 py-0 text-sm font-medium [&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2" | |
| 2319 | − > | |
| 2320 | − <ProjectFavicon | |
| 2321 | − environmentId={project.environmentId} | |
| 2322 | − cwd={project.workspaceRoot} | |
| 2323 | − className="size-4 shrink-0" | |
| 2324 | − /> | |
| 2325 | − <span className="min-w-0 truncate text-sm">{project.displayName}</span> | |
| 2326 | − <button | |
| 2327 | − type="button" | |
| 2328 | − aria-label={`Project actions for ${project.displayName}`} | |
| 2329 | − title={`Project actions for ${project.displayName}`} | |
| 2330 | − className="ml-auto inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/55 outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring" | |
| 2331 | − onPointerDown={(event) => event.stopPropagation()} | |
| 2332 | − onClick={(event) => { | |
| 2333 | − void handleProjectActions(event, project); | |
| 2334 | − }} | |
| 2297 | + )} | |
| 2298 | + <span className="min-w-0 flex-1 truncate"> | |
| 2299 | + {scopedProjectGroup?.displayName ?? "All projects"} | |
| 2300 | + </span> | |
| 2301 | + <ChevronDownIcon className="-mr-px size-4 shrink-0" /> | |
| 2302 | + </MenuTrigger> | |
| 2303 | + <MenuPopup align="start" className="w-(--anchor-width)"> | |
| 2304 | + <MenuRadioGroup | |
| 2305 | + value={projectScopeKey ?? "all"} | |
| 2306 | + onValueChange={(value) => | |
| 2307 | + setProjectScopeKey(value === "all" ? null : (value as string)) | |
| 2308 | + } | |
| 2309 | + > | |
| 2310 | + <MenuRadioItem | |
| 2311 | + value="all" | |
| 2312 | + closeOnClick | |
| 2313 | + className="h-8 min-h-8 px-1 py-0 text-sm font-medium [&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2" | |
| 2314 | + > | |
| 2315 | + <FolderIcon className="size-4 shrink-0" /> | |
| 2316 | + <span className="min-w-0 truncate text-sm">All projects</span> | |
| 2317 | + </MenuRadioItem> | |
| 2318 | + {projectGroups.map((project) => { | |
| 2319 | + const scopeKey = project.projectKey; | |
| 2320 | + return ( | |
| 2321 | + <MenuRadioItem | |
| 2322 | + key={scopeKey} | |
| 2323 | + value={scopeKey} | |
| 2324 | + closeOnClick | |
| 2325 | + className="h-8 min-h-8 px-1 py-0 text-sm font-medium [&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2" | |
| 2335 | 2326 | > |
| 2336 | − <EllipsisIcon className="size-3.5" /> | |
| 2337 | − </button> | |
| 2338 | − </MenuRadioItem> | |
| 2339 | − ); | |
| 2340 | − })} | |
| 2341 | − </MenuRadioGroup> | |
| 2342 | − </MenuPopup> | |
| 2343 | − </Menu> | |
| 2344 | − <Tooltip> | |
| 2345 | − <TooltipTrigger | |
| 2346 | − render={ | |
| 2347 | − <SidebarMenuButton | |
| 2348 | − size="sm" | |
| 2349 | − className="relative size-8 shrink-0 justify-center rounded-md bg-transparent p-0 text-sidebar-muted-foreground hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" | |
| 2350 | − onClick={openAddProjectCommandPalette} | |
| 2351 | − type="button" | |
| 2352 | − aria-label="New project" | |
| 2327 | + <ProjectFavicon | |
| 2328 | + environmentId={project.environmentId} | |
| 2329 | + cwd={project.workspaceRoot} | |
| 2330 | + className="size-4 shrink-0" | |
| 2331 | + /> | |
| 2332 | + <span className="min-w-0 truncate text-sm">{project.displayName}</span> | |
| 2333 | + <button | |
| 2334 | + type="button" | |
| 2335 | + aria-label={`Project actions for ${project.displayName}`} | |
| 2336 | + title={`Project actions for ${project.displayName}`} | |
| 2337 | + className="ml-auto inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/55 outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring" | |
| 2338 | + onPointerDown={(event) => event.stopPropagation()} | |
| 2339 | + onClick={(event) => { | |
| 2340 | + void handleProjectActions(event, project); | |
| 2341 | + }} | |
| 2342 | + > | |
| 2343 | + <EllipsisIcon className="size-3.5" /> | |
| 2344 | + </button> | |
| 2345 | + </MenuRadioItem> | |
| 2346 | + ); | |
| 2347 | + })} | |
| 2348 | + </MenuRadioGroup> | |
| 2349 | + </MenuPopup> | |
| 2350 | + </Menu> | |
| 2351 | + <Tooltip> | |
| 2352 | + <TooltipTrigger | |
| 2353 | + render={ | |
| 2354 | + <SidebarMenuButton | |
| 2355 | + size="icon" | |
| 2356 | + className="relative shrink-0 focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar" | |
| 2357 | + onClick={openAddProjectCommandPalette} | |
| 2358 | + type="button" | |
| 2359 | + aria-label="New project" | |
| 2360 | + /> | |
| 2361 | + } | |
| 2362 | + > | |
| 2363 | + <FolderPlusIcon /> | |
| 2364 | + <span | |
| 2365 | + className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden" | |
| 2366 | + aria-hidden="true" | |
| 2353 | 2367 | /> |
| 2354 | − } | |
| 2355 | − > | |
| 2356 | − <FolderPlusIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" /> | |
| 2357 | − <span | |
| 2358 | − className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden" | |
| 2359 | − aria-hidden="true" | |
| 2360 | − /> | |
| 2361 | − </TooltipTrigger> | |
| 2362 | − <TooltipPopup side="right">New project</TooltipPopup> | |
| 2363 | − </Tooltip> | |
| 2364 | − </div> | |
| 2368 | + </TooltipTrigger> | |
| 2369 | + <TooltipPopup side="right">New project</TooltipPopup> | |
| 2370 | + </Tooltip> | |
| 2371 | + </div> | |
| 2372 | + ) : null} | |
| 2365 | 2373 | </SidebarGroup> |
| 2366 | − ) : null} | |
| 2367 | − <SidebarGroup className="min-h-0 flex-1 overflow-y-auto px-2 py-1 [scrollbar-gutter:stable]"> | |
| 2374 | + } | |
| 2375 | + > | |
| 2376 | + <SidebarGroup className="px-2 pb-1 pt-0"> | |
| 2368 | 2377 | <TooltipProvider |
| 2369 | 2378 | key="sidebar-thread-tooltips-150" |
| 2370 | 2379 | delay={150} |
| @@ -2526,12 +2535,10 @@ export default function SidebarV2() { · 155 unmodified lines | ||
| 2526 | 2535 | <button |
| 2527 | 2536 | type="button" |
| 2528 | 2537 | onClick={showMoreSettled} |
| 2529 | − className="mt-1 flex h-[30px] w-full items-center justify-center gap-1.5 rounded-md border border-dashed border-border font-mono text-[11px] text-muted-foreground transition-colors hover:border-solid hover:border-input hover:bg-background/45 hover:text-foreground dark:border-white/15 dark:hover:border-white/30 dark:hover:bg-transparent" | |
| 2538 | + 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" | |
| 2530 | 2539 | > |
| 2540 | + <PlusIcon aria-hidden className="size-4 shrink-0" /> | |
| 2531 | 2541 | Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more |
| 2532 | − <span className="text-muted-foreground/50"> | |
| 2533 | − ({hiddenSettledCount} settled hidden) | |
| 2534 | − </span> | |
| 2535 | 2542 | </button> |
| 2536 | 2543 | </li> |
| 2537 | 2544 | ) : null} |
| @@ -2547,7 +2554,7 @@ export default function SidebarV2() { · 9 unmodified lines | ||
| 2547 | 2554 | onClick={openAddProjectCommandPalette} |
| 2548 | 2555 | className="inline-flex 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" |
| 2549 | 2556 | > |
| 2550 | − <PlusIcon className="size-3" /> | |
| 2557 | + <PlusIcon className="-mx-0.5 size-3" /> | |
| 2551 | 2558 | Add project |
| 2552 | 2559 | </button> |
| 2553 | 2560 | </> |
| @@ -2567,48 +2574,52 @@ export default function SidebarV2() { · 13 unmodified lines | ||
| 2567 | 2574 | }} |
| 2568 | 2575 | > |
| 2569 | 2576 | <DialogPopup className="max-w-xl"> |
| 2570 | − <DialogHeader className="gap-1.5"> | |
| 2577 | + <DialogHeader className="gap-3 pb-1!"> | |
| 2571 | 2578 | <DialogTitle className="text-balance">Project settings</DialogTitle> |
| 2572 | − <DialogDescription> | |
| 2573 | − {projectActionsTarget && projectActionsTarget.memberProjects.length > 1 | |
| 2574 | − ? `${projectActionsTarget.displayName} has an entry in each environment. Changes apply only to the entry you choose.` | |
| 2575 | − : `Manage ${projectActionsTarget?.displayName ?? "this project"} in this environment.`} | |
| 2579 | + <DialogDescription className="sr-only"> | |
| 2580 | + Manage project names, grouping rules, and environments. | |
| 2576 | 2581 | </DialogDescription> |
| 2582 | + <div className="grid gap-1.5 text-base text-muted-foreground"> | |
| 2583 | + {projectActionsTarget?.memberProjects.map((member) => ( | |
| 2584 | + <div key={member.physicalProjectKey} className="flex min-w-0 items-center gap-3"> | |
| 2585 | + <span className="flex min-w-0 items-center gap-1"> | |
| 2586 | + <FolderIcon className="size-3.5 shrink-0 opacity-60" /> | |
| 2587 | + <span className="min-w-0 truncate font-mono">{member.workspaceRoot}</span> | |
| 2588 | + <Button | |
| 2589 | + size="icon-xs" | |
| 2590 | + variant="ghost" | |
| 2591 | + className="size-4 shrink-0 rounded-sm" | |
| 2592 | + aria-label="Copy project path" | |
| 2593 | + title="Copy project path" | |
| 2594 | + onClick={() => | |
| 2595 | + copyProjectPath(member.workspaceRoot, { path: member.workspaceRoot }) | |
| 2596 | + } | |
| 2597 | + > | |
| 2598 | + <CopyIcon className="size-3.5" /> | |
| 2599 | + </Button> | |
| 2600 | + </span> | |
| 2601 | + <span className="flex min-w-0 shrink-0 items-center gap-1"> | |
| 2602 | + <ServerIcon className="size-3.5 shrink-0 opacity-60" /> | |
| 2603 | + <span className="min-w-0 truncate"> | |
| 2604 | + {member.environmentLabel ?? "Current environment"} | |
| 2605 | + </span> | |
| 2606 | + </span> | |
| 2607 | + </div> | |
| 2608 | + ))} | |
| 2609 | + </div> | |
| 2577 | 2610 | </DialogHeader> |
| 2578 | 2611 | <DialogPanel className="p-0"> |
| 2579 | 2612 | <div className="divide-y divide-border/60"> |
| 2580 | 2613 | {projectActionsTarget?.memberProjects.map((member) => ( |
| 2581 | 2614 | <section |
| 2582 | 2615 | key={member.physicalProjectKey} |
| 2583 | − className="flex min-w-0 flex-col gap-4 px-6 py-5 sm:gap-3 sm:py-4" | |
| 2616 | + className="grid min-w-0 gap-5 px-6 pb-5 pt-2 sm:gap-4 sm:pb-4 sm:pt-2" | |
| 2584 | 2617 | > |
| 2585 | − <div className="flex min-w-0 items-start gap-3"> | |
| 2586 | − <ProjectFavicon | |
| 2587 | − environmentId={member.environmentId} | |
| 2588 | − cwd={member.workspaceRoot} | |
| 2589 | − className="size-5 shrink-0 sm:size-4" | |
| 2590 | − /> | |
| 2591 | − <div className="min-w-0 flex-1"> | |
| 2592 | − <div className="flex min-w-0 items-center gap-1.5 text-base text-muted-foreground sm:text-sm"> | |
| 2593 | − <ServerIcon className="size-4 shrink-0 stroke-muted-foreground" /> | |
| 2594 | − <p className="min-w-0 truncate"> | |
| 2595 | − {member.environmentLabel ?? "Current environment"} | |
| 2596 | − </p> | |
| 2597 | − </div> | |
| 2598 | − <p | |
| 2599 | − className="truncate font-mono text-base text-muted-foreground/72 sm:text-sm" | |
| 2600 | − title={member.workspaceRoot} | |
| 2601 | − > | |
| 2602 | − {member.workspaceRoot} | |
| 2603 | − </p> | |
| 2604 | − </div> | |
| 2605 | − </div> | |
| 2606 | − <div className="grid gap-4 sm:grid-cols-2 sm:gap-3 sm:pl-7"> | |
| 2618 | + <div className="grid gap-4 sm:grid-cols-2 sm:gap-3"> | |
| 2607 | 2619 | <label className="grid min-w-0 gap-1.5"> |
| 2608 | 2620 | <span className="font-medium text-foreground">Project name</span> |
| 2609 | 2621 | <Input |
| 2610 | 2622 | key={`${member.physicalProjectKey}:${member.title}`} |
| 2611 | − size="sm" | |
| 2612 | 2623 | aria-label={`Project name in ${member.environmentLabel ?? "current environment"}`} |
| 2613 | 2624 | defaultValue={member.title} |
| 2614 | 2625 | onBlur={(event) => { |
| @@ -2639,8 +2650,7 @@ export default function SidebarV2() { · 24 unmodified lines | ||
| 2639 | 2650 | }} |
| 2640 | 2651 | > |
| 2641 | 2652 | <SelectTrigger |
| 2642 | − size="sm" | |
| 2643 | − className="w-full" | |
| 2653 | + className="w-full sm:min-h-7.5" | |
| 2644 | 2654 | aria-label={`Grouping rule for ${member.environmentLabel ?? "current environment"}`} |
| 2645 | 2655 | > |
| 2646 | 2656 | <SelectValue> |
| @@ -2672,32 +2682,23 @@ export default function SidebarV2() { · 25 unmodified lines | ||
| 2672 | 2682 | </Select> |
| 2673 | 2683 | </label> |
| 2674 | 2684 | </div> |
| 2675 | − <div className="flex flex-wrap items-center gap-2 sm:pl-7"> | |
| 2676 | − <Button | |
| 2677 | − size="sm" | |
| 2678 | − variant="ghost" | |
| 2679 | − onClick={() => | |
| 2680 | − copyProjectPath(member.workspaceRoot, { path: member.workspaceRoot }) | |
| 2681 | − } | |
| 2682 | − > | |
| 2683 | − <CopyIcon /> | |
| 2684 | − Copy path | |
| 2685 | − </Button> | |
| 2686 | − <Button | |
| 2687 | − size="sm" | |
| 2688 | − variant="ghost" | |
| 2689 | − className="text-destructive-foreground hover:bg-destructive/8 hover:text-destructive-foreground sm:ml-auto" | |
| 2690 | − onClick={() => { | |
| 2691 | − const projectGroup = projectActionsTarget; | |
| 2692 | − if (!projectGroup) return; | |
| 2693 | − setProjectActionsTarget(null); | |
| 2694 | − void handleRemoveProjectMembers(projectGroup, [member]); | |
| 2695 | − }} | |
| 2696 | − > | |
| 2697 | − <Trash2Icon /> | |
| 2698 | − Remove | |
| 2699 | − </Button> | |
| 2700 | − </div> | |
| 2685 | + {projectActionsTarget.memberProjects.length > 1 ? ( | |
| 2686 | + <div className="flex justify-end"> | |
| 2687 | + <Button | |
| 2688 | + size="sm" | |
| 2689 | + variant="ghost" | |
| 2690 | + className="text-destructive-foreground hover:bg-destructive/8 hover:text-destructive-foreground" | |
| 2691 | + onClick={() => { | |
| 2692 | + const projectGroup = projectActionsTarget; | |
| 2693 | + setProjectActionsTarget(null); | |
| 2694 | + void handleRemoveProjectMembers(projectGroup, [member]); | |
| 2695 | + }} | |
| 2696 | + > | |
| 2697 | + <Trash2Icon /> | |
| 2698 | + Remove project | |
| 2699 | + </Button> | |
| 2700 | + </div> | |
| 2701 | + ) : null} | |
| 2701 | 2702 | </section> |
| 2702 | 2703 | ))} |
| 2703 | 2704 | </div> |
| @@ -2727,8 +2728,26 @@ export default function SidebarV2() { · 23 unmodified lines | ||
| 2727 | 2728 | </div> |
| 2728 | 2729 | ) : null} |
| 2729 | 2730 | </DialogPanel> |
| 2730 | − <DialogFooter variant="bare"> | |
| 2731 | − <Button onClick={() => setProjectActionsTarget(null)}>Done</Button> | |
| 2731 | + <DialogFooter | |
| 2732 | + variant="bare" | |
| 2733 | + className={cn( | |
| 2734 | + projectActionsTarget?.memberProjects.length === 1 && "sm:justify-between", | |
| 2735 | + )} | |
| 2736 | + > | |
| 2737 | + {projectActionsTarget?.memberProjects.length === 1 ? ( | |
| 2738 | + <Button | |
| 2739 | + variant="destructive-outline" | |
| 2740 | + onClick={() => { | |
| 2741 | + const projectGroup = projectActionsTarget; | |
| 2742 | + setProjectActionsTarget(null); | |
| 2743 | + void handleRemoveProjectMembers(projectGroup, projectGroup.memberProjects); | |
| 2744 | + }} | |
| 2745 | + > | |
| 2746 | + <Trash2Icon /> | |
| 2747 | + Remove project | |
| 2748 | + </Button> | |
| 2749 | + ) : null} | |
| 2750 | + <Button onClick={() => setProjectActionsTarget(null)}>Close</Button> | |
| 2732 | 2751 | </DialogFooter> |
| 2733 | 2752 | </DialogPopup> |
| 2734 | 2753 | </Dialog> |
| @@ -95,6 +95,7 @@ import { ComposerPrimaryActions } from "./ComposerPrimaryActions"; · 94 unmodified lines | ||
| 95 | 95 | import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel"; |
| 96 | 96 | import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel"; |
| 97 | 97 | import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner"; |
| 98 | +import { ComposerControl, ComposerControlIcon, ComposerSelectControl } from "./ComposerControl"; | |
| 98 | 99 | import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight"; |
| 99 | 100 | import { searchSlashCommandItems } from "./composerSlashCommandSearch"; |
| 100 | 101 | import { |
| @@ -167,7 +168,7 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children: · 66 unmodified lines | ||
| 167 | 168 | ); |
| 168 | 169 | } |
| 169 | 170 | import { Button } from "../ui/button"; |
| 170 | −import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; | |
| 171 | +import { Select, SelectItem, SelectPopup, SelectValue } from "../ui/select"; | |
| 171 | 172 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 172 | 173 | import { toastManager } from "../ui/toast"; |
| 173 | 174 | import { |
| @@ -303,15 +304,13 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop · 129 unmodified lines | ||
| 303 | 304 | <Tooltip> |
| 304 | 305 | <TooltipTrigger |
| 305 | 306 | render={ |
| 306 | − <Button | |
| 307 | − variant="ghost" | |
| 307 | + <ComposerControl | |
| 308 | 308 | className={cn( |
| 309 | − "shrink-0 whitespace-nowrap px-2 sm:px-3", | |
| 309 | + "shrink-0 whitespace-nowrap", | |
| 310 | 310 | props.interactionMode === "plan" |
| 311 | 311 | ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300" |
| 312 | 312 | : "text-muted-foreground/70 hover:text-foreground/80", |
| 313 | 313 | )} |
| 314 | − size="sm" | |
| 315 | 314 | type="button" |
| 316 | 315 | onClick={props.onToggleInteractionMode} |
| 317 | 316 | aria-label={interactionModeTooltip} |
| @@ -319,9 +318,9 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop · 1 unmodified line | ||
| 319 | 318 | } |
| 320 | 319 | > |
| 321 | 320 | {props.interactionMode === "plan" ? ( |
| 322 | − <PencilRulerIcon className="text-current opacity-100" /> | |
| 321 | + <ComposerControlIcon icon={PencilRulerIcon} className="text-current opacity-100" /> | |
| 323 | 322 | ) : ( |
| 324 | − <BotIcon /> | |
| 323 | + <ComposerControlIcon icon={BotIcon} opticalSize="large" /> | |
| 325 | 324 | )} |
| 326 | 325 | <span className="sr-only sm:not-sr-only"> |
| 327 | 326 | {props.interactionMode === "plan" ? "Plan" : "Build"} |
| @@ -342,16 +341,9 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop · 14 unmodified lines | ||
| 342 | 341 | onValueChange={(value) => props.onRuntimeModeChange(value!)} |
| 343 | 342 | > |
| 344 | 343 | <TooltipTrigger |
| 345 | − render={ | |
| 346 | − <SelectTrigger | |
| 347 | − variant="ghost" | |
| 348 | − size="sm" | |
| 349 | − className="font-medium" | |
| 350 | − aria-label="Runtime mode" | |
| 351 | − /> | |
| 352 | − } | |
| 344 | + render={<ComposerSelectControl className="font-medium" aria-label="Runtime mode" />} | |
| 353 | 345 | > |
| 354 | − <RuntimeModeIcon className="size-4" /> | |
| 346 | + <ComposerControlIcon icon={RuntimeModeIcon} /> | |
| 355 | 347 | <SelectValue>{runtimeModeOption.label}</SelectValue> |
| 356 | 348 | </TooltipTrigger> |
| 357 | 349 | <SelectPopup alignItemWithTrigger={false}> |
| @@ -387,22 +379,21 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop · 29 unmodified lines | ||
| 387 | 379 | <Tooltip> |
| 388 | 380 | <TooltipTrigger |
| 389 | 381 | render={ |
| 390 | − <Button | |
| 391 | − variant="ghost" | |
| 382 | + <ComposerControl | |
| 392 | 383 | className={cn( |
| 393 | − "shrink-0 whitespace-nowrap px-2 sm:px-3", | |
| 384 | + "shrink-0 whitespace-nowrap", | |
| 394 | 385 | props.planSidebarOpen |
| 395 | 386 | ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300" |
| 396 | 387 | : "text-muted-foreground/70 hover:text-foreground/80", |
| 397 | 388 | )} |
| 398 | − size="sm" | |
| 399 | 389 | type="button" |
| 400 | 390 | onClick={props.onTogglePlanSidebar} |
| 401 | 391 | aria-label={planSidebarTooltip} |
| 402 | 392 | /> |
| 403 | 393 | } |
| 404 | 394 | > |
| 405 | − <ListTodoIcon | |
| 395 | + <ComposerControlIcon | |
| 396 | + icon={ListTodoIcon} | |
| 406 | 397 | className={props.planSidebarOpen ? "text-current opacity-100" : undefined} |
| 407 | 398 | /> |
| 408 | 399 | <span className="sr-only sm:not-sr-only">{props.planSidebarLabel}</span> |
| @@ -93,7 +93,7 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro · 92 unmodified lines | ||
| 93 | 93 | {showCollapsedStackCap ? ( |
| 94 | 94 | <div |
| 95 | 95 | className={cn( |
| 96 | − "pointer-events-none absolute inset-x-0 -top-3 z-0 mx-auto h-3 rounded-t-xl", | |
| 96 | + "pointer-events-none absolute inset-x-0 -top-3 z-0 mx-auto h-3 rounded-t-[22px]", | |
| 97 | 97 | "border border-b-0 border-warning/24 bg-background/96 shadow-[0_6px_18px_rgba(0,0,0,0.06)]", |
| 98 | 98 | "transition-opacity duration-150 ease-out", |
| 99 | 99 | "group-hover/banner-stack:opacity-0 group-focus-within/banner-stack:opacity-0", |
| @@ -174,7 +174,7 @@ function ComposerBannerStackAlert({ · 74 unmodified lines | ||
| 174 | 174 | return ( |
| 175 | 175 | <Alert |
| 176 | 176 | variant={item.variant} |
| 177 | − className={cn("alert-glass", item.className)} | |
| 177 | + className={cn("alert-glass rounded-[22px]", item.className)} | |
| 178 | 178 | data-variant={item.variant} |
| 179 | 179 | > |
| 180 | 180 | {item.icon} |
| @@ -139,7 +139,10 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { · 138 unmodified lines | ||
| 139 | 139 | ); |
| 140 | 140 | }} |
| 141 | 141 | > |
| 142 | − <div ref={listRef} className="dropdown-glass relative w-full overflow-hidden rounded-[20px]"> | |
| 142 | + <div | |
| 143 | + ref={listRef} | |
| 144 | + className="dropdown-glass relative w-full overflow-hidden rounded-[20px] **:data-[slot=scroll-area-scrollbar]:data-[orientation=vertical]:my-4" | |
| 145 | + > | |
| 143 | 146 | {props.items.length > 0 ? ( |
| 144 | 147 | <CommandList className="max-h-72"> |
| 145 | 148 | {groups.map((group, groupIndex) => ( |
| @@ -0,0 +1,71 @@ | ||
| 1 | +import type { ComponentProps } from "react"; | |
| 2 | +import { ChevronDownIcon, type LucideIcon } from "lucide-react"; | |
| 3 | + | |
| 4 | +import { cn } from "~/lib/utils"; | |
| 5 | +import { Button } from "../ui/button"; | |
| 6 | +import { SelectTrigger } from "../ui/select"; | |
| 7 | + | |
| 8 | +const composerControlClassName = | |
| 9 | + "h-7 min-h-7 gap-1.5 px-2.5 text-muted-foreground/70 transition-none hover:text-foreground/80 [&_svg[data-composer-control-icon]]:mx-0 [&_svg[data-composer-control-chevron]]:-mx-0.5"; | |
| 10 | + | |
| 11 | +export function ComposerControl({ | |
| 12 | + className, | |
| 13 | + size = "sm", | |
| 14 | + variant = "ghost", | |
| 15 | + ...props | |
| 16 | +}: ComponentProps<typeof Button>) { | |
| 17 | + return ( | |
| 18 | + <Button | |
| 19 | + className={cn(composerControlClassName, className)} | |
| 20 | + size={size} | |
| 21 | + variant={variant} | |
| 22 | + {...props} | |
| 23 | + /> | |
| 24 | + ); | |
| 25 | +} | |
| 26 | + | |
| 27 | +export function ComposerControlIcon({ | |
| 28 | + icon: Icon, | |
| 29 | + className, | |
| 30 | + opticalSize = "default", | |
| 31 | +}: { | |
| 32 | + icon: LucideIcon; | |
| 33 | + className?: string | undefined; | |
| 34 | + opticalSize?: "default" | "large"; | |
| 35 | +}) { | |
| 36 | + return ( | |
| 37 | + <Icon | |
| 38 | + aria-hidden="true" | |
| 39 | + className={cn("shrink-0", opticalSize === "large" ? "size-4.5" : "size-4", className)} | |
| 40 | + data-composer-control-icon | |
| 41 | + /> | |
| 42 | + ); | |
| 43 | +} | |
| 44 | + | |
| 45 | +export function ComposerControlChevron() { | |
| 46 | + return ( | |
| 47 | + <ChevronDownIcon | |
| 48 | + aria-hidden="true" | |
| 49 | + className="-mx-0.5 size-3.5 shrink-0 text-muted-foreground opacity-70" | |
| 50 | + data-composer-control-chevron | |
| 51 | + strokeWidth={2.25} | |
| 52 | + /> | |
| 53 | + ); | |
| 54 | +} | |
| 55 | + | |
| 56 | +export function ComposerSelectControl({ | |
| 57 | + className, | |
| 58 | + size = "sm", | |
| 59 | + variant = "ghost", | |
| 60 | + ...props | |
| 61 | +}: ComponentProps<typeof SelectTrigger>) { | |
| 62 | + return ( | |
| 63 | + <SelectTrigger | |
| 64 | + className={cn(composerControlClassName, className)} | |
| 65 | + icon={<ComposerControlChevron />} | |
| 66 | + size={size} | |
| 67 | + variant={variant} | |
| 68 | + {...props} | |
| 69 | + /> | |
| 70 | + ); | |
| 71 | +} | |
| @@ -101,11 +101,11 @@ export function DraftHeroHeadline({ · 100 unmodified lines | ||
| 101 | 101 | <Menu> |
| 102 | 102 | <MenuTrigger |
| 103 | 103 | aria-label={hasResolvedProject ? "Change project" : "Choose a project"} |
| 104 | − className="pointer-events-auto inline cursor-pointer border-current border-b border-dotted text-foreground underline-offset-8 transition-opacity hover:opacity-75 focus-visible:rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" | |
| 104 | + className="pointer-events-auto inline cursor-pointer border-foreground/60 border-b border-dotted text-foreground transition-colors hover:border-foreground/80 focus-visible:rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" | |
| 105 | 105 | > |
| 106 | 106 | {activeProjectDisplayName ?? "Choose a project"} |
| 107 | 107 | </MenuTrigger> |
| 108 | − <MenuPopup align="center" className="max-h-80 w-64 overflow-y-auto"> | |
| 108 | + <MenuPopup align="center" className="max-h-80 min-w-40! w-max max-w-64 overflow-y-auto"> | |
| 109 | 109 | <MenuRadioGroup |
| 110 | 110 | value={activeProjectKey} |
| 111 | 111 | onValueChange={(value) => { |
| @@ -138,7 +138,7 @@ export function DraftHeroHeadline({ · 26 unmodified lines | ||
| 138 | 138 | <button |
| 139 | 139 | type="button" |
| 140 | 140 | onClick={openAddProject} |
| 141 | − className="pointer-events-auto inline cursor-pointer border-current border-b border-dotted text-muted-foreground/60 underline-offset-8 transition-opacity hover:opacity-75 focus-visible:rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" | |
| 141 | + className="pointer-events-auto inline cursor-pointer border-muted-foreground/35 border-b border-dotted text-muted-foreground/60 transition-colors hover:border-muted-foreground/60 hover:text-muted-foreground/80 focus-visible:rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" | |
| 142 | 142 | > |
| 143 | 143 | {activeProjectTitle ?? "Add a project"} |
| 144 | 144 | </button> |
| @@ -50,7 +50,7 @@ export const ModelListRow = memo(function ModelListRow(props: { · 49 unmodified lines | ||
| 50 | 50 | disabled={Boolean(props.disabledReason)} |
| 51 | 51 | contentClassName="flex w-full items-center gap-3" |
| 52 | 52 | className={cn( |
| 53 | − "group relative w-full !min-w-0 max-w-full cursor-pointer rounded-md px-2 py-2.5 transition-[background-color,box-shadow,color]", | |
| 53 | + "group relative w-full !min-w-0 max-w-full cursor-pointer rounded-md px-2 py-2 transition-[background-color,box-shadow,color]", | |
| 54 | 54 | "hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] data-highlighted:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] data-selected:bg-foreground/[0.08] data-selected:text-foreground data-selected:ring-0 [&[data-highlighted][data-selected]]:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))]", |
| 55 | 55 | props.disabledReason && |
| 56 | 56 | "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent", |
| @@ -43,6 +43,10 @@ type ModelPickerItem = { · 42 unmodified lines | ||
| 43 | 43 | |
| 44 | 44 | const EMPTY_MODEL_JUMP_LABELS = new Map<string, string>(); |
| 45 | 45 | |
| 46 | +function ModelListSeparator() { | |
| 47 | + return <div className="h-0.5" />; | |
| 48 | +} | |
| 49 | + | |
| 46 | 50 | // Split a `${instanceId}:${slug}` combobox key back into its pieces. Slugs |
| 47 | 51 | // can contain colons (e.g. some vendor model ids), so we only split on the |
| 48 | 52 | // first colon — anything after that is the slug. |
| @@ -521,7 +525,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { · 472 unmodified lines | ||
| 521 | 525 | return ( |
| 522 | 526 | <TooltipProvider delay={0}> |
| 523 | 527 | <div |
| 524 | − className="dropdown-glass model-picker-surface relative flex h-screen max-h-96 w-screen max-w-100 flex-row overflow-hidden rounded-lg text-popover-foreground [clip-path:inset(0_round_var(--radius-lg))]" | |
| 528 | + className="dropdown-glass model-picker-surface relative flex h-screen max-h-86.5 w-screen max-w-90 flex-row overflow-hidden rounded-lg text-popover-foreground [clip-path:inset(0_round_var(--radius-lg))]" | |
| 525 | 529 | data-model-picker-content="true" |
| 526 | 530 | > |
| 527 | 531 | {/* Sidebar */} |
| @@ -571,12 +575,12 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { · 43 unmodified lines | ||
| 571 | 575 | <div |
| 572 | 576 | className={cn( |
| 573 | 577 | "flex min-h-0 flex-1 flex-col overflow-hidden bg-muted/40", |
| 574 | − showSidebar && "border-l", | |
| 578 | + showSidebar && "border-l border-border/70", | |
| 575 | 579 | )} |
| 576 | 580 | > |
| 577 | 581 | {/* Search bar */} |
| 578 | − <div className="px-4 pt-2.5"> | |
| 579 | − <div className="-translate-y-px border-b border-border/70 pb-2.5 transition-colors focus-within:border-ring"> | |
| 582 | + <div className="px-2 pt-2"> | |
| 583 | + <div className="border-b border-border/70 pb-2.5 transition-colors focus-within:border-ring"> | |
| 580 | 584 | <ComboboxInput |
| 581 | 585 | ref={searchInputRef} |
| 582 | 586 | className="[&_input]:h-6.5 [&_input]:font-sans [&_input]:leading-6.5" |
| @@ -584,7 +588,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { · 1 unmodified line | ||
| 584 | 588 | placeholder="Search models..." |
| 585 | 589 | showTrigger={false} |
| 586 | 590 | startAddon={ |
| 587 | − <SearchIcon className="-translate-x-0.5 size-4 shrink-0 text-muted-foreground/55" /> | |
| 591 | + <SearchIcon className="-translate-x-0.5 size-4 shrink-0 text-muted-foreground opacity-70" /> | |
| 588 | 592 | } |
| 589 | 593 | value={searchQuery} |
| 590 | 594 | onChange={(e) => setSearchQuery(e.target.value)} |
| @@ -618,8 +622,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { · 27 unmodified lines | ||
| 618 | 622 | </div> |
| 619 | 623 | |
| 620 | 624 | {/* Model list */} |
| 621 | − <div className="relative min-h-0 flex-1 overflow-hidden"> | |
| 622 | − <ComboboxListVirtualized className="model-picker-list size-full min-w-0 p-0"> | |
| 625 | + <div className="relative min-h-0 flex-1 overflow-hidden pr-px"> | |
| 626 | + <ComboboxListVirtualized className="size-full min-w-0 p-0 not-empty:p-0"> | |
| 623 | 627 | <LegendList<string> |
| 624 | 628 | ref={modelListRef} |
| 625 | 629 | data={filteredModelKeys} |
| @@ -656,12 +660,14 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { · 30 unmodified lines | ||
| 656 | 660 | estimatedItemSize={60} |
| 657 | 661 | drawDistance={480} |
| 658 | 662 | recycleItems |
| 663 | + contentContainerClassName="pl-2 pr-px" | |
| 664 | + ItemSeparatorComponent={ModelListSeparator} | |
| 659 | 665 | onLayout={updateModelListScrollFades} |
| 660 | 666 | onScroll={updateModelListScrollFades} |
| 661 | 667 | className={cn( |
| 662 | − "scrollbar-gutter-both h-full overflow-x-hidden overscroll-y-contain py-1.5 [--fade-size:1.5rem]", | |
| 663 | − showTopScrollFade && "mask-t-from-[calc(100%-var(--fade-size))]", | |
| 664 | − showBottomScrollFade && "mask-b-from-[calc(100%-var(--fade-size))]", | |
| 668 | + "model-picker-list h-full overflow-x-hidden overscroll-y-contain py-1.5 [--fade-size:1.5rem]", | |
| 669 | + showTopScrollFade && "model-picker-list-scroll-fade-top", | |
| 670 | + showBottomScrollFade && "model-picker-list-scroll-fade-bottom", | |
| 665 | 671 | )} |
| 666 | 672 | /> |
| 667 | 673 | </ComboboxListVirtualized> |
| @@ -78,34 +78,20 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 77 unmodified lines | ||
| 78 | 78 | if (!content) { |
| 79 | 79 | return; |
| 80 | 80 | } |
| 81 | − const selectedButton = Array.from( | |
| 81 | + const selectedItem = Array.from( | |
| 82 | 82 | content.querySelectorAll<HTMLElement>("[data-model-picker-provider]"), |
| 83 | − ).find((button) => button.dataset.modelPickerProvider === props.selectedInstanceId); | |
| 84 | − if (!selectedButton) { | |
| 83 | + ).find((item) => item.dataset.modelPickerProvider === props.selectedInstanceId); | |
| 84 | + if (!selectedItem) { | |
| 85 | 85 | setSelectedIndicatorTop(null); |
| 86 | 86 | return; |
| 87 | 87 | } |
| 88 | − const contentRect = content.getBoundingClientRect(); | |
| 89 | − const selectedButtonRect = selectedButton.getBoundingClientRect(); | |
| 90 | − setSelectedIndicatorTop( | |
| 91 | − selectedButtonRect.top - | |
| 92 | − contentRect.top + | |
| 93 | − content.scrollTop + | |
| 94 | − selectedButtonRect.height / 2 - | |
| 95 | − 10, | |
| 96 | − ); | |
| 88 | + setSelectedIndicatorTop(selectedItem.offsetTop + selectedItem.offsetHeight / 2 - 10); | |
| 97 | 89 | }, [props.instanceEntries, props.selectedInstanceId, showFavorites]); |
| 98 | 90 | |
| 99 | 91 | return ( |
| 100 | − <div | |
| 101 | − className="w-12 shrink-0 overflow-hidden border-r bg-muted/30" | |
| 102 | − data-model-picker-sidebar="true" | |
| 103 | − > | |
| 92 | + <div className="w-11 shrink-0 overflow-hidden bg-muted/30" data-model-picker-sidebar="true"> | |
| 104 | 93 | <div className="h-full overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> |
| 105 | − <div | |
| 106 | − ref={sidebarContentRef} | |
| 107 | − className="relative flex min-h-full flex-col gap-1 px-1 pb-1 pt-0.5" | |
| 108 | − > | |
| 94 | + <div ref={sidebarContentRef} className="relative flex min-h-full flex-col gap-1 p-1"> | |
| 109 | 95 | {selectedIndicatorTop !== null ? ( |
| 110 | 96 | <div |
| 111 | 97 | data-model-picker-selected-indicator="true" |
| @@ -118,8 +104,8 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 6 unmodified lines | ||
| 118 | 104 | ) : null} |
| 119 | 105 | {/* Favorites section */} |
| 120 | 106 | {showFavorites ? ( |
| 121 | − <div className="mb-1 border-b pb-1"> | |
| 122 | − <div className="relative w-full"> | |
| 107 | + <> | |
| 108 | + <div className="relative w-full" data-model-picker-provider="favorites"> | |
| 123 | 109 | <Tooltip> |
| 124 | 110 | <TooltipTrigger |
| 125 | 111 | render={ |
| @@ -129,7 +115,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 3 unmodified lines | ||
| 129 | 115 | )} |
| 130 | 116 | onClick={() => handleSelect("favorites")} |
| 131 | 117 | type="button" |
| 132 | − data-model-picker-provider="favorites" | |
| 133 | 118 | aria-label="Favorites" |
| 134 | 119 | > |
| 135 | 120 | <StarIcon className="size-5 fill-current shrink-0" aria-hidden /> |
| @@ -146,7 +131,8 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 10 unmodified lines | ||
| 146 | 131 | </TooltipPopup> |
| 147 | 132 | </Tooltip> |
| 148 | 133 | </div> |
| 149 | − </div> | |
| 134 | + <div className="border-b border-border/70" aria-hidden="true" /> | |
| 135 | + </> | |
| 150 | 136 | ) : null} |
| 151 | 137 | |
| 152 | 138 | {/* Instance buttons (one per configured instance — built-in + custom) */} |
| @@ -170,7 +156,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 17 unmodified lines | ||
| 170 | 156 | |
| 171 | 157 | const button = ( |
| 172 | 158 | <button |
| 173 | − data-model-picker-provider={entry.instanceId} | |
| 174 | 159 | className={cn( |
| 175 | 160 | "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded-md transition-colors hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] focus-visible:bg-[color-mix(in_srgb,var(--popover)_90%,var(--foreground))] focus-visible:outline-none", |
| 176 | 161 | isDisabled && "opacity-50 cursor-not-allowed hover:bg-transparent", |
| @@ -228,7 +213,11 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { · 51 unmodified lines | ||
| 228 | 213 | ); |
| 229 | 214 | |
| 230 | 215 | return ( |
| 231 | − <div key={entry.instanceId} className="relative w-full"> | |
| 216 | + <div | |
| 217 | + key={entry.instanceId} | |
| 218 | + className="relative w-full" | |
| 219 | + data-model-picker-provider={entry.instanceId} | |
| 220 | + > | |
| 232 | 221 | <Tooltip> |
| 233 | 222 | <TooltipTrigger render={trigger} /> |
| 234 | 223 | <TooltipPopup |
| @@ -5,8 +5,7 @@ import { · 4 unmodified lines | ||
| 5 | 5 | } from "@t3tools/contracts"; |
| 6 | 6 | import { memo, useEffect, useMemo, useState } from "react"; |
| 7 | 7 | import type { VariantProps } from "class-variance-authority"; |
| 8 | −import { ChevronDownIcon } from "lucide-react"; | |
| 9 | −import { Button, buttonVariants } from "../ui/button"; | |
| 8 | +import { buttonVariants } from "../ui/button"; | |
| 10 | 9 | import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; |
| 11 | 10 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 12 | 11 | import { cn } from "~/lib/utils"; |
| @@ -18,6 +17,7 @@ import { · 5 unmodified lines | ||
| 18 | 17 | getTriggerDisplayModelName, |
| 19 | 18 | } from "./providerIconUtils"; |
| 20 | 19 | import type { ProviderInstanceEntry } from "../../providerInstances"; |
| 20 | +import { ComposerControl, ComposerControlChevron } from "./ComposerControl"; | |
| 21 | 21 | |
| 22 | 22 | export const ProviderModelPicker = memo(function ProviderModelPicker(props: { |
| 23 | 23 | /** |
| @@ -146,28 +146,27 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { · 122 unmodified lines | ||
| 146 | 146 | > |
| 147 | 147 | <PopoverTrigger |
| 148 | 148 | render={ |
| 149 | − <Button | |
| 149 | + <ComposerControl | |
| 150 | 150 | aria-label={props.triggerAriaLabel} |
| 151 | − size="sm" | |
| 152 | 151 | variant={props.triggerVariant ?? "ghost"} |
| 153 | 152 | data-chat-provider-model-picker="true" |
| 154 | 153 | className={cn( |
| 155 | − "min-w-0 justify-between whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80", | |
| 156 | − props.compact ? "max-w-42 shrink-0" : "max-w-48 shrink sm:max-w-56 sm:px-3", | |
| 154 | + "min-w-0 justify-between whitespace-nowrap", | |
| 155 | + props.compact ? "max-w-42 shrink-0" : "max-w-48 shrink sm:max-w-56", | |
| 157 | 156 | props.triggerClassName, |
| 158 | 157 | )} |
| 159 | 158 | disabled={props.disabled} |
| 160 | 159 | /> |
| 161 | 160 | } |
| 162 | 161 | > |
| 163 | − <span className="flex min-w-0 flex-1 items-center gap-2"> | |
| 162 | + <span className="flex min-w-0 flex-1 items-center gap-1.5"> | |
| 164 | 163 | {activeEntry ? ( |
| 165 | 164 | <ProviderInstanceIcon |
| 166 | 165 | driverKind={activeEntry.driverKind} |
| 167 | 166 | displayName={activeEntry.displayName} |
| 168 | 167 | accentColor={activeEntry.accentColor} |
| 169 | 168 | showBadge={showInstanceBadge} |
| 170 | − className={showInstanceBadge ? "size-5" : "size-4"} | |
| 169 | + className="size-4" | |
| 171 | 170 | iconClassName={cn("size-4", props.activeProviderIconClassName)} |
| 172 | 171 | indicatorBackground="var(--input)" |
| 173 | 172 | badgeClassName={cn( |
| @@ -184,7 +183,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { · 10 unmodified lines | ||
| 184 | 183 | </Tooltip> |
| 185 | 184 | </span> |
| 186 | 185 | <span aria-hidden="true" className="flex items-center"> |
| 187 | − <ChevronDownIcon aria-hidden="true" className="!ms-0 !-me-1 size-3 shrink-0 opacity-60" /> | |
| 186 | + <ComposerControlChevron /> | |
| 188 | 187 | </span> |
| 189 | 188 | </PopoverTrigger> |
| 190 | 189 | <PopoverPopup |
| @@ -16,8 +16,8 @@ import { · 15 unmodified lines | ||
| 16 | 16 | } from "@t3tools/shared/model"; |
| 17 | 17 | import { memo, useCallback, useState } from "react"; |
| 18 | 18 | import type { VariantProps } from "class-variance-authority"; |
| 19 | −import { ChevronDownIcon, ZapIcon } from "lucide-react"; | |
| 20 | −import { Button, buttonVariants } from "../ui/button"; | |
| 19 | +import { ZapIcon } from "lucide-react"; | |
| 20 | +import { buttonVariants } from "../ui/button"; | |
| 21 | 21 | import { |
| 22 | 22 | Menu, |
| 23 | 23 | MenuGroup, |
| @@ -31,6 +31,7 @@ import { useComposerDraftStore, DraftId } from "../../composerDraftStore"; · 7 unmodified lines | ||
| 31 | 31 | import { getProviderModelCapabilities } from "../../providerModels"; |
| 32 | 32 | import { cn } from "~/lib/utils"; |
| 33 | 33 | import { Badge } from "../ui/badge"; |
| 34 | +import { ComposerControl, ComposerControlChevron, ComposerControlIcon } from "./ComposerControl"; | |
| 34 | 35 | |
| 35 | 36 | type ProviderOptions = ReadonlyArray<ProviderOptionSelection>; |
| 36 | 37 | |
| @@ -463,7 +464,7 @@ export const TraitsPicker = memo(function TraitsPicker({ · 426 unmodified lines | ||
| 463 | 464 | }); |
| 464 | 465 | const fastModeIcon = showFastModeIcon ? ( |
| 465 | 466 | <> |
| 466 | − <ZapIcon aria-hidden="true" className="size-3 shrink-0 text-foreground/80 opacity-100" /> | |
| 467 | + <ComposerControlIcon icon={ZapIcon} className="text-foreground/80 opacity-100" /> | |
| 467 | 468 | <span className="sr-only">Fast mode on</span> |
| 468 | 469 | </> |
| 469 | 470 | ) : null; |
| @@ -479,13 +480,12 @@ export const TraitsPicker = memo(function TraitsPicker({ · 9 unmodified lines | ||
| 479 | 480 | > |
| 480 | 481 | <MenuTrigger |
| 481 | 482 | render={ |
| 482 | − <Button | |
| 483 | − size="sm" | |
| 483 | + <ComposerControl | |
| 484 | 484 | variant={triggerVariant ?? "ghost"} |
| 485 | 485 | className={cn( |
| 486 | 486 | isCodexStyle |
| 487 | − ? "min-w-0 max-w-40 shrink justify-start overflow-hidden whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:max-w-48 sm:px-3 [&_svg]:mx-0" | |
| 488 | − : "shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3", | |
| 487 | + ? "min-w-0 max-w-40 shrink justify-start overflow-hidden whitespace-nowrap sm:max-w-48" | |
| 488 | + : "shrink-0 whitespace-nowrap", | |
| 489 | 489 | triggerClassName, |
| 490 | 490 | )} |
| 491 | 491 | /> |
| @@ -495,13 +495,13 @@ export const TraitsPicker = memo(function TraitsPicker({ · 3 unmodified lines | ||
| 495 | 495 | <span className="flex min-w-0 w-full items-center gap-1.5 overflow-hidden"> |
| 496 | 496 | {fastModeIcon} |
| 497 | 497 | <span className="min-w-0 truncate">{triggerLabel}</span> |
| 498 | − <ChevronDownIcon aria-hidden="true" className="size-3 shrink-0 opacity-60" /> | |
| 498 | + <ComposerControlChevron /> | |
| 499 | 499 | </span> |
| 500 | 500 | ) : ( |
| 501 | 501 | <> |
| 502 | 502 | {fastModeIcon} |
| 503 | 503 | <span>{triggerLabel}</span> |
| 504 | − <ChevronDownIcon aria-hidden="true" className="size-3 opacity-60" /> | |
| 504 | + <ComposerControlChevron /> | |
| 505 | 505 | </> |
| 506 | 506 | )} |
| 507 | 507 | </MenuTrigger> |
| @@ -53,12 +53,8 @@ function ConfiguredT3ConnectSidebarSignIn() { · 52 unmodified lines | ||
| 53 | 53 | <> |
| 54 | 54 | <SidebarMenu> |
| 55 | 55 | <SidebarMenuItem> |
| 56 | − <SidebarMenuButton | |
| 57 | − size="sm" | |
| 58 | − className="h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground" | |
| 59 | − onClick={openAuthPrompt} | |
| 60 | − > | |
| 61 | − <LogInIcon className="size-4 shrink-0" /> | |
| 56 | + <SidebarMenuButton onClick={openAuthPrompt}> | |
| 57 | + <LogInIcon /> | |
| 62 | 58 | <span>Sign in to T3 Connect</span> |
| 63 | 59 | </SidebarMenuButton> |
| 64 | 60 | </SidebarMenuItem> |
| @@ -72,7 +72,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { · 71 unmodified lines | ||
| 72 | 72 | return ( |
| 73 | 73 | <> |
| 74 | 74 | <SidebarContent className="overflow-x-hidden"> |
| 75 | − <SidebarGroup className="px-2 py-3"> | |
| 75 | + <SidebarGroup className="p-2"> | |
| 76 | 76 | <SidebarMenu> |
| 77 | 77 | {SETTINGS_NAV_ITEMS.map((item) => { |
| 78 | 78 | const Icon = item.icon; |
| @@ -80,22 +80,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { · 1 unmodified line | ||
| 80 | 80 | return ( |
| 81 | 81 | <SidebarMenuItem key={item.to}> |
| 82 | 82 | <SidebarMenuButton |
| 83 | − size="sm" | |
| 84 | 83 | isActive={isActive} |
| 85 | − className={ | |
| 86 | − isActive | |
| 87 | − ? "h-8 items-center gap-2 rounded-md bg-sidebar-row-active px-2 py-1.5 text-left text-sm font-medium text-sidebar-foreground" | |
| 88 | − : "h-8 items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm font-medium text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground" | |
| 89 | − } | |
| 90 | 84 | onClick={() => handleSectionClick(item.to)} |
| 91 | 85 | > |
| 92 | − <Icon | |
| 93 | − className={ | |
| 94 | − isActive | |
| 95 | − ? "size-4 shrink-0 text-sidebar-foreground" | |
| 96 | − : "size-4 shrink-0 text-sidebar-muted-foreground/60" | |
| 97 | − } | |
| 98 | − /> | |
| 86 | + <Icon /> | |
| 99 | 87 | <span className="truncate">{item.label}</span> |
| 100 | 88 | </SidebarMenuButton> |
| 101 | 89 | </SidebarMenuItem> |
| @@ -106,15 +94,11 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { · 4 unmodified lines | ||
| 106 | 94 | </SidebarContent> |
| 107 | 95 | <SidebarFooter className="p-2"> |
| 108 | 96 | <T3ConnectSidebarSignIn /> |
| 109 | − <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1"> | |
| 110 | − <SidebarMenu className="min-w-0"> | |
| 97 | + <div className="flex items-center gap-1"> | |
| 98 | + <SidebarMenu className="min-w-0 flex-1"> | |
| 111 | 99 | <SidebarMenuItem> |
| 112 | − <SidebarMenuButton | |
| 113 | − size="sm" | |
| 114 | − className="h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground" | |
| 115 | − onClick={handleBackClick} | |
| 116 | − > | |
| 117 | − <ArrowLeftIcon className="size-4" /> | |
| 100 | + <SidebarMenuButton onClick={handleBackClick}> | |
| 101 | + <ArrowLeftIcon /> | |
| 118 | 102 | <span>Back</span> |
| 119 | 103 | </SidebarMenuButton> |
| 120 | 104 | </SidebarMenuItem> |
| @@ -113,12 +113,8 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() { · 112 unmodified lines | ||
| 113 | 113 | <SidebarUpdatePill /> |
| 114 | 114 | <SidebarMenu> |
| 115 | 115 | <SidebarMenuItem> |
| 116 | − <SidebarMenuButton | |
| 117 | − size="sm" | |
| 118 | − className="h-8 items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-sidebar-muted-foreground/80 hover:bg-sidebar-row-hover hover:text-sidebar-foreground" | |
| 119 | − onClick={handleSettingsClick} | |
| 120 | − > | |
| 121 | − <SettingsIcon className="size-4.5 shrink-0" /> | |
| 116 | + <SidebarMenuButton onClick={handleSettingsClick}> | |
| 117 | + <SettingsIcon /> | |
| 122 | 118 | <span>Settings</span> |
| 123 | 119 | </SidebarMenuButton> |
| 124 | 120 | </SidebarMenuItem> |
| @@ -65,7 +65,7 @@ function CommandDialogPopup({ · 64 unmodified lines | ||
| 65 | 65 | <CommandDialogPrimitive.Popup |
| 66 | 66 | className={cn( |
| 67 | 67 | DIALOG_POPUP_CLASS, |
| 68 | − "pointer-events-auto max-h-105 max-w-xl text-foreground **:data-[slot=scroll-area-viewport]:data-has-overflow-y:pe-1", | |
| 68 | + "pointer-events-auto max-h-105 max-w-xl text-foreground", | |
| 69 | 69 | className, |
| 70 | 70 | )} |
| 71 | 71 | data-slot="command-dialog-popup" |
| @@ -103,16 +103,16 @@ function CommandInput({ · 31 unmodified lines | ||
| 103 | 103 | wrapperClassName?: string | undefined; |
| 104 | 104 | }) { |
| 105 | 105 | return ( |
| 106 | − <div className={cn("px-2.5 py-1.5", wrapperClassName)}> | |
| 106 | + <div className={cn("px-1.25 py-1.5", wrapperClassName)}> | |
| 107 | 107 | <AutocompleteInput |
| 108 | 108 | autoFocus |
| 109 | 109 | className={cn( |
| 110 | − "border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0 placeholder:text-muted-foreground/80", | |
| 110 | + "border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0 placeholder:text-muted-foreground/80 *:data-[slot=autocomplete-input]:ps-9! sm:*:data-[slot=autocomplete-input]:ps-8.5!", | |
| 111 | 111 | className, |
| 112 | 112 | )} |
| 113 | 113 | placeholder={placeholder} |
| 114 | 114 | size="lg" |
| 115 | − startAddon={<SearchIcon />} | |
| 115 | + startAddon={<SearchIcon className="translate-x-0.5 text-muted-foreground" />} | |
| 116 | 116 | {...props} |
| 117 | 117 | /> |
| 118 | 118 | </div> |
| @@ -198,7 +198,7 @@ function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) { · 79 unmodified lines | ||
| 198 | 198 | return ( |
| 199 | 199 | <kbd |
| 200 | 200 | className={cn( |
| 201 | − "ms-auto font-medium font-sans text-muted-foreground text-xs tracking-widest", | |
| 201 | + "ms-auto font-medium font-sans text-muted-foreground/70 text-xs tracking-widest", | |
| 202 | 202 | className, |
| 203 | 203 | )} |
| 204 | 204 | data-slot="command-shortcut" |
| @@ -211,7 +211,7 @@ function CommandFooter({ className, ...props }: React.ComponentProps<"div">) { · 6 unmodified lines | ||
| 211 | 211 | return ( |
| 212 | 212 | <div |
| 213 | 213 | className={cn( |
| 214 | − "relative flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] bg-foreground/[0.025] px-5 py-3 font-medium text-sm text-muted-foreground [&_[data-slot=kbd-group]]:font-sans [&_[data-slot=kbd]]:bg-foreground/[0.08] [&_[data-slot=kbd]]:text-foreground [&_[data-slot=kbd]]:ring-0", | |
| 214 | + "relative flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] bg-foreground/[0.025] px-4 py-2.5 font-medium text-sm text-muted-foreground [&_[data-slot=kbd-group]]:font-sans [&_[data-slot=kbd]]:bg-foreground/[0.08] [&_[data-slot=kbd]]:text-foreground [&_[data-slot=kbd]]:ring-0", | |
| 215 | 215 | className, |
| 216 | 216 | )} |
| 217 | 217 | data-slot="command-footer" |
| @@ -56,7 +56,7 @@ function ScrollBar({ · 55 unmodified lines | ||
| 56 | 56 | return ( |
| 57 | 57 | <ScrollAreaPrimitive.Scrollbar |
| 58 | 58 | className={cn( |
| 59 | − "m-1 flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:w-1.5 data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100", | |
| 59 | + "flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:mx-1 data-[orientation=horizontal]:mb-px data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:my-1 data-[orientation=vertical]:mr-px data-[orientation=vertical]:w-1.5 data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100", | |
| 60 | 60 | className, |
| 61 | 61 | )} |
| 62 | 62 | data-slot="scroll-area-scrollbar" |
| @@ -64,7 +64,7 @@ function ScrollBar({ · 1 unmodified line | ||
| 64 | 64 | {...props} |
| 65 | 65 | > |
| 66 | 66 | <ScrollAreaPrimitive.Thumb |
| 67 | − className="relative flex-1 rounded-full bg-foreground/20" | |
| 67 | + className="relative flex-1 rounded-full bg-[var(--app-scrollbar-thumb)] transition-colors hover:bg-[var(--app-scrollbar-thumb-hover)]" | |
| 68 | 68 | data-slot="scroll-area-thumb" |
| 69 | 69 | /> |
| 70 | 70 | </ScrollAreaPrimitive.Scrollbar> |
| @@ -77,8 +77,10 @@ function SelectTrigger({ · 76 unmodified lines | ||
| 77 | 77 | size = "default", |
| 78 | 78 | variant = "default", |
| 79 | 79 | children, |
| 80 | + icon, | |
| 80 | 81 | ...props |
| 81 | −}: SelectPrimitive.Trigger.Props & VariantProps<typeof selectTriggerVariants>) { | |
| 82 | +}: SelectPrimitive.Trigger.Props & | |
| 83 | + VariantProps<typeof selectTriggerVariants> & { icon?: React.ReactNode }) { | |
| 82 | 84 | return ( |
| 83 | 85 | <SelectPrimitive.Trigger |
| 84 | 86 | className={cn(selectTriggerVariants({ size, variant }), className)} |
| @@ -87,7 +89,7 @@ function SelectTrigger({ · 2 unmodified lines | ||
| 87 | 89 | > |
| 88 | 90 | {children} |
| 89 | 91 | <SelectPrimitive.Icon data-slot="select-icon"> |
| 90 | − <ChevronDownIcon className="-me-1 size-3 opacity-50" /> | |
| 92 | + {icon ?? <ChevronDownIcon className="-me-1 size-3 opacity-50" />} | |
| 91 | 93 | </SelectPrimitive.Icon> |
| 92 | 94 | </SelectPrimitive.Trigger> |
| 93 | 95 | ); |
| @@ -50,13 +50,35 @@ describe("sidebar interactive cursors", () => { · 49 unmodified lines | ||
| 50 | 50 | expect(html).toContain("size-[var(--workspace-titlebar-control-size)]!"); |
| 51 | 51 | }); |
| 52 | 52 | |
| 53 | − it("uses a pointer cursor for menu buttons by default", () => { | |
| 53 | + it("uses shared geometry and icon constraints for menu buttons by default", () => { | |
| 54 | 54 | const html = renderSidebarButton(); |
| 55 | 55 | |
| 56 | 56 | expect(html).toContain('data-slot="sidebar-menu-button"'); |
| 57 | + expect(html).toContain("h-8"); | |
| 58 | + expect(html).toContain("rounded-md"); | |
| 59 | + expect(html).toContain("px-2"); | |
| 60 | + expect(html).toContain("py-1.5"); | |
| 61 | + expect(html).toContain("]:size-4"); | |
| 62 | + expect(html).toContain("]:shrink-0"); | |
| 57 | 63 | expect(html).toContain("cursor-pointer"); |
| 58 | 64 | }); |
| 59 | 65 | |
| 66 | + it("applies the shared default treatment to icon-only menu buttons", () => { | |
| 67 | + const html = renderToStaticMarkup( | |
| 68 | + <SidebarProvider> | |
| 69 | + <SidebarMenuButton size="icon"> | |
| 70 | + <span>+</span> | |
| 71 | + </SidebarMenuButton> | |
| 72 | + </SidebarProvider>, | |
| 73 | + ); | |
| 74 | + | |
| 75 | + expect(html).toContain("size-8"); | |
| 76 | + expect(html).toContain("justify-center"); | |
| 77 | + expect(html).toContain("p-0"); | |
| 78 | + expect(html).toContain("font-medium"); | |
| 79 | + expect(html).toContain("text-sidebar-muted-foreground/80"); | |
| 80 | + }); | |
| 81 | + | |
| 60 | 82 | it("lets project drag handles override the default pointer cursor", () => { |
| 61 | 83 | const html = renderSidebarButton("cursor-grab"); |
| 62 | 84 | |
| @@ -684,19 +684,28 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof S · 683 unmodified lines | ||
| 684 | 684 | ); |
| 685 | 685 | } |
| 686 | 686 | |
| 687 | −function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { | |
| 687 | +function SidebarContent({ | |
| 688 | + className, | |
| 689 | + fixedHeader, | |
| 690 | + ...props | |
| 691 | +}: React.ComponentProps<"div"> & { | |
| 692 | + fixedHeader?: React.ReactNode; | |
| 693 | +}) { | |
| 688 | 694 | return ( |
| 689 | − <ScrollArea hideScrollbars scrollFade className="h-auto min-h-0 flex-1"> | |
| 690 | − <div | |
| 691 | − className={cn( | |
| 692 | − "flex w-full min-w-0 flex-col gap-2 group-data-[collapsible=icon]:overflow-hidden", | |
| 693 | − className, | |
| 694 | − )} | |
| 695 | − data-sidebar="content" | |
| 696 | − data-slot="sidebar-content" | |
| 697 | − {...props} | |
| 698 | − /> | |
| 699 | − </ScrollArea> | |
| 695 | + <> | |
| 696 | + {fixedHeader ? <div className="w-full shrink-0">{fixedHeader}</div> : null} | |
| 697 | + <ScrollArea hideScrollbars scrollFade className="h-auto min-h-0 flex-1"> | |
| 698 | + <div | |
| 699 | + className={cn( | |
| 700 | + "flex w-full min-w-0 flex-col gap-2 group-data-[collapsible=icon]:overflow-hidden", | |
| 701 | + className, | |
| 702 | + )} | |
| 703 | + data-sidebar="content" | |
| 704 | + data-slot="sidebar-content" | |
| 705 | + {...props} | |
| 706 | + /> | |
| 707 | + </ScrollArea> | |
| 708 | + </> | |
| 700 | 709 | ); |
| 701 | 710 | } |
| 702 | 711 | |
| @@ -783,7 +792,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { · 80 unmodified lines | ||
| 783 | 792 | } |
| 784 | 793 | |
| 785 | 794 | const sidebarMenuButtonVariants = cva( |
| 786 | − "peer/menu-button flex w-full cursor-pointer items-center gap-2 overflow-hidden rounded-lg p-2 text-left text-sm outline-hidden ring-ring transition-[width,height,padding] hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 active:bg-sidebar-row-active active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-row-selected data-[active=true]:font-medium data-[active=true]:text-sidebar-foreground data-[state=open]:hover:bg-sidebar-row-hover data-[state=open]:hover:text-sidebar-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0", | |
| 795 | + "peer/menu-button flex w-full cursor-pointer items-center gap-2 overflow-hidden text-left outline-hidden ring-ring transition-[width,height,padding] hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 active:bg-sidebar-row-active active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-row-selected data-[active=true]:font-medium data-[active=true]:text-sidebar-foreground data-[state=open]:hover:bg-sidebar-row-hover data-[state=open]:hover:text-sidebar-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-muted-foreground [&>svg]:opacity-60 hover:[&>svg]:text-sidebar-foreground hover:[&>svg]:opacity-100 active:[&>svg]:text-sidebar-foreground active:[&>svg]:opacity-100 data-[active=true]:[&>svg]:text-sidebar-foreground data-[active=true]:[&>svg]:opacity-100", | |
| 787 | 796 | { |
| 788 | 797 | defaultVariants: { |
| 789 | 798 | size: "default", |
| @@ -791,14 +800,14 @@ const sidebarMenuButtonVariants = cva( · 1 unmodified line | ||
| 791 | 800 | }, |
| 792 | 801 | variants: { |
| 793 | 802 | size: { |
| 794 | − default: "h-8 text-sm", | |
| 795 | − lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", | |
| 796 | − sm: "h-7 text-xs", | |
| 803 | + default: "h-8 rounded-md px-2.5 py-1.5 text-sm", | |
| 804 | + icon: "size-8 justify-center rounded-md p-0", | |
| 805 | + lg: "h-12 rounded-lg p-2 text-sm group-data-[collapsible=icon]:p-0!", | |
| 806 | + sm: "h-7 rounded-lg p-2 text-xs", | |
| 797 | 807 | }, |
| 798 | 808 | variant: { |
| 799 | − default: "hover:bg-sidebar-row-hover hover:text-sidebar-foreground", | |
| 800 | − outline: | |
| 801 | − "bg-sidebar-control-surface ring-1 ring-sidebar-border hover:bg-sidebar-row-hover hover:text-sidebar-foreground", | |
| 809 | + default: "font-medium text-sidebar-muted-foreground/80", | |
| 810 | + outline: "bg-sidebar-control-surface ring-1 ring-sidebar-border", | |
| 802 | 811 | }, |
| 803 | 812 | }, |
| 804 | 813 | }, |
| @@ -166,9 +166,9 @@ export function showContextMenuFallback<T extends string>( · 165 unmodified lines | ||
| 166 | 166 | |
| 167 | 167 | const menu = document.createElement("div"); |
| 168 | 168 | menu.className = |
| 169 | − "fixed z-[10000] min-w-32 max-w-sm overflow-hidden rounded-lg border border-border bg-popover bg-clip-padding text-popover-foreground shadow-lg/5 outline-none"; | |
| 169 | + "dropdown-glass fixed z-[10000] min-w-32 max-w-sm overflow-hidden rounded-lg bg-clip-padding text-popover-foreground outline-none"; | |
| 170 | 170 | menu.style.cssText = |
| 171 | − "position:fixed;z-index:10000;min-width:8rem;max-width:24rem;overflow:hidden;border-radius:var(--radius-lg);border:1px solid var(--border);background:var(--popover);background-clip:padding-box;color:var(--popover-foreground);box-shadow:0 10px 15px -3px rgb(0 0 0 / 0.05),0 4px 6px -4px rgb(0 0 0 / 0.05);outline:none;pointer-events:auto;"; | |
| 171 | + "position:fixed;z-index:10000;min-width:8rem;max-width:24rem;overflow:hidden;border-radius:var(--radius-lg);background-clip:padding-box;color:var(--popover-foreground);outline:none;pointer-events:auto;"; | |
| 172 | 172 | menu.style.left = `${preferredLeft}px`; |
| 173 | 173 | menu.style.top = `${preferredTop}px`; |
| 174 | 174 | menu.dataset.level = String(level); |
| @@ -77,6 +77,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 76 unmodified lines | ||
| 77 | 77 | |
| 78 | 78 | :root { |
| 79 | 79 | --app-scrollbar-width: 6px; |
| 80 | + --app-scrollbar-thumb: rgb(217 217 217); | |
| 81 | + --app-scrollbar-thumb-hover: rgb(191 191 191); | |
| 80 | 82 | --glass-blur: 12px; |
| 81 | 83 | --glass-opacity: 80%; |
| 82 | 84 | --glass-saturation: 1.14; |
| @@ -91,6 +93,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 8 unmodified lines | ||
| 91 | 93 | } |
| 92 | 94 | |
| 93 | 95 | .dark { |
| 96 | + --app-scrollbar-thumb: rgb(255 255 255 / 8%); | |
| 97 | + --app-scrollbar-thumb-hover: rgb(255 255 255 / 12%); | |
| 94 | 98 | --glass-blur: 16px; |
| 95 | 99 | --glass-saturation: 1.08; |
| 96 | 100 | } |
| @@ -327,12 +331,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 230 unmodified lines | ||
| 327 | 331 | rgb(0 0 0 / 96%) 82%, |
| 328 | 332 | black 100% |
| 329 | 333 | ), |
| 330 | − linear-gradient(black, black); | |
| 331 | − -webkit-mask-position: top, bottom; | |
| 334 | + linear-gradient(black, black), linear-gradient(black, black); | |
| 335 | + -webkit-mask-position: top, bottom, right; | |
| 332 | 336 | -webkit-mask-repeat: no-repeat; |
| 333 | 337 | -webkit-mask-size: |
| 334 | 338 | 100% var(--topbar-scroll-fade-height), |
| 335 | − 100% calc(100% - var(--topbar-scroll-fade-height)); | |
| 339 | + 100% calc(100% - var(--topbar-scroll-fade-height)), | |
| 340 | + var(--app-scrollbar-width) 100%; | |
| 336 | 341 | mask-image: |
| 337 | 342 | linear-gradient( |
| 338 | 343 | to bottom, |
| @@ -344,12 +349,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil · 5 unmodified lines | ||
| 344 | 349 | rgb(0 0 0 / 96%) 82%, |
| 345 | 350 | black 100% |
| 346 | 351 | ), |
| 347 | − linear-gradient(black, black); | |
| 348 | − mask-position: top, bottom; | |
| 352 | + linear-gradient(black, black), linear-gradient(black, black); | |
| 353 | + mask-position: top, bottom, right; | |
| 349 | 354 | mask-repeat: no-repeat; |
| 350 | 355 | mask-size: |
| 351 | 356 | 100% var(--topbar-scroll-fade-height), |
| 352 | − 100% calc(100% - var(--topbar-scroll-fade-height)); | |
| 357 | + 100% calc(100% - var(--topbar-scroll-fade-height)), | |
| 358 | + var(--app-scrollbar-width) 100%; | |
| 353 | 359 | } |
| 354 | 360 | |
| 355 | 361 | .workspace-titlebar-controls { |
| @@ -1035,20 +1041,54 @@ code { · 679 unmodified lines | ||
| 1035 | 1041 | } |
| 1036 | 1042 | |
| 1037 | 1043 | ::-webkit-scrollbar-thumb { |
| 1038 | − background: rgba(0, 0, 0, 0.15); | |
| 1044 | + background: var(--app-scrollbar-thumb); | |
| 1039 | 1045 | border-radius: 3px; |
| 1040 | 1046 | } |
| 1041 | 1047 | |
| 1042 | 1048 | ::-webkit-scrollbar-thumb:hover { |
| 1043 | − background: rgba(0, 0, 0, 0.25); | |
| 1049 | + background: var(--app-scrollbar-thumb-hover); | |
| 1044 | 1050 | } |
| 1045 | 1051 | |
| 1046 | −.dark ::-webkit-scrollbar-thumb { | |
| 1047 | − background: rgba(255, 255, 255, 0.1); | |
| 1052 | +.model-picker-list::-webkit-scrollbar-track { | |
| 1053 | + margin-block: 0.5rem; | |
| 1048 | 1054 | } |
| 1049 | 1055 | |
| 1050 | −.dark ::-webkit-scrollbar-thumb:hover { | |
| 1051 | − background: rgba(255, 255, 255, 0.18); | |
| 1056 | +.model-picker-list-scroll-fade-top, | |
| 1057 | +.model-picker-list-scroll-fade-bottom { | |
| 1058 | + -webkit-mask-image: var(--model-picker-list-scroll-mask), linear-gradient(black, black); | |
| 1059 | + -webkit-mask-position: left, right; | |
| 1060 | + -webkit-mask-repeat: no-repeat; | |
| 1061 | + -webkit-mask-size: | |
| 1062 | + calc(100% - var(--app-scrollbar-width)) 100%, | |
| 1063 | + var(--app-scrollbar-width) 100%; | |
| 1064 | + mask-image: var(--model-picker-list-scroll-mask), linear-gradient(black, black); | |
| 1065 | + mask-position: left, right; | |
| 1066 | + mask-repeat: no-repeat; | |
| 1067 | + mask-size: | |
| 1068 | + calc(100% - var(--app-scrollbar-width)) 100%, | |
| 1069 | + var(--app-scrollbar-width) 100%; | |
| 1070 | +} | |
| 1071 | + | |
| 1072 | +.model-picker-list-scroll-fade-top { | |
| 1073 | + --model-picker-list-scroll-mask: linear-gradient(to bottom, transparent, black var(--fade-size)); | |
| 1074 | +} | |
| 1075 | + | |
| 1076 | +.model-picker-list-scroll-fade-bottom { | |
| 1077 | + --model-picker-list-scroll-mask: linear-gradient( | |
| 1078 | + to bottom, | |
| 1079 | + black calc(100% - var(--fade-size)), | |
| 1080 | + transparent | |
| 1081 | + ); | |
| 1082 | +} | |
| 1083 | + | |
| 1084 | +.model-picker-list-scroll-fade-top.model-picker-list-scroll-fade-bottom { | |
| 1085 | + --model-picker-list-scroll-mask: linear-gradient( | |
| 1086 | + to bottom, | |
| 1087 | + transparent, | |
| 1088 | + black var(--fade-size), | |
| 1089 | + black calc(100% - var(--fade-size)), | |
| 1090 | + transparent | |
| 1091 | + ); | |
| 1052 | 1092 | } |
| 1053 | 1093 | |
| 1054 | 1094 | .turn-chip-strip { |
| @@ -1569,28 +1609,6 @@ label:has(> select#reasoning-effort) select { · 514 unmodified lines | ||
| 1569 | 1609 | animation: ultrathink-rainbow 10s linear infinite; |
| 1570 | 1610 | } |
| 1571 | 1611 | |
| 1572 | −/* Thin scrollbar for model picker */ | |
| 1573 | −.model-picker-list::-webkit-scrollbar { | |
| 1574 | − width: 4px; | |
| 1575 | −} | |
| 1576 | − | |
| 1577 | −.model-picker-list::-webkit-scrollbar-thumb { | |
| 1578 | − background: rgba(0, 0, 0, 0.1); | |
| 1579 | − border-radius: 2px; | |
| 1580 | −} | |
| 1581 | − | |
| 1582 | −.model-picker-list::-webkit-scrollbar-thumb:hover { | |
| 1583 | − background: rgba(0, 0, 0, 0.2); | |
| 1584 | −} | |
| 1585 | − | |
| 1586 | −.dark .model-picker-list::-webkit-scrollbar-thumb { | |
| 1587 | − background: rgba(255, 255, 255, 0.08); | |
| 1588 | −} | |
| 1589 | − | |
| 1590 | −.dark .model-picker-list::-webkit-scrollbar-thumb:hover { | |
| 1591 | − background: rgba(255, 255, 255, 0.15); | |
| 1592 | −} | |
| 1593 | − | |
| 1594 | 1612 | /* Composer chips are non-editable decorators, so the browser skips them when |
| 1595 | 1613 | painting text selection; this overlay stands in for the native highlight. */ |
| 1596 | 1614 | .composer-inline-chip[data-composer-chip-selected]::after { |
| @@ -74,11 +74,11 @@ function SettingsContentLayout() { · 73 unmodified lines | ||
| 74 | 74 | {!isElectron && ( |
| 75 | 75 | <header |
| 76 | 76 | className={cn( |
| 77 | − "px-3 py-2 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5", | |
| 77 | + "workspace-topbar px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5", | |
| 78 | 78 | COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS, |
| 79 | 79 | )} |
| 80 | 80 | > |
| 81 | − <div className="flex min-h-7 items-center gap-2 sm:min-h-6"> | |
| 81 | + <div className="flex w-full items-center gap-2"> | |
| 82 | 82 | <span className="text-sm font-medium text-foreground">Settings</span> |
| 83 | 83 | {showRestoreDefaults ? ( |
| 84 | 84 | <div className="ms-auto flex items-center gap-2"> |