authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-22 12:22:11-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-22 12:22:11-07:00
logce98d4107758ed7a492f94a93b3685a828caeba1
treeaf4b0ca5130b373cd40e1cd68497d87f84c60476
parent1a81ea8c153214ddc74b7046062a8957c2612e8d
signature Signed by PGP key B5690EEEBB952194

refactor(web): mark the current menu value with MenuRadioGroup (#13022)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

4 files changed, 129 insertions(+), 111 deletions(-)

apps/web/src/components/DiffPanel.tsx+55-53
......@@ -72,7 +72,8 @@ import {
7272import {
7373 DropdownMenu,
7474 DropdownMenuContent,
75 DropdownMenuItem,
75 DropdownMenuRadioGroup,
76 DropdownMenuRadioItem,
7677 DropdownMenuSub,
7778 DropdownMenuSubContent,
7879 DropdownMenuSubTrigger,
......@@ -645,6 +646,26 @@ export default function DiffPanel({
645646 if (!routeThreadRef) return;
646647 useDiffPanelStore.getState().selectBranchBaseRef(routeThreadRef, baseRef);
647648 };
649 // The scope menu has two radio groups: the top-level one treats the latest
650 // turn as "latest", while the turn sub-menu keys every turn by id so the
651 // latest turn is also marked there.
652 const selectedTurnValue = selectedTurn ? `turn:${selectedTurn.turnId}` : "";
653 const selectedScopeValue =
654 selectedTurnId === null
655 ? selectedGitScope
656 : selectedTurn?.turnId === latestTurn?.turnId
657 ? "latest"
658 : selectedTurnValue;
659 const selectScopeValue = (value: string) => {
660 if (value === "unstaged" || value === "branch") {
661 selectGitScope(value);
662 } else if (value === "latest") {
663 if (latestTurn) selectTurn(latestTurn.turnId);
664 } else {
665 const turn = orderedTurnDiffSummaries.find((summary) => `turn:${summary.turnId}` === value);
666 if (turn) selectTurn(turn.turnId);
667 }
668 };
648669
649670 const headerRow = (
650671 <>
......@@ -658,61 +679,42 @@ export default function DiffPanel({
658679 <ChevronDownIcon className="size-3.5 shrink-0 opacity-70" />
659680 </DropdownMenuTrigger>
660681 <DropdownMenuContent align="start" className="w-60">
661 <DropdownMenuItem
662 className={
663 selectedTurnId === null && selectedGitScope === "unstaged"
664 ? "bg-foreground/[0.08]"
665 : undefined
666 }
667 onClick={() => selectGitScope("unstaged")}
668 >
669 <span>Working tree</span>
670 </DropdownMenuItem>
671 <DropdownMenuItem
672 className={
673 selectedTurnId === null && selectedGitScope === "branch"
674 ? "bg-foreground/[0.08]"
675 : undefined
676 }
677 onClick={() => selectGitScope("branch")}
678 >
679 <span>Branch changes</span>
680 </DropdownMenuItem>
681 <DropdownMenuItem
682 className={
683 selectedTurnId !== null && selectedTurn?.turnId === latestTurn?.turnId
684 ? "bg-foreground/[0.08]"
685 : undefined
686 }
687 onClick={() => {
688 if (latestTurn) selectTurn(latestTurn.turnId);
689 }}
690 >
691 <span>Latest turn</span>
692 </DropdownMenuItem>
682 <DropdownMenuRadioGroup value={selectedScopeValue} onValueChange={selectScopeValue}>
683 <DropdownMenuRadioItem value="unstaged" closeOnClick>
684 <span>Working tree</span>
685 </DropdownMenuRadioItem>
686 <DropdownMenuRadioItem value="branch" closeOnClick>
687 <span>Branch changes</span>
688 </DropdownMenuRadioItem>
689 <DropdownMenuRadioItem value="latest" closeOnClick>
690 <span>Latest turn</span>
691 </DropdownMenuRadioItem>
692 </DropdownMenuRadioGroup>
693693 <DropdownMenuSub>
694694 <DropdownMenuSubTrigger>Turn</DropdownMenuSubTrigger>
695695 <DropdownMenuSubContent className="w-64">
696 {orderedTurnDiffSummaries.map((summary) => {
697 const turnCount =
698 summary.checkpointTurnCount ??
699 inferredCheckpointTurnCountByTurnId[summary.turnId] ??
700 "?";
701 return (
702 <DropdownMenuItem
703 key={summary.turnId}
704 className={
705 summary.turnId === selectedTurn?.turnId ? "bg-foreground/[0.08]" : undefined
706 }
707 onClick={() => selectTurn(summary.turnId)}
708 >
709 <span>Turn {turnCount}</span>
710 <span className="ml-auto text-xs tabular-nums text-muted-foreground">
711 {formatShortTimestamp(summary.completedAt, settings.timestampFormat)}
712 </span>
713 </DropdownMenuItem>
714 );
715 })}
696 <DropdownMenuRadioGroup value={selectedTurnValue} onValueChange={selectScopeValue}>
697 {orderedTurnDiffSummaries.map((summary) => {
698 const turnCount =
699 summary.checkpointTurnCount ??
700 inferredCheckpointTurnCountByTurnId[summary.turnId] ??
701 "?";
702 return (
703 <DropdownMenuRadioItem
704 key={summary.turnId}
705 value={`turn:${summary.turnId}`}
706 closeOnClick
707 >
708 <span className="flex items-center gap-2">
709 <span>Turn {turnCount}</span>
710 <span className="ml-auto text-xs tabular-nums text-muted-foreground">
711 {formatShortTimestamp(summary.completedAt, settings.timestampFormat)}
712 </span>
713 </span>
714 </DropdownMenuRadioItem>
715 );
716 })}
717 </DropdownMenuRadioGroup>
716718 </DropdownMenuSubContent>
717719 </DropdownMenuSub>
718720 </DropdownMenuContent>
apps/web/src/components/files/FileBreadcrumbs.tsx+47-33
......@@ -10,6 +10,8 @@ import {
1010 MenuGroup,
1111 MenuItem,
1212 MenuPopup,
13 MenuRadioGroup,
14 MenuRadioItem,
1315 MenuSeparator,
1416 MenuTrigger,
1517} from "~/components/ui/menu";
......@@ -141,39 +143,51 @@ function BreadcrumbMenuContent(props: {
141143 : "This folder is empty."}
142144 </MenuItem>
143145 ) : (
144 children.map((entry) => {
145 const isCurrentFile = entry.kind === "file" && entry.path === props.currentFilePath;
146 return (
147 <MenuItem
148 key={entry.path}
149 closeOnClick={entry.kind === "file"}
150 aria-current={isCurrentFile ? "page" : undefined}
151 className={cn(
152 isCurrentFile && "bg-foreground/[0.08]",
153 entry.ignored && "text-muted-foreground",
154 )}
155 onClick={() => {
156 if (entry.kind === "directory") {
157 props.onDirectoryChange(entry.path);
158 return;
159 }
160 props.onOpenChange(false);
161 props.onOpenFile(entry.path);
162 }}
163 >
164 <PierreEntryIcon pathValue={entry.path} kind={entry.kind} theme={resolvedTheme} />
165 <Tooltip>
166 <TooltipTrigger render={<span className="min-w-0 flex-1 truncate" />}>
167 {entry.label}
168 </TooltipTrigger>
169 <TooltipPopup side="right" className="max-w-80">
170 {entry.path}
171 </TooltipPopup>
172 </Tooltip>
173 {entry.kind === "directory" ? <ChevronRightIcon /> : null}
174 </MenuItem>
175 );
176 })
146 // Files form a radio group keyed by path so the open file is marked as checked;
147 // directories only navigate the menu, so they stay plain items.
148 <MenuRadioGroup
149 value={props.currentFilePath}
150 onValueChange={(path) => {
151 props.onOpenChange(false);
152 props.onOpenFile(path);
153 }}
154 >
155 {children.map((entry) => {
156 const isCurrentFile = entry.kind === "file" && entry.path === props.currentFilePath;
157 const row = (
158 <>
159 <PierreEntryIcon pathValue={entry.path} kind={entry.kind} theme={resolvedTheme} />
160 <Tooltip>
161 <TooltipTrigger render={<span className="min-w-0 flex-1 truncate" />}>
162 {entry.label}
163 </TooltipTrigger>
164 <TooltipPopup side="right">{entry.path}</TooltipPopup>
165 </Tooltip>
166 </>
167 );
168 return entry.kind === "directory" ? (
169 <MenuItem
170 key={entry.path}
171 closeOnClick={false}
172 className={entry.ignored ? "text-muted-foreground" : undefined}
173 onClick={() => props.onDirectoryChange(entry.path)}
174 >
175 {row}
176 <ChevronRightIcon />
177 </MenuItem>
178 ) : (
179 <MenuRadioItem
180 key={entry.path}
181 value={entry.path}
182 closeOnClick
183 aria-current={isCurrentFile ? "page" : undefined}
184 className={entry.ignored ? "text-muted-foreground" : undefined}
185 >
186 <span className="flex min-w-0 items-center gap-2">{row}</span>
187 </MenuRadioItem>
188 );
189 })}
190 </MenuRadioGroup>
177191 )}
178192 </MenuGroup>
179193 {entriesQuery.error && entriesQuery.data !== null ? (
apps/web/src/components/pullRequest/PullRequestCodeTab.tsx+26-24
......@@ -70,6 +70,8 @@ import {
7070 DropdownMenu,
7171 DropdownMenuContent,
7272 DropdownMenuItem,
73 DropdownMenuRadioGroup,
74 DropdownMenuRadioItem,
7375 DropdownMenuTrigger,
7476} from "../ui/menu";
7577import { toastManager } from "../ui/toast";
......@@ -1076,31 +1078,31 @@ function PullRequestCodeTab({
10761078 <ChevronDownIcon className="size-3.5 shrink-0 opacity-70" />
10771079 </DropdownMenuTrigger>
10781080 <DropdownMenuContent align="start" className="w-80">
1079 <DropdownMenuItem
1080 className={commit === null ? "bg-foreground/[0.08]" : undefined}
1081 onClick={() => onSelectedCommitChange(null)}
1081 <DropdownMenuRadioGroup
1082 value={commit ?? "all"}
1083 onValueChange={(value) => onSelectedCommitChange(value === "all" ? null : value)}
10821084 >
1083 <span>All commits</span>
1084 </DropdownMenuItem>
1085 {orderedCommits.slice(0, visibleCommitCount).map((entry) => (
1086 <DropdownMenuItem
1087 key={entry.oid}
1088 className={entry.oid === commit ? "bg-foreground/[0.08]" : undefined}
1089 onClick={() => onSelectedCommitChange(entry.oid)}
1090 >
1091 {/* Headlines run long, and the abbreviated oid after one is what a reader
1092 matches against the commit list on the host. */}
1093 <Tooltip>
1094 <TooltipTrigger
1095 render={<span className="min-w-0 truncate">{entry.messageHeadline}</span>}
1096 />
1097 <TooltipPopup side="top">{entry.messageHeadline}</TooltipPopup>
1098 </Tooltip>
1099 <span className="ml-auto shrink-0 font-mono text-xs text-muted-foreground">
1100 {entry.oid.slice(0, 7)}
1101 </span>
1102 </DropdownMenuItem>
1103 ))}
1085 <DropdownMenuRadioItem value="all" closeOnClick>
1086 <span>All commits</span>
1087 </DropdownMenuRadioItem>
1088 {orderedCommits.slice(0, visibleCommitCount).map((entry) => (
1089 <DropdownMenuRadioItem key={entry.oid} value={entry.oid} closeOnClick>
1090 {/* Headlines run long, and the abbreviated oid after one is what a reader
1091 matches against the commit list on the host. */}
1092 <span className="flex items-center gap-2">
1093 <Tooltip>
1094 <TooltipTrigger
1095 render={<span className="min-w-0 truncate">{entry.messageHeadline}</span>}
1096 />
1097 <TooltipPopup side="top">{entry.messageHeadline}</TooltipPopup>
1098 </Tooltip>
1099 <span className="ml-auto shrink-0 font-mono text-xs text-muted-foreground">
1100 {entry.oid.slice(0, 7)}
1101 </span>
1102 </span>
1103 </DropdownMenuRadioItem>
1104 ))}
1105 </DropdownMenuRadioGroup>
11041106 {orderedCommits.length > visibleCommitCount ? (
11051107 // Kept out of the radio group: it changes how much of the list is on screen
11061108 // rather than what the diff is scoped to.
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 = 1165;
11export const RESTYLE_CEILING = 1159;
1212
1313const RULE = "shadcn(no-restyle)";
1414