vojo/src/app/features/room/RoomViewProfilePanel.css.ts

250 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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',
});