import { style } from '@vanilla-extract/css'; import { color, config, toRem } from 'folds'; import { VOJO_HORSESHOE_GAP_PX, VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe'; // Desktop wrapper for the resizable thread drawer. Sizing and the // absolutely-positioned resize handle live here; the inner aside // (`ThreadDrawer` below) owns the rounded TL/BL carves + bg. // `overflow: visible` on the wrapper is critical — the handle sits // at `left: -GAP_PX` inside the horseshoe void, and the aside's // `overflow: hidden` would otherwise clip it. export const ThreadDrawerResizable = style({ position: 'relative', flexShrink: 0, flexGrow: 0, maxHeight: '100%', minHeight: 0, display: 'flex', flexDirection: 'column', }); // Resize handle mirror of `PageNavResizeHandle` — sits in the 12px // horseshoe void to the LEFT of the drawer (page-nav's handle sits in // the gap to the right of the nav). Same cosmetics: thin indicator bar // that fades in on hover/focus and stretches/shrinks at the clamps. export const ThreadDrawerResizeHandle = style({ position: 'absolute', top: 0, bottom: 0, left: `-${toRem(VOJO_HORSESHOE_GAP_PX)}`, width: toRem(VOJO_HORSESHOE_GAP_PX), cursor: 'col-resize', zIndex: 1, background: 'transparent', touchAction: 'none', outline: 'none', selectors: { '&::before': { content: '""', position: 'absolute', top: '50%', left: '50%', width: 2, height: toRem(36), transform: 'translate(-50%, -50%)', borderRadius: 1, backgroundColor: color.Surface.OnContainer, opacity: 0, transition: 'opacity 140ms ease, height 140ms ease, width 140ms ease, background-color 140ms ease', }, '&:hover::before, &:focus-visible::before': { opacity: 0.25, }, '&:focus-visible::before': { backgroundColor: color.Primary.Main, opacity: 0.45, }, '&[data-dragging="true"]::before': { opacity: 0.55, height: toRem(48), backgroundColor: color.Primary.Main, }, '&[data-dragging="true"][data-at-min="true"]::before': { height: toRem(28), width: 3, opacity: 0.85, }, '&[data-dragging="true"][data-at-max="true"]::before': { height: toRem(76), width: 2, opacity: 0.9, }, }, }); // Layout copies element-web `_ThreadPanel.pcss:73-84`: // `max-height: 100%` clamps the column to the parent's viewport-bound // height; `min-height: 0` on the inner Scroll wrapper lets it shrink // under content (otherwise flex-children grow with content and push // the composer off-screen — bug observed on cold-load with many // replies). Comment in element-web: «don't displace the composer». // // Horseshoe: rounded TL/BL mirrors the page-nav <-> chat and chat <-> // profile/media seams. Room.tsx paints the 12px void gap to the left // of the drawer (`showThreadHorseshoe`) and the chat-column gets an // explicit Background bg so the void can't bleed through. // // Sizing (`width`, `flexShrink`) was moved to `ThreadDrawerResizable` // so the aside fills its resizable wrapper and the wrapper owns the // flex-row contract with Room.tsx. export const ThreadDrawer = style({ flexGrow: 1, width: '100%', display: 'flex', flexDirection: 'column', // Match the chat surface (`RoomView.tsx` paints // `color.SurfaceVariant.Container` as its Page bg). Bubbles inside // the drawer are `color.Surface.Container` so the «darker card on // lighter surface» contrast reads identically to the main timeline. backgroundColor: color.SurfaceVariant.Container, minHeight: 0, overflow: 'hidden', borderTopLeftRadius: toRem(VOJO_HORSESHOE_RADIUS_PX), borderBottomLeftRadius: toRem(VOJO_HORSESHOE_RADIUS_PX), }); // Mobile push: drawer occupies the whole content column, not a side pane. // `padding-top: var(--vojo-safe-top)` keeps the drawer's `ThreadDrawerHeader` // (back arrow + thread title + counter) clear of the Android status-bar // icons. `#root` no longer reserves the inset itself (`src/index.css`), // and the chat-column wrapper that hosts this drawer on mobile has no // safe-top of its own (the silhouette in `RoomViewProfilePanel` owns the // inset for the chat header — but the thread drawer mounts as a sibling // and would otherwise land directly under the system icons). The desktop // `ThreadDrawer` style sits inside the chat row that has `env()` left/right // only, so it inherits no top inset and doesn't need this padding (and // would just leave dead space below the chat-column-wrapper's chrome). export const ThreadDrawerMobile = style({ flexGrow: 1, width: '100%', maxHeight: '100%', display: 'flex', flexDirection: 'column', // Same chat-surface tone as the desktop drawer above. backgroundColor: color.SurfaceVariant.Container, minHeight: 0, paddingTop: 'var(--vojo-safe-top, 0px)', }); export const ThreadDrawerHeader = style({ flexShrink: 0, padding: `0 ${config.space.S200} 0 ${config.space.S400}`, borderBottomWidth: config.borderWidth.B300, }); // Critical pair: `flexGrow: 1` lets the scroll wrapper take remaining // space, `minHeight: 0` is what allows it to actually SHRINK under // content (without it, flex-children grow to natural content size → // composer pushed off viewport). `overflow: hidden` clips so the // inner folds `` (which is `overflow: auto`) is the only // scroller. Same trio that element-web uses on the timeline wrapper. export const ThreadDrawerScroll = style({ flexGrow: 1, minHeight: 0, overflow: 'hidden', position: 'relative', }); // M4a: top padding bumped from S400 (16px) to S700 (32px). ``'s // action bar (`MessageOptionsBase`, top: -30px in styles.css.ts:8-16) // sits 30px above its row. Mid-list rows have the previous row's height // absorbing that negative position, but the FIRST row (root event) // only has this container's top padding above it — S400 wasn't enough, // so the bar clipped at the scroll viewport's top edge on hover. S700 // gives the bar 32px of clearance, enough to render the ~28-32px hover // bar fully on the root. // // Left/right padding zeroed: `ChannelRow` already provides an S400 // (16px) horizontal gutter per row, and stacking two S400 paddings // pushed the avatar 32px from the drawer edge — a perceptibly off // «double-gutter» look in the narrow side pane. With this gutter // removed, the avatar sits at row-padding (16px) from the drawer's // rounded edge, matching the channels main-timeline rhythm. export const ThreadDrawerContent = style({ display: 'flex', flexDirection: 'column', gap: config.space.S400, paddingTop: config.space.S700, paddingBottom: config.space.S400, }); export const ThreadDivider = style({ height: 1, backgroundColor: color.Surface.ContainerLine, margin: `0 ${config.space.S400}`, flexShrink: 0, }); // Two-line drawer header caption + subtitle (M3 polish). The caption is // uppercase tracking-wide ALL-CAPS «ТРЕД», the subtitle is the channel // reference «в #channel». export const ThreadDrawerHeaderCaption = style({ textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600, color: color.SurfaceVariant.OnContainer, opacity: 0.6, }); export const ThreadDrawerHeaderSubtitle = style({ color: color.Surface.OnContainer, fontWeight: 500, }); // Counter row between the root and replies: dot + «N ОТВЕТОВ» uppercase // label. Replaces the static 1-px divider when at least one reply exists. export const ThreadCounterRow = style({ display: 'flex', alignItems: 'center', gap: config.space.S200, padding: `${config.space.S100} ${config.space.S400}`, borderTop: `1px solid ${color.Surface.ContainerLine}`, borderBottom: `1px solid ${color.Surface.ContainerLine}`, flexShrink: 0, }); export const ThreadCounterDot = style({ flexShrink: 0, width: '6px', height: '6px', borderRadius: '50%', backgroundColor: color.Primary.Main, }); export const ThreadCounterText = style({ textTransform: 'uppercase', letterSpacing: '0.08em', fontSize: '11px', fontWeight: 600, color: color.SurfaceVariant.OnContainer, opacity: 0.7, }); // Thread composer wrap — geometry mirrors the personal-chat composer // in `RoomView.tsx`: a 12px outer pad on the sides + bottom (matches // the horseshoe void gap) frames a card whose visual chrome is owned // by `ChatComposer` from `RoomView.css.ts`. The class itself is // reapplied to the inner wrap in `ThreadDrawer.tsx` so the same // `globalStyle` rules (`Surface.Container` bg, 32px radius, dark // touch-hover gate) reach the Editor inside. export const ThreadComposer = style({ flexShrink: 0, padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} ${toRem(VOJO_HORSESHOE_GAP_PX)}`, }); // Bubble chrome itself lives in `Channel.css.ts` and applies via the // row's `data-bubble="true"` marker (set by `ChannelLayout` when // `headerInBubble` is enabled). Both the thread drawer and the // channels main timeline opt in, so the look is shared. export const ThreadEmptyState = style({ padding: `${config.space.S500} ${config.space.S300}`, textAlign: 'center', }); export const ThreadErrorState = style({ padding: config.space.S400, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: config.space.S300, });