Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions packages/webapp/src/CloudApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { AccessApprovalDialog } from './AccessApprovalDialog';
import { useAccessProposals } from './use-access-proposals';
import type { ConnectionsFocus, WorkspaceDetailsTab } from './WorkspaceDetailsDialog';
import { ShellNav } from './shell/ShellNav';
import { routeShowsMobileRail } from './shell/mobile-rail';
import { isSecondaryRoute, SecondaryRoutes } from './shell/SecondaryRoutes';
import { NewTabControl } from './shell/NewTabControl';
import { WorkPanes } from './shell/WorkPanes';
Expand Down Expand Up @@ -283,9 +284,10 @@ function CloudAppContent({ client, resolver }: CloudAppProps) {
if (!mobileWebApp) setDrawerOpen(false);
}, [mobileWebApp]);

const railIsWorkspaceScreen = routeShowsMobileRail(route);
useEffect(() => {
setDrawerOpen(false);
}, [route.page, route.workspaceId]);
setDrawerOpen(mobileWebApp && railIsWorkspaceScreen);
}, [mobileWebApp, railIsWorkspaceScreen, route.workspaceId]);

useEffect(() => {
if (!mobileWebApp) return;
Expand Down Expand Up @@ -1685,7 +1687,10 @@ function CloudAppContent({ client, resolver }: CloudAppProps) {
if (mobileWebApp) setDrawerOpen(false);
setDetails({ workspaceId, tab: 'members', focusAddMember: true });
}}
onCreateWorkspace={() => setShowCreateWorkspace(true)}
onCreateWorkspace={() => {
if (mobileWebApp) setDrawerOpen(false);
setShowCreateWorkspace(true);
}}
onOpenSettings={() => navigateToSettings('profile')}
onSelectSession={selectTtydSession}
onCloseSession={closeTtydSession}
Expand Down
80 changes: 41 additions & 39 deletions packages/webapp/src/WorkspaceMembersEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,47 +248,49 @@ function MemberRow({
}}
/>
)}
{showMachine && <MachineStateChip machine={machine} pendingAction={pendingAction} />}
{showMachine && (
<MachineTypeSelect
machines={machines}
value={machineTypeId}
defaultMachineTypeId={defaultMachineTypeId}
volumeLocation={machineLocation(machine, machines)}
ariaLabel={`Machine type for ${name}`}
disabled={rowBusy}
onChange={onMachineTypeChange}
/>
)}
{showVolume && (machine === null ? (
<label className="workspace-member-volume">
<input
type="checkbox"
aria-label={`Persistent volume for ${name}`}
checked={persistentVolume}
<div className="workspace-member-machine">
{showMachine && <MachineStateChip machine={machine} pendingAction={pendingAction} />}
{showMachine && (
<MachineTypeSelect
machines={machines}
value={machineTypeId}
defaultMachineTypeId={defaultMachineTypeId}
volumeLocation={machineLocation(machine, machines)}
ariaLabel={`Machine type for ${name}`}
disabled={rowBusy}
onChange={(event) => onPersistentVolumeChange(event.currentTarget.checked)}
onChange={onMachineTypeChange}
/>
<span>Persistent volume</span>
</label>
) : (
// The disk exists, so the row reports it instead of offering a choice
// this route cannot make: how full it is, or that there is none.
<VolumeMeter volumeId={machine.volumeId} usedPercent={machine.volumeUsedPercent} />
))}
{showMachine && actions.length > 0 && (
<WebAppSelectMenu
ariaLabel={`Machine actions for ${name}`}
className="workspace-member-actions"
value=""
prefix="⋯"
options={actions.map((action) => ({ value: action, label: MACHINE_ACTION_LABELS[action] }))}
onChange={(next) => {
// SAFETY: the options are exactly the MachineAction values above.
onMachineAction?.(next as MachineAction);
}}
/>
)}
)}
{showVolume && (machine === null ? (
<label className="workspace-member-volume">
<input
type="checkbox"
aria-label={`Persistent volume for ${name}`}
checked={persistentVolume}
disabled={rowBusy}
onChange={(event) => onPersistentVolumeChange(event.currentTarget.checked)}
/>
<span>Persistent volume</span>
</label>
) : (
// The disk exists, so the row reports it instead of offering a choice
// this route cannot make: how full it is, or that there is none.
<VolumeMeter volumeId={machine.volumeId} usedPercent={machine.volumeUsedPercent} />
))}
{showMachine && actions.length > 0 && (
<WebAppSelectMenu
ariaLabel={`Machine actions for ${name}`}
className="workspace-member-actions"
value=""
prefix="⋯"
options={actions.map((action) => ({ value: action, label: MACHINE_ACTION_LABELS[action] }))}
onChange={(next) => {
// SAFETY: the options are exactly the MachineAction values above.
onMachineAction?.(next as MachineAction);
}}
/>
)}
</div>
{!readOnly && !pinned && (
<button
className="workspace-member-remove"
Expand Down
8 changes: 8 additions & 0 deletions packages/webapp/src/create-workspace-dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -491,16 +491,23 @@
width: 100%;
height: 100dvh;
min-height: 0;
overflow-y: auto;
border: 0;
}

.create-workspace-main {
flex: none;
gap: 24px;
padding: 18px 14px;
overflow: visible;
}

.create-workspace-header {
position: sticky;
z-index: 1;
top: 0;
padding-left: 12px;
background: var(--surface-2);
}

.create-workspace-header__title {
Expand All @@ -525,6 +532,7 @@

.create-workspace-actions {
display: grid;
justify-content: stretch;
gap: 9px;
padding: 12px 14px;
}}
Expand Down
81 changes: 37 additions & 44 deletions packages/webapp/src/shell/ShellNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export type ShellNavProps = {

/** Columns one and two of the shell. Above the mobile breakpoint this is
* `display: contents`, so the strip and the rail are real grid children of the
* shell; below it the wrapper becomes the off-canvas drawer and carries both. */
* shell; below it the wrapper becomes the workspace's own full-width screen and
* carries both. It covers the whole viewport there, so there is no scrim: the
* strip's own close button and the Escape key are what dismiss it. */
export function ShellNav({
workspaces,
viewer,
Expand Down Expand Up @@ -81,49 +83,40 @@ export function ShellNav({
onCloseDrawer,
}: ShellNavProps) {
return (
<>
<div className={`shell-nav${drawerOpen ? ' shell-nav--open' : ''}`}>
<WorkspaceStrip
workspaces={workspaces}
viewer={viewer}
activeWorkspaceId={activeWorkspaceId}
onSelectWorkspace={onSelectWorkspace}
onRenameWorkspace={onRenameWorkspace}
onOpenWorkspaceSettings={onOpenWorkspaceSettings}
onInviteToWorkspace={onInviteToWorkspace}
onCreateWorkspace={onCreateWorkspace}
onOpenSettings={onOpenSettings}
onCloseDrawer={onCloseDrawer}
/>
{showRail && (
<SessionRail
workspace={activeWorkspace}
sessions={sessions}
activeSessionId={activeSessionId}
livePorts={livePorts}
previewLinks={previewLinks}
{...(onVendorHost === undefined ? {} : { onVendorHost })}
{...(sessionsNeedNewerMachine === undefined ? {} : { sessionsNeedNewerMachine })}
{...(sessionsNeedMachine === undefined ? {} : { sessionsNeedMachine })}
{...(sessionsStalled === undefined ? {} : { sessionsStalled })}
onSelectSession={onSelectSession}
onCloseSession={onCloseSession}
onSpawnSession={onSpawnSession}
onOpenPreview={onOpenPreview}
onOpenPreviewLink={onOpenPreviewLink}
onOpenMembers={onOpenWorkspaceMembers}
onOpenDetails={onOpenWorkspaceDetails}
onOpenMachine={onOpenWorkspaceMachine}
/>
)}
</div>
<button
className={`shell-nav-scrim${drawerOpen ? ' shell-nav-scrim--open' : ''}`}
type="button"
aria-label="Close navigation"
tabIndex={-1}
onClick={onCloseDrawer}
<div className={`shell-nav${drawerOpen ? ' shell-nav--open' : ''}`}>
<WorkspaceStrip
workspaces={workspaces}
viewer={viewer}
activeWorkspaceId={activeWorkspaceId}
onSelectWorkspace={onSelectWorkspace}
onRenameWorkspace={onRenameWorkspace}
onOpenWorkspaceSettings={onOpenWorkspaceSettings}
onInviteToWorkspace={onInviteToWorkspace}
onCreateWorkspace={onCreateWorkspace}
onOpenSettings={onOpenSettings}
onCloseDrawer={onCloseDrawer}
/>
</>
{showRail && (
<SessionRail
workspace={activeWorkspace}
sessions={sessions}
activeSessionId={activeSessionId}
livePorts={livePorts}
previewLinks={previewLinks}
{...(onVendorHost === undefined ? {} : { onVendorHost })}
{...(sessionsNeedNewerMachine === undefined ? {} : { sessionsNeedNewerMachine })}
{...(sessionsNeedMachine === undefined ? {} : { sessionsNeedMachine })}
{...(sessionsStalled === undefined ? {} : { sessionsStalled })}
onSelectSession={onSelectSession}
onCloseSession={onCloseSession}
onSpawnSession={onSpawnSession}
onOpenPreview={onOpenPreview}
onOpenPreviewLink={onOpenPreviewLink}
onOpenMembers={onOpenWorkspaceMembers}
onOpenDetails={onOpenWorkspaceDetails}
onOpenMachine={onOpenWorkspaceMachine}
/>
)}
</div>
);
}
7 changes: 7 additions & 0 deletions packages/webapp/src/shell/mobile-rail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { AppRoute } from '../sessions-page-state';

/** On a phone the rail is the workspace's screen, so picking a session must
* read as a page change. */
export function routeShowsMobileRail(route: AppRoute): boolean {
return route.page === 'webApp' && route.chat === 'landing';
}
14 changes: 1 addition & 13 deletions packages/webapp/src/strip-rail.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
rail are real children of the shell grid. Below it, it is the drawer. */
.shell-nav { display: contents; }

.shell-nav-scrim,
.shell-strip__close { display: none; }

/* --------------------------------------------------------------- column 1 */
Expand Down Expand Up @@ -543,7 +542,7 @@
top: var(--mobile-viewport-top, 0);
left: 0;
display: flex;
width: min(88vw, 348px);
width: 100%;
height: var(--mobile-viewport-height, 100dvh);
background: var(--paper);
transform: translateX(-102%);
Expand Down Expand Up @@ -578,17 +577,6 @@

.shell-new,
.shell-s { min-height: 40px; }

.shell-nav-scrim {
position: fixed;
z-index: 380;
inset: 0;
display: none;
border: 0;
background: color-mix(in oklab, var(--paper) 68%, transparent);
}

.shell-nav-scrim--open { display: block; }
}

@media (prefers-reduced-motion: reduce) {
Expand Down
16 changes: 13 additions & 3 deletions packages/webapp/src/workspace-details-dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
.workspace-members-empty { margin: 14px 12px; color: var(--faint); font-size: 11px; }

.workspace-member-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto auto auto auto auto 28px; align-items: center; gap: 10px; min-height: 48px; padding: 0 12px; border-bottom: 0; }
.workspace-member-machine { display: contents; }
.workspace-member-row + .workspace-member-row { border-top: 1px solid var(--mod-8); }
.workspace-member-name { display: grid; min-width: 0; gap: 2px; }
.workspace-member-name strong,
Expand Down Expand Up @@ -207,9 +208,18 @@
/* Two classes, so this beats the untabbed override further down whatever
the source order. */
.workspace-details-dialog.my-machine-dialog { height: auto; max-height: calc(100dvh - 24px); }
/* The row keeps the avatar, the name and the remove control; the selects
* and the chip wrap under them rather than squeeze. */
.workspace-member-row { grid-template-columns: 28px minmax(0, 1fr) 28px; }
/* The first row keeps the person controls. Machine controls wrap in a band
* below them rather than auto-placing into narrow columns. */
.workspace-member-row { grid-template-columns: 28px minmax(0, 1fr) auto 28px; }
.workspace-member-machine {
display: flex;
order: 1;
grid-column: 1 / -1;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 0 0 10px 38px;
}
}

/* The compact machine-type select sits in a member row, so it takes its
Expand Down
6 changes: 6 additions & 0 deletions packages/webapp/test/WorkspaceDetailsDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ describe('WorkspaceDetailsDialog', () => {
// The machine state chip, and the type select on the member who holds one.
expect(view.container.textContent).toContain('running');
expect(view.container.querySelector('[aria-label="Machine type for Ada Owner"]')).not.toBeNull();
const machineBand = view.container.querySelector('.workspace-member-machine');
expect(machineBand, 'member machine controls must share one mobile band').not.toBeNull();
expect(machineBand?.querySelector('.machine-chip')).not.toBeNull();
expect(machineBand?.querySelector('[aria-label="Machine type for Ada Owner"]')).not.toBeNull();
expect(machineBand?.querySelector('[role="meter"]')).not.toBeNull();
expect(machineBand?.querySelector('[aria-label="Machine actions for Ada Owner"]')).not.toBeNull();
// A viewer never holds a machine (§2.2), so no type select and no menu.
expect(view.container.querySelector('[aria-label="Machine type for Grace Viewer"]')).toBeNull();
expect(view.container.querySelector('[aria-label="Machine actions for Grace Viewer"]')).toBeNull();
Expand Down
Loading
Loading