authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-22 21:33:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-22 21:33:03-07:00
log0e512db34d787b21b827f07c6a62fba7fb00369e
tree042470d52a00edf8e081bf8640e4908c65c6a91c
parent71c9b693137c070b4887b365c68659f1a2fb7aed
signature Signed by PGP key B5690EEEBB952194

refactor(web): composer controls own their look instead of restyling ui components (#13209)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

15 files changed, 100 insertions(+), 137 deletions(-)

apps/web/src/components/chat/ChatComposer.tsx+24-29
......@@ -1100,14 +1100,8 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
11001100 render={
11011101 <ComposerControl
11021102 size={size}
1103 className={cn(
1104 "shrink-0 whitespace-nowrap",
1105 props.interactionMode === "plan"
1106 ? "bg-accent text-accent-foreground hover:bg-accent/80"
1107 : size === "xs"
1108 ? undefined
1109 : "text-secondary-label hover:text-foreground",
1110 )}
1103 className="shrink-0 whitespace-nowrap"
1104 aria-pressed={props.interactionMode === "plan"}
11111105 type="button"
11121106 onClick={props.onToggleInteractionMode}
11131107 aria-label={interactionModeTooltip}
......@@ -1152,7 +1146,6 @@ const ComposerFooterModeControls = memo(function ComposerFooterModeControls(prop
11521146 <ComposerSelectControl
11531147 data-composer-shortcut="composer.mode"
11541148 size={size}
1155 className={size === "xs" ? undefined : "font-medium"}
11561149 aria-label="Runtime mode"
11571150 />
11581151 }
......@@ -4994,10 +4987,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
49944987 .slice(restingBlockDefs.length - restingHiddenBlockCount)
49954988 .map((def) => def.id);
49964989 const composerControls = showProviderUnavailable ? (
4997 <Button
4990 <ComposerControl
49984991 type="button"
4999 size="sm"
5000 variant="ghost"
50014992 disabled={!providerSetupInstanceId}
50024993 onClick={() => {
50034994 if (providerSetupInstanceId) {
......@@ -5005,11 +4996,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
50054996 }
50064997 }}
50074998 data-chat-provider-unavailable="true"
5008 className="shrink-0 gap-2 px-2 text-secondary-label sm:px-3"
4999 className="shrink-0"
50095000 >
50105001 <CircleAlertIcon className="size-4" />
50115002 {providerSetupInstanceId ? "Open provider settings" : "No provider available"}
5012 </Button>
5003 </ComposerControl>
50135004 ) : (
50145005 <>
50155006 {composerControlsInStrip && restingControlsHaveLeadingContext ? (
......@@ -6590,9 +6581,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
65906581 <TooltipTrigger
65916582 render={
65926583 <Button
6593 variant="ghost"
6584 variant="overlay"
65946585 size="icon-xs"
6595 className="absolute bottom-1 left-1 bg-background/85 hover:bg-background/95"
6586 className="absolute bottom-1 left-1"
65966587 onClick={() =>
65976588 retryAttachmentUpload({
65986589 environmentId,
......@@ -6609,19 +6600,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
66096600 <TooltipPopup side="top">{upload.reason}</TooltipPopup>
66106601 </Tooltip>
66116602 )}
6612 <Button
6613 variant="ghost"
6614 size="icon-xs"
6603 {/* Snap-shot frames reveal their remove button on hover or focus. */}
6604 <span
66156605 className={cn(
6616 "absolute right-1 top-1 bg-background/80 hover:bg-background/90",
6606 "absolute right-1 top-1 flex",
66176607 image.source?.kind === "snap-shot" &&
6618 "opacity-0 transition-opacity pointer-coarse:opacity-100 focus-visible:opacity-100 group-hover/attachment:opacity-100 group-focus-within/attachment:opacity-100",
6608 "opacity-0 transition-opacity pointer-coarse:opacity-100 group-hover/attachment:opacity-100 group-focus-within/attachment:opacity-100",
66196609 )}
6620 onClick={() => removeComposerImage(image.id)}
6621 aria-label={`Remove ${image.name}`}
66226610 >
6623 <XIcon />
6624 </Button>
6611 <Button
6612 variant="media-close"
6613 size="icon-xs"
6614 onClick={() => removeComposerImage(image.id)}
6615 aria-label={`Remove ${image.name}`}
6616 >
6617 <XIcon />
6618 </Button>
6619 </span>
66256620 </SnapShotAttachmentFrame>
66266621 );
66276622 })
......@@ -6685,9 +6680,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
66856680 <TooltipTrigger
66866681 render={
66876682 <Button
6688 variant="ghost"
6683 variant="overlay"
66896684 size="icon-xs"
6690 className="absolute bottom-1 left-1 bg-background/85 hover:bg-background/95"
6685 className="absolute bottom-1 left-1"
66916686 onClick={() =>
66926687 retryAttachmentUpload({
66936688 environmentId,
......@@ -6705,9 +6700,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
67056700 </Tooltip>
67066701 )}
67076702 <Button
6708 variant="ghost"
6703 variant="media-close"
67096704 size="icon-xs"
6710 className="absolute right-1 top-1 bg-background/80 hover:bg-background/90"
6705 className="absolute right-1 top-1"
67116706 onClick={() => removeComposerFileFromDraft(file.id)}
67126707 aria-label={`Remove ${file.name}`}
67136708 >
apps/web/src/components/chat/CompactComposerControlsMenu.tsx+1-2
......@@ -38,8 +38,7 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls
3838 render={
3939 <ComposerControl
4040 size={size}
41 variant="ghost"
42 className={size === "xs" ? "shrink-0" : "shrink-0 px-2"}
41 className="shrink-0"
4342 aria-label="More composer controls"
4443 data-composer-shortcut={
4544 props.traitsMenuContent ? "composer.mode composer.effort" : "composer.mode"
apps/web/src/components/chat/ComposerControl.tsx+40-38
......@@ -1,45 +1,49 @@
1import { mergeProps } from "@base-ui/react/merge-props";
2import { Select as SelectPrimitive } from "@base-ui/react/select";
3import { useRender } from "@base-ui/react/use-render";
14import type { ComponentProps } from "react";
25import { ChevronDownIcon, type LucideIcon } from "lucide-react";
36
47import { cn } from "~/lib/utils";
5import { Button } from "../ui/button";
6import { SelectTrigger } from "../ui/select";
78import { Separator } from "../ui/separator";
89
910export type ComposerControlSize = "sm" | "xs";
1011
11type ComposerControlProps = Omit<ComponentProps<typeof Button>, "size"> & {
12 size?: ComposerControlSize;
13};
12/**
13 * The composer toolbar's control look. `sm` is the expanded toolbar; `xs` is the dimmer resting
14 * strip. `aria-pressed` marks a toggle that is on (plan mode). This is an app control, not a
15 * restyled Button, so it owns its classes.
16 */
17function composerControlClassName(size: ComposerControlSize, className?: string) {
18 return cn(
19 "relative inline-flex shrink-0 cursor-pointer items-center justify-center whitespace-nowrap rounded-[var(--control-radius)] border border-transparent text-base outline-none hover:bg-accent data-pressed:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 data-disabled:pointer-events-none data-disabled:opacity-64 pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&:active:not([aria-haspopup])]:scale-[0.97] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:-mx-0.5 [&_svg[data-composer-control-icon]]:mx-0 [&_svg:not([class*='text-'])]:text-[var(--control-icon-color)]",
20 size === "xs"
21 ? "h-7 gap-1 px-[calc(--spacing(2)-1px)] font-normal text-muted-foreground/70 text-sm [--control-icon-color:currentColor] hover:text-foreground/80 sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5 [&_svg[data-composer-control-chevron]]:ms-0 [&_svg[data-composer-control-chevron]]:-me-1"
22 : "h-7 gap-1.5 px-2.5 font-medium text-secondary-label [--control-icon-color:var(--contrast-muted-foreground)] hover:text-foreground sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4",
23 "aria-pressed:bg-accent aria-pressed:text-accent-foreground aria-pressed:hover:bg-accent/80",
24 className,
25 );
26}
1427
15type ComposerSelectControlProps = Omit<ComponentProps<typeof SelectTrigger>, "size"> & {
28type ComposerControlProps = useRender.ComponentProps<"button"> & {
1629 size?: ComposerControlSize;
1730};
1831
19const composerControlClassName =
20 "rounded-[var(--control-radius)] text-secondary-label transition-none hover:text-foreground [&_svg[data-composer-control-chevron]]:-mx-0.5 [&_svg[data-composer-control-icon]]:mx-0";
21const expandedComposerControlClassName = "h-7 min-h-7 gap-1.5 px-2.5";
22const restingComposerControlClassName =
23 "[--control-icon-color:currentColor] font-normal text-muted-foreground/70 hover:text-foreground/80 [&_svg[data-composer-control-chevron]]:-me-1 [&_svg[data-composer-control-chevron]]:ms-0";
24
2532export function ComposerControl({
2633 className,
2734 size = "sm",
28 variant = "ghost",
35 render,
2936 ...props
3037}: ComposerControlProps) {
31 return (
32 <Button
33 className={cn(
34 composerControlClassName,
35 size === "xs" ? restingComposerControlClassName : expandedComposerControlClassName,
36 className,
37 )}
38 size={size}
39 variant={variant}
40 {...props}
41 />
42 );
38 const defaultProps = {
39 className: composerControlClassName(size, className),
40 type: render ? undefined : ("button" as const),
41 };
42 return useRender({
43 defaultTagName: "button",
44 props: mergeProps<"button">(defaultProps, props),
45 render,
46 });
4347}
4448
4549export function ComposerControlIcon({
......@@ -105,21 +109,19 @@ export function ComposerControlSeparator({
105109
106110export function ComposerSelectControl({
107111 className,
112 children,
108113 size = "sm",
109 variant = "ghost",
110114 ...props
111}: ComposerSelectControlProps) {
115}: Omit<SelectPrimitive.Trigger.Props, "className"> & {
116 className?: string | undefined;
117 size?: ComposerControlSize;
118}) {
112119 return (
113 <SelectTrigger
114 className={cn(
115 composerControlClassName,
116 size === "xs" ? restingComposerControlClassName : expandedComposerControlClassName,
117 className,
118 )}
119 icon={<ComposerControlChevron size={size} />}
120 size={size}
121 variant={variant}
122 {...props}
123 />
120 <SelectPrimitive.Trigger className={composerControlClassName(size, className)} {...props}>
121 {children}
122 <SelectPrimitive.Icon>
123 <ComposerControlChevron size={size} />
124 </SelectPrimitive.Icon>
125 </SelectPrimitive.Trigger>
124126 );
125127}
apps/web/src/components/chat/ComposerPrimaryActions.tsx+20-24
......@@ -52,6 +52,11 @@ const formatPendingPrimaryActionLabel = (input: {
5252 return input.questionIndex > 0 ? "Submit answers" : "Submit answer";
5353};
5454
55// The composer's labeled primary actions (Submit, Refine, Implement) share the send button's
56// message-action pill, so they are composer-owned buttons rather than restyled Buttons.
57const messageActionPillClassName =
58 "inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-full bg-message-action font-medium text-base text-message-action-foreground shadow-xs shadow-message-action/24 outline-none hover:bg-message-action-hover focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 disabled:shadow-none sm:text-sm";
59
5560const preventPointerFocus: PointerEventHandler<HTMLElement> = (event) => {
5661 event.preventDefault();
5762};
......@@ -112,7 +117,6 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
112117 <Button
113118 size="icon-sm"
114119 variant="outline"
115 className="rounded-full"
116120 {...pointerFocusProps}
117121 onClick={onPreviousPendingQuestion}
118122 disabled={pendingAction.isResponding}
......@@ -124,7 +128,6 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
124128 <Button
125129 size="sm"
126130 variant="outline"
127 className="rounded-full"
128131 {...pointerFocusProps}
129132 onClick={onPreviousPendingQuestion}
130133 disabled={pendingAction.isResponding}
......@@ -133,13 +136,9 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
133136 </Button>
134137 )
135138 ) : null}
136 <Button
139 <button
137140 type="submit"
138 size="sm"
139 className={cn(
140 "rounded-full bg-message-action text-message-action-foreground hover:bg-message-action-hover",
141 compact ? "px-3" : "px-4",
142 )}
141 className={cn(messageActionPillClassName, "h-8 sm:h-7", compact ? "px-3" : "px-4")}
143142 {...pointerFocusProps}
144143 disabled={
145144 isEnvironmentUnavailable ||
......@@ -153,7 +152,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
153152 isResponding: pendingAction.isResponding,
154153 questionIndex: pendingAction.questionIndex,
155154 })}
156 </Button>
155 </button>
157156 </div>
158157 );
159158 }
......@@ -161,39 +160,36 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
161160 if (showPlanFollowUpPrompt) {
162161 if (promptHasText) {
163162 return (
164 <Button
163 <button
165164 type="submit"
166 size="sm"
167 className={cn(
168 "rounded-full bg-message-action text-message-action-foreground hover:bg-message-action-hover",
169 compact ? "h-9 px-3 sm:h-8" : "h-9 px-4 sm:h-8",
170 )}
165 className={cn(messageActionPillClassName, "h-9 sm:h-8", compact ? "px-3" : "px-4")}
171166 {...pointerFocusProps}
172167 disabled={isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable}
173168 >
174169 {isConnecting || isSendBusy ? "Sending..." : "Refine"}
175 </Button>
170 </button>
176171 );
177172 }
178173
179174 return (
180175 <div data-chat-composer-implement-actions="true" className="flex items-center justify-end">
181 <Button
176 <button
182177 type="submit"
183 size="sm"
184 className="h-9 rounded-l-full rounded-r-none bg-message-action px-4 text-message-action-foreground hover:bg-message-action-hover sm:h-8"
178 className={cn(messageActionPillClassName, "h-9 rounded-r-none px-4 sm:h-8")}
185179 {...pointerFocusProps}
186180 disabled={isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable}
187181 >
188182 {isConnecting || isSendBusy ? "Sending..." : "Implement"}
189 </Button>
183 </button>
190184 <Menu>
191185 <MenuTrigger
192186 render={
193 <Button
194 size="sm"
195 variant="default"
196 className="h-9 rounded-l-none rounded-r-full border-l-message-action-foreground/20 bg-message-action px-2 text-message-action-foreground hover:bg-message-action-hover sm:h-8"
187 <button
188 type="button"
189 className={cn(
190 messageActionPillClassName,
191 "h-9 rounded-l-none border-l border-message-action-foreground/20 px-2 sm:h-8",
192 )}
197193 aria-label="Implementation actions"
198194 {...pointerFocusProps}
199195 disabled={isSendBusy || isSendDisabled || isConnecting || isEnvironmentUnavailable}
apps/web/src/components/chat/ModelListRow.tsx+4-8
......@@ -53,10 +53,9 @@ export const ModelListRow = memo(function ModelListRow(props: {
5353 value={modelPickerModelKey(props.instanceId, props.model.slug)}
5454 disabled={Boolean(props.disabledReason)}
5555 className={cn(
56 "group relative w-full !min-w-0 max-w-full cursor-pointer rounded-md px-2 py-2 transition-[background-color,box-shadow,color]",
57 "hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] data-highlighted:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-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(--contrast-foreground))]",
56 "group relative w-full !min-w-0 max-w-full cursor-pointer",
5857 props.disabledReason &&
59 "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent",
58 "data-disabled:pointer-events-auto data-disabled:cursor-not-allowed",
6059 )}
6160 >
6261 <div className="min-w-0 flex-1 text-left">
......@@ -103,11 +102,8 @@ export const ModelListRow = memo(function ModelListRow(props: {
103102 render={
104103 <Button
105104 size="icon-xs"
106 variant="ghost"
107 className={cn(
108 "-mr-1 shrink-0 text-muted-foreground/70 opacity-64 transition-[color,opacity] hover:text-foreground hover:opacity-100 group-hover:opacity-100",
109 props.isFavorite && "text-foreground opacity-100",
110 )}
105 variant="ghost-muted"
106 className="-mr-1 shrink-0"
111107 onClick={(event) => {
112108 event.stopPropagation();
113109 props.onToggleFavorite();
apps/web/src/components/chat/ModelPickerContent.tsx+7-11
......@@ -39,7 +39,7 @@ import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"
3939import { cn } from "~/lib/utils";
4040import { getVirtualizedScrollFadeClassName } from "../ui/scroll-area";
4141import { TooltipProvider } from "../ui/tooltip";
42import { Button } from "../ui/button";
42import { InlineButton } from "../ui/button";
4343import {
4444 isProviderInstancePickerReady,
4545 isProviderInstancePickerVisible,
......@@ -941,7 +941,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
941941
942942 {/* Model list */}
943943 <div className="relative min-h-0 flex-1 overflow-hidden pr-px">
944 <ComboboxListVirtualized className="not-empty:p-0">
944 <ComboboxListVirtualized>
945945 <LegendList<string>
946946 ref={modelListRef}
947947 data={filteredItemKeys}
......@@ -955,7 +955,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
955955 index={index}
956956 value={modelKey}
957957 aria-expanded={legacySection.isExpanded}
958 className="group w-full cursor-pointer rounded-md px-2 py-2"
958 className="group w-full cursor-pointer"
959959 >
960960 <div className="min-w-0 flex-1 text-left">
961961 <div className="text-xs font-medium leading-snug">Legacy models</div>
......@@ -1031,26 +1031,22 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
10311031 <p className="line-clamp-3 text-muted-foreground">
10321032 {getProviderStatusMessage(entry.snapshot)}
10331033 </p>
1034 <Button
1035 className="mt-1 px-0 text-foreground"
1034 <InlineButton
1035 className="mt-1"
10361036 onClick={() => {
10371037 props.onRequestClose?.();
10381038 props.onOpenProviderSetup?.(entry.instanceId);
10391039 }}
1040 size="xs"
1041 variant="link"
10421040 >
10431041 {providerSetupEntries.length > 1
10441042 ? `Set up ${entry.displayName}`
10451043 : "Open provider setup"}
1046 </Button>
1044 </InlineButton>
10471045 </div>
10481046 ))}
10491047 </div>
10501048 ) : (
1051 <ComboboxEmpty className="not-empty:py-6 empty:h-0 text-xs font-normal leading-snug">
1052 No models found
1053 </ComboboxEmpty>
1049 <ComboboxEmpty className="empty:h-0">No models found</ComboboxEmpty>
10541050 )}
10551051 </div>
10561052 </Combobox>
apps/web/src/components/chat/ProviderModelPicker.tsx-3
......@@ -6,7 +6,6 @@ import {
66} from "@t3tools/contracts";
77import { memo, useEffect, useMemo, useState } from "react";
88import { Badge } from "../ui/badge";
9import type { ButtonVariant } from "../ui/button";
109import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
1110import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
1211import { cn } from "~/lib/utils";
......@@ -48,7 +47,6 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
4847 disabled?: boolean;
4948 terminalOpen?: boolean;
5049 open?: boolean;
51 triggerVariant?: ButtonVariant;
5250 triggerClassName?: string;
5351 /** Aggregate settings can show a neutral value without claiming one provider is selected. */
5452 triggerLabel?: string;
......@@ -206,7 +204,6 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
206204 render={
207205 <ComposerControl
208206 aria-label={props.triggerAriaLabel ?? allModelNames}
209 variant={props.triggerVariant ?? "ghost"}
210207 size={size}
211208 data-chat-provider-model-picker="true"
212209 className={cn(
apps/web/src/components/chat/TraitsPicker.tsx-4
......@@ -17,7 +17,6 @@ import {
1717} from "@t3tools/shared/model";
1818import { memo, useCallback } from "react";
1919import { GaugeIcon, ZapIcon } from "lucide-react";
20import type { ButtonVariant } from "../ui/button";
2120import {
2221 Menu,
2322 MenuGroup,
......@@ -276,7 +275,6 @@ export interface TraitsMenuContentProps {
276275 modelOptions?: ProviderOptions | null | undefined;
277276 allowPromptInjectedEffort?: boolean;
278277 planModeEnabled: boolean;
279 triggerVariant?: ButtonVariant;
280278 triggerClassName?: string;
281279 isComposerOwned?: boolean;
282280}
......@@ -543,7 +541,6 @@ export const TraitsPicker = memo(function TraitsPicker({
543541 modelOptions,
544542 allowPromptInjectedEffort = true,
545543 planModeEnabled,
546 triggerVariant,
547544 triggerClassName,
548545 isComposerOwned,
549546 size = "sm",
......@@ -622,7 +619,6 @@ export const TraitsPicker = memo(function TraitsPicker({
622619 <ComposerControl
623620 aria-label={accessibleLabel}
624621 data-composer-shortcut={isComposerOwned ? "composer.effort" : undefined}
625 variant={triggerVariant ?? "ghost"}
626622 size={size}
627623 className={cn(
628624 isCodexStyle
apps/web/src/components/chat/composerProviderState.tsx-4
......@@ -15,7 +15,6 @@ import {
1515} from "@t3tools/shared/model";
1616import type { ReactNode } from "react";
1717
18import type { ButtonVariant } from "../ui/button";
1918import type { DraftId } from "../../composerDraftStore";
2019import { getProviderModelCapabilities } from "../../providerModels";
2120import type { ComposerControlSize } from "./ComposerControl";
......@@ -54,7 +53,6 @@ type TraitsRenderInput = {
5453 planModeEnabled: boolean;
5554 size?: ComposerControlSize;
5655 hidden?: boolean;
57 triggerVariant?: ButtonVariant;
5856 triggerClassName?: string;
5957 isComposerOwned?: boolean;
6058};
......@@ -177,7 +175,6 @@ function renderTraitsControl(
177175 planModeEnabled,
178176 size,
179177 hidden,
180 triggerVariant,
181178 triggerClassName,
182179 isComposerOwned,
183180 } = input;
......@@ -216,7 +213,6 @@ function renderTraitsControl(
216213 planModeEnabled={planModeEnabled}
217214 {...(size !== undefined ? { size } : {})}
218215 {...(hidden !== undefined ? { hidden } : {})}
219 {...(triggerVariant !== undefined ? { triggerVariant } : {})}
220216 {...(triggerClassName !== undefined ? { triggerClassName } : {})}
221217 {...(isComposerOwned ? { isComposerOwned } : {})}
222218 />
apps/web/src/components/settings/ProjectDefaultsSettings.tsx-2
......@@ -162,7 +162,6 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting
162162 lockedProvider={null}
163163 instanceEntries={entries}
164164 modelOptionsByInstance={modelOptions}
165 triggerVariant="outline"
166165 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
167166 {...(mixedModel ? { triggerLabel: "Mixed" } : {})}
168167 getModelDisabledReason={modelDisabledReason}
......@@ -187,7 +186,6 @@ export function ProjectDefaultsSettings({ category }: { category: ProjectSetting
187186 modelOptions={selection.options ?? []}
188187 allowPromptInjectedEffort={false}
189188 planModeEnabled={settings.planModeEnabled}
190 triggerVariant="outline"
191189 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
192190 onModelOptionsChange={(options) =>
193191 setModel(createModelSelection(selection.instanceId, selection.model, options))
apps/web/src/components/settings/SettingsPanels.tsx-2
......@@ -3113,7 +3113,6 @@ export function GeneralSettingsPanel() {
31133113 lockedProvider={null}
31143114 instanceEntries={textGenerationModelInstanceEntries}
31153115 modelOptionsByInstance={textGenerationModelOptionsByInstance}
3116 triggerVariant="outline"
31173116 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
31183117 {...(mixedTextGenerationModel ? { triggerLabel: "Mixed" } : {})}
31193118 getModelDisabledReason={textGenerationModelDisabledReason}
......@@ -3164,7 +3163,6 @@ export function GeneralSettingsPanel() {
31643163 modelOptions={textGenModelOptions}
31653164 allowPromptInjectedEffort={false}
31663165 planModeEnabled={settings.planModeEnabled}
3167 triggerVariant="outline"
31683166 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
31693167 onModelOptionsChange={(nextOptions) => {
31703168 updateSettings({
apps/web/src/components/settings/SourceControlWritingSettings.tsx-1
......@@ -304,7 +304,6 @@ export function SourceControlWritingSettingsSection() {
304304 lockedProvider={null}
305305 instanceEntries={instanceEntries}
306306 modelOptionsByInstance={modelOptionsByInstance}
307 triggerVariant="outline"
308307 triggerClassName={SETTINGS_PICKER_TRIGGER_CLASSNAME}
309308 triggerAriaLabel="Source control writer model"
310309 {...(mixedWriterModel ? { triggerLabel: "Mixed" } : {})}
apps/web/src/components/settings/settingsLayout.tsx+2-7
......@@ -137,13 +137,8 @@ export function SettingsSearchTarget({
137137 );
138138}
139139
140/**
141 * Trigger classes for the composer model/traits pickers when they sit in a
142 * settings row: match the `sm` control box (the composer pins them to 28px at
143 * every breakpoint) and drop the composer's max-width.
144 */
145export const SETTINGS_PICKER_TRIGGER_CLASSNAME =
146 "h-8 min-h-8 min-w-0 max-w-none shrink-0 text-foreground/90 hover:text-foreground sm:h-7 sm:min-h-7";
140/** Layout for the composer model/traits pickers in a settings row: drop the composer's max-width. */
141export const SETTINGS_PICKER_TRIGGER_CLASSNAME = "min-w-0 max-w-none shrink-0";
147142
148143/** Info affordance explaining how a setting interacts with the shared background policy. */
149144export function PolicyTooltip({ children }: { readonly children: string }) {
apps/web/src/components/ui/combobox.tsx+1-1
......@@ -194,7 +194,7 @@ function ComboboxItem({
194194 return (
195195 <ComboboxPrimitive.Item
196196 className={cn(
197 "flex min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-pointer items-center rounded-sm px-2 py-1 text-base outline-none hover:bg-accent data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-selected:bg-foreground/[0.08] data-selected:text-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground [&[data-highlighted][data-selected]]:bg-accent [&[data-highlighted][data-selected]]: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",
197 "flex min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-pointer items-center rounded-sm px-2 py-1 text-base outline-none not-data-disabled:hover:bg-accent data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-selected:bg-foreground/[0.08] data-selected:text-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground [&[data-highlighted][data-selected]]:bg-accent [&[data-highlighted][data-selected]]: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",
198198 className,
199199 )}
200200 data-slot="combobox-item"
scripts/lint-restyle-ceiling.ts+1-1
......@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";
88// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
99// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
1010// migrate a file, and delete this script when the rule becomes an error.
11export const RESTYLE_CEILING = 101;
11export const RESTYLE_CEILING = 0;
1212
1313const RULE = "shadcn(no-restyle)";
1414