| ... | ... | @@ -171,7 +171,7 @@ const LAUNCHER_SHORTCUT_BLOCKING_LAYERS = [ |
| 171 | 171 | '[data-slot="autocomplete-popup"]', |
| 172 | 172 | ].join(","); |
| 173 | 173 | |
| 174 | | /** One-line unavailability hints for the empty-state cards. */ |
| 174 | /** One-line unavailability hints for the empty-state rows. */ |
| 175 | 175 | const SURFACE_UNAVAILABLE_HINTS = { |
| 176 | 176 | browser: "Only available in the desktop app.", |
| 177 | 177 | terminal: "Available when a project is open.", |
| ... | ... | @@ -304,7 +304,7 @@ function SurfaceMenuItem(props: { |
| 304 | 304 | } |
| 305 | 305 | |
| 306 | 306 | /** |
| 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 |
| 308 | 308 | * without palette chrome: a surface's letter opens it directly from anywhere |
| 309 | 309 | * outside a typing context, and arrows plus Enter work while the launcher is |
| 310 | 310 | * focused. The highlight only appears on hover or arrow use. Unavailable |
| ... | ... | @@ -337,7 +337,6 @@ function RightPanelEmptyState(props: { |
| 337 | 337 | const actions = [ |
| 338 | 338 | { |
| 339 | 339 | label: "Browser", |
| 340 | | description: "Open a local app or URL.", |
| 341 | 340 | icon: Globe2, |
| 342 | 341 | shortcut: "B", |
| 343 | 342 | available: props.browserAvailable, |
| ... | ... | @@ -347,7 +346,6 @@ function RightPanelEmptyState(props: { |
| 347 | 346 | }, |
| 348 | 347 | { |
| 349 | 348 | label: "Terminal", |
| 350 | | description: "Start a shell in this workspace.", |
| 351 | 349 | icon: TerminalSquare, |
| 352 | 350 | shortcut: "T", |
| 353 | 351 | available: props.terminalAvailable, |
| ... | ... | @@ -357,7 +355,6 @@ function RightPanelEmptyState(props: { |
| 357 | 355 | }, |
| 358 | 356 | { |
| 359 | 357 | label: "Files", |
| 360 | | description: "Browse and read workspace files.", |
| 361 | 358 | icon: Files, |
| 362 | 359 | shortcut: "F", |
| 363 | 360 | available: props.filesAvailable, |
| ... | ... | @@ -367,7 +364,6 @@ function RightPanelEmptyState(props: { |
| 367 | 364 | }, |
| 368 | 365 | { |
| 369 | 366 | label: "Diff", |
| 370 | | description: "Review changes in this thread.", |
| 371 | 367 | icon: FileDiff, |
| 372 | 368 | shortcut: "D", |
| 373 | 369 | available: props.diffAvailable, |
| ... | ... | @@ -377,7 +373,6 @@ function RightPanelEmptyState(props: { |
| 377 | 373 | }, |
| 378 | 374 | { |
| 379 | 375 | label: "Pull request", |
| 380 | | description: "Open this branch's pull request.", |
| 381 | 376 | icon: GitPullRequest, |
| 382 | 377 | shortcut: "P", |
| 383 | 378 | available: props.pullRequestAvailable, |
| ... | ... | @@ -387,7 +382,6 @@ function RightPanelEmptyState(props: { |
| 387 | 382 | }, |
| 388 | 383 | { |
| 389 | 384 | label: "Linked pull requests", |
| 390 | | description: "Every pull request this thread has linked, stacks included.", |
| 391 | 385 | icon: GitPullRequestArrow, |
| 392 | 386 | shortcut: "L", |
| 393 | 387 | available: props.pullRequestsAvailable, |
| ... | ... | @@ -397,7 +391,6 @@ function RightPanelEmptyState(props: { |
| 397 | 391 | }, |
| 398 | 392 | { |
| 399 | 393 | label: "Agents", |
| 400 | | description: "Follow subagents and workflows.", |
| 401 | 394 | icon: Bot, |
| 402 | 395 | shortcut: "A", |
| 403 | 396 | available: props.agentsAvailable, |
| ... | ... | @@ -464,9 +457,8 @@ function RightPanelEmptyState(props: { |
| 464 | 457 | return; |
| 465 | 458 | } |
| 466 | 459 | 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; |
| 470 | 462 | const action = availableActions[highlightIndex]; |
| 471 | 463 | if (!action) return; |
| 472 | 464 | event.preventDefault(); |
| ... | ... | @@ -500,10 +492,6 @@ function RightPanelEmptyState(props: { |
| 500 | 492 | ); |
| 501 | 493 | }; |
| 502 | 494 | |
| 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 | | |
| 507 | 495 | return ( |
| 508 | 496 | <div |
| 509 | 497 | ref={focusOnMount} |
| ... | ... | @@ -514,24 +502,19 @@ function RightPanelEmptyState(props: { |
| 514 | 502 | className={cn( |
| 515 | 503 | "flex min-h-0 flex-1 items-center justify-center overflow-y-auto px-6 pt-6 outline-none", |
| 516 | 504 | // 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. |
| 518 | 506 | "pb-[calc(var(--workspace-topbar-height)+--spacing(6))]", |
| 519 | 507 | )} |
| 520 | 508 | > |
| 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"> |
| 529 | 512 | {actions.map((action) => |
| 530 | 513 | 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 |
| 532 | 515 | // 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. |
| 535 | 518 | <div |
| 536 | 519 | key={action.label} |
| 537 | 520 | className="group relative" |
| ... | ... | @@ -546,25 +529,23 @@ function RightPanelEmptyState(props: { |
| 546 | 529 | type="button" |
| 547 | 530 | onClick={action.onClick} |
| 548 | 531 | 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", |
| 555 | 534 | )} |
| 556 | 535 | > |
| 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} |
| 564 | 544 | </span> |
| 545 | <Kbd>{action.shortcut}</Kbd> |
| 565 | 546 | </button> |
| 566 | 547 | {/* |
| 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 |
| 568 | 549 | default profile, the chevron picks another. Only worth showing |
| 569 | 550 | once there is something to choose between. |
| 570 | 551 | */} |
| ... | ... | @@ -574,7 +555,7 @@ function RightPanelEmptyState(props: { |
| 574 | 555 | render={ |
| 575 | 556 | <Button |
| 576 | 557 | 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]" |
| 578 | 559 | size="icon-xs" |
| 579 | 560 | variant="ghost-muted" |
| 580 | 561 | /> |
| ... | ... | @@ -601,22 +582,21 @@ function RightPanelEmptyState(props: { |
| 601 | 582 | ) : null} |
| 602 | 583 | </div> |
| 603 | 584 | ) : ( |
| 604 | | <div |
| 585 | <DisabledReasonTooltip |
| 605 | 586 | 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 | /> |
| 620 | 600 | ), |
| 621 | 601 | )} |
| 622 | 602 | </div> |