236 lines
8.8 KiB
TypeScript
236 lines
8.8 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.
|
|
export const panelContent = style({
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
paddingTop: 'var(--vojo-safe-top, 0px)',
|
|
});
|
|
|
|
export const panelInner = style({
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
height: '100%',
|
|
});
|
|
|
|
// Functional overflow without a visible scrollbar. The card's
|
|
// content (hero + ~4 info rows + chip row) almost always fits inside
|
|
// the rail height, but moderation alerts can push it past — we keep
|
|
// the panel scrollable for that case while suppressing the
|
|
// scrollbar chrome (it's not a useful affordance on a 42vh rail and
|
|
// the user explicitly asked us to drop it).
|
|
export const panelScroll = style({
|
|
flex: 1,
|
|
minHeight: 0,
|
|
overflow: 'auto',
|
|
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',
|
|
});
|