authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-22 21:33:01-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-22 21:33:01-07:00
log866fc907592c9746b407484087d66c409e8034fa
tree5c2f1f7ff106515694e5b2e1449721cd322284b7
parent361b047f5f559abccacc802cd5ab9992b01473ea
signature Signed by PGP key B5690EEEBB952194

refactor(web): menu triggers and items stop restyling ui/menu (#13205)

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

12 files changed, 78 insertions(+), 96 deletions(-)

apps/web/src/components/BranchToolbar.tsx+3-3
......@@ -41,7 +41,7 @@ import {
4141} from "./BranchToolbarBranchSelector";
4242import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector";
4343import { BranchToolbarEnvModeSelector } from "./BranchToolbarEnvModeSelector";
44import { Button } from "./ui/button";
44import { ComposerControl } from "./chat/ComposerControl";
4545import {
4646 Menu,
4747 MenuGroup,
......@@ -206,8 +206,8 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
206206 return (
207207 <Menu>
208208 <MenuTrigger
209 render={<Button variant="ghost" size="xs" />}
210 className="min-w-0 max-w-[48%] flex-initial justify-start font-normal text-muted-foreground/70 text-xs! hover:text-foreground/80"
209 render={<ComposerControl size="xs" />}
210 className="min-w-0 max-w-[48%] flex-initial justify-start"
211211 data-composer-context-control
212212 data-composer-shortcut={[
213213 showEnvironmentPicker && !envLocked ? "composer.host" : "",
apps/web/src/components/LegacySidebar.tsx+1-3
......@@ -2732,9 +2732,7 @@ function ProjectSortMenu({
27322732 <Menu>
27332733 <Tooltip>
27342734 <TooltipTrigger
2735 render={
2736 <MenuTrigger className="inline-flex h-6 min-w-6 cursor-pointer items-center justify-center rounded-md px-[calc(--spacing(1)-1px)] text-icon-muted transition-colors hover:bg-accent hover:text-foreground" />
2737 }
2735 render={<MenuTrigger render={<Button size="icon-xs" variant="ghost-muted" />} />}
27382736 >
27392737 <ArrowUpDownIcon className="size-3.5" />
27402738 </TooltipTrigger>
apps/web/src/components/ProjectScriptsControl.tsx+12-23
......@@ -101,8 +101,6 @@ export default function ProjectScriptsControl({
101101 ),
102102 [fileScripts, scripts],
103103 );
104 const dropdownItemClassName =
105 "data-highlighted:bg-transparent data-highlighted:text-foreground hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground data-highlighted:hover:bg-accent data-highlighted:hover:text-accent-foreground data-highlighted:focus-visible:bg-accent data-highlighted:focus-visible:text-accent-foreground";
106104
107105 const openAddDialog = () => {
108106 setEditorRequest({ scriptId: null, initial: EMPTY_PROJECT_SCRIPT_INPUT });
......@@ -153,7 +151,6 @@ export default function ProjectScriptsControl({
153151 <MenuItem
154152 density={presentation === "menu" ? "touch" : "default"}
155153 key={`${fileScript.name} ${fileScript.command}`}
156 className={dropdownItemClassName}
157154 onClick={() => void importFileScript(fileScript)}
158155 >
159156 <ScriptIcon icon={fileScript.icon ?? "play"} className="size-4" />
......@@ -178,7 +175,7 @@ export default function ProjectScriptsControl({
178175 <MenuItem
179176 density={presentation === "menu" ? "touch" : "default"}
180177 key={script.id}
181 className={`group ${dropdownItemClassName}`}
178 className="group"
182179 onClick={() => onRunScript(script)}
183180 >
184181 <ScriptIcon icon={script.icon} className="size-4" />
......@@ -186,17 +183,15 @@ export default function ProjectScriptsControl({
186183 {script.runOnWorktreeCreate ? `${script.name} (setup)` : script.name}
187184 </MenuItemLabel>
188185 <span className="relative ms-auto flex h-6 min-w-6 items-center justify-end">
189 {shortcutLabel && (
190 <MenuShortcut
191 className={
192 presentation === "menu"
193 ? "ms-0 mr-7"
194 : "ms-0 transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0"
195 }
196 >
197 {shortcutLabel}
198 </MenuShortcut>
199 )}
186 {shortcutLabel &&
187 (presentation === "menu" ? (
188 <MenuShortcut className="ms-0 mr-7">{shortcutLabel}</MenuShortcut>
189 ) : (
190 // The shortcut yields its slot to the edit button on hover.
191 <span className="transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0">
192 <MenuShortcut className="ms-0">{shortcutLabel}</MenuShortcut>
193 </span>
194 ))}
200195 <Button
201196 type="button"
202197 variant="ghost"
......@@ -220,11 +215,7 @@ export default function ProjectScriptsControl({
220215 );
221216 })}
222217 {importMenuItems}
223 <MenuItem
224 density={presentation === "menu" ? "touch" : "default"}
225 className={dropdownItemClassName}
226 onClick={openAddDialog}
227 >
218 <MenuItem density={presentation === "menu" ? "touch" : "default"} onClick={openAddDialog}>
228219 <PlusIcon className="size-4" />
229220 <MenuItemLabel>Add action</MenuItemLabel>
230221 </MenuItem>
......@@ -296,7 +287,6 @@ export default function ProjectScriptsControl({
296287 </Tooltip>
297288 <GroupSeparator className="hidden @3xl/header-actions:block" />
298289 <Menu
299 highlightItemOnHover={false}
300290 open={actionsMenuOpen.scripts}
301291 onOpenChange={(open) =>
302292 setActionsMenuOpen({ presentation, scripts: open, imports: false })
......@@ -312,7 +302,6 @@ export default function ProjectScriptsControl({
312302 </Group>
313303 ) : importableScripts.length > 0 ? (
314304 <Menu
315 highlightItemOnHover={false}
316305 open={actionsMenuOpen.imports}
317306 onOpenChange={(open) =>
318307 setActionsMenuOpen({ presentation, scripts: false, imports: open })
......@@ -327,7 +316,7 @@ export default function ProjectScriptsControl({
327316 </MenuTrigger>
328317 <MenuPopup align="end">
329318 {importMenuItems}
330 <MenuItem className={dropdownItemClassName} onClick={openAddDialog}>
319 <MenuItem onClick={openAddDialog}>
331320 <PlusIcon className="size-4" />
332321 Add action
333322 </MenuItem>
apps/web/src/components/chat/DraftHeroHeadline.tsx+23-22
......@@ -29,6 +29,7 @@ import {
2929 MenuTrigger,
3030} from "../ui/menu";
3131import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
32import { InlineButton } from "../ui/button";
3233import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
3334
3435interface DraftHeroHeadlineProps {
......@@ -140,10 +141,13 @@ export function DraftHeroHeadline({
140141 // project title) so the hero sentence reads naturally: an
141142 // aria-label here would replace the title with an action phrase
142143 // mid-sentence and baffle screen-reader users.
143 <MenuTrigger className="pointer-events-auto inline-block max-w-64 truncate border-foreground/60 border-b border-dotted align-baseline text-foreground transition-colors hover:border-foreground/80 focus-visible:rounded-sm focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring" />
144 <MenuTrigger
145 render={<InlineButton tone="picker" />}
146 className="pointer-events-auto max-w-64 align-baseline"
147 />
144148 }
145149 >
146 {activeProjectDisplayName ?? "Choose a project"}
150 <span className="min-w-0 truncate">{activeProjectDisplayName ?? "Choose a project"}</span>
147151 </TooltipTrigger>
148152 {activeProjectDisplayName ? (
149153 <TooltipPopup side="top">{activeProjectDisplayName}</TooltipPopup>
......@@ -189,26 +193,23 @@ export function DraftHeroHeadline({
189193 >
190194 {projectPickerEntries.map(({ group }) => {
191195 return (
192 <MenuRadioItem
193 key={group.projectKey}
194 value={group.projectKey}
195 closeOnClick
196 className="[&>span:last-child]:flex [&>span:last-child]:min-w-0 [&>span:last-child]:items-center [&>span:last-child]:gap-2"
197 >
198 <ProjectFavicon project={group} className="size-4 shrink-0" />
199 <Tooltip>
200 <TooltipTrigger render={<span className="block min-w-0 truncate" />}>
201 {group.displayName}
202 </TooltipTrigger>
203 <TooltipPopup side="top">{group.displayName}</TooltipPopup>
204 </Tooltip>
205 {showProjectEnvironments ? (
206 <ProjectEnvironmentBadge
207 group={group}
208 primaryEnvironmentId={primaryEnvironmentId}
209 machineByEnvironmentId={environmentMachineById}
210 />
211 ) : null}
196 <MenuRadioItem key={group.projectKey} value={group.projectKey} closeOnClick>
197 <span className="flex min-w-0 items-center gap-2">
198 <ProjectFavicon project={group} className="size-4 shrink-0" />
199 <Tooltip>
200 <TooltipTrigger render={<span className="block min-w-0 truncate" />}>
201 {group.displayName}
202 </TooltipTrigger>
203 <TooltipPopup side="top">{group.displayName}</TooltipPopup>
204 </Tooltip>
205 {showProjectEnvironments ? (
206 <ProjectEnvironmentBadge
207 group={group}
208 primaryEnvironmentId={primaryEnvironmentId}
209 machineByEnvironmentId={environmentMachineById}
210 />
211 ) : null}
212 </span>
212213 </MenuRadioItem>
213214 );
214215 })}
apps/web/src/components/files/FileBreadcrumbs.tsx+10-3
......@@ -155,7 +155,16 @@ function BreadcrumbMenuContent(props: {
155155 <>
156156 <PierreEntryIcon pathValue={entry.path} kind={entry.kind} theme={resolvedTheme} />
157157 <Tooltip>
158 <TooltipTrigger render={<span className="min-w-0 flex-1 truncate" />}>
158 <TooltipTrigger
159 render={
160 <span
161 className={cn(
162 "min-w-0 flex-1 truncate",
163 entry.ignored && "text-muted-foreground",
164 )}
165 />
166 }
167 >
159168 {entry.label}
160169 </TooltipTrigger>
161170 <TooltipPopup side="right">{entry.path}</TooltipPopup>
......@@ -166,7 +175,6 @@ function BreadcrumbMenuContent(props: {
166175 <MenuItem
167176 key={entry.path}
168177 closeOnClick={false}
169 className={entry.ignored ? "text-muted-foreground" : undefined}
170178 onClick={() => props.onDirectoryChange(entry.path)}
171179 >
172180 {row}
......@@ -178,7 +186,6 @@ function BreadcrumbMenuContent(props: {
178186 value={entry.path}
179187 closeOnClick
180188 aria-current={isCurrentFile ? "page" : undefined}
181 className={entry.ignored ? "text-muted-foreground" : undefined}
182189 >
183190 <span className="flex min-w-0 items-center gap-2">{row}</span>
184191 </MenuRadioItem>
apps/web/src/components/preview/PreviewMoreMenu.tsx+5-4
......@@ -206,10 +206,11 @@ export function PreviewMoreMenu({
206206 each one drove the popup far past its width.
207207 */}
208208 {profileName ? (
209 // Truncation sits on the label itself: it renders a block box, so
210 // `text-overflow` on an inline child inside it never applies and a
211 // long name would push the popup past its width instead.
212 <MenuGroupLabel className="max-w-64 truncate">Profile: {profileName}</MenuGroupLabel>
209 // Truncation needs a block box: `text-overflow` on an inline child
210 // never applies and a long name would push the popup past its width.
211 <MenuGroupLabel className="max-w-64">
212 <span className="block truncate">Profile: {profileName}</span>
213 </MenuGroupLabel>
213214 ) : null}
214215 <MenuItem
215216 onClick={() =>
apps/web/src/components/pullRequest/PullRequestStackHeader.tsx+12-10
......@@ -11,16 +11,18 @@ export function PullRequestStackHeader({
1111 stale?: boolean;
1212}) {
1313 return (
14 <MenuGroupLabel className="flex items-center justify-between gap-2">
15 <span>Stack #{number}</span>
16 {notice ? (
17 <Tooltip>
18 <TooltipTrigger render={<span role="status" className="text-xs font-normal" />}>
19 {stale ? "May be stale" : "Refreshing…"}
20 </TooltipTrigger>
21 <TooltipPopup>{notice}</TooltipPopup>
22 </Tooltip>
23 ) : null}
14 <MenuGroupLabel>
15 <div className="flex items-center justify-between gap-2">
16 <span>Stack #{number}</span>
17 {notice ? (
18 <Tooltip>
19 <TooltipTrigger render={<span role="status" className="text-xs font-normal" />}>
20 {stale ? "May be stale" : "Refreshing…"}
21 </TooltipTrigger>
22 <TooltipPopup>{notice}</TooltipPopup>
23 </Tooltip>
24 ) : null}
25 </div>
2426 </MenuGroupLabel>
2527 );
2628}
apps/web/src/components/settings/EnvironmentIconPicker.tsx+1-1
......@@ -102,7 +102,7 @@ export function EnvironmentIconMenu({
102102 <MenuSubPopup>
103103 {lock !== null ? (
104104 <>
105 <MenuItem disabled className="whitespace-normal text-xs">
105 <MenuItem disabled className="whitespace-normal">
106106 {lock}
107107 </MenuItem>
108108 <MenuSeparator />
apps/web/src/components/settings/SnapShotSettings.tsx+3-16
......@@ -54,10 +54,7 @@ import {
5454 type CaptureSetupStep,
5555} from "./SnapShotSetupDialog.logic";
5656
57const soundOptionRowClassName =
58 "grid grid-cols-[1fr_auto] rounded-sm has-data-checked:bg-foreground/[0.08]";
59const soundOptionItemClassName = "data-checked:bg-transparent";
60const soundPreviewClassName = "min-h-7 w-7 justify-center px-0";
57const soundOptionRowClassName = "grid grid-cols-[1fr_auto]";
6158
6259function captureSettingsError(title: string, error: unknown) {
6360 return { title, message: error instanceof Error ? error.message : "Try again." };
......@@ -520,16 +517,11 @@ export function SnapShotSettings() {
520517 Off
521518 </MenuRadioItem>
522519 <div className={soundOptionRowClassName}>
523 <MenuRadioItem
524 className={soundOptionItemClassName}
525 closeOnClick
526 value="soft-pop"
527 >
520 <MenuRadioItem closeOnClick value="soft-pop">
528521 Whoosh <span className="text-muted-foreground">(Default)</span>
529522 </MenuRadioItem>
530523 <MenuItem
531524 aria-label="Play Whoosh"
532 className={soundPreviewClassName}
533525 closeOnClick={false}
534526 onClick={() => playSnapShotSound("soft-pop")}
535527 >
......@@ -537,16 +529,11 @@ export function SnapShotSettings() {
537529 </MenuItem>
538530 </div>
539531 <div className={soundOptionRowClassName}>
540 <MenuRadioItem
541 className={soundOptionItemClassName}
542 closeOnClick
543 value="camera-shutter"
544 >
532 <MenuRadioItem closeOnClick value="camera-shutter">
545533 Click
546534 </MenuRadioItem>
547535 <MenuItem
548536 aria-label="Play Click"
549 className={soundPreviewClassName}
550537 closeOnClick={false}
551538 onClick={() => playSnapShotSound("camera-shutter")}
552539 >
apps/web/src/components/usage/UsagePage.tsx+1-1
......@@ -738,7 +738,7 @@ function UsageEnvironmentFilter({
738738 <Menu>
739739 <MenuTrigger
740740 render={<InlineButton />}
741 className="group/usage-environment min-w-0 max-w-full gap-1"
741 className="group/usage-environment min-w-0 max-w-full"
742742 >
743743 <span className="min-w-0 truncate">{label}</span>
744744 <span className="flex size-3.5 shrink-0 items-center justify-center text-muted-foreground">
apps/web/src/routes/_chat.pull-requests.tsx+6-9
......@@ -2234,16 +2234,13 @@ function CompactFilterMenu<Value extends string>({
22342234 aria-label={triggerLabel || iconOnly ? `${label}: ${current.label}` : label}
22352235 title={iconOnly ? `${label}: ${current.label}` : undefined}
22362236 render={
2237 outlined ? <Button variant="outline" size={iconOnly ? "icon" : "default"} /> : undefined
2238 }
2239 className={
2240 outlined
2241 ? className
2242 : cn(
2243 "inline-flex h-7 min-w-0 items-center gap-1 rounded-md px-1.5 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground",
2244 className,
2245 )
2237 outlined ? (
2238 <Button variant="outline" size={iconOnly ? "icon" : "default"} />
2239 ) : (
2240 <Button variant="ghost-muted" size="sm" />
2241 )
22462242 }
2243 className={cn("min-w-0", className)}
22472244 >
22482245 {iconOnly ? (
22492246 <current.Icon aria-hidden className="size-4" />
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 = 582;
11export const RESTYLE_CEILING = 499;
1212
1313const RULE = "shadcn(no-restyle)";
1414