| 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,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"; |
| 4 | import type { ComponentType } from "react"; | 4 | import type { ComponentType } from "react"; |
| 5 | import { useState } from "react"; | 5 | import { useState } from "react"; |
| 6 | import { useAssetUrl } from "../assets/assetUrls"; | 6 | import { useAssetUrl } from "../assets/assetUrls"; |
| 7 | import { cn } from "~/lib/utils"; | ||
| 7 | 8 | ||
| 8 | const loadedProjectFaviconSrcs = new Set<string>(); | 9 | const loadedProjectFaviconSrcs = new Set<string>(); |
| 9 | 10 | ||
| ... | @@ -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 | } |
| 45 | 46 | ||
| 46 | function ProjectFaviconImage({ | 47 | function ProjectFaviconImage({ |
| ... | @@ -64,7 +65,11 @@ function ProjectFaviconImage({ | ... | @@ -64,7 +65,11 @@ function ProjectFaviconImage({ |
| 64 | <img | 65 | <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 | <SidebarMenuButton | 2219 | <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 | ); |
| 2823 | 2822 | ||
| 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 | <CommandDialogTrigger | 2828 | <SidebarMenu> |
| 2830 | render={ | 2829 | <SidebarMenuItem> |
| 2831 | <SidebarMenuButton | 2830 | <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 | <TooltipPopup | 242 | <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 | <ProjectFavicon | 256 | <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 | <ProviderInstanceIcon | 286 | <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 | <button | 825 | <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 | <span | 920 | <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 | Settle | 941 | 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 | <CommandDialogTrigger | 2229 | <div className="flex items-center gap-1"> |
| 2228 | render={ | 2230 | <div className="min-w-0 flex-1"> |
| 2229 | <SidebarMenuButton | 2231 | <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 | <SidebarMenuButton | 2233 | <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 | <span | 2242 | <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 | <MenuTrigger | 2258 | 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 | <ProjectFavicon | 2263 | > |
| 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 | <MenuRadioGroup | 2278 | {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 | <MenuRadioItem | 2289 | {scopedProjectGroup ? ( |
| 2304 | value="all" | 2290 | <ProjectFavicon |
| 2305 | closeOnClick | 2291 | 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 | <MenuRadioItem | 2302 | </MenuTrigger> |
| 2315 | key={scopeKey} | 2303 | <MenuPopup align="start" className="w-(--anchor-width)"> |
| 2316 | value={scopeKey} | 2304 | <MenuRadioGroup |
| 2317 | closeOnClick | 2305 | 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 | <ProjectFavicon | 2308 | } |
| 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 | <button | 2314 | > |
| 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 | <TooltipTrigger | 2336 | 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 | <SidebarMenuButton | 2338 | 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 | <span | 2371 | </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 | <TooltipProvider | 2377 | <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 | <button | 2535 | <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)} more | 2541 | 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 project | 2558 | 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 > 1 | 2580 | 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 | <section | 2614 | <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 | <Input | 2621 | <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 | <SelectTrigger | 2652 | <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 | <Button | 2686 | <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 path | 2694 | void handleRemoveProjectMembers(projectGroup, [member]); |
| 2685 | </Button> | 2695 | }} |
| 2686 | <Button | 2696 | > |
| 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"; |
| 95 | import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel"; | 95 | import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel"; |
| 96 | import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel"; | 96 | import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel"; |
| 97 | import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner"; | 97 | import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner"; |
| 98 | import { ComposerControl, ComposerControlIcon, ComposerSelectControl } from "./ComposerControl"; | ||
| 98 | import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight"; | 99 | import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight"; |
| 99 | import { searchSlashCommandItems } from "./composerSlashCommandSearch"; | 100 | import { searchSlashCommandItems } from "./composerSlashCommandSearch"; |
| 100 | import { | 101 | import { |
| ... | @@ -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 | } |
| 169 | import { Button } from "../ui/button"; | 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 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | 172 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 172 | import { toastManager } from "../ui/toast"; | 173 | import { toastManager } from "../ui/toast"; |
| 173 | import { | 174 | import { |
| ... | @@ -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 | <TooltipTrigger | 305 | <TooltipTrigger |
| 305 | render={ | 306 | render={ |
| 306 | <Button | 307 | <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 | <TooltipTrigger | 343 | <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 | <TooltipTrigger | 380 | <TooltipTrigger |
| 389 | render={ | 381 | render={ |
| 390 | <Button | 382 | <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.planSidebarOpen | 385 | 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 | <ListTodoIcon | 395 | <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 | <div | 94 | <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 | <Alert | 175 | <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 @@ | ||
| 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,11 +101,11 @@ export function DraftHeroHeadline({ |
| 101 | <Menu> | 101 | <Menu> |
| 102 | <MenuTrigger | 102 | <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 | <MenuRadioGroup | 109 | <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 | <button | 138 | <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 = { |
| 43 | 43 | ||
| 44 | const EMPTY_MODEL_JUMP_LABELS = new Map<string, string>(); | 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 | // Split a `${instanceId}:${slug}` combobox key back into its pieces. Slugs | 50 | // 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 the | 51 | // 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 | <div | 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 | 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 | <div | 575 | <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 | <ComboboxInput | 584 | <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> |
| 619 | 623 | ||
| 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 | recycleItems | 662 | 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]); |
| 98 | 90 | ||
| 99 | return ( | 91 | return ( |
| 100 | <div | 92 | <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 | <div | 94 | <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 | <div | 96 | <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 | <TooltipTrigger | 110 | <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} |
| 151 | 137 | ||
| 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: { |
| 170 | 156 | ||
| 171 | const button = ( | 157 | const button = ( |
| 172 | <button | 158 | <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 | ); |
| 229 | 214 | ||
| 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 | <TooltipPopup | 223 | <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"; |
| 6 | import { memo, useEffect, useMemo, useState } from "react"; | 6 | import { memo, useEffect, useMemo, useState } from "react"; |
| 7 | import type { VariantProps } from "class-variance-authority"; | 7 | import type { VariantProps } from "class-variance-authority"; |
| 8 | import { ChevronDownIcon } from "lucide-react"; | 8 | import { buttonVariants } from "../ui/button"; |
| 9 | import { Button, buttonVariants } from "../ui/button"; | ||
| 10 | import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; | 9 | import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; |
| 11 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | 10 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 12 | import { cn } from "~/lib/utils"; | 11 | import { cn } from "~/lib/utils"; |
| ... | @@ -18,6 +17,7 @@ import { | ... | @@ -18,6 +17,7 @@ import { |
| 18 | getTriggerDisplayModelName, | 17 | getTriggerDisplayModelName, |
| 19 | } from "./providerIconUtils"; | 18 | } from "./providerIconUtils"; |
| 20 | import type { ProviderInstanceEntry } from "../../providerInstances"; | 19 | import type { ProviderInstanceEntry } from "../../providerInstances"; |
| 20 | import { ComposerControl, ComposerControlChevron } from "./ComposerControl"; | ||
| 21 | 21 | ||
| 22 | export const ProviderModelPicker = memo(function ProviderModelPicker(props: { | 22 | export 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 | <PopoverTrigger | 147 | <PopoverTrigger |
| 148 | render={ | 148 | render={ |
| 149 | <Button | 149 | <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 | <ProviderInstanceIcon | 164 | <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 | <PopoverPopup | 189 | <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"; |
| 17 | import { memo, useCallback, useState } from "react"; | 17 | import { memo, useCallback, useState } from "react"; |
| 18 | import type { VariantProps } from "class-variance-authority"; | 18 | import type { VariantProps } from "class-variance-authority"; |
| 19 | import { ChevronDownIcon, ZapIcon } from "lucide-react"; | 19 | import { ZapIcon } from "lucide-react"; |
| 20 | import { Button, buttonVariants } from "../ui/button"; | 20 | import { buttonVariants } from "../ui/button"; |
| 21 | import { | 21 | import { |
| 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"; |
| 31 | import { getProviderModelCapabilities } from "../../providerModels"; | 31 | import { getProviderModelCapabilities } from "../../providerModels"; |
| 32 | import { cn } from "~/lib/utils"; | 32 | import { cn } from "~/lib/utils"; |
| 33 | import { Badge } from "../ui/badge"; | 33 | import { Badge } from "../ui/badge"; |
| 34 | import { ComposerControl, ComposerControlChevron, ComposerControlIcon } from "./ComposerControl"; | ||
| 34 | 35 | ||
| 35 | type ProviderOptions = ReadonlyArray<ProviderOptionSelection>; | 36 | type ProviderOptions = ReadonlyArray<ProviderOptionSelection>; |
| 36 | 37 | ||
| ... | @@ -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 | <MenuTrigger | 481 | <MenuTrigger |
| 481 | render={ | 482 | render={ |
| 482 | <Button | 483 | <ComposerControl |
| 483 | size="sm" | ||
| 484 | variant={triggerVariant ?? "ghost"} | 484 | variant={triggerVariant ?? "ghost"} |
| 485 | className={cn( | 485 | className={cn( |
| 486 | isCodexStyle | 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" | 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 | <SidebarMenuButton | 56 | <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 | <SidebarMenuButton | 82 | <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 | <Icon | 86 | <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 | <SidebarMenuButton | 100 | <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 | <SidebarMenuButton | 116 | <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.Popup | 65 | <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 | <AutocompleteInput | 107 | <AutocompleteInput |
| 108 | autoFocus | 108 | 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 | <kbd | 199 | <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 | <div | 212 | <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.Scrollbar | 57 | <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.Thumb | 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 | 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 | ...props | 81 | ...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.Trigger | 85 | <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 | }); |
| 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 | const html = renderSidebarButton(); | 54 | const html = renderSidebarButton(); |
| 55 | 55 | ||
| 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 | }); |
| 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 | 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"); |
| 62 | 84 |
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 | } |
| 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 | return ( | 694 | return ( |
| 689 | <ScrollArea hideScrollbars scrollFade className="h-auto min-h-0 flex-1"> | 695 | <> |
| 690 | <div | 696 | {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 | } |
| 702 | 711 | ||
| ... | @@ -783,7 +792,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { | ... | @@ -783,7 +792,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { |
| 783 | } | 792 | } |
| 784 | 793 | ||
| 785 | const sidebarMenuButtonVariants = cva( | 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 | 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>( |
| 166 | 166 | ||
| 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 |
| 77 | 77 | ||
| 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 | } |
| 92 | 94 | ||
| 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 | } |
| 354 | 360 | ||
| 355 | .workspace-titlebar-controls { | 361 | .workspace-titlebar-controls { |
| ... | @@ -1035,20 +1041,54 @@ code { | ... | @@ -1035,20 +1041,54 @@ code { |
| 1035 | } | 1041 | } |
| 1036 | 1042 | ||
| 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 | } |
| 1041 | 1047 | ||
| 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 | } |
| 1045 | 1051 | ||
| 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 | } |
| 1049 | 1055 | ||
| 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 | } |
| 1053 | 1093 | ||
| 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 | } |
| 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 | /* Composer chips are non-editable decorators, so the browser skips them when | 1612 | /* 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 | <header | 75 | <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"> |