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";
9595import { toastManager } from "../ui/toast";
9696import {
9797 BotIcon,
98 CheckIcon,
9899 CircleAlertIcon,
99100 ListTodoIcon,
100101 PencilRulerIcon,
......@@ -273,16 +274,25 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
273274 {runtimeModeOptions.map((mode) => {
274275 const option = runtimeModeConfig[mode];
275276 const OptionIcon = option.icon;
277 const isSelected = props.runtimeMode === mode;
276278 return (
277 <SelectItem key={mode} value={mode} className="min-w-64 py-2">
278 <div className="grid min-w-0 gap-0.5">
279 <span className="inline-flex items-center gap-1.5 font-medium text-foreground">
280 <OptionIcon className="size-3.5 shrink-0 text-muted-foreground" />
281 {option.label}
282 </span>
283 <span className="text-muted-foreground text-xs leading-4">
284 {option.description}
285 </span>
279 <SelectItem key={mode} value={mode} hideIndicator className="min-w-64 py-2">
280 <div className="flex min-w-0 items-center gap-3">
281 <div className="grid min-w-0 flex-1 gap-0.5">
282 <span className="inline-flex items-center gap-1.5 font-medium text-foreground">
283 <OptionIcon className="size-3.5 shrink-0 text-muted-foreground" />
284 {option.label}
285 </span>
286 <span className="text-muted-foreground text-xs leading-4">
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 />
286296 </div>
287297 </SelectItem>
288298 );
apps/web/src/components/chat/TraitsPicker.tsx+84-61
......@@ -16,7 +16,7 @@ import {
1616} from "@t3tools/shared/model";
1717import { memo, useCallback, useState } from "react";
1818import type { VariantProps } from "class-variance-authority";
19import { ChevronDownIcon } from "lucide-react";
19import { CheckIcon, ChevronDownIcon } from "lucide-react";
2020import { Button, buttonVariants } from "../ui/button";
2121import {
2222 Menu,
......@@ -297,67 +297,90 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
297297
298298 return (
299299 <>
300 {selectDescriptors.map((descriptor, index) => (
301 <div key={descriptor.id}>
302 {index > 0 ? <MenuDivider /> : null}
303 <MenuGroup>
304 <div className="px-2 pt-1.5 pb-1 font-medium text-muted-foreground text-xs">
305 {descriptor.label}
306 </div>
307 {ultrathinkInBodyText && descriptor.id === primarySelectDescriptor?.id ? (
308 <div className="px-2 pb-1.5 text-muted-foreground/80 text-xs">
309 Your prompt contains &quot;ultrathink&quot; in the text. Remove it to change this
310 option.
300 {selectDescriptors.map((descriptor, index) => {
301 const selectedValue =
302 ultrathinkPromptControlled && descriptor.id === primarySelectDescriptor?.id
303 ? "ultrathink"
304 : (getDescriptorStringValue(descriptor) ?? "");
305
306 return (
307 <div key={descriptor.id}>
308 {index > 0 ? <MenuDivider /> : null}
309 <MenuGroup>
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}
311360 </div>
312 ) : null}
313 <MenuRadioGroup
314 value={
315 ultrathinkPromptControlled && descriptor.id === primarySelectDescriptor?.id
316 ? "ultrathink"
317 : (getDescriptorStringValue(descriptor) ?? "")
318 }
319 onValueChange={(value) => handleSelectChange(descriptor, value)}
320 >
321 {descriptor.options.map((option) => (
322 <MenuRadioItem
323 key={option.id}
324 value={option.id}
325 disabled={ultrathinkInBodyText && descriptor.id === primarySelectDescriptor?.id}
326 >
327 {option.label}
328 {option.isDefault ? (
329 <>
330 {" "}
331 <DefaultBadge />
332 </>
333 ) : null}
334 </MenuRadioItem>
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 <MenuRadioGroup
362 value={selectedValue}
363 onValueChange={(value) => {
364 updateDescriptors(
365 replaceDescriptorCurrentValue(descriptors, descriptor.id, value === "on"),
366 );
367 }}
368 >
369 {(["on", "off"] as const).map((value) => (
370 <MenuRadioItem key={value} value={value} hideIndicator>
371 <span className="flex w-full min-w-0 items-center justify-between gap-3">
372 <span>{value === "on" ? "On" : "Off"}</span>
373 {value === selectedValue ? (
374 <CheckIcon className="size-3.5 shrink-0 text-blue-400" />
375 ) : null}
376 </span>
377 </MenuRadioItem>
378 ))}
379 </MenuRadioGroup>
380 </MenuGroup>
381 </div>
382 );
383 })}
361384 </>
362385 );
363386});
apps/web/src/components/ui/menu.tsx+28-18
......@@ -147,32 +147,42 @@ function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
147147 return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;
148148}
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}) {
151158 return (
152159 <MenuPrimitive.RadioItem
153160 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",
155163 className,
156164 )}
157165 data-slot="menu-radio-item"
158166 {...props}
159167 >
160 <MenuPrimitive.RadioItemIndicator className="col-start-1">
161 <svg
162 fill="none"
163 height="24"
164 stroke="currentColor"
165 strokeLinecap="round"
166 strokeLinejoin="round"
167 strokeWidth="2"
168 viewBox="0 0 24 24"
169 width="24"
170 xmlns="http://www.w3.org/2000/svg"
171 >
172 <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
173 </svg>
174 </MenuPrimitive.RadioItemIndicator>
175 <span className="col-start-2">{children}</span>
168 {hideIndicator ? null : (
169 <MenuPrimitive.RadioItemIndicator className="col-start-1">
170 <svg
171 fill="none"
172 height="24"
173 stroke="currentColor"
174 strokeLinecap="round"
175 strokeLinejoin="round"
176 strokeWidth="2"
177 viewBox="0 0 24 24"
178 width="24"
179 xmlns="http://www.w3.org/2000/svg"
180 >
181 <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
182 </svg>
183 </MenuPrimitive.RadioItemIndicator>
184 )}
185 <span className={hideIndicator ? "col-start-1" : "col-start-2"}>{children}</span>
176186 </MenuPrimitive.RadioItem>
177187 );
178188}