authorgravatar for me@maxkatz.meMax Katz <me@maxkatz.me> 2026-07-28 08:43:49+04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-27 21:43:49-07:00
logdd5ea324885aea4980a49bb42d28f83023c40ea7
tree16e69903030f823bcc8713f801ca6275e9bcaac2
parentc13a021e432e94728fe88ff983fb9601c6d6b943
signature Signed by PGP key B5690EEEBB952194

fix(web): 33 web UI fixes (#4700)


25 files changed, 544 insertions(+), 432 deletions(-)

apps/web/src/components/CommandPaletteResults.tsx+3-3
......@@ -41,7 +41,7 @@ export function CommandPaletteResults(props: CommandPaletteResultsProps) {
4141 <CommandList>
4242 {props.groups.map((group) => (
4343 <CommandGroup items={group.items} key={group.value}>
44 <CommandGroupLabel>{group.label}</CommandGroupLabel>
44 <CommandGroupLabel className="ps-[9px]">{group.label}</CommandGroupLabel>
4545 <CommandCollection>
4646 {(item) =>
4747 item.disabled ? (
......@@ -133,13 +133,13 @@ function CommandPaletteResultRow(props: {
133133 )}
134134 {props.item.titleTrailingContent}
135135 {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">
137137 {props.item.timestamp}
138138 </span>
139139 ) : null}
140140 {shortcutLabel ? <CommandShortcut>{shortcutLabel}</CommandShortcut> : null}
141141 {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" />
143143 ) : null}
144144 </CommandItem>
145145 );
apps/web/src/components/ProjectFavicon.tsx+7-2
......@@ -4,6 +4,7 @@ import { FolderIcon } from "lucide-react";
44import type { ComponentType } from "react";
55import { useState } from "react";
66import { useAssetUrl } from "../assets/assetUrls";
7import { cn } from "~/lib/utils";
78
89const loadedProjectFaviconSrcs = new Set<string>();
910
......@@ -40,7 +41,7 @@ function ProjectFaviconFallback({
4041 readonly className?: string | undefined;
4142 readonly icon: ComponentType<{ className?: string }>;
4243}) {
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)} />;
4445}
4546
4647function ProjectFaviconImage({
......@@ -64,7 +65,11 @@ function ProjectFaviconImage({
6465 <img
6566 src={src}
6667 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 )}
6873 onLoad={() => {
6974 loadedProjectFaviconSrcs.add(src);
7075 setStatus("loaded");
apps/web/src/components/Sidebar.tsx+29-27
......@@ -2218,9 +2218,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
22182218 <div className="group/project-header relative">
22192219 <SidebarMenuButton
22202220 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" : ""
22242223 }`}
22252224 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}
22262225 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.listeners : {})}
......@@ -2822,30 +2821,33 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
28222821 );
28232822
28242823 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 >
28492851 {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? (
28502852 <SidebarGroup className="px-2 pt-2 pb-0">
28512853 <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8">
apps/web/src/components/SidebarV2.tsx+235-216
......@@ -242,38 +242,40 @@ function SidebarV2ThreadTooltip({
242242 <TooltipPopup
243243 side="right"
244244 align="start"
245 sideOffset={8}
245 sideOffset={4}
246246 variant="glass"
247247 className="max-w-80 text-left whitespace-normal"
248248 >
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">
252254 {projectTitle ? (
253255 <div className="flex min-w-0 items-center gap-2">
254256 <ProjectFavicon
255257 environmentId={thread.environmentId}
256258 cwd={projectCwd ?? ""}
257 className="size-4 shrink-0 stroke-muted-foreground"
259 className="size-3 shrink-0 stroke-muted-foreground"
258260 />
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>
260262 </div>
261263 ) : null}
262264 {environmentLabel ? (
263265 <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>
266268 </div>
267269 ) : null}
268270 {thread.branch ? (
269271 <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>
272274 </div>
273275 ) : null}
274276 {branchMismatch ? (
275277 <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" />
277279 <div className="min-w-0 flex-1 wrap-break-word leading-5">
278280 You're currently checked out on another branch.
279281 </div>
......@@ -284,15 +286,15 @@ function SidebarV2ThreadTooltip({
284286 <ProviderInstanceIcon
285287 driverKind={driverKind}
286288 displayName={thread.session?.providerName ?? modelInstanceId}
287 iconClassName="size-4 shrink-0"
289 iconClassName="size-3 shrink-0 grayscale opacity-60"
288290 />
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>
290292 </div>
291293 ) : null}
292294 {thread.session?.lastError ? (
293295 <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>
296298 </div>
297299 ) : null}
298300 </div>
......@@ -815,9 +817,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
815817 type="button"
816818 aria-label="Un-settle thread"
817819 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"
819821 >
820 <Undo2Icon className="size-3" />
822 <Undo2Icon className="mb-px size-3.5" />
821823 </button>
822824 ) : (
823825 <button
......@@ -917,7 +919,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
917919 {props.settlementSupported || showSnoozeButton ? (
918920 <span
919921 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",
921923 snoozeMenuOpen && "opacity-100",
922924 )}
923925 >
......@@ -933,9 +935,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
933935 type="button"
934936 aria-label="Settle thread"
935937 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"
937939 >
938 <CheckIcon className="size-3" />
940 <CheckIcon className="size-3.5" />
939941 Settle
940942 </button>
941943 ) : null}
......@@ -2220,151 +2222,158 @@ export default function SidebarV2() {
22202222 return (
22212223 <>
22222224 <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
22502232 render={
22512233 <SidebarMenuButton
2252 size="sm"
22532234 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"
22582238 />
22592239 }
22602240 >
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"
22872268 />
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 />
23012287 }
23022288 >
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 ) : (
23082296 <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"
23352326 >
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"
23532367 />
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}
23652373 </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">
23682377 <TooltipProvider
23692378 key="sidebar-thread-tooltips-150"
23702379 delay={150}
......@@ -2526,12 +2535,10 @@ export default function SidebarV2() {
25262535 <button
25272536 type="button"
25282537 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"
25302539 >
2540 <PlusIcon aria-hidden className="size-4 shrink-0" />
25312541 Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more
2532 <span className="text-muted-foreground/50">
2533 ({hiddenSettledCount} settled hidden)
2534 </span>
25352542 </button>
25362543 </li>
25372544 ) : null}
......@@ -2547,7 +2554,7 @@ export default function SidebarV2() {
25472554 onClick={openAddProjectCommandPalette}
25482555 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"
25492556 >
2550 <PlusIcon className="size-3" />
2557 <PlusIcon className="-mx-0.5 size-3" />
25512558 Add project
25522559 </button>
25532560 </>
......@@ -2567,48 +2574,52 @@ export default function SidebarV2() {
25672574 }}
25682575 >
25692576 <DialogPopup className="max-w-xl">
2570 <DialogHeader className="gap-1.5">
2577 <DialogHeader className="gap-3 pb-1!">
25712578 <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.
25762581 </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>
25772610 </DialogHeader>
25782611 <DialogPanel className="p-0">
25792612 <div className="divide-y divide-border/60">
25802613 {projectActionsTarget?.memberProjects.map((member) => (
25812614 <section
25822615 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"
25842617 >
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">
26072619 <label className="grid min-w-0 gap-1.5">
26082620 <span className="font-medium text-foreground">Project name</span>
26092621 <Input
26102622 key={`${member.physicalProjectKey}:${member.title}`}
2611 size="sm"
26122623 aria-label={`Project name in ${member.environmentLabel ?? "current environment"}`}
26132624 defaultValue={member.title}
26142625 onBlur={(event) => {
......@@ -2639,8 +2650,7 @@ export default function SidebarV2() {
26392650 }}
26402651 >
26412652 <SelectTrigger
2642 size="sm"
2643 className="w-full"
2653 className="w-full sm:min-h-7.5"
26442654 aria-label={`Grouping rule for ${member.environmentLabel ?? "current environment"}`}
26452655 >
26462656 <SelectValue>
......@@ -2672,32 +2682,23 @@ export default function SidebarV2() {
26722682 </Select>
26732683 </label>
26742684 </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}
27012702 </section>
27022703 ))}
27032704 </div>
......@@ -2727,8 +2728,26 @@ export default function SidebarV2() {
27272728 </div>
27282729 ) : null}
27292730 </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>
27322751 </DialogFooter>
27332752 </DialogPopup>
27342753 </Dialog>
apps/web/src/components/chat/ChatComposer.tsx+12-21
......@@ -95,6 +95,7 @@ import { ComposerPrimaryActions } from "./ComposerPrimaryActions";
9595import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel";
9696import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";
9797import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner";
98import { ComposerControl, ComposerControlIcon, ComposerSelectControl } from "./ComposerControl";
9899import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight";
99100import { searchSlashCommandItems } from "./composerSlashCommandSearch";
100101import {
......@@ -167,7 +168,7 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children:
167168 );
168169}
169170import { Button } from "../ui/button";
170import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
171import { Select, SelectItem, SelectPopup, SelectValue } from "../ui/select";
171172import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
172173import { toastManager } from "../ui/toast";
173174import {
......@@ -303,15 +304,13 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
303304 <Tooltip>
304305 <TooltipTrigger
305306 render={
306 <Button
307 variant="ghost"
307 <ComposerControl
308308 className={cn(
309 "shrink-0 whitespace-nowrap px-2 sm:px-3",
309 "shrink-0 whitespace-nowrap",
310310 props.interactionMode === "plan"
311311 ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300"
312312 : "text-muted-foreground/70 hover:text-foreground/80",
313313 )}
314 size="sm"
315314 type="button"
316315 onClick={props.onToggleInteractionMode}
317316 aria-label={interactionModeTooltip}
......@@ -319,9 +318,9 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
319318 }
320319 >
321320 {props.interactionMode === "plan" ? (
322 <PencilRulerIcon className="text-current opacity-100" />
321 <ComposerControlIcon icon={PencilRulerIcon} className="text-current opacity-100" />
323322 ) : (
324 <BotIcon />
323 <ComposerControlIcon icon={BotIcon} opticalSize="large" />
325324 )}
326325 <span className="sr-only sm:not-sr-only">
327326 {props.interactionMode === "plan" ? "Plan" : "Build"}
......@@ -342,16 +341,9 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
342341 onValueChange={(value) => props.onRuntimeModeChange(value!)}
343342 >
344343 <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" />}
353345 >
354 <RuntimeModeIcon className="size-4" />
346 <ComposerControlIcon icon={RuntimeModeIcon} />
355347 <SelectValue>{runtimeModeOption.label}</SelectValue>
356348 </TooltipTrigger>
357349 <SelectPopup alignItemWithTrigger={false}>
......@@ -387,22 +379,21 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
387379 <Tooltip>
388380 <TooltipTrigger
389381 render={
390 <Button
391 variant="ghost"
382 <ComposerControl
392383 className={cn(
393 "shrink-0 whitespace-nowrap px-2 sm:px-3",
384 "shrink-0 whitespace-nowrap",
394385 props.planSidebarOpen
395386 ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300"
396387 : "text-muted-foreground/70 hover:text-foreground/80",
397388 )}
398 size="sm"
399389 type="button"
400390 onClick={props.onTogglePlanSidebar}
401391 aria-label={planSidebarTooltip}
402392 />
403393 }
404394 >
405 <ListTodoIcon
395 <ComposerControlIcon
396 icon={ListTodoIcon}
406397 className={props.planSidebarOpen ? "text-current opacity-100" : undefined}
407398 />
408399 <span className="sr-only sm:not-sr-only">{props.planSidebarLabel}</span>
apps/web/src/components/chat/ComposerBannerStack.tsx+2-2
......@@ -93,7 +93,7 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
9393 {showCollapsedStackCap ? (
9494 <div
9595 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]",
9797 "border border-b-0 border-warning/24 bg-background/96 shadow-[0_6px_18px_rgba(0,0,0,0.06)]",
9898 "transition-opacity duration-150 ease-out",
9999 "group-hover/banner-stack:opacity-0 group-focus-within/banner-stack:opacity-0",
......@@ -174,7 +174,7 @@ function ComposerBannerStackAlert({
174174 return (
175175 <Alert
176176 variant={item.variant}
177 className={cn("alert-glass", item.className)}
177 className={cn("alert-glass rounded-[22px]", item.className)}
178178 data-variant={item.variant}
179179 >
180180 {item.icon}
apps/web/src/components/chat/ComposerCommandMenu.tsx+4-1
......@@ -139,7 +139,10 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {
139139 );
140140 }}
141141 >
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 >
143146 {props.items.length > 0 ? (
144147 <CommandList className="max-h-72">
145148 {groups.map((group, groupIndex) => (
apps/web/src/components/chat/ComposerControl.tsx created+71
......@@ -0,0 +1,71 @@
1import type { ComponentProps } from "react";
2import { ChevronDownIcon, type LucideIcon } from "lucide-react";
3
4import { cn } from "~/lib/utils";
5import { Button } from "../ui/button";
6import { SelectTrigger } from "../ui/select";
7
8const 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
11export 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
27export 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
45export 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
56export 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}
apps/web/src/components/chat/DraftHeroHeadline.tsx+3-3
......@@ -101,11 +101,11 @@ export function DraftHeroHeadline({
101101 <Menu>
102102 <MenuTrigger
103103 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"
105105 >
106106 {activeProjectDisplayName ?? "Choose a project"}
107107 </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">
109109 <MenuRadioGroup
110110 value={activeProjectKey}
111111 onValueChange={(value) => {
......@@ -138,7 +138,7 @@ export function DraftHeroHeadline({
138138 <button
139139 type="button"
140140 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"
142142 >
143143 {activeProjectTitle ?? "Add a project"}
144144 </button>
apps/web/src/components/chat/ModelListRow.tsx+1-1
......@@ -50,7 +50,7 @@ export const ModelListRow = memo(function ModelListRow(props: {
5050 disabled={Boolean(props.disabledReason)}
5151 contentClassName="flex w-full items-center gap-3"
5252 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]",
5454 "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))]",
5555 props.disabledReason &&
5656 "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent",
apps/web/src/components/chat/ModelPickerContent.tsx+16-10
......@@ -43,6 +43,10 @@ type ModelPickerItem = {
4343
4444const EMPTY_MODEL_JUMP_LABELS = new Map<string, string>();
4545
46function ModelListSeparator() {
47 return <div className="h-0.5" />;
48}
49
4650// Split a `${instanceId}:${slug}` combobox key back into its pieces. Slugs
4751// can contain colons (e.g. some vendor model ids), so we only split on the
4852// first colon — anything after that is the slug.
......@@ -521,7 +525,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
521525 return (
522526 <TooltipProvider delay={0}>
523527 <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))]"
525529 data-model-picker-content="true"
526530 >
527531 {/* Sidebar */}
......@@ -571,12 +575,12 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
571575 <div
572576 className={cn(
573577 "flex min-h-0 flex-1 flex-col overflow-hidden bg-muted/40",
574 showSidebar && "border-l",
578 showSidebar && "border-l border-border/70",
575579 )}
576580 >
577581 {/* 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">
580584 <ComboboxInput
581585 ref={searchInputRef}
582586 className="[&_input]:h-6.5 [&_input]:font-sans [&_input]:leading-6.5"
......@@ -584,7 +588,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
584588 placeholder="Search models..."
585589 showTrigger={false}
586590 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" />
588592 }
589593 value={searchQuery}
590594 onChange={(e) => setSearchQuery(e.target.value)}
......@@ -618,8 +622,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
618622 </div>
619623
620624 {/* 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">
623627 <LegendList<string>
624628 ref={modelListRef}
625629 data={filteredModelKeys}
......@@ -656,12 +660,14 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
656660 estimatedItemSize={60}
657661 drawDistance={480}
658662 recycleItems
663 contentContainerClassName="pl-2 pr-px"
664 ItemSeparatorComponent={ModelListSeparator}
659665 onLayout={updateModelListScrollFades}
660666 onScroll={updateModelListScrollFades}
661667 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",
665671 )}
666672 />
667673 </ComboboxListVirtualized>
apps/web/src/components/chat/ModelPickerSidebar.tsx+15-26
......@@ -78,34 +78,20 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
7878 if (!content) {
7979 return;
8080 }
81 const selectedButton = Array.from(
81 const selectedItem = Array.from(
8282 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) {
8585 setSelectedIndicatorTop(null);
8686 return;
8787 }
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);
9789 }, [props.instanceEntries, props.selectedInstanceId, showFavorites]);
9890
9991 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">
10493 <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">
10995 {selectedIndicatorTop !== null ? (
11096 <div
11197 data-model-picker-selected-indicator="true"
......@@ -118,8 +104,8 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
118104 ) : null}
119105 {/* Favorites section */}
120106 {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">
123109 <Tooltip>
124110 <TooltipTrigger
125111 render={
......@@ -129,7 +115,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
129115 )}
130116 onClick={() => handleSelect("favorites")}
131117 type="button"
132 data-model-picker-provider="favorites"
133118 aria-label="Favorites"
134119 >
135120 <StarIcon className="size-5 fill-current shrink-0" aria-hidden />
......@@ -146,7 +131,8 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
146131 </TooltipPopup>
147132 </Tooltip>
148133 </div>
149 </div>
134 <div className="border-b border-border/70" aria-hidden="true" />
135 </>
150136 ) : null}
151137
152138 {/* Instance buttons (one per configured instance — built-in + custom) */}
......@@ -170,7 +156,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
170156
171157 const button = (
172158 <button
173 data-model-picker-provider={entry.instanceId}
174159 className={cn(
175160 "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",
176161 isDisabled && "opacity-50 cursor-not-allowed hover:bg-transparent",
......@@ -228,7 +213,11 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
228213 );
229214
230215 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 >
232221 <Tooltip>
233222 <TooltipTrigger render={trigger} />
234223 <TooltipPopup
apps/web/src/components/chat/ProviderModelPicker.tsx+8-9
......@@ -5,8 +5,7 @@ import {
55} from "@t3tools/contracts";
66import { memo, useEffect, useMemo, useState } from "react";
77import type { VariantProps } from "class-variance-authority";
8import { ChevronDownIcon } from "lucide-react";
9import { Button, buttonVariants } from "../ui/button";
8import { buttonVariants } from "../ui/button";
109import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
1110import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
1211import { cn } from "~/lib/utils";
......@@ -18,6 +17,7 @@ import {
1817 getTriggerDisplayModelName,
1918} from "./providerIconUtils";
2019import type { ProviderInstanceEntry } from "../../providerInstances";
20import { ComposerControl, ComposerControlChevron } from "./ComposerControl";
2121
2222export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
2323 /**
......@@ -146,28 +146,27 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
146146 >
147147 <PopoverTrigger
148148 render={
149 <Button
149 <ComposerControl
150150 aria-label={props.triggerAriaLabel}
151 size="sm"
152151 variant={props.triggerVariant ?? "ghost"}
153152 data-chat-provider-model-picker="true"
154153 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",
157156 props.triggerClassName,
158157 )}
159158 disabled={props.disabled}
160159 />
161160 }
162161 >
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">
164163 {activeEntry ? (
165164 <ProviderInstanceIcon
166165 driverKind={activeEntry.driverKind}
167166 displayName={activeEntry.displayName}
168167 accentColor={activeEntry.accentColor}
169168 showBadge={showInstanceBadge}
170 className={showInstanceBadge ? "size-5" : "size-4"}
169 className="size-4"
171170 iconClassName={cn("size-4", props.activeProviderIconClassName)}
172171 indicatorBackground="var(--input)"
173172 badgeClassName={cn(
......@@ -184,7 +183,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
184183 </Tooltip>
185184 </span>
186185 <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 />
188187 </span>
189188 </PopoverTrigger>
190189 <PopoverPopup
apps/web/src/components/chat/TraitsPicker.tsx+9-9
......@@ -16,8 +16,8 @@ import {
1616} from "@t3tools/shared/model";
1717import { memo, useCallback, useState } from "react";
1818import type { VariantProps } from "class-variance-authority";
19import { ChevronDownIcon, ZapIcon } from "lucide-react";
20import { Button, buttonVariants } from "../ui/button";
19import { ZapIcon } from "lucide-react";
20import { buttonVariants } from "../ui/button";
2121import {
2222 Menu,
2323 MenuGroup,
......@@ -31,6 +31,7 @@ import { useComposerDraftStore, DraftId } from "../../composerDraftStore";
3131import { getProviderModelCapabilities } from "../../providerModels";
3232import { cn } from "~/lib/utils";
3333import { Badge } from "../ui/badge";
34import { ComposerControl, ComposerControlChevron, ComposerControlIcon } from "./ComposerControl";
3435
3536type ProviderOptions = ReadonlyArray<ProviderOptionSelection>;
3637
......@@ -463,7 +464,7 @@ export const TraitsPicker = memo(function TraitsPicker({
463464 });
464465 const fastModeIcon = showFastModeIcon ? (
465466 <>
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" />
467468 <span className="sr-only">Fast mode on</span>
468469 </>
469470 ) : null;
......@@ -479,13 +480,12 @@ export const TraitsPicker = memo(function TraitsPicker({
479480 >
480481 <MenuTrigger
481482 render={
482 <Button
483 size="sm"
483 <ComposerControl
484484 variant={triggerVariant ?? "ghost"}
485485 className={cn(
486486 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",
489489 triggerClassName,
490490 )}
491491 />
......@@ -495,13 +495,13 @@ export const TraitsPicker = memo(function TraitsPicker({
495495 <span className="flex min-w-0 w-full items-center gap-1.5 overflow-hidden">
496496 {fastModeIcon}
497497 <span className="min-w-0 truncate">{triggerLabel}</span>
498 <ChevronDownIcon aria-hidden="true" className="size-3 shrink-0 opacity-60" />
498 <ComposerControlChevron />
499499 </span>
500500 ) : (
501501 <>
502502 {fastModeIcon}
503503 <span>{triggerLabel}</span>
504 <ChevronDownIcon aria-hidden="true" className="size-3 opacity-60" />
504 <ComposerControlChevron />
505505 </>
506506 )}
507507 </MenuTrigger>
apps/web/src/components/clerk/T3ConnectSidebarSignIn.tsx+2-6
......@@ -53,12 +53,8 @@ function ConfiguredT3ConnectSidebarSignIn() {
5353 <>
5454 <SidebarMenu>
5555 <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 />
6258 <span>Sign in to T3 Connect</span>
6359 </SidebarMenuButton>
6460 </SidebarMenuItem>
apps/web/src/components/settings/SettingsSidebarNav.tsx+6-22
......@@ -72,7 +72,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
7272 return (
7373 <>
7474 <SidebarContent className="overflow-x-hidden">
75 <SidebarGroup className="px-2 py-3">
75 <SidebarGroup className="p-2">
7676 <SidebarMenu>
7777 {SETTINGS_NAV_ITEMS.map((item) => {
7878 const Icon = item.icon;
......@@ -80,22 +80,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
8080 return (
8181 <SidebarMenuItem key={item.to}>
8282 <SidebarMenuButton
83 size="sm"
8483 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 }
9084 onClick={() => handleSectionClick(item.to)}
9185 >
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 />
9987 <span className="truncate">{item.label}</span>
10088 </SidebarMenuButton>
10189 </SidebarMenuItem>
......@@ -106,15 +94,11 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
10694 </SidebarContent>
10795 <SidebarFooter className="p-2">
10896 <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">
11199 <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 />
118102 <span>Back</span>
119103 </SidebarMenuButton>
120104 </SidebarMenuItem>
apps/web/src/components/sidebar/SidebarChrome.tsx+2-6
......@@ -113,12 +113,8 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() {
113113 <SidebarUpdatePill />
114114 <SidebarMenu>
115115 <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 />
122118 <span>Settings</span>
123119 </SidebarMenuButton>
124120 </SidebarMenuItem>
apps/web/src/components/ui/command.tsx+6-6
......@@ -65,7 +65,7 @@ function CommandDialogPopup({
6565 <CommandDialogPrimitive.Popup
6666 className={cn(
6767 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",
6969 className,
7070 )}
7171 data-slot="command-dialog-popup"
......@@ -103,16 +103,16 @@ function CommandInput({
103103 wrapperClassName?: string | undefined;
104104}) {
105105 return (
106 <div className={cn("px-2.5 py-1.5", wrapperClassName)}>
106 <div className={cn("px-1.25 py-1.5", wrapperClassName)}>
107107 <AutocompleteInput
108108 autoFocus
109109 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!",
111111 className,
112112 )}
113113 placeholder={placeholder}
114114 size="lg"
115 startAddon={<SearchIcon />}
115 startAddon={<SearchIcon className="translate-x-0.5 text-muted-foreground" />}
116116 {...props}
117117 />
118118 </div>
......@@ -198,7 +198,7 @@ function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
198198 return (
199199 <kbd
200200 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",
202202 className,
203203 )}
204204 data-slot="command-shortcut"
......@@ -211,7 +211,7 @@ function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
211211 return (
212212 <div
213213 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",
215215 className,
216216 )}
217217 data-slot="command-footer"
apps/web/src/components/ui/scroll-area.tsx+2-2
......@@ -56,7 +56,7 @@ function ScrollBar({
5656 return (
5757 <ScrollAreaPrimitive.Scrollbar
5858 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",
6060 className,
6161 )}
6262 data-slot="scroll-area-scrollbar"
......@@ -64,7 +64,7 @@ function ScrollBar({
6464 {...props}
6565 >
6666 <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)]"
6868 data-slot="scroll-area-thumb"
6969 />
7070 </ScrollAreaPrimitive.Scrollbar>
apps/web/src/components/ui/select.tsx+4-2
......@@ -77,8 +77,10 @@ function SelectTrigger({
7777 size = "default",
7878 variant = "default",
7979 children,
80 icon,
8081 ...props
81}: SelectPrimitive.Trigger.Props & VariantProps<typeof selectTriggerVariants>) {
82}: SelectPrimitive.Trigger.Props &
83 VariantProps<typeof selectTriggerVariants> & { icon?: React.ReactNode }) {
8284 return (
8385 <SelectPrimitive.Trigger
8486 className={cn(selectTriggerVariants({ size, variant }), className)}
......@@ -87,7 +89,7 @@ function SelectTrigger({
8789 >
8890 {children}
8991 <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" />}
9193 </SelectPrimitive.Icon>
9294 </SelectPrimitive.Trigger>
9395 );
apps/web/src/components/ui/sidebar.test.tsx+23-1
......@@ -50,13 +50,35 @@ describe("sidebar interactive cursors", () => {
5050 expect(html).toContain("size-[var(--workspace-titlebar-control-size)]!");
5151 });
5252
53 it("uses a pointer cursor for menu buttons by default", () => {
53 it("uses shared geometry and icon constraints for menu buttons by default", () => {
5454 const html = renderSidebarButton();
5555
5656 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");
5763 expect(html).toContain("cursor-pointer");
5864 });
5965
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
6082 it("lets project drag handles override the default pointer cursor", () => {
6183 const html = renderSidebarButton("cursor-grab");
6284
apps/web/src/components/ui/sidebar.tsx+28-19
......@@ -684,19 +684,28 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof S
684684 );
685685}
686686
687function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
687function SidebarContent({
688 className,
689 fixedHeader,
690 ...props
691}: React.ComponentProps<"div"> & {
692 fixedHeader?: React.ReactNode;
693}) {
688694 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 </>
700709 );
701710}
702711
......@@ -783,7 +792,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
783792}
784793
785794const 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",
787796 {
788797 defaultVariants: {
789798 size: "default",
......@@ -791,14 +800,14 @@ const sidebarMenuButtonVariants = cva(
791800 },
792801 variants: {
793802 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",
797807 },
798808 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",
802811 },
803812 },
804813 },
apps/web/src/contextMenuFallback.ts+2-2
......@@ -166,9 +166,9 @@ export function showContextMenuFallback<T extends string>(
166166
167167 const menu = document.createElement("div");
168168 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";
170170 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;";
172172 menu.style.left = `${preferredLeft}px`;
173173 menu.style.top = `${preferredTop}px`;
174174 menu.dataset.level = String(level);
apps/web/src/index.css+52-34
......@@ -77,6 +77,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
7777
7878:root {
7979 --app-scrollbar-width: 6px;
80 --app-scrollbar-thumb: rgb(217 217 217);
81 --app-scrollbar-thumb-hover: rgb(191 191 191);
8082 --glass-blur: 12px;
8183 --glass-opacity: 80%;
8284 --glass-saturation: 1.14;
......@@ -91,6 +93,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
9193}
9294
9395.dark {
96 --app-scrollbar-thumb: rgb(255 255 255 / 8%);
97 --app-scrollbar-thumb-hover: rgb(255 255 255 / 12%);
9498 --glass-blur: 16px;
9599 --glass-saturation: 1.08;
96100}
......@@ -327,12 +331,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
327331 rgb(0 0 0 / 96%) 82%,
328332 black 100%
329333 ),
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;
332336 -webkit-mask-repeat: no-repeat;
333337 -webkit-mask-size:
334338 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%;
336341 mask-image:
337342 linear-gradient(
338343 to bottom,
......@@ -344,12 +349,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
344349 rgb(0 0 0 / 96%) 82%,
345350 black 100%
346351 ),
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;
349354 mask-repeat: no-repeat;
350355 mask-size:
351356 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%;
353359 }
354360
355361 .workspace-titlebar-controls {
......@@ -1035,20 +1041,54 @@ code {
10351041}
10361042
10371043::-webkit-scrollbar-thumb {
1038 background: rgba(0, 0, 0, 0.15);
1044 background: var(--app-scrollbar-thumb);
10391045 border-radius: 3px;
10401046}
10411047
10421048::-webkit-scrollbar-thumb:hover {
1043 background: rgba(0, 0, 0, 0.25);
1049 background: var(--app-scrollbar-thumb-hover);
10441050}
10451051
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;
10481054}
10491055
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 );
10521092}
10531093
10541094.turn-chip-strip {
......@@ -1569,28 +1609,6 @@ label:has(> select#reasoning-effort) select {
15691609 animation: ultrathink-rainbow 10s linear infinite;
15701610}
15711611
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
15941612/* Composer chips are non-editable decorators, so the browser skips them when
15951613 painting text selection; this overlay stands in for the native highlight. */
15961614.composer-inline-chip[data-composer-chip-selected]::after {
apps/web/src/routes/settings.tsx+2-2
......@@ -74,11 +74,11 @@ function SettingsContentLayout() {
7474 {!isElectron && (
7575 <header
7676 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",
7878 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
7979 )}
8080 >
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">
8282 <span className="text-sm font-medium text-foreground">Settings</span>
8383 {showRestoreDefaults ? (
8484 <div className="ms-auto flex items-center gap-2">