fix(mobile): full-screen member profile + permissions row wrap (M4)

Mobile-audit batch 4. Desktop paths unchanged (verified by two review passes).

- UserRoomProfileRenderer: the member/room profile was always an anchored,
  fixed-width (340px), non-scrolling PopOut, so on a phone the moderation
  actions / device list / notes fell off the bottom, unreachable. On
  ScreenSize.Mobile it now renders a full-screen, internally-scrollable Modal
  with an explicit Close button (the full-screen sheet covers the backdrop and
  the profile has no self-close, so a tap-to-dismiss / X is required); desktop
  keeps the exact same anchored PopOut. Uses the provider-free useScreenSize().
- PowersEditor: the Color/Name/Power row wraps on narrow widths (wrap="Wrap")
  instead of squishing the name field; inert at desktop widths.

The mobile close button addresses a dismissal-trap both reviewers flagged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 22:30:50 -04:00
co-authored by Claude Opus 4.8
parent 836e4a6679
commit 4c298a36b4
2 changed files with 74 additions and 17 deletions
+73 -16
View File
@@ -1,5 +1,19 @@
import React from 'react';
import { Menu, PopOut, toRem } from 'folds';
import {
Box,
Header,
Icon,
IconButton,
Icons,
Menu,
Modal,
Overlay,
OverlayBackdrop,
OverlayCenter,
PopOut,
config,
toRem,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useCloseUserRoomProfile, useUserRoomProfileState } from '../state/hooks/userRoomProfile';
import { UserRoomProfile } from './user-profile';
@@ -8,6 +22,20 @@ import { useAllJoinedRoomsSet, useGetRoom } from '../hooks/useGetRoom';
import { stopPropagation } from '../utils/keyboard';
import { SpaceProvider } from '../hooks/useSpace';
import { RoomProvider } from '../hooks/useRoom';
import { ScreenSize, useScreenSize } from '../hooks/useScreenSize';
// Matches useModalStyle's mobile branch: fill the phone screen with internal
// scroll so tall profiles (moderation actions, device list, notes) are fully
// reachable — the anchored 340px popout below can't scroll and clipped them.
const MOBILE_FULLSCREEN = {
width: '100%',
height: '100%',
maxWidth: '100%',
maxHeight: '100%',
borderRadius: 0,
display: 'flex',
flexDirection: 'column',
} as const;
function UserRoomProfileContextMenu({ state }: { state: UserRoomProfileState }) {
const { roomId, spaceId, userId, cords, position } = state;
@@ -15,32 +43,61 @@ function UserRoomProfileContextMenu({ state }: { state: UserRoomProfileState })
const getRoom = useGetRoom(allJoinedRooms);
const room = getRoom(roomId);
const space = spaceId ? getRoom(spaceId) : undefined;
const screenSize = useScreenSize();
const close = useCloseUserRoomProfile();
if (!room) return null;
const profile = (
<SpaceProvider value={space ?? null}>
<RoomProvider value={room}>
<UserRoomProfile userId={userId} />
</RoomProvider>
</SpaceProvider>
);
const focusTrapOptions = {
initialFocus: false,
onDeactivate: close,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
};
// On phones, render as a full-screen scrollable modal instead of an anchored,
// fixed-width, unscrollable popout.
if (screenSize === ScreenSize.Mobile) {
return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap focusTrapOptions={focusTrapOptions}>
<Modal size="500" style={MOBILE_FULLSCREEN}>
{/* Full-screen covers the backdrop (no tap-to-dismiss) and the
profile has no self-close, so provide an explicit close. */}
<Header size="600" style={{ flexShrink: 0, paddingRight: config.space.S200 }}>
<Box grow="Yes" />
<IconButton size="300" radii="300" onClick={close} aria-label="Close">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box grow="Yes" style={{ overflow: 'hidden auto' }}>
{profile}
</Box>
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}
return (
<PopOut
anchor={cords}
position={position ?? 'Top'}
align="Start"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: close,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Menu style={{ width: toRem(340) }}>
<SpaceProvider value={space ?? null}>
<RoomProvider value={room}>
<UserRoomProfile userId={userId} />
</RoomProvider>
</SpaceProvider>
</Menu>
<FocusTrap focusTrapOptions={focusTrapOptions}>
<Menu style={{ width: toRem(340) }}>{profile}</Menu>
</FocusTrap>
}
/>
@@ -119,7 +119,7 @@ function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) {
return (
<Box onSubmit={handleSubmit} as="form" direction="Column" gap="400">
<Box direction="Column" gap="300">
<Box gap="200">
<Box gap="200" wrap="Wrap">
<Box shrink="No" direction="Column" gap="100">
<Text size="L400">Color</Text>
<Box gap="200">