diff --git a/src/app/features/room/MembersDrawer.css.ts b/src/app/features/room/MembersDrawer.css.ts
index 860ceda0..e59ef7b9 100644
--- a/src/app/features/room/MembersDrawer.css.ts
+++ b/src/app/features/room/MembersDrawer.css.ts
@@ -1,8 +1,18 @@
import { keyframes, style } from '@vanilla-extract/css';
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({
width: toRem(266),
+ overflow: 'hidden',
+ borderTopLeftRadius: toRem(VOJO_HORSESHOE_RADIUS_PX),
+ borderBottomLeftRadius: toRem(VOJO_HORSESHOE_RADIUS_PX),
});
export const MembersDrawerHeader = style({
diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx
index 32edb635..e12b00b9 100644
--- a/src/app/features/room/Room.tsx
+++ b/src/app/features/room/Room.tsx
@@ -38,6 +38,22 @@ type RoomProps = {
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 (
+
+ );
+}
+
export function Room({ renderRoomView }: RoomProps) {
const { eventId } = useParams();
const room = useRoom();
@@ -115,18 +131,40 @@ export function Room({ renderRoomView }: RoomProps) {
// parent void can't bleed through any transparent slivers.
const profileOpen = !!useAtomValue(userRoomProfileAtom);
const mediaOpen = !!useAtomValue(mediaViewerAtom);
+ const callView = room.isCallRoom();
const showProfileHorseshoe = profileOpen && !isMobile && !showThreadDrawer;
// Media viewer side pane on desktop — same horseshoe seam idiom
// as the profile pane. The two are mutually exclusive in practice
// (the open hooks clear the other atom), so at most one shows at
- // a time; both feed into `showAnyHorseshoe` for the chat-column
- // bg + the void-gap render gate.
+ // a time; both feed into `paintParentVoid` for the chat-column bg
+ // and locally gate the shared `` seam.
const showMediaHorseshoe = mediaOpen && !isMobile && !showThreadDrawer;
// Thread drawer side pane on desktop. Mobile hides the chat column
// entirely (`drawerHidesChat`) so the seam doesn't apply there.
const showThreadHorseshoe = showThreadDrawer && !isMobile;
- const showAnyHorseshoe =
- showProfileHorseshoe || showMediaHorseshoe || showThreadHorseshoe;
+ // Members drawer side pane — mirrors the same horseshoe seam as the
+ // profile/media/thread panes. Gated on the exact same conditions that
+ // mount `` 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 `` 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(
window,
@@ -152,8 +190,6 @@ export function Room({ renderRoomView }: RoomProps) {
)
);
- const callView = room.isCallRoom();
-
// Disable the atom-driven media viewer when the desktop thread
// drawer is open — the side-pane mount block below is gated on
// `!showThreadDrawer`, so the new viewer's right pane wouldn't
@@ -181,7 +217,7 @@ export function Room({ renderRoomView }: RoomProps) {
- {showAnyHorseshoe && (
-
- )}
+ {(showProfileHorseshoe || showMediaHorseshoe) && }
>
@@ -277,21 +305,13 @@ export function Room({ renderRoomView }: RoomProps) {
screenSize === ScreenSize.Desktop &&
isDrawer && (
<>
-
+ {showMembersHorseshoe && }
>
)}
{showThreadDrawer && decodedRootId && parentRoomPath && (
<>
- {showThreadHorseshoe && (
-
- )}
+ {showThreadHorseshoe && }