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) {...@@ -41,7 +41,7 @@ export function CommandPaletteResults(props: CommandPaletteResultsProps) {
41 <CommandList>41 <CommandList>
42 {props.groups.map((group) => (42 {props.groups.map((group) => (
43 <CommandGroup items={group.items} key={group.value}>43 <CommandGroup items={group.items} key={group.value}>
44 <CommandGroupLabel>{group.label}</CommandGroupLabel>44 <CommandGroupLabel className="ps-[9px]">{group.label}</CommandGroupLabel>
45 <CommandCollection>45 <CommandCollection>
46 {(item) =>46 {(item) =>
47 item.disabled ? (47 item.disabled ? (
...@@ -133,13 +133,13 @@ function CommandPaletteResultRow(props: {...@@ -133,13 +133,13 @@ function CommandPaletteResultRow(props: {
133 )}133 )}
134 {props.item.titleTrailingContent}134 {props.item.titleTrailingContent}
135 {props.item.timestamp ? (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 {props.item.timestamp}137 {props.item.timestamp}
138 </span>138 </span>
139 ) : null}139 ) : null}
140 {shortcutLabel ? <CommandShortcut>{shortcutLabel}</CommandShortcut> : null}140 {shortcutLabel ? <CommandShortcut>{shortcutLabel}</CommandShortcut> : null}
141 {props.item.kind === "submenu" ? (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 ) : null}143 ) : null}
144 </CommandItem>144 </CommandItem>
145 );145 );
apps/web/src/components/ProjectFavicon.tsx+7-2
...@@ -4,6 +4,7 @@ import { FolderIcon } from "lucide-react";...@@ -4,6 +4,7 @@ import { FolderIcon } from "lucide-react";
4import type { ComponentType } from "react";4import type { ComponentType } from "react";
5import { useState } from "react";5import { useState } from "react";
6import { useAssetUrl } from "../assets/assetUrls";6import { useAssetUrl } from "../assets/assetUrls";
7import { cn } from "~/lib/utils";
78
8const loadedProjectFaviconSrcs = new Set<string>();9const loadedProjectFaviconSrcs = new Set<string>();
910
...@@ -40,7 +41,7 @@ function ProjectFaviconFallback({...@@ -40,7 +41,7 @@ function ProjectFaviconFallback({
40 readonly className?: string | undefined;41 readonly className?: string | undefined;
41 readonly icon: ComponentType<{ className?: string }>;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}
4546
46function ProjectFaviconImage({47function ProjectFaviconImage({
...@@ -64,7 +65,11 @@ function ProjectFaviconImage({...@@ -64,7 +65,11 @@ function ProjectFaviconImage({
64 <img65 <img
65 src={src}66 src={src}
66 alt=""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 onLoad={() => {73 onLoad={() => {
69 loadedProjectFaviconSrcs.add(src);74 loadedProjectFaviconSrcs.add(src);
70 setStatus("loaded");75 setStatus("loaded");
apps/web/src/components/Sidebar.tsx+29-27
...@@ -2218,9 +2218,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2218,9 +2218,8 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2218 <div className="group/project-header relative">2218 <div className="group/project-header relative">
2219 <SidebarMenuButton2219 <SidebarMenuButton
2220 ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined}2220 ref={isManualProjectSorting ? dragHandleProps?.setActivatorNodeRef : undefined}
2221 size="sm"2221 className={`pr-8 group-hover/project-header:bg-sidebar-row-hover group-hover/project-header:text-sidebar-foreground max-sm:pr-14 ${
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 ${2222 isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : ""
2223 isManualProjectSorting ? "cursor-grab active:cursor-grabbing" : "cursor-pointer"
2224 }`}2223 }`}
2225 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}2224 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.attributes : {})}
2226 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.listeners : {})}2225 {...(isManualProjectSorting && dragHandleProps ? dragHandleProps.listeners : {})}
...@@ -2822,30 +2821,33 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(...@@ -2822,30 +2821,33 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
2822 );2821 );
28232822
2824 return (2823 return (
2825 <SidebarContent className="gap-0">2824 <SidebarContent
2826 <SidebarGroup className="px-2 pt-2 pb-1">2825 className="gap-0"
2827 <SidebarMenu>2826 fixedHeader={
2828 <SidebarMenuItem>2827 <SidebarGroup className="px-2 pt-2 pb-1">
2829 <CommandDialogTrigger2828 <SidebarMenu>
2830 render={2829 <SidebarMenuItem>
2831 <SidebarMenuButton2830 <CommandDialogTrigger
2832 size="sm"2831 render={
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"2832 <SidebarMenuButton
2834 data-testid="command-palette-trigger"2833 className="focus-visible:ring-0"
2835 />2834 data-testid="command-palette-trigger"
2836 }2835 />
2837 >2836 }
2838 <SearchIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />2837 >
2839 <span className="flex-1 truncate text-left text-sm font-medium">Search</span>2838 <SearchIcon />
2840 {commandPaletteShortcutLabel ? (2839 <span className="flex-1 truncate">Search</span>
2841 <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px]">2840 {commandPaletteShortcutLabel ? (
2842 {commandPaletteShortcutLabel}2841 <Kbd className="h-4 min-w-0 rounded-sm px-1.5 text-[10px]">
2843 </Kbd>2842 {commandPaletteShortcutLabel}
2844 ) : null}2843 </Kbd>
2845 </CommandDialogTrigger>2844 ) : null}
2846 </SidebarMenuItem>2845 </CommandDialogTrigger>
2847 </SidebarMenu>2846 </SidebarMenuItem>
2848 </SidebarGroup>2847 </SidebarMenu>
2848 </SidebarGroup>
2849 }
2850 >
2849 {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? (2851 {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? (
2850 <SidebarGroup className="px-2 pt-2 pb-0">2852 <SidebarGroup className="px-2 pt-2 pb-0">
2851 <Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8">2853 <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({...@@ -242,38 +242,40 @@ function SidebarV2ThreadTooltip({
242 <TooltipPopup242 <TooltipPopup
243 side="right"243 side="right"
244 align="start"244 align="start"
245 sideOffset={8}245 sideOffset={4}
246 variant="glass"246 variant="glass"
247 className="max-w-80 text-left whitespace-normal"247 className="max-w-80 text-left whitespace-normal"
248 >248 >
249 <div className="flex max-w-80 flex-col gap-2 p-2">249 <div className="flex min-w-0 max-w-80 flex-col gap-2 px-0.5 py-1.5">
250 <div className="whitespace-nowrap text-sm font-medium text-foreground">{thread.title}</div>250 <div className="min-w-0 truncate text-xs leading-none font-medium text-foreground">
251 <div className="grid gap-1.5 text-xs text-muted-foreground">251 {thread.title}
252 </div>
253 <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground">
252 {projectTitle ? (254 {projectTitle ? (
253 <div className="flex min-w-0 items-center gap-2">255 <div className="flex min-w-0 items-center gap-2">
254 <ProjectFavicon256 <ProjectFavicon
255 environmentId={thread.environmentId}257 environmentId={thread.environmentId}
256 cwd={projectCwd ?? ""}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 </div>262 </div>
261 ) : null}263 ) : null}
262 {environmentLabel ? (264 {environmentLabel ? (
263 <div className="flex min-w-0 items-center gap-2">265 <div className="flex min-w-0 items-center gap-2">
264 <ServerIcon className="size-4 shrink-0 stroke-muted-foreground" />266 <ServerIcon className="size-3 shrink-0 stroke-muted-foreground" />
265 <div className="min-w-0 wrap-break-word text-foreground/90">{environmentLabel}</div>267 <div className="min-w-0 truncate text-foreground/75">{environmentLabel}</div>
266 </div>268 </div>
267 ) : null}269 ) : null}
268 {thread.branch ? (270 {thread.branch ? (
269 <div className="flex min-w-0 items-center gap-2">271 <div className="flex min-w-0 items-center gap-2">
270 <GitBranchIcon className="size-4 shrink-0 stroke-muted-foreground" />272 <GitBranchIcon className="size-3 shrink-0 stroke-muted-foreground" />
271 <div className="min-w-0 wrap-break-word text-foreground/90">{thread.branch}</div>273 <div className="min-w-0 truncate text-foreground/75">{thread.branch}</div>
272 </div>274 </div>
273 ) : null}275 ) : null}
274 {branchMismatch ? (276 {branchMismatch ? (
275 <div className="flex min-w-0 items-start gap-2 text-warning">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 <div className="min-w-0 flex-1 wrap-break-word leading-5">279 <div className="min-w-0 flex-1 wrap-break-word leading-5">
278 You're currently checked out on another branch.280 You're currently checked out on another branch.
279 </div>281 </div>
...@@ -284,15 +286,15 @@ function SidebarV2ThreadTooltip({...@@ -284,15 +286,15 @@ function SidebarV2ThreadTooltip({
284 <ProviderInstanceIcon286 <ProviderInstanceIcon
285 driverKind={driverKind}287 driverKind={driverKind}
286 displayName={thread.session?.providerName ?? modelInstanceId}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 </div>292 </div>
291 ) : null}293 ) : null}
292 {thread.session?.lastError ? (294 {thread.session?.lastError ? (
293 <div className="flex min-w-0 items-center gap-2 text-red-600 dark:text-red-400">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" />296 <CircleAlertIcon className="size-3 shrink-0 stroke-current" />
295 <div className="min-w-0 wrap-break-word">{thread.session.lastError}</div>297 <div className="min-w-0 truncate">Error occurred</div>
296 </div>298 </div>
297 ) : null}299 ) : null}
298 </div>300 </div>
...@@ -815,9 +817,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {...@@ -815,9 +817,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
815 type="button"817 type="button"
816 aria-label="Un-settle thread"818 aria-label="Un-settle thread"
817 onClick={handleUnsettleClick}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 </button>823 </button>
822 ) : (824 ) : (
823 <button825 <button
...@@ -917,7 +919,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {...@@ -917,7 +919,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
917 {props.settlementSupported || showSnoozeButton ? (919 {props.settlementSupported || showSnoozeButton ? (
918 <span920 <span
919 className={cn(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 snoozeMenuOpen && "opacity-100",923 snoozeMenuOpen && "opacity-100",
922 )}924 )}
923 >925 >
...@@ -933,9 +935,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {...@@ -933,9 +935,9 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
933 type="button"935 type="button"
934 aria-label="Settle thread"936 aria-label="Settle thread"
935 onClick={handleSettleClick}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 Settle941 Settle
940 </button>942 </button>
941 ) : null}943 ) : null}
...@@ -2220,151 +2222,158 @@ export default function SidebarV2() {...@@ -2220,151 +2222,158 @@ export default function SidebarV2() {
2220 return (2222 return (
2221 <>2223 <>
2222 <SidebarChromeHeader isElectron={isElectron} />2224 <SidebarChromeHeader isElectron={isElectron} />
2223 <SidebarContent className="gap-0">2225 <SidebarContent
2224 <SidebarGroup className="px-2 pb-2 pt-3">2226 className="gap-0"
2225 <div className="flex items-center gap-1">2227 fixedHeader={
2226 <div className="min-w-0 flex-1">2228 <SidebarGroup className="gap-1 p-2">
2227 <CommandDialogTrigger2229 <div className="flex items-center gap-1">
2228 render={2230 <div className="min-w-0 flex-1">
2229 <SidebarMenuButton2231 <CommandDialogTrigger
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
2250 render={2232 render={
2251 <SidebarMenuButton2233 <SidebarMenuButton
2252 size="sm"
2253 type="button"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"2235 aria-label="Search threads and commands"
2255 onClick={handleNewThreadClick}2236 className="focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"
2256 disabled={projects.length === 0}2237 data-testid="command-palette-trigger"
2257 aria-label="New thread"
2258 />2238 />
2259 }2239 }
2260 >2240 >
2261 <SquarePenIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />2241 <SearchIcon />
2262 <span2242 <div className="flex-1 truncate text-left">Search</div>
2263 className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden"2243 {commandPaletteShortcutLabel ? (
2264 aria-hidden="true"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">
2265 />2245 {commandPaletteShortcutLabel}
2266 </TooltipTrigger>2246 </Kbd>
2267 <TooltipPopup side="right">2247 ) : null}
2268 {newThreadShortcutLabel ? `New thread (${newThreadShortcutLabel})` : "New thread"}2248 </CommandDialogTrigger>
2269 </TooltipPopup>2249 </div>
2270 </Tooltip>2250 <div className="shrink-0">
2271 </div>2251 <Tooltip>
2272 </div>2252 <TooltipTrigger
2273 </SidebarGroup>2253 render={
2274 {projectGroups.length > 0 ? (2254 <SidebarMenuButton
2275 <SidebarGroup className="px-2 pb-2 pt-0">2255 size="icon"
2276 <div className="flex items-center gap-1">2256 type="button"
2277 <Menu open={projectScopeMenuOpen} onOpenChange={setProjectScopeMenuOpen}>2257 className="relative focus-visible:ring-offset-2 focus-visible:ring-offset-sidebar"
2278 <MenuTrigger2258 onClick={handleNewThreadClick}
2279 aria-label="Filter threads by project"2259 disabled={projects.length === 0}
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"2260 aria-label="New thread"
2281 >2261 />
2282 {scopedProjectGroup ? (2262 }
2283 <ProjectFavicon2263 >
2284 environmentId={scopedProjectGroup.environmentId}2264 <SquarePenIcon />
2285 cwd={scopedProjectGroup.workspaceRoot}2265 <span
2286 className="size-4 shrink-0"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 ) : (2269 </TooltipTrigger>
2289 <FolderIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />2270 <TooltipPopup side="right">
2290 )}2271 {newThreadShortcutLabel
2291 <span className="min-w-0 flex-1 truncate">2272 ? `New thread (${newThreadShortcutLabel})`
2292 {scopedProjectGroup?.displayName ?? "All projects"}2273 : "New thread"}
2293 </span>2274 </TooltipPopup>
2294 <ChevronDownIcon className="size-4 shrink-0 text-sidebar-muted-foreground/70" />2275 </Tooltip>
2295 </MenuTrigger>2276 </div>
2296 <MenuPopup align="start" className="w-(--anchor-width)">2277 </div>
2297 <MenuRadioGroup2278 {projectGroups.length > 0 ? (
2298 value={projectScopeKey ?? "all"}2279 <div className="flex items-center gap-1">
2299 onValueChange={(value) =>2280 <Menu open={projectScopeMenuOpen} onOpenChange={setProjectScopeMenuOpen}>
2300 setProjectScopeKey(value === "all" ? null : (value as string))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 <MenuRadioItem2289 {scopedProjectGroup ? (
2304 value="all"2290 <ProjectFavicon
2305 closeOnClick2291 environmentId={scopedProjectGroup.environmentId}
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"2292 cwd={scopedProjectGroup.workspaceRoot}
2307 >2293 className="size-4 shrink-0"
2294 />
2295 ) : (
2308 <FolderIcon className="size-4 shrink-0" />2296 <FolderIcon className="size-4 shrink-0" />
2309 <span className="min-w-0 truncate text-sm">All projects</span>2297 )}
2310 </MenuRadioItem>2298 <span className="min-w-0 flex-1 truncate">
2311 {projectGroups.map((project) => {2299 {scopedProjectGroup?.displayName ?? "All projects"}
2312 const scopeKey = project.projectKey;2300 </span>
2313 return (2301 <ChevronDownIcon className="-mr-px size-4 shrink-0" />
2314 <MenuRadioItem2302 </MenuTrigger>
2315 key={scopeKey}2303 <MenuPopup align="start" className="w-(--anchor-width)">
2316 value={scopeKey}2304 <MenuRadioGroup
2317 closeOnClick2305 value={projectScopeKey ?? "all"}
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"2306 onValueChange={(value) =>
2319 >2307 setProjectScopeKey(value === "all" ? null : (value as string))
2320 <ProjectFavicon2308 }
2321 environmentId={project.environmentId}2309 >
2322 cwd={project.workspaceRoot}2310 <MenuRadioItem
2323 className="size-4 shrink-0"2311 value="all"
2324 />2312 closeOnClick
2325 <span className="min-w-0 truncate text-sm">{project.displayName}</span>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"
2326 <button2314 >
2327 type="button"2315 <FolderIcon className="size-4 shrink-0" />
2328 aria-label={`Project actions for ${project.displayName}`}2316 <span className="min-w-0 truncate text-sm">All projects</span>
2329 title={`Project actions for ${project.displayName}`}2317 </MenuRadioItem>
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"2318 {projectGroups.map((project) => {
2331 onPointerDown={(event) => event.stopPropagation()}2319 const scopeKey = project.projectKey;
2332 onClick={(event) => {2320 return (
2333 void handleProjectActions(event, project);2321 <MenuRadioItem
2334 }}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" />2327 <ProjectFavicon
2337 </button>2328 environmentId={project.environmentId}
2338 </MenuRadioItem>2329 cwd={project.workspaceRoot}
2339 );2330 className="size-4 shrink-0"
2340 })}2331 />
2341 </MenuRadioGroup>2332 <span className="min-w-0 truncate text-sm">{project.displayName}</span>
2342 </MenuPopup>2333 <button
2343 </Menu>2334 type="button"
2344 <Tooltip>2335 aria-label={`Project actions for ${project.displayName}`}
2345 <TooltipTrigger2336 title={`Project actions for ${project.displayName}`}
2346 render={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"
2347 <SidebarMenuButton2338 onPointerDown={(event) => event.stopPropagation()}
2348 size="sm"2339 onClick={(event) => {
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"2340 void handleProjectActions(event, project);
2350 onClick={openAddProjectCommandPalette}2341 }}
2351 type="button"2342 >
2352 aria-label="New project"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 }2368 </TooltipTrigger>
2355 >2369 <TooltipPopup side="right">New project</TooltipPopup>
2356 <FolderPlusIcon className="size-4 shrink-0 text-sidebar-muted-foreground/80" />2370 </Tooltip>
2357 <span2371 </div>
2358 className="pointer-events-none absolute left-1/2 top-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden"2372 ) : null}
2359 aria-hidden="true"
2360 />
2361 </TooltipTrigger>
2362 <TooltipPopup side="right">New project</TooltipPopup>
2363 </Tooltip>
2364 </div>
2365 </SidebarGroup>2373 </SidebarGroup>
2366 ) : null}2374 }
2367 <SidebarGroup className="min-h-0 flex-1 overflow-y-auto px-2 py-1 [scrollbar-gutter:stable]">2375 >
2376 <SidebarGroup className="px-2 pb-1 pt-0">
2368 <TooltipProvider2377 <TooltipProvider
2369 key="sidebar-thread-tooltips-150"2378 key="sidebar-thread-tooltips-150"
2370 delay={150}2379 delay={150}
...@@ -2526,12 +2535,10 @@ export default function SidebarV2() {...@@ -2526,12 +2535,10 @@ export default function SidebarV2() {
2526 <button2535 <button
2527 type="button"2536 type="button"
2528 onClick={showMoreSettled}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 Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more2541 Show {Math.min(hiddenSettledCount, SETTLED_TAIL_PAGE_COUNT)} more
2532 <span className="text-muted-foreground/50">
2533 ({hiddenSettledCount} settled hidden)
2534 </span>
2535 </button>2542 </button>
2536 </li>2543 </li>
2537 ) : null}2544 ) : null}
...@@ -2547,7 +2554,7 @@ export default function SidebarV2() {...@@ -2547,7 +2554,7 @@ export default function SidebarV2() {
2547 onClick={openAddProjectCommandPalette}2554 onClick={openAddProjectCommandPalette}
2548 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"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 Add project2558 Add project
2552 </button>2559 </button>
2553 </>2560 </>
...@@ -2567,48 +2574,52 @@ export default function SidebarV2() {...@@ -2567,48 +2574,52 @@ export default function SidebarV2() {
2567 }}2574 }}
2568 >2575 >
2569 <DialogPopup className="max-w-xl">2576 <DialogPopup className="max-w-xl">
2570 <DialogHeader className="gap-1.5">2577 <DialogHeader className="gap-3 pb-1!">
2571 <DialogTitle className="text-balance">Project settings</DialogTitle>2578 <DialogTitle className="text-balance">Project settings</DialogTitle>
2572 <DialogDescription>2579 <DialogDescription className="sr-only">
2573 {projectActionsTarget && projectActionsTarget.memberProjects.length > 12580 Manage project names, grouping rules, and environments.
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.`}
2576 </DialogDescription>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 </DialogHeader>2610 </DialogHeader>
2578 <DialogPanel className="p-0">2611 <DialogPanel className="p-0">
2579 <div className="divide-y divide-border/60">2612 <div className="divide-y divide-border/60">
2580 {projectActionsTarget?.memberProjects.map((member) => (2613 {projectActionsTarget?.memberProjects.map((member) => (
2581 <section2614 <section
2582 key={member.physicalProjectKey}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">2618 <div className="grid gap-4 sm:grid-cols-2 sm: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">
2607 <label className="grid min-w-0 gap-1.5">2619 <label className="grid min-w-0 gap-1.5">
2608 <span className="font-medium text-foreground">Project name</span>2620 <span className="font-medium text-foreground">Project name</span>
2609 <Input2621 <Input
2610 key={`${member.physicalProjectKey}:${member.title}`}2622 key={`${member.physicalProjectKey}:${member.title}`}
2611 size="sm"
2612 aria-label={`Project name in ${member.environmentLabel ?? "current environment"}`}2623 aria-label={`Project name in ${member.environmentLabel ?? "current environment"}`}
2613 defaultValue={member.title}2624 defaultValue={member.title}
2614 onBlur={(event) => {2625 onBlur={(event) => {
...@@ -2639,8 +2650,7 @@ export default function SidebarV2() {...@@ -2639,8 +2650,7 @@ export default function SidebarV2() {
2639 }}2650 }}
2640 >2651 >
2641 <SelectTrigger2652 <SelectTrigger
2642 size="sm"2653 className="w-full sm:min-h-7.5"
2643 className="w-full"
2644 aria-label={`Grouping rule for ${member.environmentLabel ?? "current environment"}`}2654 aria-label={`Grouping rule for ${member.environmentLabel ?? "current environment"}`}
2645 >2655 >
2646 <SelectValue>2656 <SelectValue>
...@@ -2672,32 +2682,23 @@ export default function SidebarV2() {...@@ -2672,32 +2682,23 @@ export default function SidebarV2() {
2672 </Select>2682 </Select>
2673 </label>2683 </label>
2674 </div>2684 </div>
2675 <div className="flex flex-wrap items-center gap-2 sm:pl-7">2685 {projectActionsTarget.memberProjects.length > 1 ? (
2676 <Button2686 <div className="flex justify-end">
2677 size="sm"2687 <Button
2678 variant="ghost"2688 size="sm"
2679 onClick={() =>2689 variant="ghost"
2680 copyProjectPath(member.workspaceRoot, { path: member.workspaceRoot })2690 className="text-destructive-foreground hover:bg-destructive/8 hover:text-destructive-foreground"
2681 }2691 onClick={() => {
2682 >2692 const projectGroup = projectActionsTarget;
2683 <CopyIcon />2693 setProjectActionsTarget(null);
2684 Copy path2694 void handleRemoveProjectMembers(projectGroup, [member]);
2685 </Button>2695 }}
2686 <Button2696 >
2687 size="sm"2697 <Trash2Icon />
2688 variant="ghost"2698 Remove project
2689 className="text-destructive-foreground hover:bg-destructive/8 hover:text-destructive-foreground sm:ml-auto"2699 </Button>
2690 onClick={() => {2700 </div>
2691 const projectGroup = projectActionsTarget;2701 ) : null}
2692 if (!projectGroup) return;
2693 setProjectActionsTarget(null);
2694 void handleRemoveProjectMembers(projectGroup, [member]);
2695 }}
2696 >
2697 <Trash2Icon />
2698 Remove
2699 </Button>
2700 </div>
2701 </section>2702 </section>
2702 ))}2703 ))}
2703 </div>2704 </div>
...@@ -2727,8 +2728,26 @@ export default function SidebarV2() {...@@ -2727,8 +2728,26 @@ export default function SidebarV2() {
2727 </div>2728 </div>
2728 ) : null}2729 ) : null}
2729 </DialogPanel>2730 </DialogPanel>
2730 <DialogFooter variant="bare">2731 <DialogFooter
2731 <Button onClick={() => setProjectActionsTarget(null)}>Done</Button>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 </DialogFooter>2751 </DialogFooter>
2733 </DialogPopup>2752 </DialogPopup>
2734 </Dialog>2753 </Dialog>
apps/web/src/components/chat/ChatComposer.tsx+12-21
...@@ -95,6 +95,7 @@ import { ComposerPrimaryActions } from "./ComposerPrimaryActions";...@@ -95,6 +95,7 @@ import { ComposerPrimaryActions } from "./ComposerPrimaryActions";
95import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel";95import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel";
96import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";96import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";
97import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner";97import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner";
98import { ComposerControl, ComposerControlIcon, ComposerSelectControl } from "./ComposerControl";
98import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight";99import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight";
99import { searchSlashCommandItems } from "./composerSlashCommandSearch";100import { searchSlashCommandItems } from "./composerSlashCommandSearch";
100import {101import {
...@@ -167,7 +168,7 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children:...@@ -167,7 +168,7 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children:
167 );168 );
168}169}
169import { Button } from "../ui/button";170import { Button } from "../ui/button";
170import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";171import { Select, SelectItem, SelectPopup, SelectValue } from "../ui/select";
171import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";172import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
172import { toastManager } from "../ui/toast";173import { toastManager } from "../ui/toast";
173import {174import {
...@@ -303,15 +304,13 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop...@@ -303,15 +304,13 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
303 <Tooltip>304 <Tooltip>
304 <TooltipTrigger305 <TooltipTrigger
305 render={306 render={
306 <Button307 <ComposerControl
307 variant="ghost"
308 className={cn(308 className={cn(
309 "shrink-0 whitespace-nowrap px-2 sm:px-3",309 "shrink-0 whitespace-nowrap",
310 props.interactionMode === "plan"310 props.interactionMode === "plan"
311 ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300"311 ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300"
312 : "text-muted-foreground/70 hover:text-foreground/80",312 : "text-muted-foreground/70 hover:text-foreground/80",
313 )}313 )}
314 size="sm"
315 type="button"314 type="button"
316 onClick={props.onToggleInteractionMode}315 onClick={props.onToggleInteractionMode}
317 aria-label={interactionModeTooltip}316 aria-label={interactionModeTooltip}
...@@ -319,9 +318,9 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop...@@ -319,9 +318,9 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
319 }318 }
320 >319 >
321 {props.interactionMode === "plan" ? (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 <span className="sr-only sm:not-sr-only">325 <span className="sr-only sm:not-sr-only">
327 {props.interactionMode === "plan" ? "Plan" : "Build"}326 {props.interactionMode === "plan" ? "Plan" : "Build"}
...@@ -342,16 +341,9 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop...@@ -342,16 +341,9 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
342 onValueChange={(value) => props.onRuntimeModeChange(value!)}341 onValueChange={(value) => props.onRuntimeModeChange(value!)}
343 >342 >
344 <TooltipTrigger343 <TooltipTrigger
345 render={344 render={<ComposerSelectControl className="font-medium" aria-label="Runtime mode" />}
346 <SelectTrigger
347 variant="ghost"
348 size="sm"
349 className="font-medium"
350 aria-label="Runtime mode"
351 />
352 }
353 >345 >
354 <RuntimeModeIcon className="size-4" />346 <ComposerControlIcon icon={RuntimeModeIcon} />
355 <SelectValue>{runtimeModeOption.label}</SelectValue>347 <SelectValue>{runtimeModeOption.label}</SelectValue>
356 </TooltipTrigger>348 </TooltipTrigger>
357 <SelectPopup alignItemWithTrigger={false}>349 <SelectPopup alignItemWithTrigger={false}>
...@@ -387,22 +379,21 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop...@@ -387,22 +379,21 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
387 <Tooltip>379 <Tooltip>
388 <TooltipTrigger380 <TooltipTrigger
389 render={381 render={
390 <Button382 <ComposerControl
391 variant="ghost"
392 className={cn(383 className={cn(
393 "shrink-0 whitespace-nowrap px-2 sm:px-3",384 "shrink-0 whitespace-nowrap",
394 props.planSidebarOpen385 props.planSidebarOpen
395 ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300"386 ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300"
396 : "text-muted-foreground/70 hover:text-foreground/80",387 : "text-muted-foreground/70 hover:text-foreground/80",
397 )}388 )}
398 size="sm"
399 type="button"389 type="button"
400 onClick={props.onTogglePlanSidebar}390 onClick={props.onTogglePlanSidebar}
401 aria-label={planSidebarTooltip}391 aria-label={planSidebarTooltip}
402 />392 />
403 }393 }
404 >394 >
405 <ListTodoIcon395 <ComposerControlIcon
396 icon={ListTodoIcon}
406 className={props.planSidebarOpen ? "text-current opacity-100" : undefined}397 className={props.planSidebarOpen ? "text-current opacity-100" : undefined}
407 />398 />
408 <span className="sr-only sm:not-sr-only">{props.planSidebarLabel}</span>399 <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...@@ -93,7 +93,7 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
93 {showCollapsedStackCap ? (93 {showCollapsedStackCap ? (
94 <div94 <div
95 className={cn(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 "border border-b-0 border-warning/24 bg-background/96 shadow-[0_6px_18px_rgba(0,0,0,0.06)]",97 "border border-b-0 border-warning/24 bg-background/96 shadow-[0_6px_18px_rgba(0,0,0,0.06)]",
98 "transition-opacity duration-150 ease-out",98 "transition-opacity duration-150 ease-out",
99 "group-hover/banner-stack:opacity-0 group-focus-within/banner-stack:opacity-0",99 "group-hover/banner-stack:opacity-0 group-focus-within/banner-stack:opacity-0",
...@@ -174,7 +174,7 @@ function ComposerBannerStackAlert({...@@ -174,7 +174,7 @@ function ComposerBannerStackAlert({
174 return (174 return (
175 <Alert175 <Alert
176 variant={item.variant}176 variant={item.variant}
177 className={cn("alert-glass", item.className)}177 className={cn("alert-glass rounded-[22px]", item.className)}
178 data-variant={item.variant}178 data-variant={item.variant}
179 >179 >
180 {item.icon}180 {item.icon}
apps/web/src/components/chat/ComposerCommandMenu.tsx+4-1
...@@ -139,7 +139,10 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {...@@ -139,7 +139,10 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {
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 {props.items.length > 0 ? (146 {props.items.length > 0 ? (
144 <CommandList className="max-h-72">147 <CommandList className="max-h-72">
145 {groups.map((group, groupIndex) => (148 {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({...@@ -101,11 +101,11 @@ export function DraftHeroHeadline({
101 <Menu>101 <Menu>
102 <MenuTrigger102 <MenuTrigger
103 aria-label={hasResolvedProject ? "Change project" : "Choose a project"}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 {activeProjectDisplayName ?? "Choose a project"}106 {activeProjectDisplayName ?? "Choose a project"}
107 </MenuTrigger>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 <MenuRadioGroup109 <MenuRadioGroup
110 value={activeProjectKey}110 value={activeProjectKey}
111 onValueChange={(value) => {111 onValueChange={(value) => {
...@@ -138,7 +138,7 @@ export function DraftHeroHeadline({...@@ -138,7 +138,7 @@ export function DraftHeroHeadline({
138 <button138 <button
139 type="button"139 type="button"
140 onClick={openAddProject}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 {activeProjectTitle ?? "Add a project"}143 {activeProjectTitle ?? "Add a project"}
144 </button>144 </button>
apps/web/src/components/chat/ModelListRow.tsx+1-1
...@@ -50,7 +50,7 @@ export const ModelListRow = memo(function ModelListRow(props: {...@@ -50,7 +50,7 @@ export const ModelListRow = memo(function ModelListRow(props: {
50 disabled={Boolean(props.disabledReason)}50 disabled={Boolean(props.disabledReason)}
51 contentClassName="flex w-full items-center gap-3"51 contentClassName="flex w-full items-center gap-3"
52 className={cn(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 "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))]",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 props.disabledReason &&55 props.disabledReason &&
56 "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent",56 "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 = {...@@ -43,6 +43,10 @@ type ModelPickerItem = {
4343
44const EMPTY_MODEL_JUMP_LABELS = new Map<string, string>();44const EMPTY_MODEL_JUMP_LABELS = new Map<string, string>();
4545
46function ModelListSeparator() {
47 return <div className="h-0.5" />;
48}
49
46// Split a `${instanceId}:${slug}` combobox key back into its pieces. Slugs50// Split a `${instanceId}:${slug}` combobox key back into its pieces. Slugs
47// can contain colons (e.g. some vendor model ids), so we only split on the51// can contain colons (e.g. some vendor model ids), so we only split on the
48// first colon — anything after that is the slug.52// first colon — anything after that is the slug.
...@@ -521,7 +525,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -521,7 +525,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
521 return (525 return (
522 <TooltipProvider delay={0}>526 <TooltipProvider delay={0}>
523 <div527 <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 data-model-picker-content="true"529 data-model-picker-content="true"
526 >530 >
527 {/* Sidebar */}531 {/* Sidebar */}
...@@ -571,12 +575,12 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -571,12 +575,12 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
571 <div575 <div
572 className={cn(576 className={cn(
573 "flex min-h-0 flex-1 flex-col overflow-hidden bg-muted/40",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 {/* Search bar */}581 {/* Search bar */}
578 <div className="px-4 pt-2.5">582 <div className="px-2 pt-2">
579 <div className="-translate-y-px border-b border-border/70 pb-2.5 transition-colors focus-within:border-ring">583 <div className="border-b border-border/70 pb-2.5 transition-colors focus-within:border-ring">
580 <ComboboxInput584 <ComboboxInput
581 ref={searchInputRef}585 ref={searchInputRef}
582 className="[&_input]:h-6.5 [&_input]:font-sans [&_input]:leading-6.5"586 className="[&_input]:h-6.5 [&_input]:font-sans [&_input]:leading-6.5"
...@@ -584,7 +588,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -584,7 +588,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
584 placeholder="Search models..."588 placeholder="Search models..."
585 showTrigger={false}589 showTrigger={false}
586 startAddon={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 value={searchQuery}593 value={searchQuery}
590 onChange={(e) => setSearchQuery(e.target.value)}594 onChange={(e) => setSearchQuery(e.target.value)}
...@@ -618,8 +622,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -618,8 +622,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
618 </div>622 </div>
619623
620 {/* Model list */}624 {/* Model list */}
621 <div className="relative min-h-0 flex-1 overflow-hidden">625 <div className="relative min-h-0 flex-1 overflow-hidden pr-px">
622 <ComboboxListVirtualized className="model-picker-list size-full min-w-0 p-0">626 <ComboboxListVirtualized className="size-full min-w-0 p-0 not-empty:p-0">
623 <LegendList<string>627 <LegendList<string>
624 ref={modelListRef}628 ref={modelListRef}
625 data={filteredModelKeys}629 data={filteredModelKeys}
...@@ -656,12 +660,14 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -656,12 +660,14 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
656 estimatedItemSize={60}660 estimatedItemSize={60}
657 drawDistance={480}661 drawDistance={480}
658 recycleItems662 recycleItems
663 contentContainerClassName="pl-2 pr-px"
664 ItemSeparatorComponent={ModelListSeparator}
659 onLayout={updateModelListScrollFades}665 onLayout={updateModelListScrollFades}
660 onScroll={updateModelListScrollFades}666 onScroll={updateModelListScrollFades}
661 className={cn(667 className={cn(
662 "scrollbar-gutter-both h-full overflow-x-hidden overscroll-y-contain py-1.5 [--fade-size:1.5rem]",668 "model-picker-list 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))]",669 showTopScrollFade && "model-picker-list-scroll-fade-top",
664 showBottomScrollFade && "mask-b-from-[calc(100%-var(--fade-size))]",670 showBottomScrollFade && "model-picker-list-scroll-fade-bottom",
665 )}671 )}
666 />672 />
667 </ComboboxListVirtualized>673 </ComboboxListVirtualized>
apps/web/src/components/chat/ModelPickerSidebar.tsx+15-26
...@@ -78,34 +78,20 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {...@@ -78,34 +78,20 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
78 if (!content) {78 if (!content) {
79 return;79 return;
80 }80 }
81 const selectedButton = Array.from(81 const selectedItem = Array.from(
82 content.querySelectorAll<HTMLElement>("[data-model-picker-provider]"),82 content.querySelectorAll<HTMLElement>("[data-model-picker-provider]"),
83 ).find((button) => button.dataset.modelPickerProvider === props.selectedInstanceId);83 ).find((item) => item.dataset.modelPickerProvider === props.selectedInstanceId);
84 if (!selectedButton) {84 if (!selectedItem) {
85 setSelectedIndicatorTop(null);85 setSelectedIndicatorTop(null);
86 return;86 return;
87 }87 }
88 const contentRect = content.getBoundingClientRect();88 setSelectedIndicatorTop(selectedItem.offsetTop + selectedItem.offsetHeight / 2 - 10);
89 const selectedButtonRect = selectedButton.getBoundingClientRect();
90 setSelectedIndicatorTop(
91 selectedButtonRect.top -
92 contentRect.top +
93 content.scrollTop +
94 selectedButtonRect.height / 2 -
95 10,
96 );
97 }, [props.instanceEntries, props.selectedInstanceId, showFavorites]);89 }, [props.instanceEntries, props.selectedInstanceId, showFavorites]);
9890
99 return (91 return (
100 <div92 <div className="w-11 shrink-0 overflow-hidden bg-muted/30" data-model-picker-sidebar="true">
101 className="w-12 shrink-0 overflow-hidden border-r bg-muted/30"
102 data-model-picker-sidebar="true"
103 >
104 <div className="h-full overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">93 <div className="h-full overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
105 <div94 <div ref={sidebarContentRef} className="relative flex min-h-full flex-col gap-1 p-1">
106 ref={sidebarContentRef}
107 className="relative flex min-h-full flex-col gap-1 px-1 pb-1 pt-0.5"
108 >
109 {selectedIndicatorTop !== null ? (95 {selectedIndicatorTop !== null ? (
110 <div96 <div
111 data-model-picker-selected-indicator="true"97 data-model-picker-selected-indicator="true"
...@@ -118,8 +104,8 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {...@@ -118,8 +104,8 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
118 ) : null}104 ) : null}
119 {/* Favorites section */}105 {/* Favorites section */}
120 {showFavorites ? (106 {showFavorites ? (
121 <div className="mb-1 border-b pb-1">107 <>
122 <div className="relative w-full">108 <div className="relative w-full" data-model-picker-provider="favorites">
123 <Tooltip>109 <Tooltip>
124 <TooltipTrigger110 <TooltipTrigger
125 render={111 render={
...@@ -129,7 +115,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {...@@ -129,7 +115,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
129 )}115 )}
130 onClick={() => handleSelect("favorites")}116 onClick={() => handleSelect("favorites")}
131 type="button"117 type="button"
132 data-model-picker-provider="favorites"
133 aria-label="Favorites"118 aria-label="Favorites"
134 >119 >
135 <StarIcon className="size-5 fill-current shrink-0" aria-hidden />120 <StarIcon className="size-5 fill-current shrink-0" aria-hidden />
...@@ -146,7 +131,8 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {...@@ -146,7 +131,8 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
146 </TooltipPopup>131 </TooltipPopup>
147 </Tooltip>132 </Tooltip>
148 </div>133 </div>
149 </div>134 <div className="border-b border-border/70" aria-hidden="true" />
135 </>
150 ) : null}136 ) : null}
151137
152 {/* Instance buttons (one per configured instance — built-in + custom) */}138 {/* Instance buttons (one per configured instance — built-in + custom) */}
...@@ -170,7 +156,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {...@@ -170,7 +156,6 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
170156
171 const button = (157 const button = (
172 <button158 <button
173 data-model-picker-provider={entry.instanceId}
174 className={cn(159 className={cn(
175 "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",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 isDisabled && "opacity-50 cursor-not-allowed hover:bg-transparent",161 isDisabled && "opacity-50 cursor-not-allowed hover:bg-transparent",
...@@ -228,7 +213,11 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {...@@ -228,7 +213,11 @@ export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
228 );213 );
229214
230 return (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 <Tooltip>221 <Tooltip>
233 <TooltipTrigger render={trigger} />222 <TooltipTrigger render={trigger} />
234 <TooltipPopup223 <TooltipPopup
apps/web/src/components/chat/ProviderModelPicker.tsx+8-9
...@@ -5,8 +5,7 @@ import {...@@ -5,8 +5,7 @@ import {
5} from "@t3tools/contracts";5} from "@t3tools/contracts";
6import { memo, useEffect, useMemo, useState } from "react";6import { memo, useEffect, useMemo, useState } from "react";
7import type { VariantProps } from "class-variance-authority";7import type { VariantProps } from "class-variance-authority";
8import { ChevronDownIcon } from "lucide-react";8import { buttonVariants } from "../ui/button";
9import { Button, buttonVariants } from "../ui/button";
10import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";9import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
11import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";10import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
12import { cn } from "~/lib/utils";11import { cn } from "~/lib/utils";
...@@ -18,6 +17,7 @@ import {...@@ -18,6 +17,7 @@ import {
18 getTriggerDisplayModelName,17 getTriggerDisplayModelName,
19} from "./providerIconUtils";18} from "./providerIconUtils";
20import type { ProviderInstanceEntry } from "../../providerInstances";19import type { ProviderInstanceEntry } from "../../providerInstances";
20import { ComposerControl, ComposerControlChevron } from "./ComposerControl";
2121
22export const ProviderModelPicker = memo(function ProviderModelPicker(props: {22export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
23 /**23 /**
...@@ -146,28 +146,27 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {...@@ -146,28 +146,27 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
146 >146 >
147 <PopoverTrigger147 <PopoverTrigger
148 render={148 render={
149 <Button149 <ComposerControl
150 aria-label={props.triggerAriaLabel}150 aria-label={props.triggerAriaLabel}
151 size="sm"
152 variant={props.triggerVariant ?? "ghost"}151 variant={props.triggerVariant ?? "ghost"}
153 data-chat-provider-model-picker="true"152 data-chat-provider-model-picker="true"
154 className={cn(153 className={cn(
155 "min-w-0 justify-between whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80",154 "min-w-0 justify-between whitespace-nowrap",
156 props.compact ? "max-w-42 shrink-0" : "max-w-48 shrink sm:max-w-56 sm:px-3",155 props.compact ? "max-w-42 shrink-0" : "max-w-48 shrink sm:max-w-56",
157 props.triggerClassName,156 props.triggerClassName,
158 )}157 )}
159 disabled={props.disabled}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 {activeEntry ? (163 {activeEntry ? (
165 <ProviderInstanceIcon164 <ProviderInstanceIcon
166 driverKind={activeEntry.driverKind}165 driverKind={activeEntry.driverKind}
167 displayName={activeEntry.displayName}166 displayName={activeEntry.displayName}
168 accentColor={activeEntry.accentColor}167 accentColor={activeEntry.accentColor}
169 showBadge={showInstanceBadge}168 showBadge={showInstanceBadge}
170 className={showInstanceBadge ? "size-5" : "size-4"}169 className="size-4"
171 iconClassName={cn("size-4", props.activeProviderIconClassName)}170 iconClassName={cn("size-4", props.activeProviderIconClassName)}
172 indicatorBackground="var(--input)"171 indicatorBackground="var(--input)"
173 badgeClassName={cn(172 badgeClassName={cn(
...@@ -184,7 +183,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {...@@ -184,7 +183,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
184 </Tooltip>183 </Tooltip>
185 </span>184 </span>
186 <span aria-hidden="true" className="flex items-center">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 </span>187 </span>
189 </PopoverTrigger>188 </PopoverTrigger>
190 <PopoverPopup189 <PopoverPopup
apps/web/src/components/chat/TraitsPicker.tsx+9-9
...@@ -16,8 +16,8 @@ import {...@@ -16,8 +16,8 @@ import {
16} from "@t3tools/shared/model";16} from "@t3tools/shared/model";
17import { memo, useCallback, useState } from "react";17import { memo, useCallback, useState } from "react";
18import type { VariantProps } from "class-variance-authority";18import type { VariantProps } from "class-variance-authority";
19import { ChevronDownIcon, ZapIcon } from "lucide-react";19import { ZapIcon } from "lucide-react";
20import { Button, buttonVariants } from "../ui/button";20import { buttonVariants } from "../ui/button";
21import {21import {
22 Menu,22 Menu,
23 MenuGroup,23 MenuGroup,
...@@ -31,6 +31,7 @@ import { useComposerDraftStore, DraftId } from "../../composerDraftStore";...@@ -31,6 +31,7 @@ import { useComposerDraftStore, DraftId } from "../../composerDraftStore";
31import { getProviderModelCapabilities } from "../../providerModels";31import { getProviderModelCapabilities } from "../../providerModels";
32import { cn } from "~/lib/utils";32import { cn } from "~/lib/utils";
33import { Badge } from "../ui/badge";33import { Badge } from "../ui/badge";
34import { ComposerControl, ComposerControlChevron, ComposerControlIcon } from "./ComposerControl";
3435
35type ProviderOptions = ReadonlyArray<ProviderOptionSelection>;36type ProviderOptions = ReadonlyArray<ProviderOptionSelection>;
3637
...@@ -463,7 +464,7 @@ export const TraitsPicker = memo(function TraitsPicker({...@@ -463,7 +464,7 @@ export const TraitsPicker = memo(function TraitsPicker({
463 });464 });
464 const fastModeIcon = showFastModeIcon ? (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 <span className="sr-only">Fast mode on</span>468 <span className="sr-only">Fast mode on</span>
468 </>469 </>
469 ) : null;470 ) : null;
...@@ -479,13 +480,12 @@ export const TraitsPicker = memo(function TraitsPicker({...@@ -479,13 +480,12 @@ export const TraitsPicker = memo(function TraitsPicker({
479 >480 >
480 <MenuTrigger481 <MenuTrigger
481 render={482 render={
482 <Button483 <ComposerControl
483 size="sm"
484 variant={triggerVariant ?? "ghost"}484 variant={triggerVariant ?? "ghost"}
485 className={cn(485 className={cn(
486 isCodexStyle486 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"487 ? "min-w-0 max-w-40 shrink justify-start overflow-hidden whitespace-nowrap sm:max-w-48"
488 : "shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3",488 : "shrink-0 whitespace-nowrap",
489 triggerClassName,489 triggerClassName,
490 )}490 )}
491 />491 />
...@@ -495,13 +495,13 @@ export const TraitsPicker = memo(function TraitsPicker({...@@ -495,13 +495,13 @@ export const TraitsPicker = memo(function TraitsPicker({
495 <span className="flex min-w-0 w-full items-center gap-1.5 overflow-hidden">495 <span className="flex min-w-0 w-full items-center gap-1.5 overflow-hidden">
496 {fastModeIcon}496 {fastModeIcon}
497 <span className="min-w-0 truncate">{triggerLabel}</span>497 <span className="min-w-0 truncate">{triggerLabel}</span>
498 <ChevronDownIcon aria-hidden="true" className="size-3 shrink-0 opacity-60" />498 <ComposerControlChevron />
499 </span>499 </span>
500 ) : (500 ) : (
501 <>501 <>
502 {fastModeIcon}502 {fastModeIcon}
503 <span>{triggerLabel}</span>503 <span>{triggerLabel}</span>
504 <ChevronDownIcon aria-hidden="true" className="size-3 opacity-60" />504 <ComposerControlChevron />
505 </>505 </>
506 )}506 )}
507 </MenuTrigger>507 </MenuTrigger>
apps/web/src/components/clerk/T3ConnectSidebarSignIn.tsx+2-6
...@@ -53,12 +53,8 @@ function ConfiguredT3ConnectSidebarSignIn() {...@@ -53,12 +53,8 @@ function ConfiguredT3ConnectSidebarSignIn() {
53 <>53 <>
54 <SidebarMenu>54 <SidebarMenu>
55 <SidebarMenuItem>55 <SidebarMenuItem>
56 <SidebarMenuButton56 <SidebarMenuButton onClick={openAuthPrompt}>
57 size="sm"57 <LogInIcon />
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" />
62 <span>Sign in to T3 Connect</span>58 <span>Sign in to T3 Connect</span>
63 </SidebarMenuButton>59 </SidebarMenuButton>
64 </SidebarMenuItem>60 </SidebarMenuItem>
apps/web/src/components/settings/SettingsSidebarNav.tsx+6-22
...@@ -72,7 +72,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -72,7 +72,7 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
72 return (72 return (
73 <>73 <>
74 <SidebarContent className="overflow-x-hidden">74 <SidebarContent className="overflow-x-hidden">
75 <SidebarGroup className="px-2 py-3">75 <SidebarGroup className="p-2">
76 <SidebarMenu>76 <SidebarMenu>
77 {SETTINGS_NAV_ITEMS.map((item) => {77 {SETTINGS_NAV_ITEMS.map((item) => {
78 const Icon = item.icon;78 const Icon = item.icon;
...@@ -80,22 +80,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -80,22 +80,10 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
80 return (80 return (
81 <SidebarMenuItem key={item.to}>81 <SidebarMenuItem key={item.to}>
82 <SidebarMenuButton82 <SidebarMenuButton
83 size="sm"
84 isActive={isActive}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 onClick={() => handleSectionClick(item.to)}84 onClick={() => handleSectionClick(item.to)}
91 >85 >
92 <Icon86 <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 />
99 <span className="truncate">{item.label}</span>87 <span className="truncate">{item.label}</span>
100 </SidebarMenuButton>88 </SidebarMenuButton>
101 </SidebarMenuItem>89 </SidebarMenuItem>
...@@ -106,15 +94,11 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {...@@ -106,15 +94,11 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) {
106 </SidebarContent>94 </SidebarContent>
107 <SidebarFooter className="p-2">95 <SidebarFooter className="p-2">
108 <T3ConnectSidebarSignIn />96 <T3ConnectSidebarSignIn />
109 <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1">97 <div className="flex items-center gap-1">
110 <SidebarMenu className="min-w-0">98 <SidebarMenu className="min-w-0 flex-1">
111 <SidebarMenuItem>99 <SidebarMenuItem>
112 <SidebarMenuButton100 <SidebarMenuButton onClick={handleBackClick}>
113 size="sm"101 <ArrowLeftIcon />
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" />
118 <span>Back</span>102 <span>Back</span>
119 </SidebarMenuButton>103 </SidebarMenuButton>
120 </SidebarMenuItem>104 </SidebarMenuItem>
apps/web/src/components/sidebar/SidebarChrome.tsx+2-6
...@@ -113,12 +113,8 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() {...@@ -113,12 +113,8 @@ export const SidebarChromeFooter = memo(function SidebarChromeFooter() {
113 <SidebarUpdatePill />113 <SidebarUpdatePill />
114 <SidebarMenu>114 <SidebarMenu>
115 <SidebarMenuItem>115 <SidebarMenuItem>
116 <SidebarMenuButton116 <SidebarMenuButton onClick={handleSettingsClick}>
117 size="sm"117 <SettingsIcon />
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" />
122 <span>Settings</span>118 <span>Settings</span>
123 </SidebarMenuButton>119 </SidebarMenuButton>
124 </SidebarMenuItem>120 </SidebarMenuItem>
apps/web/src/components/ui/command.tsx+6-6
...@@ -65,7 +65,7 @@ function CommandDialogPopup({...@@ -65,7 +65,7 @@ function CommandDialogPopup({
65 <CommandDialogPrimitive.Popup65 <CommandDialogPrimitive.Popup
66 className={cn(66 className={cn(
67 DIALOG_POPUP_CLASS,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 className,69 className,
70 )}70 )}
71 data-slot="command-dialog-popup"71 data-slot="command-dialog-popup"
...@@ -103,16 +103,16 @@ function CommandInput({...@@ -103,16 +103,16 @@ function CommandInput({
103 wrapperClassName?: string | undefined;103 wrapperClassName?: string | undefined;
104}) {104}) {
105 return (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 <AutocompleteInput107 <AutocompleteInput
108 autoFocus108 autoFocus
109 className={cn(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 className,111 className,
112 )}112 )}
113 placeholder={placeholder}113 placeholder={placeholder}
114 size="lg"114 size="lg"
115 startAddon={<SearchIcon />}115 startAddon={<SearchIcon className="translate-x-0.5 text-muted-foreground" />}
116 {...props}116 {...props}
117 />117 />
118 </div>118 </div>
...@@ -198,7 +198,7 @@ function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) {...@@ -198,7 +198,7 @@ function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
198 return (198 return (
199 <kbd199 <kbd
200 className={cn(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 className,202 className,
203 )}203 )}
204 data-slot="command-shortcut"204 data-slot="command-shortcut"
...@@ -211,7 +211,7 @@ function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {...@@ -211,7 +211,7 @@ function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
211 return (211 return (
212 <div212 <div
213 className={cn(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 className,215 className,
216 )}216 )}
217 data-slot="command-footer"217 data-slot="command-footer"
apps/web/src/components/ui/scroll-area.tsx+2-2
...@@ -56,7 +56,7 @@ function ScrollBar({...@@ -56,7 +56,7 @@ function ScrollBar({
56 return (56 return (
57 <ScrollAreaPrimitive.Scrollbar57 <ScrollAreaPrimitive.Scrollbar
58 className={cn(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 className,60 className,
61 )}61 )}
62 data-slot="scroll-area-scrollbar"62 data-slot="scroll-area-scrollbar"
...@@ -64,7 +64,7 @@ function ScrollBar({...@@ -64,7 +64,7 @@ function ScrollBar({
64 {...props}64 {...props}
65 >65 >
66 <ScrollAreaPrimitive.Thumb66 <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 data-slot="scroll-area-thumb"68 data-slot="scroll-area-thumb"
69 />69 />
70 </ScrollAreaPrimitive.Scrollbar>70 </ScrollAreaPrimitive.Scrollbar>
apps/web/src/components/ui/select.tsx+4-2
...@@ -77,8 +77,10 @@ function SelectTrigger({...@@ -77,8 +77,10 @@ function SelectTrigger({
77 size = "default",77 size = "default",
78 variant = "default",78 variant = "default",
79 children,79 children,
80 icon,
80 ...props81 ...props
81}: SelectPrimitive.Trigger.Props & VariantProps<typeof selectTriggerVariants>) {82}: SelectPrimitive.Trigger.Props &
83 VariantProps<typeof selectTriggerVariants> & { icon?: React.ReactNode }) {
82 return (84 return (
83 <SelectPrimitive.Trigger85 <SelectPrimitive.Trigger
84 className={cn(selectTriggerVariants({ size, variant }), className)}86 className={cn(selectTriggerVariants({ size, variant }), className)}
...@@ -87,7 +89,7 @@ function SelectTrigger({...@@ -87,7 +89,7 @@ function SelectTrigger({
87 >89 >
88 {children}90 {children}
89 <SelectPrimitive.Icon data-slot="select-icon">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 </SelectPrimitive.Icon>93 </SelectPrimitive.Icon>
92 </SelectPrimitive.Trigger>94 </SelectPrimitive.Trigger>
93 );95 );
apps/web/src/components/ui/sidebar.test.tsx+23-1
...@@ -50,13 +50,35 @@ describe("sidebar interactive cursors", () => {...@@ -50,13 +50,35 @@ describe("sidebar interactive cursors", () => {
50 expect(html).toContain("size-[var(--workspace-titlebar-control-size)]!");50 expect(html).toContain("size-[var(--workspace-titlebar-control-size)]!");
51 });51 });
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", () => {
54 const html = renderSidebarButton();54 const html = renderSidebarButton();
5555
56 expect(html).toContain('data-slot="sidebar-menu-button"');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 expect(html).toContain("cursor-pointer");63 expect(html).toContain("cursor-pointer");
58 });64 });
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
60 it("lets project drag handles override the default pointer cursor", () => {82 it("lets project drag handles override the default pointer cursor", () => {
61 const html = renderSidebarButton("cursor-grab");83 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...@@ -684,19 +684,28 @@ function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof S
684 );684 );
685}685}
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}) {
688 return (694 return (
689 <ScrollArea hideScrollbars scrollFade className="h-auto min-h-0 flex-1">695 <>
690 <div696 {fixedHeader ? <div className="w-full shrink-0">{fixedHeader}</div> : null}
691 className={cn(697 <ScrollArea hideScrollbars scrollFade className="h-auto min-h-0 flex-1">
692 "flex w-full min-w-0 flex-col gap-2 group-data-[collapsible=icon]:overflow-hidden",698 <div
693 className,699 className={cn(
694 )}700 "flex w-full min-w-0 flex-col gap-2 group-data-[collapsible=icon]:overflow-hidden",
695 data-sidebar="content"701 className,
696 data-slot="sidebar-content"702 )}
697 {...props}703 data-sidebar="content"
698 />704 data-slot="sidebar-content"
699 </ScrollArea>705 {...props}
706 />
707 </ScrollArea>
708 </>
700 );709 );
701}710}
702711
...@@ -783,7 +792,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {...@@ -783,7 +792,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
783}792}
784793
785const sidebarMenuButtonVariants = cva(794const 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 defaultVariants: {797 defaultVariants: {
789 size: "default",798 size: "default",
...@@ -791,14 +800,14 @@ const sidebarMenuButtonVariants = cva(...@@ -791,14 +800,14 @@ const sidebarMenuButtonVariants = cva(
791 },800 },
792 variants: {801 variants: {
793 size: {802 size: {
794 default: "h-8 text-sm",803 default: "h-8 rounded-md px-2.5 py-1.5 text-sm",
795 lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",804 icon: "size-8 justify-center rounded-md p-0",
796 sm: "h-7 text-xs",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 variant: {808 variant: {
799 default: "hover:bg-sidebar-row-hover hover:text-sidebar-foreground",809 default: "font-medium text-sidebar-muted-foreground/80",
800 outline:810 outline: "bg-sidebar-control-surface ring-1 ring-sidebar-border",
801 "bg-sidebar-control-surface ring-1 ring-sidebar-border hover:bg-sidebar-row-hover hover:text-sidebar-foreground",
802 },811 },
803 },812 },
804 },813 },
apps/web/src/contextMenuFallback.ts+2-2
...@@ -166,9 +166,9 @@ export function showContextMenuFallback<T extends string>(...@@ -166,9 +166,9 @@ export function showContextMenuFallback<T extends string>(
166166
167 const menu = document.createElement("div");167 const menu = document.createElement("div");
168 menu.className =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 menu.style.cssText =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 menu.style.left = `${preferredLeft}px`;172 menu.style.left = `${preferredLeft}px`;
173 menu.style.top = `${preferredTop}px`;173 menu.style.top = `${preferredTop}px`;
174 menu.dataset.level = String(level);174 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...@@ -77,6 +77,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
7777
78:root {78:root {
79 --app-scrollbar-width: 6px;79 --app-scrollbar-width: 6px;
80 --app-scrollbar-thumb: rgb(217 217 217);
81 --app-scrollbar-thumb-hover: rgb(191 191 191);
80 --glass-blur: 12px;82 --glass-blur: 12px;
81 --glass-opacity: 80%;83 --glass-opacity: 80%;
82 --glass-saturation: 1.14;84 --glass-saturation: 1.14;
...@@ -91,6 +93,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -91,6 +93,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
91}93}
9294
93.dark {95.dark {
96 --app-scrollbar-thumb: rgb(255 255 255 / 8%);
97 --app-scrollbar-thumb-hover: rgb(255 255 255 / 12%);
94 --glass-blur: 16px;98 --glass-blur: 16px;
95 --glass-saturation: 1.08;99 --glass-saturation: 1.08;
96}100}
...@@ -327,12 +331,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -327,12 +331,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
327 rgb(0 0 0 / 96%) 82%,331 rgb(0 0 0 / 96%) 82%,
328 black 100%332 black 100%
329 ),333 ),
330 linear-gradient(black, black);334 linear-gradient(black, black), linear-gradient(black, black);
331 -webkit-mask-position: top, bottom;335 -webkit-mask-position: top, bottom, right;
332 -webkit-mask-repeat: no-repeat;336 -webkit-mask-repeat: no-repeat;
333 -webkit-mask-size:337 -webkit-mask-size:
334 100% var(--topbar-scroll-fade-height),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 mask-image:341 mask-image:
337 linear-gradient(342 linear-gradient(
338 to bottom,343 to bottom,
...@@ -344,12 +349,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -344,12 +349,13 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
344 rgb(0 0 0 / 96%) 82%,349 rgb(0 0 0 / 96%) 82%,
345 black 100%350 black 100%
346 ),351 ),
347 linear-gradient(black, black);352 linear-gradient(black, black), linear-gradient(black, black);
348 mask-position: top, bottom;353 mask-position: top, bottom, right;
349 mask-repeat: no-repeat;354 mask-repeat: no-repeat;
350 mask-size:355 mask-size:
351 100% var(--topbar-scroll-fade-height),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 }
354360
355 .workspace-titlebar-controls {361 .workspace-titlebar-controls {
...@@ -1035,20 +1041,54 @@ code {...@@ -1035,20 +1041,54 @@ code {
1035}1041}
10361042
1037::-webkit-scrollbar-thumb {1043::-webkit-scrollbar-thumb {
1038 background: rgba(0, 0, 0, 0.15);1044 background: var(--app-scrollbar-thumb);
1039 border-radius: 3px;1045 border-radius: 3px;
1040}1046}
10411047
1042::-webkit-scrollbar-thumb:hover {1048::-webkit-scrollbar-thumb:hover {
1043 background: rgba(0, 0, 0, 0.25);1049 background: var(--app-scrollbar-thumb-hover);
1044}1050}
10451051
1046.dark ::-webkit-scrollbar-thumb {1052.model-picker-list::-webkit-scrollbar-track {
1047 background: rgba(255, 255, 255, 0.1);1053 margin-block: 0.5rem;
1048}1054}
10491055
1050.dark ::-webkit-scrollbar-thumb:hover {1056.model-picker-list-scroll-fade-top,
1051 background: rgba(255, 255, 255, 0.18);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}
10531093
1054.turn-chip-strip {1094.turn-chip-strip {
...@@ -1569,28 +1609,6 @@ label:has(> select#reasoning-effort) select {...@@ -1569,28 +1609,6 @@ label:has(> select#reasoning-effort) select {
1569 animation: ultrathink-rainbow 10s linear infinite;1609 animation: ultrathink-rainbow 10s linear infinite;
1570}1610}
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
1594/* Composer chips are non-editable decorators, so the browser skips them when1612/* Composer chips are non-editable decorators, so the browser skips them when
1595 painting text selection; this overlay stands in for the native highlight. */1613 painting text selection; this overlay stands in for the native highlight. */
1596.composer-inline-chip[data-composer-chip-selected]::after {1614.composer-inline-chip[data-composer-chip-selected]::after {
apps/web/src/routes/settings.tsx+2-2
...@@ -74,11 +74,11 @@ function SettingsContentLayout() {...@@ -74,11 +74,11 @@ function SettingsContentLayout() {
74 {!isElectron && (74 {!isElectron && (
75 <header75 <header
76 className={cn(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 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,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 <span className="text-sm font-medium text-foreground">Settings</span>82 <span className="text-sm font-medium text-foreground">Settings</span>
83 {showRestoreDefaults ? (83 {showRestoreDefaults ? (
84 <div className="ms-auto flex items-center gap-2">84 <div className="ms-auto flex items-center gap-2">