fix(site): wrap workspace topbar controls on narrow viewports (#27313)

On mobile-width viewports the workspace page grew to ~1080px wide (390px
viewport), pushing the schedule controls, status indicator, and action
buttons off-screen. Users had to zoom out or scroll horizontally to
reach Stop/Restart/Favorite/Share.

The topbar header and the right-side controls group used non-wrapping
flex rows, and every child is a `whitespace-nowrap` button, so the row's
minimum width (~725px for the controls alone) forced the document wider
than the viewport.

## Changes

- `WorkspaceTopbar`: allow the topbar header and the right controls
group to wrap; right controls stay right-aligned when they wrap onto
their own row.
- `WorkspaceActions`: allow the button row to wrap.
- `TopbarData`: merge a caller-provided `className` (no existing caller
passed one); the workspace breadcrumbs use it to wrap on very narrow
screens.

## Verification

Compared before/after with agent-browser on Storybook stories
(`Workspace: Running`, `WorkspaceTopbar: Outdated / WithQuotaWithOrgs /
Dormant / ConnectedWithMaxDeadline`):

| Viewport | Before `scrollWidth` | After `scrollWidth` |
|---|---|---|
| 320 | 1080 (overflow) | 305 (fits) |
| 390 | 1080 (overflow) | 375 (fits) |
| 768 | 1080 (overflow) | 753 (fits) |
| 1024 | 1080 (overflow) | 1009 (fits) |
| 1440 | 1425 (fits) | 1425, pixel-identical (0% diff) |

At widths >= 1280 the layout is unchanged (0% pixel diff at 1440 across
the stories above). Between ~1080-1230px the controls now wrap onto a
second right-aligned row instead of squeezing label text onto two lines
inside a single row.

Validation: `biome check`, `tsc --noEmit`, and `pnpm test:storybook` for
`Workspace.stories.tsx` + `WorkspaceTopbar.stories.tsx` (36 passed).

> This PR was prepared by Mux, an AI coding agent, on Mike's behalf.
This commit is contained in:
Michael Suchacz
2026-07-17 18:12:25 +02:00
committed by GitHub
parent 1ac106255b
commit 36e36e2048
3 changed files with 17 additions and 6 deletions
+10 -2
View File
@@ -44,8 +44,16 @@ export const TopbarButton: React.FC<ButtonProps> = ({ ...props }) => {
return <Button variant="outline" size="sm" {...props} />;
};
export const TopbarData: FC<HTMLAttributes<HTMLDivElement>> = (props) => {
return <div {...props} className="flex gap-2 items-center justify-center" />;
export const TopbarData: FC<HTMLAttributes<HTMLDivElement>> = ({
className,
...props
}) => {
return (
<div
{...props}
className={cn("flex gap-2 items-center justify-center", className)}
/>
);
};
export const TopbarDivider: FC<
@@ -163,7 +163,10 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
};
return (
<div className="flex items-center gap-2" data-testid="workspace-actions">
<div
className="flex flex-wrap items-center justify-end gap-2"
data-testid="workspace-actions"
>
{/* Restarting must be handled separately, because it otherwise would appear as stopping */}
{isUpdating
? buttonMapping.updating
@@ -117,7 +117,7 @@ export const WorkspaceTopbar: FC<WorkspaceTopbarProps> = ({
);
return (
<Topbar className="[grid-area:topbar]">
<Topbar className="[grid-area:topbar] flex-wrap gap-y-2">
<Tooltip>
<TooltipTrigger asChild>
<TopbarIconButton asChild>
@@ -130,7 +130,7 @@ export const WorkspaceTopbar: FC<WorkspaceTopbarProps> = ({
</Tooltip>
<div className="flex items-center gap-y-6 gap-x-2 flex-wrap px-3 py-2 mr-auto">
<TopbarData>
<TopbarData className="flex-wrap">
<OwnerBreadcrumb
ownerName={workspace.owner_name}
ownerAvatarUrl={workspace.owner_avatar_url}
@@ -217,7 +217,7 @@ export const WorkspaceTopbar: FC<WorkspaceTopbarProps> = ({
</div>
{!isImmutable && (
<div className="flex items-center gap-4">
<div className="flex flex-wrap grow items-center justify-end gap-x-4 gap-y-2 min-h-12">
<WorkspaceScheduleControls
workspace={workspace}
template={template}