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({
800800 </Tooltip>
801801 )}
802802 <ToggleGroup
803 className="shrink-0 gap-1"
804 size="sm"
803 aria-label="Diff layout"
804 className="shrink-0"
805 variant="segmented"
805806 value={[diffLayout]}
806807 onValueChange={(value) => {
807808 const next = value[0];
......@@ -810,10 +811,10 @@ export default function DiffPanel({
810811 }
811812 }}
812813 >
813 <Toggle aria-label="Stacked diff view" value="stacked" variant="ghost">
814 <Toggle aria-label="Stacked diff view" value="stacked">
814815 <Rows3Icon className="size-3.5" />
815816 </Toggle>
816 <Toggle aria-label="Split diff view" value="split" variant="ghost">
817 <Toggle aria-label="Split diff view" value="split">
817818 <Columns2Icon className="size-3.5" />
818819 </Toggle>
819820 </ToggleGroup>
apps/web/src/components/GitActionsControl.tsx+20-22
......@@ -42,6 +42,7 @@ import { Radio as RadioPrimitive } from "@base-ui/react/radio";
4242import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "~/components/Icons";
4343import { RadioGroup } from "~/components/ui/radio-group";
4444import { Spinner } from "~/components/ui/spinner";
45import { toggleVariants } from "~/components/ui/toggle";
4546import { cn } from "~/lib/utils";
4647import {
4748 buildGitActionProgressStages,
......@@ -831,32 +832,29 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
831832 Protocol
832833 </span>
833834 <RadioGroup
835 className="w-fit flex-row gap-0.5 rounded-lg bg-input/40 p-0.5"
834836 value={publishProtocol}
835 onValueChange={(value) =>
836 setPublishProtocol(value as SourceControlCloneProtocol)
837 }
837 onValueChange={(protocol) => {
838 if (protocol === "ssh" || protocol === "https") {
839 setPublishProtocol(protocol);
840 }
841 }}
838842 aria-labelledby="publish-protocol-label"
839843 disabled={publishRepositoryAction.isPending}
840 className="grid grid-cols-2 gap-2"
841844 >
842 {(["ssh", "https"] as const).map((value) => {
843 const isSelected = publishProtocol === value;
844 return (
845 <RadioPrimitive.Root
846 key={value}
847 value={value}
848 className={cn(
849 "rounded-md border px-3 py-1.5 text-center text-sm font-medium outline-none transition",
850 "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
851 isSelected
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"
853 : "border-border bg-background text-muted-foreground hover:border-foreground/20 hover:text-foreground dark:border-transparent dark:bg-white/[0.035]",
854 )}
855 >
856 {value === "ssh" ? "SSH" : "HTTPS"}
857 </RadioPrimitive.Root>
858 );
859 })}
845 {(["ssh", "https"] as const).map((protocol) => (
846 <RadioPrimitive.Root
847 key={protocol}
848 value={protocol}
849 data-pressed={publishProtocol === protocol ? "" : undefined}
850 className={toggleVariants({
851 variant: "segmented",
852 size: "segmented",
853 })}
854 >
855 {protocol.toUpperCase()}
856 </RadioPrimitive.Root>
857 ))}
860858 </RadioGroup>
861859 </div>
862860 </div>
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+5-4
......@@ -1138,8 +1138,9 @@ export function PullRequestCodeTab({
11381138 </Tooltip>
11391139 ) : null}
11401140 <ToggleGroup
1141 className="shrink-0 gap-1"
1142 size="sm"
1141 aria-label="Diff layout"
1142 className="shrink-0"
1143 variant="segmented"
11431144 value={[diffLayout]}
11441145 onValueChange={(value) => {
11451146 const next = value[0];
......@@ -1148,10 +1149,10 @@ export function PullRequestCodeTab({
11481149 }
11491150 }}
11501151 >
1151 <Toggle aria-label="Stacked diff view" value="stacked" variant="ghost">
1152 <Toggle aria-label="Stacked diff view" value="stacked">
11521153 <Rows3Icon className="size-3.5" />
11531154 </Toggle>
1154 <Toggle aria-label="Split diff view" value="split" variant="ghost">
1155 <Toggle aria-label="Split diff view" value="split">
11551156 <Columns2Icon className="size-3.5" />
11561157 </Toggle>
11571158 </ToggleGroup>
apps/web/src/components/pullRequest/PullRequestMarkdownEditor.tsx+14-18
......@@ -5,6 +5,7 @@ import { cn } from "~/lib/utils";
55
66import { Button } from "../ui/button";
77import { Textarea } from "../ui/textarea";
8import { Toggle, ToggleGroup } from "../ui/toggle-group";
89import { PullRequestMarkdown } from "./PullRequestMarkdown";
910
1011/**
......@@ -64,24 +65,19 @@ export function PullRequestMarkdownEditor({
6465 onCancel();
6566 }}
6667 >
67 <div className="flex items-center gap-1">
68 <Button
69 size="xs"
70 variant={preview ? "ghost" : "outline"}
71 disabled={saving}
72 onClick={() => setPreview(false)}
73 >
74 Write
75 </Button>
76 <Button
77 size="xs"
78 variant={preview ? "outline" : "ghost"}
79 disabled={saving}
80 onClick={() => setPreview(true)}
81 >
82 Preview
83 </Button>
84 </div>
68 <ToggleGroup
69 aria-label="Markdown editor mode"
70 variant="segmented"
71 value={[preview ? "preview" : "write"]}
72 disabled={saving}
73 onValueChange={(next) => {
74 const mode = next[0];
75 if (mode === "write" || mode === "preview") setPreview(mode === "preview");
76 }}
77 >
78 <Toggle value="write">Write</Toggle>
79 <Toggle value="preview">Preview</Toggle>
80 </ToggleGroup>
8581 {preview ? (
8682 <div className="rounded-lg border border-border/60 px-3 py-2">
8783 {empty ? (
apps/web/src/components/settings/DiagnosticsSettings.tsx+15-12
......@@ -36,6 +36,7 @@ import { usePrimaryEnvironment } from "../../state/environments";
3636import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";
3737import { Button } from "../ui/button";
3838import { ScrollArea } from "../ui/scroll-area";
39import { Toggle, ToggleGroup } from "../ui/toggle-group";
3940import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
4041import { toastManager } from "../ui/toast";
4142import { ExpandableText } from "./ExpandableText";
......@@ -599,21 +600,23 @@ function ResourceHistoryWindowSelector({
599600 onSelect: (windowMs: number) => void;
600601}) {
601602 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 >
603614 {RESOURCE_HISTORY_WINDOWS.map((option) => (
604 <button
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 >
615 <Toggle key={option.windowMs} value={String(option.windowMs)}>
613616 {option.label}
614 </button>
617 </Toggle>
615618 ))}
616 </div>
619 </ToggleGroup>
617620 );
618621}
619622
apps/web/src/components/settings/ProviderSettingsPanel.tsx+14-11
......@@ -72,6 +72,7 @@ import {
7272 NumberFieldInput,
7373} from "../ui/number-field";
7474import { ScrollArea } from "../ui/scroll-area";
75import { Toggle, ToggleGroup } from "../ui/toggle-group";
7576import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
7677import { stackedThreadToast, toastManager } from "../ui/toast";
7778import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog";
......@@ -80,7 +81,6 @@ import { ProviderInstanceCard } from "./ProviderInstanceCard";
8081import { UsageProviderSettings } from "./UsageProviderSettings";
8182import { ProviderSetupSection, readAntigravityAuthMethod } from "./ProviderSetupSection";
8283import { DRIVER_OPTIONS, getDriverOption } from "./providerDriverMeta";
83import { providerSettingsTabClassName } from "./providerSettingsTabs";
8484import { searchableSetting } from "./settingsSearch";
8585import {
8686 backgroundActivityOverrideSettings,
......@@ -326,22 +326,25 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) {
326326 const deviceTabs =
327327 !onlyPrimaryDevice && options.length > 0 ? (
328328 <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 >
330339 {options.map((environment) => {
331340 const machine = resolveEnvironmentMachineKind(environment.serverConfig);
332 const selected = environment.environmentId === effectiveEnvironmentId;
333341 const detail = providerEnvironmentDetail(environment);
334342 const statusText = connectionStatusTitle(environment.connection);
335343 return (
336344 <Tooltip key={environment.environmentId}>
337345 <TooltipTrigger
338346 render={
339 <button
340 type="button"
341 aria-pressed={selected}
342 className={cn(providerSettingsTabClassName(selected), "gap-2 text-left")}
343 onClick={() => setSelectedEnvironmentId(environment.environmentId)}
344 >
347 <Toggle value={environment.environmentId} className="gap-2 text-left">
345348 <EnvironmentMachineIcon
346349 kind={machine}
347350 className="size-3.5 shrink-0"
......@@ -357,7 +360,7 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) {
357360 <span className="sr-only">
358361 {detail}, {statusText}
359362 </span>
360 </button>
363 </Toggle>
361364 }
362365 />
363366 <TooltipPopup side="top">
......@@ -366,7 +369,7 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) {
366369 </Tooltip>
367370 );
368371 })}
369 </div>
372 </ToggleGroup>
370373 </ScrollArea>
371374 ) : null;
372375
apps/web/src/components/settings/ResourceTelemetryDiagnostics.tsx+13-12
......@@ -45,6 +45,7 @@ import { useAtomCommand } from "../../state/use-atom-command";
4545import { formatRelativeTime } from "../../timestampFormat";
4646import { Button } from "../ui/button";
4747import { ScrollArea } from "../ui/scroll-area";
48import { Toggle, ToggleGroup } from "../ui/toggle-group";
4849import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
4950import { toastManager } from "../ui/toast";
5051import {
......@@ -378,21 +379,21 @@ function HistoryWindowSelector({
378379 onSelect: (windowMs: number) => void;
379380}) {
380381 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 >
382391 {HISTORY_WINDOWS.map((option) => (
383 <button
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 <Toggle key={option.windowMs} value={String(option.windowMs)}>
392393 {option.label}
393 </button>
394 </Toggle>
394395 ))}
395 </div>
396 </ToggleGroup>
396397 );
397398}
398399
apps/web/src/components/settings/ThemeEditorPanel.tsx+19-12
......@@ -38,6 +38,7 @@ import { cn } from "../../lib/utils";
3838import { Button } from "../ui/button";
3939import { Input } from "../ui/input";
4040import { Switch } from "../ui/switch";
41import { Toggle, ToggleGroup } from "../ui/toggle-group";
4142import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
4243import { getThemeRoleLabel, ThemeColorField } from "./ThemeColorPicker";
4344import {
......@@ -942,24 +943,17 @@ export function ThemeEditorPanel({
942943 );
943944
944945 const renderAppearanceButton = (appearance: ThemeAppearance) => {
945 const isActive = activeAppearance === appearance;
946946 const lockReason = appearanceLockReason(appearance);
947947 // A locked mode stays hoverable so the tooltip can say why it is off;
948948 // a real disabled attribute would swallow the pointer events.
949949 const button = (
950 <Button
950 <Toggle
951951 aria-disabled={lockReason !== null}
952 aria-pressed={isActive}
953 size="sm"
952 value={appearance}
954953 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 }}
960954 >
961955 {appearance === "light" ? "Light" : "Dark"}
962 </Button>
956 </Toggle>
963957 );
964958 if (lockReason === null) return button;
965959 return (
......@@ -973,10 +967,23 @@ export function ThemeEditorPanel({
973967 const renderAppearanceButtons = () => (
974968 <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,2fr)] items-center gap-3">
975969 <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 >
977984 {renderAppearanceButton("light")}
978985 {renderAppearanceButton("dark")}
979 </div>
986 </ToggleGroup>
980987 </div>
981988 );
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
1616
1717function ToggleGroup({
1818 className,
19 variant = "default",
19 variant = "segmented",
2020 size = variant === "segmented" ? "segmented" : "default",
2121 orientation = "horizontal",
2222 children,