authorgravatar for 69873514+sandersonstabo@users.noreply.github.comss <69873514+sandersonstabo@users.noreply.github.com> 2026-07-18 13:50:33+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-18 13:50:33+02:00
log5c66110e269cc5a852763e5f06a81f2a763d3181
tree988805f7a6913fd53b48b92667c0a92f0a225737
parentda3c2030f710c4e1243035613eda94f6526afa1f
signature Signed by PGP key B5690EEEBB952194

[fix/feat:ui] Make selected menu checks blue (#3234)

Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 131 insertions(+), 88 deletions(-)

apps/web/src/components/chat/ChatComposer.tsx+19-9
...@@ -95,6 +95,7 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";...@@ -95,6 +95,7 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
95import { toastManager } from "../ui/toast";95import { toastManager } from "../ui/toast";
96import {96import {
97 BotIcon,97 BotIcon,
98 CheckIcon,
98 CircleAlertIcon,99 CircleAlertIcon,
99 ListTodoIcon,100 ListTodoIcon,
100 PencilRulerIcon,101 PencilRulerIcon,
...@@ -273,16 +274,25 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop...@@ -273,16 +274,25 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
273 {runtimeModeOptions.map((mode) => {274 {runtimeModeOptions.map((mode) => {
274 const option = runtimeModeConfig[mode];275 const option = runtimeModeConfig[mode];
275 const OptionIcon = option.icon;276 const OptionIcon = option.icon;
277 const isSelected = props.runtimeMode === mode;
276 return (278 return (
277 <SelectItem key={mode} value={mode} className="min-w-64 py-2">279 <SelectItem key={mode} value={mode} hideIndicator className="min-w-64 py-2">
278 <div className="grid min-w-0 gap-0.5">280 <div className="flex min-w-0 items-center gap-3">
279 <span className="inline-flex items-center gap-1.5 font-medium text-foreground">281 <div className="grid min-w-0 flex-1 gap-0.5">
280 <OptionIcon className="size-3.5 shrink-0 text-muted-foreground" />282 <span className="inline-flex items-center gap-1.5 font-medium text-foreground">
281 {option.label}283 <OptionIcon className="size-3.5 shrink-0 text-muted-foreground" />
282 </span>284 {option.label}
283 <span className="text-muted-foreground text-xs leading-4">285 </span>
284 {option.description}286 <span className="text-muted-foreground text-xs leading-4">
285 </span>287 {option.description}
288 </span>
289 </div>
290 <CheckIcon
291 className={cn(
292 "size-4 text-blue-400",
293 isSelected ? "opacity-100" : "opacity-0",
294 )}
295 />
286 </div>296 </div>
287 </SelectItem>297 </SelectItem>
288 );298 );
apps/web/src/components/chat/TraitsPicker.tsx+84-61
...@@ -16,7 +16,7 @@ import {...@@ -16,7 +16,7 @@ import {
16} from "@t3tools/shared/model";16} from "@t3tools/shared/model";
17import { memo, useCallback, useState } from "react";17import { memo, useCallback, useState } from "react";
18import type { VariantProps } from "class-variance-authority";18import type { VariantProps } from "class-variance-authority";
19import { ChevronDownIcon } from "lucide-react";19import { CheckIcon, ChevronDownIcon } from "lucide-react";
20import { Button, buttonVariants } from "../ui/button";20import { Button, buttonVariants } from "../ui/button";
21import {21import {
22 Menu,22 Menu,
...@@ -297,67 +297,90 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({...@@ -297,67 +297,90 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
297297
298 return (298 return (
299 <>299 <>
300 {selectDescriptors.map((descriptor, index) => (300 {selectDescriptors.map((descriptor, index) => {
301 <div key={descriptor.id}>301 const selectedValue =
302 {index > 0 ? <MenuDivider /> : null}302 ultrathinkPromptControlled && descriptor.id === primarySelectDescriptor?.id
303 <MenuGroup>303 ? "ultrathink"
304 <div className="px-2 pt-1.5 pb-1 font-medium text-muted-foreground text-xs">304 : (getDescriptorStringValue(descriptor) ?? "");
305 {descriptor.label}305
306 </div>306 return (
307 {ultrathinkInBodyText && descriptor.id === primarySelectDescriptor?.id ? (307 <div key={descriptor.id}>
308 <div className="px-2 pb-1.5 text-muted-foreground/80 text-xs">308 {index > 0 ? <MenuDivider /> : null}
309 Your prompt contains &quot;ultrathink&quot; in the text. Remove it to change this309 <MenuGroup>
310 option.310 <div className="px-2 pt-1.5 pb-1 font-medium text-muted-foreground text-xs">
311 {descriptor.label}
312 </div>
313 {ultrathinkInBodyText && descriptor.id === primarySelectDescriptor?.id ? (
314 <div className="px-2 pb-1.5 text-muted-foreground/80 text-xs">
315 Your prompt contains &quot;ultrathink&quot; in the text. Remove it to change this
316 option.
317 </div>
318 ) : null}
319 <MenuRadioGroup
320 value={selectedValue}
321 onValueChange={(value) => handleSelectChange(descriptor, value)}
322 >
323 {descriptor.options.map((option) => (
324 <MenuRadioItem
325 key={option.id}
326 value={option.id}
327 hideIndicator
328 disabled={ultrathinkInBodyText && descriptor.id === primarySelectDescriptor?.id}
329 >
330 <span className="flex w-full min-w-0 items-center justify-between gap-3">
331 <span className="min-w-0 truncate">
332 {option.label}
333 {option.isDefault ? (
334 <>
335 {" "}
336 <DefaultBadge />
337 </>
338 ) : null}
339 </span>
340 {option.id === selectedValue ? (
341 <CheckIcon className="size-3.5 shrink-0 text-blue-400" />
342 ) : null}
343 </span>
344 </MenuRadioItem>
345 ))}
346 </MenuRadioGroup>
347 </MenuGroup>
348 </div>
349 );
350 })}
351 {booleanDescriptors.map((descriptor, index) => {
352 const selectedValue = descriptor.currentValue === true ? "on" : "off";
353
354 return (
355 <div key={descriptor.id}>
356 {index > 0 || selectDescriptors.length > 0 ? <MenuDivider /> : null}
357 <MenuGroup>
358 <div className="px-2 py-1.5 font-medium text-muted-foreground text-xs">
359 {descriptor.label}
311 </div>360 </div>
312 ) : null}361 <MenuRadioGroup
313 <MenuRadioGroup362 value={selectedValue}
314 value={363 onValueChange={(value) => {
315 ultrathinkPromptControlled && descriptor.id === primarySelectDescriptor?.id364 updateDescriptors(
316 ? "ultrathink"365 replaceDescriptorCurrentValue(descriptors, descriptor.id, value === "on"),
317 : (getDescriptorStringValue(descriptor) ?? "")366 );
318 }367 }}
319 onValueChange={(value) => handleSelectChange(descriptor, value)}368 >
320 >369 {(["on", "off"] as const).map((value) => (
321 {descriptor.options.map((option) => (370 <MenuRadioItem key={value} value={value} hideIndicator>
322 <MenuRadioItem371 <span className="flex w-full min-w-0 items-center justify-between gap-3">
323 key={option.id}372 <span>{value === "on" ? "On" : "Off"}</span>
324 value={option.id}373 {value === selectedValue ? (
325 disabled={ultrathinkInBodyText && descriptor.id === primarySelectDescriptor?.id}374 <CheckIcon className="size-3.5 shrink-0 text-blue-400" />
326 >375 ) : null}
327 {option.label}376 </span>
328 {option.isDefault ? (377 </MenuRadioItem>
329 <>378 ))}
330 {" "}379 </MenuRadioGroup>
331 <DefaultBadge />380 </MenuGroup>
332 </>381 </div>
333 ) : null}382 );
334 </MenuRadioItem>383 })}
335 ))}
336 </MenuRadioGroup>
337 </MenuGroup>
338 </div>
339 ))}
340 {booleanDescriptors.map((descriptor, index) => (
341 <div key={descriptor.id}>
342 {index > 0 || selectDescriptors.length > 0 ? <MenuDivider /> : null}
343 <MenuGroup>
344 <div className="px-2 py-1.5 font-medium text-muted-foreground text-xs">
345 {descriptor.label}
346 </div>
347 <MenuRadioGroup
348 value={descriptor.currentValue === true ? "on" : "off"}
349 onValueChange={(value) => {
350 updateDescriptors(
351 replaceDescriptorCurrentValue(descriptors, descriptor.id, value === "on"),
352 );
353 }}
354 >
355 <MenuRadioItem value="on">On</MenuRadioItem>
356 <MenuRadioItem value="off">Off</MenuRadioItem>
357 </MenuRadioGroup>
358 </MenuGroup>
359 </div>
360 ))}
361 </>384 </>
362 );385 );
363});386});
apps/web/src/components/ui/menu.tsx+28-18
...@@ -147,32 +147,42 @@ function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {...@@ -147,32 +147,42 @@ function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
147 return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;147 return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;
148}148}
149149
150function MenuRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) {150function MenuRadioItem({
151 className,
152 children,
153 hideIndicator = false,
154 ...props
155}: MenuPrimitive.RadioItem.Props & {
156 hideIndicator?: boolean;
157}) {
151 return (158 return (
152 <MenuPrimitive.RadioItem159 <MenuPrimitive.RadioItem
153 className={cn(160 className={cn(
154 "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",161 "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default items-center gap-2 rounded-sm py-1 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
162 hideIndicator ? "grid-cols-[1fr] ps-3 pe-3" : "grid-cols-[1rem_1fr] ps-2 pe-4",
155 className,163 className,
156 )}164 )}
157 data-slot="menu-radio-item"165 data-slot="menu-radio-item"
158 {...props}166 {...props}
159 >167 >
160 <MenuPrimitive.RadioItemIndicator className="col-start-1">168 {hideIndicator ? null : (
161 <svg169 <MenuPrimitive.RadioItemIndicator className="col-start-1">
162 fill="none"170 <svg
163 height="24"171 fill="none"
164 stroke="currentColor"172 height="24"
165 strokeLinecap="round"173 stroke="currentColor"
166 strokeLinejoin="round"174 strokeLinecap="round"
167 strokeWidth="2"175 strokeLinejoin="round"
168 viewBox="0 0 24 24"176 strokeWidth="2"
169 width="24"177 viewBox="0 0 24 24"
170 xmlns="http://www.w3.org/2000/svg"178 width="24"
171 >179 xmlns="http://www.w3.org/2000/svg"
172 <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />180 >
173 </svg>181 <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
174 </MenuPrimitive.RadioItemIndicator>182 </svg>
175 <span className="col-start-2">{children}</span>183 </MenuPrimitive.RadioItemIndicator>
184 )}
185 <span className={hideIndicator ? "col-start-1" : "col-start-2"}>{children}</span>
176 </MenuPrimitive.RadioItem>186 </MenuPrimitive.RadioItem>
177 );187 );
178}188}