feat(members): carve rounded TL/BL on members drawer with 12px void seam to chat and extract VoidGap helper consolidating four per-pane seams

This commit is contained in:
heaven 2026-05-14 21:11:36 +03:00
parent 8400ef54ee
commit 646cb7b124
2 changed files with 58 additions and 28 deletions

View file

@ -1,8 +1,18 @@
import { keyframes, style } from '@vanilla-extract/css'; import { keyframes, style } from '@vanilla-extract/css';
import { config, toRem } from 'folds'; import { config, toRem } from 'folds';
import { VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe';
// Left edge carves TL + BL the same way `RoomViewProfileSidePanel` does
// across the 12px horseshoe void gap rendered by Room.tsx — same design
// language as the page-nav <-> chat split. `overflow: hidden` keeps the
// rounded corners clean against header / scroll content; the void
// colour beneath is painted by the parent flex row, not by the panel
// itself.
export const MembersDrawer = style({ export const MembersDrawer = style({
width: toRem(266), width: toRem(266),
overflow: 'hidden',
borderTopLeftRadius: toRem(VOJO_HORSESHOE_RADIUS_PX),
borderBottomLeftRadius: toRem(VOJO_HORSESHOE_RADIUS_PX),
}); });
export const MembersDrawerHeader = style({ export const MembersDrawerHeader = style({

View file

@ -38,6 +38,22 @@ type RoomProps = {
renderRoomView?: (props: { eventId?: string }) => React.ReactNode; renderRoomView?: (props: { eventId?: string }) => React.ReactNode;
}; };
// 12px black seam between the chat column and the right-side pane (profile,
// media, thread, members). Every active right-side pane renders one of
// these locally; the parent flex row is painted the same void colour by
// `Room` so each pane's TL/BL rounded carve can expose the void cleanly.
function VoidGap() {
return (
<Box
shrink="No"
style={{
width: toRem(VOJO_HORSESHOE_GAP_PX),
backgroundColor: VOJO_HORSESHOE_VOID_COLOR,
}}
/>
);
}
export function Room({ renderRoomView }: RoomProps) { export function Room({ renderRoomView }: RoomProps) {
const { eventId } = useParams(); const { eventId } = useParams();
const room = useRoom(); const room = useRoom();
@ -115,18 +131,40 @@ export function Room({ renderRoomView }: RoomProps) {
// parent void can't bleed through any transparent slivers. // parent void can't bleed through any transparent slivers.
const profileOpen = !!useAtomValue(userRoomProfileAtom); const profileOpen = !!useAtomValue(userRoomProfileAtom);
const mediaOpen = !!useAtomValue(mediaViewerAtom); const mediaOpen = !!useAtomValue(mediaViewerAtom);
const callView = room.isCallRoom();
const showProfileHorseshoe = profileOpen && !isMobile && !showThreadDrawer; const showProfileHorseshoe = profileOpen && !isMobile && !showThreadDrawer;
// Media viewer side pane on desktop — same horseshoe seam idiom // Media viewer side pane on desktop — same horseshoe seam idiom
// as the profile pane. The two are mutually exclusive in practice // as the profile pane. The two are mutually exclusive in practice
// (the open hooks clear the other atom), so at most one shows at // (the open hooks clear the other atom), so at most one shows at
// a time; both feed into `showAnyHorseshoe` for the chat-column // a time; both feed into `paintParentVoid` for the chat-column bg
// bg + the void-gap render gate. // and locally gate the shared `<VoidGap />` seam.
const showMediaHorseshoe = mediaOpen && !isMobile && !showThreadDrawer; const showMediaHorseshoe = mediaOpen && !isMobile && !showThreadDrawer;
// Thread drawer side pane on desktop. Mobile hides the chat column // Thread drawer side pane on desktop. Mobile hides the chat column
// entirely (`drawerHidesChat`) so the seam doesn't apply there. // entirely (`drawerHidesChat`) so the seam doesn't apply there.
const showThreadHorseshoe = showThreadDrawer && !isMobile; const showThreadHorseshoe = showThreadDrawer && !isMobile;
const showAnyHorseshoe = // Members drawer side pane — mirrors the same horseshoe seam as the
showProfileHorseshoe || showMediaHorseshoe || showThreadHorseshoe; // profile/media/thread panes. Gated on the exact same conditions that
// mount `<MembersDrawer>` below (group room, desktop, drawer setting on,
// no thread overlay, no call surface) so the void gap appears iff the
// pane appears.
const showMembersHorseshoe =
!callView &&
!isOneOnOne &&
!showThreadDrawer &&
screenSize === ScreenSize.Desktop &&
isDrawer;
// True whenever any right-side pane is mounted. Drives the parent flex
// row's void background and the chat column's explicit Background paint
// — both prevent the chat-side surface from bleeding through the carved
// TL/BL of whichever pane is open. The per-pane `<VoidGap />` decisions
// remain local to each render site (a single `paintParentVoid` gate
// would over-render when only members is open — there is no
// profile/media slot to anchor it to).
const paintParentVoid =
showProfileHorseshoe ||
showMediaHorseshoe ||
showThreadHorseshoe ||
showMembersHorseshoe;
useKeyDown( useKeyDown(
window, window,
@ -152,8 +190,6 @@ export function Room({ renderRoomView }: RoomProps) {
) )
); );
const callView = room.isCallRoom();
// Disable the atom-driven media viewer when the desktop thread // Disable the atom-driven media viewer when the desktop thread
// drawer is open — the side-pane mount block below is gated on // drawer is open — the side-pane mount block below is gated on
// `!showThreadDrawer`, so the new viewer's right pane wouldn't // `!showThreadDrawer`, so the new viewer's right pane wouldn't
@ -181,7 +217,7 @@ export function Room({ renderRoomView }: RoomProps) {
<Box <Box
grow="Yes" grow="Yes"
style={ style={
showAnyHorseshoe paintParentVoid
? { backgroundColor: VOJO_HORSESHOE_VOID_COLOR } ? { backgroundColor: VOJO_HORSESHOE_VOID_COLOR }
: undefined : undefined
} }
@ -191,7 +227,7 @@ export function Room({ renderRoomView }: RoomProps) {
grow="Yes" grow="Yes"
direction="Column" direction="Column"
className={ className={
showAnyHorseshoe paintParentVoid
? ContainerColor({ variant: 'Background' }) ? ContainerColor({ variant: 'Background' })
: undefined : undefined
} }
@ -213,7 +249,7 @@ export function Room({ renderRoomView }: RoomProps) {
grow="Yes" grow="Yes"
direction="Column" direction="Column"
className={ className={
showAnyHorseshoe paintParentVoid
? ContainerColor({ variant: 'Background' }) ? ContainerColor({ variant: 'Background' })
: undefined : undefined
} }
@ -245,15 +281,7 @@ export function Room({ renderRoomView }: RoomProps) {
exclusive via the open hooks. */} exclusive via the open hooks. */}
{!isMobile && !showThreadDrawer && ( {!isMobile && !showThreadDrawer && (
<> <>
{showAnyHorseshoe && ( {(showProfileHorseshoe || showMediaHorseshoe) && <VoidGap />}
<Box
shrink="No"
style={{
width: toRem(VOJO_HORSESHOE_GAP_PX),
backgroundColor: VOJO_HORSESHOE_VOID_COLOR,
}}
/>
)}
<RoomViewProfileSidePanel /> <RoomViewProfileSidePanel />
<RoomViewMediaSidePanel /> <RoomViewMediaSidePanel />
</> </>
@ -277,21 +305,13 @@ export function Room({ renderRoomView }: RoomProps) {
screenSize === ScreenSize.Desktop && screenSize === ScreenSize.Desktop &&
isDrawer && ( isDrawer && (
<> <>
<Line variant="Background" direction="Vertical" size="300" /> {showMembersHorseshoe && <VoidGap />}
<MembersDrawer key={room.roomId} room={room} members={members} /> <MembersDrawer key={room.roomId} room={room} members={members} />
</> </>
)} )}
{showThreadDrawer && decodedRootId && parentRoomPath && ( {showThreadDrawer && decodedRootId && parentRoomPath && (
<> <>
{showThreadHorseshoe && ( {showThreadHorseshoe && <VoidGap />}
<Box
shrink="No"
style={{
width: toRem(VOJO_HORSESHOE_GAP_PX),
backgroundColor: VOJO_HORSESHOE_VOID_COLOR,
}}
/>
)}
<ThreadDrawer <ThreadDrawer
key={`${room.roomId}/${decodedRootId}`} key={`${room.roomId}/${decodedRootId}`}
room={room} room={room}