authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-04 17:51:40-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 17:51:40-04:00
log5a2f3ebf6ee6eab6124ad68272c2fb5d74bc0f5d
tree6daffe963d655f8df9dbc6eb6dddadd893ed0aeb
parent389bbcc8d9dd5c463668944f7093052301c8a850
signature Signed by PGP key B5690EEEBB952194

fix(web): use segmented controls for mode switches (#9781)


10 files changed, 106 insertions(+), 106 deletions(-)

apps/web/src/components/DiffPanel.tsx+5-4
...@@ -800,8 +800,9 @@ export default function DiffPanel({...@@ -800,8 +800,9 @@ export default function DiffPanel({
800 </Tooltip>800 </Tooltip>
801 )}801 )}
802 <ToggleGroup802 <ToggleGroup
803 className="shrink-0 gap-1"803 aria-label="Diff layout"
804 size="sm"804 className="shrink-0"
805 variant="segmented"
805 value={[diffLayout]}806 value={[diffLayout]}
806 onValueChange={(value) => {807 onValueChange={(value) => {
807 const next = value[0];808 const next = value[0];
...@@ -810,10 +811,10 @@ export default function DiffPanel({...@@ -810,10 +811,10 @@ export default function DiffPanel({
810 }811 }
811 }}812 }}
812 >813 >
813 <Toggle aria-label="Stacked diff view" value="stacked" variant="ghost">814 <Toggle aria-label="Stacked diff view" value="stacked">
814 <Rows3Icon className="size-3.5" />815 <Rows3Icon className="size-3.5" />
815 </Toggle>816 </Toggle>
816 <Toggle aria-label="Split diff view" value="split" variant="ghost">817 <Toggle aria-label="Split diff view" value="split">
817 <Columns2Icon className="size-3.5" />818 <Columns2Icon className="size-3.5" />
818 </Toggle>819 </Toggle>
819 </ToggleGroup>820 </ToggleGroup>
apps/web/src/components/GitActionsControl.tsx+20-22
...@@ -42,6 +42,7 @@ import { Radio as RadioPrimitive } from "@base-ui/react/radio";...@@ -42,6 +42,7 @@ import { Radio as RadioPrimitive } from "@base-ui/react/radio";
42import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "~/components/Icons";42import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "~/components/Icons";
43import { RadioGroup } from "~/components/ui/radio-group";43import { RadioGroup } from "~/components/ui/radio-group";
44import { Spinner } from "~/components/ui/spinner";44import { Spinner } from "~/components/ui/spinner";
45import { toggleVariants } from "~/components/ui/toggle";
45import { cn } from "~/lib/utils";46import { cn } from "~/lib/utils";
46import {47import {
47 buildGitActionProgressStages,48 buildGitActionProgressStages,
...@@ -831,32 +832,29 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {...@@ -831,32 +832,29 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
831 Protocol832 Protocol
832 </span>833 </span>
833 <RadioGroup834 <RadioGroup
835 className="w-fit flex-row gap-0.5 rounded-lg bg-input/40 p-0.5"
834 value={publishProtocol}836 value={publishProtocol}
835 onValueChange={(value) =>837 onValueChange={(protocol) => {
836 setPublishProtocol(value as SourceControlCloneProtocol)838 if (protocol === "ssh" || protocol === "https") {
837 }839 setPublishProtocol(protocol);
840 }
841 }}
838 aria-labelledby="publish-protocol-label"842 aria-labelledby="publish-protocol-label"
839 disabled={publishRepositoryAction.isPending}843 disabled={publishRepositoryAction.isPending}
840 className="grid grid-cols-2 gap-2"
841 >844 >
842 {(["ssh", "https"] as const).map((value) => {845 {(["ssh", "https"] as const).map((protocol) => (
843 const isSelected = publishProtocol === value;846 <RadioPrimitive.Root
844 return (847 key={protocol}
845 <RadioPrimitive.Root848 value={protocol}
846 key={value}849 data-pressed={publishProtocol === protocol ? "" : undefined}
847 value={value}850 className={toggleVariants({
848 className={cn(851 variant: "segmented",
849 "rounded-md border px-3 py-1.5 text-center text-sm font-medium outline-none transition",852 size: "segmented",
850 "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",853 })}
851 isSelected854 >
852 ? "border-primary bg-background ring-2 ring-primary/35 text-foreground dark:border-transparent dark:bg-primary/10 dark:ring-1 dark:ring-primary/30"855 {protocol.toUpperCase()}
853 : "border-border bg-background text-muted-foreground hover:border-foreground/20 hover:text-foreground dark:border-transparent dark:bg-white/[0.035]",856 </RadioPrimitive.Root>
854 )}857 ))}
855 >
856 {value === "ssh" ? "SSH" : "HTTPS"}
857 </RadioPrimitive.Root>
858 );
859 })}
860 </RadioGroup>858 </RadioGroup>
861 </div>859 </div>
862 </div>860 </div>
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+5-4
...@@ -1138,8 +1138,9 @@ export function PullRequestCodeTab({...@@ -1138,8 +1138,9 @@ export function PullRequestCodeTab({
1138 </Tooltip>1138 </Tooltip>
1139 ) : null}1139 ) : null}
1140 <ToggleGroup1140 <ToggleGroup
1141 className="shrink-0 gap-1"1141 aria-label="Diff layout"
1142 size="sm"1142 className="shrink-0"
1143 variant="segmented"
1143 value={[diffLayout]}1144 value={[diffLayout]}
1144 onValueChange={(value) => {1145 onValueChange={(value) => {
1145 const next = value[0];1146 const next = value[0];
...@@ -1148,10 +1149,10 @@ export function PullRequestCodeTab({...@@ -1148,10 +1149,10 @@ export function PullRequestCodeTab({
1148 }1149 }
1149 }}1150 }}
1150 >1151 >
1151 <Toggle aria-label="Stacked diff view" value="stacked" variant="ghost">1152 <Toggle aria-label="Stacked diff view" value="stacked">
1152 <Rows3Icon className="size-3.5" />1153 <Rows3Icon className="size-3.5" />
1153 </Toggle>1154 </Toggle>
1154 <Toggle aria-label="Split diff view" value="split" variant="ghost">1155 <Toggle aria-label="Split diff view" value="split">
1155 <Columns2Icon className="size-3.5" />1156 <Columns2Icon className="size-3.5" />
1156 </Toggle>1157 </Toggle>
1157 </ToggleGroup>1158 </ToggleGroup>
apps/web/src/components/pullRequest/PullRequestMarkdownEditor.tsx+14-18
...@@ -5,6 +5,7 @@ import { cn } from "~/lib/utils";...@@ -5,6 +5,7 @@ import { cn } from "~/lib/utils";
55
6import { Button } from "../ui/button";6import { Button } from "../ui/button";
7import { Textarea } from "../ui/textarea";7import { Textarea } from "../ui/textarea";
8import { Toggle, ToggleGroup } from "../ui/toggle-group";
8import { PullRequestMarkdown } from "./PullRequestMarkdown";9import { PullRequestMarkdown } from "./PullRequestMarkdown";
910
10/**11/**
...@@ -64,24 +65,19 @@ export function PullRequestMarkdownEditor({...@@ -64,24 +65,19 @@ export function PullRequestMarkdownEditor({
64 onCancel();65 onCancel();
65 }}66 }}
66 >67 >
67 <div className="flex items-center gap-1">68 <ToggleGroup
68 <Button69 aria-label="Markdown editor mode"
69 size="xs"70 variant="segmented"
70 variant={preview ? "ghost" : "outline"}71 value={[preview ? "preview" : "write"]}
71 disabled={saving}72 disabled={saving}
72 onClick={() => setPreview(false)}73 onValueChange={(next) => {
73 >74 const mode = next[0];
74 Write75 if (mode === "write" || mode === "preview") setPreview(mode === "preview");
75 </Button>76 }}
76 <Button77 >
77 size="xs"78 <Toggle value="write">Write</Toggle>
78 variant={preview ? "outline" : "ghost"}79 <Toggle value="preview">Preview</Toggle>
79 disabled={saving}80 </ToggleGroup>
80 onClick={() => setPreview(true)}
81 >
82 Preview
83 </Button>
84 </div>
85 {preview ? (81 {preview ? (
86 <div className="rounded-lg border border-border/60 px-3 py-2">82 <div className="rounded-lg border border-border/60 px-3 py-2">
87 {empty ? (83 {empty ? (
apps/web/src/components/settings/DiagnosticsSettings.tsx+15-12
...@@ -36,6 +36,7 @@ import { usePrimaryEnvironment } from "../../state/environments";...@@ -36,6 +36,7 @@ import { usePrimaryEnvironment } from "../../state/environments";
36import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";36import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";
37import { Button } from "../ui/button";37import { Button } from "../ui/button";
38import { ScrollArea } from "../ui/scroll-area";38import { ScrollArea } from "../ui/scroll-area";
39import { Toggle, ToggleGroup } from "../ui/toggle-group";
39import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";40import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
40import { toastManager } from "../ui/toast";41import { toastManager } from "../ui/toast";
41import { ExpandableText } from "./ExpandableText";42import { ExpandableText } from "./ExpandableText";
...@@ -599,21 +600,23 @@ function ResourceHistoryWindowSelector({...@@ -599,21 +600,23 @@ function ResourceHistoryWindowSelector({
599 onSelect: (windowMs: number) => void;600 onSelect: (windowMs: number) => void;
600}) {601}) {
601 return (602 return (
602 <div className="flex items-center rounded-md border border-border/60 p-0.5">603 <ToggleGroup
604 aria-label="Process history period"
605 variant="segmented"
606 value={[String(selectedWindowMs)]}
607 onValueChange={(next) => {
608 const selected = RESOURCE_HISTORY_WINDOWS.find(
609 (option) => String(option.windowMs) === next[0],
610 );
611 if (selected) onSelect(selected.windowMs);
612 }}
613 >
603 {RESOURCE_HISTORY_WINDOWS.map((option) => (614 {RESOURCE_HISTORY_WINDOWS.map((option) => (
604 <button615 <Toggle key={option.windowMs} value={String(option.windowMs)}>
605 key={option.windowMs}
606 type="button"
607 className={cn(
608 "cursor-pointer h-6 rounded-sm px-2 text-[11px] font-medium text-muted-foreground hover:text-foreground",
609 selectedWindowMs === option.windowMs && "bg-muted text-foreground",
610 )}
611 onClick={() => onSelect(option.windowMs)}
612 >
613 {option.label}616 {option.label}
614 </button>617 </Toggle>
615 ))}618 ))}
616 </div>619 </ToggleGroup>
617 );620 );
618}621}
619622
apps/web/src/components/settings/ProviderSettingsPanel.tsx+14-11
...@@ -72,6 +72,7 @@ import {...@@ -72,6 +72,7 @@ import {
72 NumberFieldInput,72 NumberFieldInput,
73} from "../ui/number-field";73} from "../ui/number-field";
74import { ScrollArea } from "../ui/scroll-area";74import { ScrollArea } from "../ui/scroll-area";
75import { Toggle, ToggleGroup } from "../ui/toggle-group";
75import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";76import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
76import { stackedThreadToast, toastManager } from "../ui/toast";77import { stackedThreadToast, toastManager } from "../ui/toast";
77import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog";78import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog";
...@@ -80,7 +81,6 @@ import { ProviderInstanceCard } from "./ProviderInstanceCard";...@@ -80,7 +81,6 @@ import { ProviderInstanceCard } from "./ProviderInstanceCard";
80import { UsageProviderSettings } from "./UsageProviderSettings";81import { UsageProviderSettings } from "./UsageProviderSettings";
81import { ProviderSetupSection, readAntigravityAuthMethod } from "./ProviderSetupSection";82import { ProviderSetupSection, readAntigravityAuthMethod } from "./ProviderSetupSection";
82import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta";83import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta";
83import { providerSettingsTabClassName } from "./providerSettingsTabs";
84import { searchableSetting } from "./settingsSearch";84import { searchableSetting } from "./settingsSearch";
85import {85import {
86 backgroundActivityOverrideSettings,86 backgroundActivityOverrideSettings,
...@@ -326,22 +326,25 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) {...@@ -326,22 +326,25 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) {
326 const deviceTabs =326 const deviceTabs =
327 !onlyPrimaryDevice && options.length > 0 ? (327 !onlyPrimaryDevice && options.length > 0 ? (
328 <ScrollArea hideScrollbars scrollFade className="h-11 min-w-0 flex-1 rounded-none">328 <ScrollArea hideScrollbars scrollFade className="h-11 min-w-0 flex-1 rounded-none">
329 <div role="group" aria-label="Devices" className="flex h-full w-max min-w-full px-1">329 <ToggleGroup
330 aria-label="Devices"
331 variant="segmented"
332 className="my-2"
333 value={effectiveEnvironmentId ? [effectiveEnvironmentId] : []}
334 onValueChange={(next) => {
335 const environment = options.find((option) => option.environmentId === next[0]);
336 if (environment) setSelectedEnvironmentId(environment.environmentId);
337 }}
338 >
330 {options.map((environment) => {339 {options.map((environment) => {
331 const machine = resolveEnvironmentMachineKind(environment.serverConfig);340 const machine = resolveEnvironmentMachineKind(environment.serverConfig);
332 const selected = environment.environmentId === effectiveEnvironmentId;
333 const detail = providerEnvironmentDetail(environment);341 const detail = providerEnvironmentDetail(environment);
334 const statusText = connectionStatusTitle(environment.connection);342 const statusText = connectionStatusTitle(environment.connection);
335 return (343 return (
336 <Tooltip key={environment.environmentId}>344 <Tooltip key={environment.environmentId}>
337 <TooltipTrigger345 <TooltipTrigger
338 render={346 render={
339 <button347 <Toggle value={environment.environmentId} className="gap-2 text-left">
340 type="button"
341 aria-pressed={selected}
342 className={cn(providerSettingsTabClassName(selected), "gap-2 text-left")}
343 onClick={() => setSelectedEnvironmentId(environment.environmentId)}
344 >
345 <EnvironmentMachineIcon348 <EnvironmentMachineIcon
346 kind={machine}349 kind={machine}
347 className="size-3.5 shrink-0"350 className="size-3.5 shrink-0"
...@@ -357,7 +360,7 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) {...@@ -357,7 +360,7 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) {
357 <span className="sr-only">360 <span className="sr-only">
358 {detail}, {statusText}361 {detail}, {statusText}
359 </span>362 </span>
360 </button>363 </Toggle>
361 }364 }
362 />365 />
363 <TooltipPopup side="top">366 <TooltipPopup side="top">
...@@ -366,7 +369,7 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) {...@@ -366,7 +369,7 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) {
366 </Tooltip>369 </Tooltip>
367 );370 );
368 })}371 })}
369 </div>372 </ToggleGroup>
370 </ScrollArea>373 </ScrollArea>
371 ) : null;374 ) : null;
372375
apps/web/src/components/settings/ResourceTelemetryDiagnostics.tsx+13-12
...@@ -45,6 +45,7 @@ import { useAtomCommand } from "../../state/use-atom-command";...@@ -45,6 +45,7 @@ import { useAtomCommand } from "../../state/use-atom-command";
45import { formatRelativeTime } from "../../timestampFormat";45import { formatRelativeTime } from "../../timestampFormat";
46import { Button } from "../ui/button";46import { Button } from "../ui/button";
47import { ScrollArea } from "../ui/scroll-area";47import { ScrollArea } from "../ui/scroll-area";
48import { Toggle, ToggleGroup } from "../ui/toggle-group";
48import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";49import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
49import { toastManager } from "../ui/toast";50import { toastManager } from "../ui/toast";
50import {51import {
...@@ -378,21 +379,21 @@ function HistoryWindowSelector({...@@ -378,21 +379,21 @@ function HistoryWindowSelector({
378 onSelect: (windowMs: number) => void;379 onSelect: (windowMs: number) => void;
379}) {380}) {
380 return (381 return (
381 <div className="flex items-center rounded-md border border-border/60 p-0.5">382 <ToggleGroup
383 aria-label="Resource history period"
384 variant="segmented"
385 value={[String(selectedWindowMs)]}
386 onValueChange={(next) => {
387 const selected = HISTORY_WINDOWS.find((option) => String(option.windowMs) === next[0]);
388 if (selected) onSelect(selected.windowMs);
389 }}
390 >
382 {HISTORY_WINDOWS.map((option) => (391 {HISTORY_WINDOWS.map((option) => (
383 <button392 <Toggle key={option.windowMs} value={String(option.windowMs)}>
384 key={option.windowMs}
385 type="button"
386 className={cn(
387 "cursor-pointer h-6 rounded-sm px-2 text-[11px] font-medium text-muted-foreground hover:text-foreground",
388 selectedWindowMs === option.windowMs && "bg-muted text-foreground",
389 )}
390 onClick={() => onSelect(option.windowMs)}
391 >
392 {option.label}393 {option.label}
393 </button>394 </Toggle>
394 ))}395 ))}
395 </div>396 </ToggleGroup>
396 );397 );
397}398}
398399
apps/web/src/components/settings/ThemeEditorPanel.tsx+19-12
...@@ -38,6 +38,7 @@ import { cn } from "../../lib/utils";...@@ -38,6 +38,7 @@ import { cn } from "../../lib/utils";
38import { Button } from "../ui/button";38import { Button } from "../ui/button";
39import { Input } from "../ui/input";39import { Input } from "../ui/input";
40import { Switch } from "../ui/switch";40import { Switch } from "../ui/switch";
41import { Toggle, ToggleGroup } from "../ui/toggle-group";
41import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";42import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
42import { getThemeRoleLabel, ThemeColorField } from "./ThemeColorPicker";43import { getThemeRoleLabel, ThemeColorField } from "./ThemeColorPicker";
43import {44import {
...@@ -942,24 +943,17 @@ export function ThemeEditorPanel({...@@ -942,24 +943,17 @@ export function ThemeEditorPanel({
942 );943 );
943944
944 const renderAppearanceButton = (appearance: ThemeAppearance) => {945 const renderAppearanceButton = (appearance: ThemeAppearance) => {
945 const isActive = activeAppearance === appearance;
946 const lockReason = appearanceLockReason(appearance);946 const lockReason = appearanceLockReason(appearance);
947 // A locked mode stays hoverable so the tooltip can say why it is off;947 // A locked mode stays hoverable so the tooltip can say why it is off;
948 // a real disabled attribute would swallow the pointer events.948 // a real disabled attribute would swallow the pointer events.
949 const button = (949 const button = (
950 <Button950 <Toggle
951 aria-disabled={lockReason !== null}951 aria-disabled={lockReason !== null}
952 aria-pressed={isActive}952 value={appearance}
953 size="sm"
954 className={lockReason !== null ? "opacity-50" : undefined}953 className={lockReason !== null ? "opacity-50" : undefined}
955 style={isActive ? { boxShadow: "inset 0 0 0 1px var(--ring)" } : undefined}
956 variant={isActive ? "secondary" : "outline"}
957 onClick={() => {
958 if (lockReason === null) setActiveAppearance(appearance);
959 }}
960 >954 >
961 {appearance === "light" ? "Light" : "Dark"}955 {appearance === "light" ? "Light" : "Dark"}
962 </Button>956 </Toggle>
963 );957 );
964 if (lockReason === null) return button;958 if (lockReason === null) return button;
965 return (959 return (
...@@ -973,10 +967,23 @@ export function ThemeEditorPanel({...@@ -973,10 +967,23 @@ export function ThemeEditorPanel({
973 const renderAppearanceButtons = () => (967 const renderAppearanceButtons = () => (
974 <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,2fr)] items-center gap-3">968 <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,2fr)] items-center gap-3">
975 <span className="text-sm font-medium">Appearance</span>969 <span className="text-sm font-medium">Appearance</span>
976 <div aria-label="Theme appearance" className="grid grid-cols-2 gap-2" role="group">970 <ToggleGroup
971 aria-label="Theme appearance"
972 variant="segmented"
973 value={[activeAppearance]}
974 onValueChange={(next) => {
975 const appearance = next[0];
976 if (
977 (appearance === "light" || appearance === "dark") &&
978 appearanceLockReason(appearance) === null
979 ) {
980 setActiveAppearance(appearance);
981 }
982 }}
983 >
977 {renderAppearanceButton("light")}984 {renderAppearanceButton("light")}
978 {renderAppearanceButton("dark")}985 {renderAppearanceButton("dark")}
979 </div>986 </ToggleGroup>
980 </div>987 </div>
981 );988 );
982989
apps/web/src/components/settings/providerSettingsTabs.ts deleted-10
...@@ -1,10 +0,0 @@
1import { cn } from "../../lib/utils";
2
3export function providerSettingsTabClassName(selected: boolean): string {
4 return cn(
5 "relative flex h-full shrink-0 cursor-pointer items-center rounded-sm px-3 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
6 selected
7 ? "text-foreground after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:bg-primary"
8 : "text-muted-foreground hover:text-foreground",
9 );
10}
apps/web/src/components/ui/toggle-group.tsx+1-1
...@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariant...@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariant
1616
17function ToggleGroup({17function ToggleGroup({
18 className,18 className,
19 variant = "default",19 variant = "segmented",
20 size = variant === "segmented" ? "segmented" : "default",20 size = variant === "segmented" ? "segmented" : "default",
21 orientation = "horizontal",21 orientation = "horizontal",
22 children,22 children,