authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-10 16:41:54-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-10 16:41:54-03:00
logdfa345b3697a9117364cc6730a1598d6c27096f2
tree47b96a8f9299fa7054907b4752c3b627dcd487e4
parentd2eeacd8ccde4d8763ca315b4cddf2964db93329
signature Signed by PGP key B5690EEEBB952194

feat(web): use a compact right-panel surface menu (#11111)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

4 files changed, 47 insertions(+), 61 deletions(-)

apps/web/src/components/DiffPanelShell.tsx+1-1
......@@ -13,7 +13,7 @@ function getDiffPanelHeaderRowClassName(mode: DiffPanelMode) {
1313 "flex items-center justify-between gap-2",
1414 mode === "embedded" ? "px-2" : "px-4",
1515 shouldUseDragRegion
16 ? "drag-region h-[52px] border-b border-border wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]"
16 ? "drag-region h-[var(--workspace-topbar-height)] border-b border-border wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]"
1717 : "flex h-10 min-h-10 shrink-0 items-center border-b border-border/60 bg-background in-data-[preview-panel-mode=inline]:mb-3 in-data-[preview-panel-mode=inline]:h-7 in-data-[preview-panel-mode=inline]:min-h-7 in-data-[preview-panel-mode=inline]:border-b-transparent",
1818 );
1919}
apps/web/src/components/RightPanelTabs.tsx+38-58
......@@ -171,7 +171,7 @@ const LAUNCHER_SHORTCUT_BLOCKING_LAYERS = [
171171 '[data-slot="autocomplete-popup"]',
172172].join(",");
173173
174/** One-line unavailability hints for the empty-state cards. */
174/** One-line unavailability hints for the empty-state rows. */
175175const SURFACE_UNAVAILABLE_HINTS = {
176176 browser: "Only available in the desktop app.",
177177 terminal: "Available when a project is open.",
......@@ -304,7 +304,7 @@ function SurfaceMenuItem(props: {
304304}
305305
306306/**
307 * Card launcher shown when the right panel has no surfaces. Keyboard-first
307 * List launcher shown when the right panel has no surfaces. Keyboard-first
308308 * without palette chrome: a surface's letter opens it directly from anywhere
309309 * outside a typing context, and arrows plus Enter work while the launcher is
310310 * focused. The highlight only appears on hover or arrow use. Unavailable
......@@ -337,7 +337,6 @@ function RightPanelEmptyState(props: {
337337 const actions = [
338338 {
339339 label: "Browser",
340 description: "Open a local app or URL.",
341340 icon: Globe2,
342341 shortcut: "B",
343342 available: props.browserAvailable,
......@@ -347,7 +346,6 @@ function RightPanelEmptyState(props: {
347346 },
348347 {
349348 label: "Terminal",
350 description: "Start a shell in this workspace.",
351349 icon: TerminalSquare,
352350 shortcut: "T",
353351 available: props.terminalAvailable,
......@@ -357,7 +355,6 @@ function RightPanelEmptyState(props: {
357355 },
358356 {
359357 label: "Files",
360 description: "Browse and read workspace files.",
361358 icon: Files,
362359 shortcut: "F",
363360 available: props.filesAvailable,
......@@ -367,7 +364,6 @@ function RightPanelEmptyState(props: {
367364 },
368365 {
369366 label: "Diff",
370 description: "Review changes in this thread.",
371367 icon: FileDiff,
372368 shortcut: "D",
373369 available: props.diffAvailable,
......@@ -377,7 +373,6 @@ function RightPanelEmptyState(props: {
377373 },
378374 {
379375 label: "Pull request",
380 description: "Open this branch's pull request.",
381376 icon: GitPullRequest,
382377 shortcut: "P",
383378 available: props.pullRequestAvailable,
......@@ -387,7 +382,6 @@ function RightPanelEmptyState(props: {
387382 },
388383 {
389384 label: "Linked pull requests",
390 description: "Every pull request this thread has linked, stacks included.",
391385 icon: GitPullRequestArrow,
392386 shortcut: "L",
393387 available: props.pullRequestsAvailable,
......@@ -397,7 +391,6 @@ function RightPanelEmptyState(props: {
397391 },
398392 {
399393 label: "Agents",
400 description: "Follow subagents and workflows.",
401394 icon: Bot,
402395 shortcut: "A",
403396 available: props.agentsAvailable,
......@@ -464,9 +457,8 @@ function RightPanelEmptyState(props: {
464457 return;
465458 }
466459 if (event.key === "Enter") {
467 // A focused card button owns its own activation; only open from the
468 // highlight when the container itself has focus.
469 if (event.target instanceof HTMLElement && event.target.closest("button")) return;
460 // Only activate the highlight when the launcher itself has focus.
461 if (event.target !== event.currentTarget) return;
470462 const action = availableActions[highlightIndex];
471463 if (!action) return;
472464 event.preventDefault();
......@@ -500,10 +492,6 @@ function RightPanelEmptyState(props: {
500492 );
501493 };
502494
503 const cardShellClass =
504 "rounded-lg border border-border/80 bg-card dark:border-transparent dark:shadow-none dark:inset-ring-1 dark:inset-ring-white/5";
505 const highlightedCardClass = "bg-accent/60 dark:inset-ring-white/20";
506
507495 return (
508496 <div
509497 ref={focusOnMount}
......@@ -514,24 +502,19 @@ function RightPanelEmptyState(props: {
514502 className={cn(
515503 "flex min-h-0 flex-1 items-center justify-center overflow-y-auto px-6 pt-6 outline-none",
516504 // The panel topbar sits above this container; matching bottom padding
517 // keeps the cards centered against the full panel, not the leftover.
505 // keeps the list centered against the full panel, not the leftover.
518506 "pb-[calc(var(--workspace-topbar-height)+--spacing(6))]",
519507 )}
520508 >
521 <div className="relative w-full max-w-lg">
522 <div className="absolute inset-x-0 bottom-full mb-5 text-center">
523 <h3 className="font-medium text-foreground text-sm">Open a surface</h3>
524 <p className="mt-1 text-muted-foreground text-xs">
525 Choose what to show in the right panel.
526 </p>
527 </div>
528 <div className="grid grid-cols-2 gap-2">
509 <div className="w-full max-w-xs">
510 <h3 className="mb-3 text-center font-medium text-foreground text-sm">Open a surface</h3>
511 <div className="flex flex-col gap-0.5">
529512 {actions.map((action) =>
530513 action.available ? (
531 // The card is itself a button, so the profile chooser sits beside
514 // The row is itself a button, so the profile chooser sits beside
532515 // it in a wrapper rather than inside it. Hover lives on the
533 // wrapper: the chooser overlays the card, and a pointer moving
534 // onto it must not read as leaving the card.
516 // wrapper: the chooser overlays the row, and a pointer moving
517 // onto it must not read as leaving the row.
535518 <div
536519 key={action.label}
537520 className="group relative"
......@@ -546,25 +529,23 @@ function RightPanelEmptyState(props: {
546529 type="button"
547530 onClick={action.onClick}
548531 className={cn(
549 // Full height: the wrapper is the grid item that stretches
550 // to the row, so the button must fill it to stay level with
551 // its neighbour and keep the chooser anchored inside.
552 "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",
553 cardShellClass,
554 isHighlighted(action) && highlightedCardClass,
532 "flex h-8 w-full cursor-pointer items-center gap-2.5 rounded-[var(--control-radius)] px-2.5 text-left text-sm transition-colors group-hover:bg-accent/60",
533 isHighlighted(action) && "bg-accent/60",
555534 )}
556535 >
557 <Kbd className="absolute top-3 right-3">{action.shortcut}</Kbd>
558 <span className="flex items-center gap-2 pe-8">
559 {actionIcon(action)}
560 <span className="font-medium text-sm">{action.label}</span>
561 </span>
562 <span className="mt-1.5 text-muted-foreground text-xs leading-relaxed">
563 {action.description}
536 {actionIcon(action, "size-4")}
537 <span
538 className={cn(
539 "min-w-0 flex-1 truncate",
540 action.label === "Browser" && props.browserProfiles.length > 1 && "pr-7",
541 )}
542 >
543 {action.label}
564544 </span>
545 <Kbd>{action.shortcut}</Kbd>
565546 </button>
566547 {/*
567 Same choice the tab bar's "+" menu offers: the card opens the
548 Same choice the tab bar's "+" menu offers: the row opens the
568549 default profile, the chevron picks another. Only worth showing
569550 once there is something to choose between.
570551 */}
......@@ -574,7 +555,7 @@ function RightPanelEmptyState(props: {
574555 render={
575556 <Button
576557 aria-label="Open browser in a profile"
577 className="absolute right-3 bottom-3 [--control-icon-color:currentColor]"
558 className="absolute top-1/2 right-8 -translate-y-1/2 [--control-icon-color:currentColor]"
578559 size="icon-xs"
579560 variant="ghost-muted"
580561 />
......@@ -601,22 +582,21 @@ function RightPanelEmptyState(props: {
601582 ) : null}
602583 </div>
603584 ) : (
604 <div
585 <DisabledReasonTooltip
605586 key={action.label}
606 className={cn(
607 "relative flex w-full flex-col items-start p-4 opacity-40",
608 cardShellClass,
609 )}
610 >
611 <Kbd className="absolute top-3 right-3">{action.shortcut}</Kbd>
612 <span className="flex items-center gap-2 pe-8">
613 {actionIcon(action)}
614 <span className="font-medium text-sm">{action.label}</span>
615 </span>
616 <span className="mt-1.5 text-muted-foreground text-xs leading-relaxed">
617 {action.disabledReason}
618 </span>
619 </div>
587 reason={action.disabledReason}
588 trigger={
589 <div
590 tabIndex={0}
591 aria-disabled="true"
592 className="flex h-8 w-full cursor-default items-center gap-2.5 rounded-[var(--control-radius)] px-2.5 text-left text-sm opacity-50"
593 >
594 {actionIcon(action, "size-4")}
595 <span className="min-w-0 flex-1 truncate">{action.label}</span>
596 <Kbd>{action.shortcut}</Kbd>
597 </div>
598 }
599 />
620600 ),
621601 )}
622602 </div>
apps/web/src/components/ui/toast.tsx+1-1
......@@ -560,7 +560,7 @@ function Toasts({ position }: { position: ToastPosition }) {
560560 <Toast.Portal data-slot="toast-portal">
561561 <Toast.Viewport
562562 className={cn(
563 "fixed z-100 mx-auto flex w-[calc(100%-var(--toast-inset)*2)] max-w-90 [--toast-header-offset:52px] [--toast-inset:--spacing(4)] sm:[--toast-inset:--spacing(8)]",
563 "fixed z-100 mx-auto flex w-[calc(100%-var(--toast-inset)*2)] max-w-90 [--toast-header-offset:var(--workspace-topbar-height)] [--toast-inset:--spacing(4)] sm:[--toast-inset:--spacing(8)]",
564564 // Vertical positioning
565565 "data-[position*=top]:top-[calc(var(--toast-inset)+var(--toast-header-offset))]",
566566 "data-[position*=bottom]:bottom-(--toast-inset)",
apps/web/src/routes/__root.tsx+7-1
......@@ -243,7 +243,13 @@ function GlassAppearanceSync() {
243243 const glassOpacity = useClientSettings((settings) => settings.glassOpacity);
244244
245245 useEffect(() => {
246 document.documentElement.style.setProperty("--glass-opacity", `${glassOpacity}%`);
246 const style = document.documentElement.style;
247 style.setProperty("--glass-opacity", `${glassOpacity}%`);
248 if (glassOpacity === 100) {
249 style.setProperty("--glass-blur", "0px");
250 } else {
251 style.removeProperty("--glass-blur");
252 }
247253 }, [glassOpacity]);
248254
249255 return null;