feat(horseshoe): add 12px void gap between chat and profile pane with rounded TL/BL on profile, drop page-nav right rounding

This commit is contained in:
heaven 2026-05-11 20:33:51 +03:00
parent de2354f1da
commit 785b679b61
4 changed files with 86 additions and 42 deletions

View file

@ -62,28 +62,12 @@ export function PageRoot({ nav, children }: PageRootProps) {
if (horseshoe) {
return (
<Box grow="Yes" className={ContainerColor({ variant: 'Background' })}>
{/* Page-nav slot paints two void squares at its top-right and
bottom-right via `background-image` (the
`linear-gradient(SAME, SAME)` idiom = a solid-colour image).
The wrapper's bg sits naturally beneath its in-flow
children, so PageNavHeader and the bottom row paint their
normal Background colour over the void everywhere except in
their own rounded TR / BR carves which expose the void.
No absolute positioning, no z-index gymnastics, and the
resize handle inside `ResizablePageNav` stays a sibling of
the clipped inner column so it isn't clipped. */}
<Box
shrink="No"
style={{
display: 'flex',
backgroundImage: `linear-gradient(${VOJO_HORSESHOE_VOID_COLOR}, ${VOJO_HORSESHOE_VOID_COLOR}), linear-gradient(${VOJO_HORSESHOE_VOID_COLOR}, ${VOJO_HORSESHOE_VOID_COLOR})`,
backgroundSize: `${horseshoeRadius} ${horseshoeRadius}, ${horseshoeRadius} ${horseshoeRadius}`,
backgroundPosition: 'top right, bottom right',
backgroundRepeat: 'no-repeat, no-repeat',
}}
>
{nav}
</Box>
{/* Page-nav slot its right edge stays square (the original
TR / BR rounding was reverted). The 12px void gap below
still creates a visible seam between page-nav and the chat
panel, and the resize handle inside `ResizablePageNav` is
shifted into that void via the inline style below. */}
{nav}
<Box
shrink="No"
style={{

View file

@ -1,7 +1,6 @@
import { style } from '@vanilla-extract/css';
import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
import { DefaultReset, color, config, toRem } from 'folds';
import { VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe';
export const PageNavResizable = style({
position: 'relative',
@ -89,20 +88,16 @@ export const PageNav = recipe({
export type PageNavVariants = RecipeVariants<typeof PageNav>;
// Web-only horseshoe shell wrapping every page-nav's inner column.
// `overflow:hidden + border-radius` clips the nav's content into a
// shape with rounded TR and BR corners; the wrapper PageRoot puts
// behind the nav paints `#090909` at those same corners via background-
// image, so the carved area reads as the horseshoe void. An explicit
// `Background.Container` bg is required: folds `<Header>` is fully
// transparent (it only sets color/border), and some routes (Bots,
// Channels) don't paint anything at the bottom — without this bg the
// PageRoot void would bleed through the entire header / footer instead
// of just the carved corner. Applied conditionally in `PageNav` /
// `ResizablePageNav` below; native skips it entirely.
// Previously carved rounded TR / BR corners against a void backdrop;
// the rounding was reverted while keeping the 12px void gap between
// page-nav and chat panel, so this class now just enforces an opaque
// Background bg + clips overflow. The bg keeps the page-nav header /
// footer painted even on routes (Bots, Channels) that don't paint
// anything of their own; `overflow:hidden` is defensive against any
// child that might bleed past the inner column. Applied conditionally
// in `PageNav` / `ResizablePageNav` below; native skips it entirely.
export const PageNavInnerWebHorseshoe = style({
overflow: 'hidden',
borderTopRightRadius: toRem(VOJO_HORSESHOE_RADIUS_PX),
borderBottomRightRadius: toRem(VOJO_HORSESHOE_RADIUS_PX),
backgroundColor: color.Background.Container,
});

View file

@ -1,9 +1,12 @@
import React, { useCallback } from 'react';
import { Box, Line } from 'folds';
import { Box, Line, toRem } from 'folds';
import { useMatch, useParams } from 'react-router-dom';
import { isKeyHotkey } from 'is-hotkey';
import { useAtomValue } from 'jotai';
import { RoomView } from './RoomView';
import { userRoomProfileAtom } from '../../state/userRoomProfile';
import { ContainerColor } from '../../styles/ContainerColor.css';
import { VOJO_HORSESHOE_GAP_PX, VOJO_HORSESHOE_VOID_COLOR } from '../../styles/horseshoe';
import { MembersDrawer } from './MembersDrawer';
import { ThreadDrawer } from './ThreadDrawer';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
@ -91,6 +94,24 @@ export function Room({ renderRoomView }: RoomProps) {
const unreadThreadingEnabled = useUnreadThreadingEnabled();
// Profile horseshoe: when the right-side profile pane is open on
// desktop/tablet, paint a 12px void seam between the chat column and
// the profile, and carve rounded TL/BL corners on the profile pane.
// The chat column itself keeps a straight right edge (no rounding) —
// only the profile carves, so the void gap reads as one black bar
// with a single rounded contour on the right side of the seam.
// Mirrors the page-nav <-> chat split from PageRoot (commit 363bd9d)
// minus the chat-side rounding. Mobile and the thread-drawer case
// don't mount the side pane, so the wrap is gated on both.
//
// Void colour is painted on the parent flex row (covering everything
// not painted by an opaque child) so the profile pane's TL/BL carves
// expose void rather than the chat-panel-inner's Background from
// PageRoot. The chat column applies an explicit Background bg so the
// parent void can't bleed through any transparent slivers.
const profileOpen = !!useAtomValue(userRoomProfileAtom);
const showProfileHorseshoe = profileOpen && !isMobile && !showThreadDrawer;
useKeyDown(
window,
useCallback(
@ -120,9 +141,22 @@ export function Room({ renderRoomView }: RoomProps) {
return (
<PowerLevelsContextProvider value={powerLevels}>
<ThreadDrawerOpenProvider value={showThreadDrawer}>
<Box grow="Yes">
<Box
grow="Yes"
style={
showProfileHorseshoe
? { backgroundColor: VOJO_HORSESHOE_VOID_COLOR }
: undefined
}
>
{callView && (screenSize === ScreenSize.Desktop || !chat) && (
<Box grow="Yes" direction="Column">
<Box
grow="Yes"
direction="Column"
className={
showProfileHorseshoe ? ContainerColor({ variant: 'Background' }) : undefined
}
>
<RoomViewProfilePanel header={<RoomViewHeader callView />}>
<Box grow="Yes">
<CallView />
@ -131,7 +165,13 @@ export function Room({ renderRoomView }: RoomProps) {
</Box>
)}
{!callView && !drawerHidesChat && (
<Box grow="Yes" direction="Column">
<Box
grow="Yes"
direction="Column"
className={
showProfileHorseshoe ? ContainerColor({ variant: 'Background' }) : undefined
}
>
<RoomViewProfilePanel header={<RoomViewHeader />}>
<Box grow="Yes">
{renderRoomView?.({ eventId }) ?? <RoomView eventId={eventId} />}
@ -143,8 +183,23 @@ export function Room({ renderRoomView }: RoomProps) {
{/* Tablet / Desktop: profile renders as a third pane to the
right of the chat. Mobile uses the top horseshoe inside
`RoomViewProfilePanel`, so we don't mount the side pane
there. */}
{!isMobile && !showThreadDrawer && <RoomViewProfileSidePanel />}
there. The 12px void gap (same as page-nav <-> chat split
from PageRoot) sits between the chat column and the pane
so the seam reads identically to the rest of the app. */}
{!isMobile && !showThreadDrawer && (
<>
{showProfileHorseshoe && (
<Box
shrink="No"
style={{
width: toRem(VOJO_HORSESHOE_GAP_PX),
backgroundColor: VOJO_HORSESHOE_VOID_COLOR,
}}
/>
)}
<RoomViewProfileSidePanel />
</>
)}
{callView && chat && (
<>

View file

@ -1,18 +1,28 @@
import { style } from '@vanilla-extract/css';
import { color, config, toRem } from 'folds';
import { VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe';
// Right-side profile pane sized like the members drawer family —
// wide enough for the identity card + chips without dominating the
// chat. Clamp keeps it readable on narrow desktops and prevents
// runaway width on ultra-wide displays.
//
// Left edge is rounded (TL + BL) to mirror the chat column's TR / BR
// carves 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 the header /
// scroll content; the void colour beneath is painted by the parent
// flex row, not by the panel itself.
export const panel = style({
flexShrink: 0,
width: `clamp(${toRem(300)}, 25%, ${toRem(380)})`,
display: 'flex',
flexDirection: 'column',
backgroundColor: color.Surface.Container,
borderLeft: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
minHeight: 0,
overflow: 'hidden',
borderTopLeftRadius: toRem(VOJO_HORSESHOE_RADIUS_PX),
borderBottomLeftRadius: toRem(VOJO_HORSESHOE_RADIUS_PX),
});
// Match the chat header's left gutter (RoomViewHeaderDm uses S200