authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-02 15:57:50-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 15:57:50-07:00
log064392ffc7fc58fcc3018fd2ba2df33fbb21f1de
treee7f75f36425fee59c10157f7911aecdfb649d0f3
parentb59b7d0af9536a2d61ddfcf8d33420b890b2faca
signature Signed by PGP key B5690EEEBB952194

fix(web): offer browser profiles from the empty-panel launcher (#9279)


1 files changed, 69 insertions(+), 17 deletions(-)

apps/web/src/components/RightPanelTabs.tsx+69-17
...@@ -8,6 +8,7 @@ import type {...@@ -8,6 +8,7 @@ import type {
8import { getTerminalLabel } from "@t3tools/shared/terminalLabels";8import { getTerminalLabel } from "@t3tools/shared/terminalLabels";
9import {9import {
10 Bot,10 Bot,
11 ChevronDown,
11 FileDiff,12 FileDiff,
12 Files,13 Files,
13 GitPullRequest,14 GitPullRequest,
...@@ -283,6 +284,8 @@ function SurfaceMenuItem(props: {...@@ -283,6 +284,8 @@ function SurfaceMenuItem(props: {
283 */284 */
284function RightPanelEmptyState(props: {285function RightPanelEmptyState(props: {
285 onAddBrowser: () => void;286 onAddBrowser: () => void;
287 onAddBrowserInProfile: (profileId: string) => void;
288 browserProfiles: ReadonlyArray<{ readonly id: string; readonly name: string }>;
286 onAddTerminal: () => void;289 onAddTerminal: () => void;
287 onAddDiff: () => void;290 onAddDiff: () => void;
288 onAddFiles: () => void;291 onAddFiles: () => void;
...@@ -473,31 +476,78 @@ function RightPanelEmptyState(props: {...@@ -473,31 +476,78 @@ function RightPanelEmptyState(props: {
473 <div className="grid grid-cols-2 gap-2">476 <div className="grid grid-cols-2 gap-2">
474 {actions.map((action) =>477 {actions.map((action) =>
475 action.available ? (478 action.available ? (
476 <button479 // The card is itself a button, so the profile chooser sits beside
480 // it in a wrapper rather than inside it. Hover lives on the
481 // wrapper: the chooser overlays the card, and a pointer moving
482 // onto it must not read as leaving the card.
483 <div
477 key={action.label}484 key={action.label}
478 type="button"485 className="group relative"
479 onClick={action.onClick}
480 onMouseEnter={() => setHighlight(availableActions.indexOf(action))}486 onMouseEnter={() => setHighlight(availableActions.indexOf(action))}
481 onMouseLeave={() =>487 onMouseLeave={() =>
482 setHighlight((current) =>488 setHighlight((current) =>
483 current === availableActions.indexOf(action) ? -1 : current,489 current === availableActions.indexOf(action) ? -1 : current,
484 )490 )
485 }491 }
486 className={cn(
487 "relative flex w-full cursor-pointer flex-col items-start p-4 text-left transition hover:border-border hover:bg-accent/60",
488 cardShellClass,
489 isHighlighted(action) && highlightedCardClass,
490 )}
491 >492 >
492 <Kbd className="absolute top-3 right-3">{action.shortcut}</Kbd>493 <button
493 <span className="flex items-center gap-2 pe-8">494 type="button"
494 {actionIcon(action)}495 onClick={action.onClick}
495 <span className="font-medium text-sm">{action.label}</span>496 className={cn(
496 </span>497 // Full height: the wrapper is the grid item that stretches
497 <span className="mt-1.5 text-muted-foreground text-xs leading-relaxed">498 // to the row, so the button must fill it to stay level with
498 {action.description}499 // its neighbour and keep the chooser anchored inside.
499 </span>500 "relative flex h-full w-full cursor-pointer flex-col items-start p-4 text-left transition group-hover:border-border group-hover:bg-accent/60",
500 </button>501 cardShellClass,
502 isHighlighted(action) && highlightedCardClass,
503 )}
504 >
505 <Kbd className="absolute top-3 right-3">{action.shortcut}</Kbd>
506 <span className="flex items-center gap-2 pe-8">
507 {actionIcon(action)}
508 <span className="font-medium text-sm">{action.label}</span>
509 </span>
510 <span className="mt-1.5 text-muted-foreground text-xs leading-relaxed">
511 {action.description}
512 </span>
513 </button>
514 {/*
515 Same choice the tab bar's "+" menu offers: the card opens the
516 default profile, the chevron picks another. Only worth showing
517 once there is something to choose between.
518 */}
519 {action.label === "Browser" && props.browserProfiles.length > 1 ? (
520 <Menu>
521 <MenuTrigger
522 render={
523 <Button
524 aria-label="Open browser in a profile"
525 className="absolute right-3 bottom-3 [--control-icon-color:currentColor]"
526 size="icon-xs"
527 variant="ghost-muted"
528 />
529 }
530 >
531 <ChevronDown className="size-3.5" />
532 </MenuTrigger>
533 <MenuPopup
534 align="end"
535 side="bottom"
536 sideOffset={6}
537 className="min-w-40 max-w-56"
538 >
539 {props.browserProfiles.map((profile) => (
540 <MenuItem
541 key={profile.id}
542 onClick={() => props.onAddBrowserInProfile(profile.id)}
543 >
544 <span className="min-w-0 truncate">{profile.name}</span>
545 </MenuItem>
546 ))}
547 </MenuPopup>
548 </Menu>
549 ) : null}
550 </div>
501 ) : (551 ) : (
502 <div552 <div
503 key={action.label}553 key={action.label}
...@@ -1053,6 +1103,8 @@ export function RightPanelTabs(props: RightPanelTabsProps) {...@@ -1053,6 +1103,8 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
1053 {props.activeSurfaceId === null ? (1103 {props.activeSurfaceId === null ? (
1054 <RightPanelEmptyState1104 <RightPanelEmptyState
1055 onAddBrowser={props.onAddBrowser}1105 onAddBrowser={props.onAddBrowser}
1106 onAddBrowserInProfile={props.onAddBrowserInProfile}
1107 browserProfiles={browserProfiles}
1056 onAddTerminal={props.onAddTerminal}1108 onAddTerminal={props.onAddTerminal}
1057 onAddDiff={props.onAddDiff}1109 onAddDiff={props.onAddDiff}
1058 onAddFiles={props.onAddFiles}1110 onAddFiles={props.onAddFiles}