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