250 lines
9.6 KiB
TypeScript
250 lines
9.6 KiB
TypeScript
import { style } from '@vanilla-extract/css';
|
||
import { color, toRem } from 'folds';
|
||
|
||
// Shared with the call-horseshoe surface so both ends of the chat
|
||
// (top profile rail, bottom call rail) read with identical geometry.
|
||
export const HORSESHOE_RADIUS_PX = 32;
|
||
export const HORSESHOE_GAP_PX = 12;
|
||
|
||
// Outer container — flex column hosting the silhouette (panel +
|
||
// header live inside) and the chat body below. `overflow: hidden`
|
||
// clips the chat body's rounded top corners against the container's
|
||
// own edges when the user-card rail is open and the chatBody margin-
|
||
// top void gap opens up; without this clip the rounded-corner cut
|
||
// areas would reveal whatever's behind the chat column instead of
|
||
// the horseshoe void colour painted by `containerStyle`.
|
||
export const container = style({
|
||
position: 'relative',
|
||
display: 'flex',
|
||
flex: 1,
|
||
flexDirection: 'column',
|
||
minWidth: 0,
|
||
minHeight: 0,
|
||
overflow: 'hidden',
|
||
});
|
||
|
||
// === Silhouette === Single-element wrapper that owns the
|
||
// rounded-bottom geometry shared by the user-card panel and the chat
|
||
// header. Both live as direct children of this one box, so a single
|
||
// `border-bottom-{left,right}-radius` cuts the bottom of the
|
||
// combined block — there's no «hand-off» from header-bottom-radius
|
||
// to panel-bottom-radius mid-drag because there's only one radius
|
||
// to begin with. Replaces the legacy `horseshoeEmerge` curve that
|
||
// ramped two separate radii in the last 15% of drag.
|
||
//
|
||
// `overflow: hidden` is what makes the radius actually crop content
|
||
// (without it the rounded bottom would just be an SVG-like outline
|
||
// behind opaque children). The flex column stacks panelViewport (top)
|
||
// over headerViewport (bottom), so the silhouette's bottom edge
|
||
// always coincides with whichever child is currently last-visible —
|
||
// which is exactly the "bottom edge of the silhouette" the rounding
|
||
// sits on.
|
||
export const silhouette = style({
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
flexShrink: 0,
|
||
overflow: 'hidden',
|
||
backgroundColor: color.Background.Container,
|
||
willChange: 'border-bottom-left-radius, border-bottom-right-radius',
|
||
});
|
||
|
||
// === Panel viewport === Variable-height clipping window for the
|
||
// fixed-height panel content (railHeight tall, anchored bottom:0).
|
||
// As the viewport grows from 0 to railHeight during drag, more of
|
||
// panelContent is revealed from its bottom edge upward
|
||
// (handle → scroll content → avatar top) — equivalent visual to the
|
||
// legacy `transform: translateY(expandedPx - railHeight)` trick that
|
||
// shifted the whole panel down from above viewport.
|
||
//
|
||
// `touchAction: pan-y` keeps the panel-internal scroll responsive;
|
||
// the drag-up close gesture preventDefault's explicitly inside the
|
||
// touchmove handler. `userSelect: none` keeps mouse drag clean — on
|
||
// the pointer path a left-button drag would otherwise race the
|
||
// browser's native text-selection drag (selection highlight grows
|
||
// during the gesture and the user has to click once to deselect
|
||
// after release).
|
||
export const panelViewport = style({
|
||
position: 'relative',
|
||
width: '100%',
|
||
overflow: 'hidden',
|
||
willChange: 'height',
|
||
touchAction: 'pan-y',
|
||
userSelect: 'none',
|
||
});
|
||
|
||
// Anchor at the TOP of `panelViewport` so the card slides downward as
|
||
// `panelViewport.height` grows: hero avatar visible first, info rows
|
||
// behind it, drag handle last. Previously `bottom: 0` made the handle
|
||
// reveal first and the hero last — which read as "dark rising from the
|
||
// chat header" once the silhouette also extended through the status-bar
|
||
// zone above. Trade-off: drag handle is hidden mid-drag, only visible
|
||
// when fully open; the panel surface stays drag-sensitive throughout.
|
||
//
|
||
// `padding-top: var(--vojo-safe-top)` keeps the user-card content (hero
|
||
// avatar + info rows) clear of the Android status-bar icons in the
|
||
// open state. The padding zone shows the silhouette's bg
|
||
// (`Background.Container`, #0d0e11) through panelContent's transparent
|
||
// background, so the status-bar zone reads as part of the dark user
|
||
// card when the panel is open.
|
||
//
|
||
// `box-sizing: border-box` makes the inline `height: ${railHeightPx}`
|
||
// include the safe-top padding — so the rail measured in
|
||
// `RoomViewProfilePanel.tsx` (content + padTop + handle) exactly
|
||
// matches the actual visible height. Without `border-box` the height
|
||
// would be content-area only and the rail would visually overshoot by
|
||
// `var(--vojo-safe-top)`, leaving an unfilled gap at the bottom.
|
||
export const panelContent = style({
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
boxSizing: 'border-box',
|
||
paddingTop: 'var(--vojo-safe-top, 0px)',
|
||
});
|
||
|
||
export const panelInner = style({
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
height: '100%',
|
||
});
|
||
|
||
// Holds the user-card content. `overflow-y` is driven inline from
|
||
// `RoomViewProfilePanel.tsx` based on whether the measured content
|
||
// height exceeds the safety cap (`MAX_RAIL_FRACTION × viewport`):
|
||
//
|
||
// • Common fit case (content ≤ cap) → `overflow-y: hidden`. The
|
||
// rail is sized to content, so there's literally nothing to
|
||
// scroll. This is the user's explicit «запретил бы драгу внутри»
|
||
// ask — no drag-inside-drag inside the user card.
|
||
// • Rare overflow case (e.g. stacked moderation alerts) → switches
|
||
// to `auto`, so the user can still reach the clipped tail.
|
||
//
|
||
// Scrollbar chrome is suppressed either way; the auto case relies on
|
||
// the standard touch / wheel scroll affordances.
|
||
export const panelScroll = style({
|
||
flex: 1,
|
||
minHeight: 0,
|
||
scrollbarWidth: 'none',
|
||
selectors: {
|
||
'&::-webkit-scrollbar': {
|
||
display: 'none',
|
||
},
|
||
},
|
||
});
|
||
|
||
// Bottom drag handle — visible cue that the panel is draggable. The
|
||
// whole panel surface is also drag-sensitive (see TSX), but the
|
||
// handle stays as an obvious affordance for users who don't try the
|
||
// surface gesture.
|
||
export const panelHandle = style({
|
||
flexShrink: 0,
|
||
height: toRem(20),
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
cursor: 'grab',
|
||
touchAction: 'none',
|
||
selectors: {
|
||
'&:active': { cursor: 'grabbing' },
|
||
},
|
||
});
|
||
|
||
export const panelHandleBar = style({
|
||
width: toRem(36),
|
||
height: toRem(4),
|
||
borderRadius: toRem(4),
|
||
backgroundColor: color.Surface.ContainerLine,
|
||
});
|
||
|
||
// Avatar full-view mode — fills the panel content area with the
|
||
// user's avatar at full size when the user taps the avatar inside
|
||
// the open panel. Click reverts to the regular profile content.
|
||
export const avatarFullView = style({
|
||
width: '100%',
|
||
height: '100%',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
cursor: 'pointer',
|
||
position: 'relative',
|
||
background: 'transparent',
|
||
border: 'none',
|
||
padding: 0,
|
||
});
|
||
|
||
export const avatarFullImage = style({
|
||
width: '100%',
|
||
height: '100%',
|
||
objectFit: 'cover',
|
||
});
|
||
|
||
export const avatarFullFallback = style({
|
||
width: '100%',
|
||
height: '100%',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
fontSize: toRem(96),
|
||
fontWeight: 600,
|
||
color: color.Surface.Container,
|
||
textTransform: 'uppercase',
|
||
});
|
||
|
||
// === Header viewport === Holds the chat header as the silhouette's
|
||
// bottom child. Animated via an explicit `height` set inline (driven
|
||
// by `(1 - expandedFraction) * headerNaturalHeight`, where
|
||
// `headerNaturalHeight` is measured via `useLayoutEffect` +
|
||
// `ResizeObserver` on the inner block). The legacy
|
||
// `grid-template-rows: 1fr → 0fr` trick was abandoned because Folds
|
||
// `<Header size="600">` enforces a `min-height` that prevented the
|
||
// grid track from collapsing to 0 — leaving a light-blue strip at
|
||
// the bottom of the user card. `overflow: hidden` keeps the chat
|
||
// header content clipped while the viewport height interpolates.
|
||
// `userSelect: none` keeps a mouse drag-down-to-open gesture from
|
||
// racing native text-selection on the chat title — same rationale
|
||
// as on `panelViewport`.
|
||
export const headerViewport = style({
|
||
flexShrink: 0,
|
||
overflow: 'hidden',
|
||
willChange: 'height',
|
||
userSelect: 'none',
|
||
});
|
||
|
||
// `padding-top: var(--vojo-safe-top)` pushes the chat header's content
|
||
// (back arrow, avatar, name) below the Android status-bar icons in
|
||
// edge-to-edge mode — `#root` no longer reserves the inset itself
|
||
// (src/index.css), so the chat header would otherwise land directly
|
||
// under the system icons. The padding zone is painted with
|
||
// `SurfaceVariant.Container` (the chat-header bg below it) so the
|
||
// visible strip against the status-bar icons reads as one continuous
|
||
// header band — no seam at the inset boundary.
|
||
//
|
||
// `box-sizing: border-box` is load-bearing for the height animation
|
||
// that `headerViewport` runs (see comment there): when the outer height
|
||
// reaches 0 px on full rail-open, the `padding-top` is included in the
|
||
// 0-tall box and the SurfaceVariant strip collapses with it. With
|
||
// `content-box` it would persist as a `var(--vojo-safe-top)`-tall band
|
||
// at the bottom of the user card.
|
||
export const headerViewportInner = style({
|
||
boxSizing: 'border-box',
|
||
minHeight: 0,
|
||
overflow: 'hidden',
|
||
paddingTop: 'var(--vojo-safe-top, 0px)',
|
||
backgroundColor: color.SurfaceVariant.Container,
|
||
});
|
||
|
||
// === Chat body === Timeline + input below the silhouette. The
|
||
// rounded top corners and the `margin-top` void gap appear together
|
||
// with the silhouette's rounded bottom — all three are driven by the
|
||
// same `horseshoeRamp` (live formula while finger-dragging, binary
|
||
// with CSS transition during release animation), so the two horseshoes
|
||
// and the void emerge in lockstep at drag-start without any per-element
|
||
// «emerge curve» of their own.
|
||
export const chatBody = style({
|
||
display: 'flex',
|
||
flex: 1,
|
||
flexDirection: 'column',
|
||
minWidth: 0,
|
||
minHeight: 0,
|
||
willChange: 'margin-top, border-top-left-radius, border-top-right-radius',
|
||
});
|