import { globalStyle, style } from '@vanilla-extract/css'; import { color, config, toRem } from 'folds'; // 36px circular avatar — a notch above folds `Avatar size="200"` (32px) // for visual weight matching the channels mockup. Consumers override // the folds preset via inline style; the shared `CHANNEL_AVATAR_PX` // constant keeps the CSS slot width and the inline override in sync. export const CHANNEL_AVATAR_PX = 36; const ChannelAvatarWidth = toRem(CHANNEL_AVATAR_PX); export const ChannelRow = style({ display: 'flex', alignItems: 'flex-start', gap: config.space.S300, // S200 (8px) sits inside MessageBase's S400 (16px) left pad, so the // avatar's left edge lands ~24px from the screen edge — tighter than // the previous 32px (S400+S400) without crowding the bubble cluster. // Mirror on the right for symmetric breathing room. paddingLeft: config.space.S200, paddingRight: config.space.S200, paddingTop: config.space.S100, paddingBottom: config.space.S100, minWidth: 0, // Hover bg subtle so adjacent rows still read as distinct units even // without bubble borders. `@media (hover: hover)` keeps this inert on // touch where there's no pointer to follow. '@media': { '(hover: hover) and (pointer: fine)': { selectors: { '&:hover': { backgroundColor: color.SurfaceVariant.Container, }, }, }, }, }); // Fixed-width slot keeps the body column aligned across collapsed rows // (where `avatar` is `undefined` — the slot still occupies `ChannelAvatarWidth`, // so the body's left edge stays put). export const ChannelAvatarSlot = style({ width: ChannelAvatarWidth, flexShrink: 0, display: 'flex', justifyContent: 'center', paddingTop: toRem(2), }); export const ChannelBody = style({ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: config.space.S100, }); export const ChannelHeader = style({ display: 'flex', alignItems: 'baseline', gap: config.space.S200, minWidth: 0, }); export const ChannelMessageBody = style({ minWidth: 0, }); export const ChannelReactions = style({ minWidth: 0, marginTop: config.space.S100, }); export const ChannelThreadSummary = style({ minWidth: 0, marginTop: config.space.S100, }); // Horizontal line + centered label. Spans the full row width including // the avatar slot so the line reads as a section break, not a per-message // chrome element. export const ChannelDayDividerRoot = style({ display: 'flex', alignItems: 'center', gap: config.space.S300, paddingLeft: config.space.S400, paddingRight: config.space.S400, paddingTop: config.space.S400, paddingBottom: config.space.S400, }); export const ChannelDayDividerLine = style({ flex: 1, height: '1px', backgroundColor: color.SurfaceVariant.ContainerLine, }); export const ChannelDayDividerLabel = style({ fontSize: toRem(11), fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase', color: color.SurfaceVariant.OnContainer, opacity: 0.7, flexShrink: 0, }); // Sysline (membership / room.create / pinned-events). Compact single // row aligned with the body gutter — the sysline sits where messages' // body would, indented past the avatar slot, so the column reads // continuous. export const ChannelSysline = style({ // Indent past the avatar gutter so the sysline body aligns with the // body column of the surrounding message rows (avatar + gap + row pad). // Tracks `ChannelRow.paddingLeft/Right` (S200) in lockstep. paddingLeft: `calc(${ChannelAvatarWidth} + ${config.space.S300} + ${config.space.S200})`, paddingRight: config.space.S200, paddingTop: config.space.S100, paddingBottom: config.space.S100, color: color.SurfaceVariant.OnContainer, }); export const ChannelSyslineIcon = style({ flexShrink: 0, opacity: 0.5, }); export const ChannelSyslineBody = style({ minWidth: 0, flex: 1, }); // Bubble chrome applied when `ChannelLayout` is invoked with // `headerInBubble` (thread drawer and channels main timeline pass it). // Mirrors `StreamBubble` from the DM timeline so a channel row reads // like a chat-bubble cluster: dark `Surface.Container` card with an // asymmetric notch corner per `data-own`, sized `fit-content` so short // bubbles shrink-wrap instead of stretching across the column. // Reactions and the thread-summary card live as siblings of the body // in `ChannelLayout`, so they stay OUTSIDE the bubble — identical // composition to Stream. The `[data-bubble="true"]` row marker keeps // the un-bubbled channel/sysline layout (pre-redesign callers) opt-in // rather than forcing the look on every consumer. globalStyle(`${ChannelRow}[data-bubble="true"] ${ChannelMessageBody}`, { backgroundColor: color.Surface.Container, color: color.SurfaceVariant.OnContainer, border: `1px solid ${color.Surface.ContainerLine}`, paddingTop: config.space.S200, paddingBottom: config.space.S200, paddingLeft: toRem(15), paddingRight: toRem(15), display: 'inline-block', width: 'fit-content', maxWidth: '100%', minWidth: 0, position: 'relative', zIndex: 1, // Clips the thread-summary footer's hover bg against the bubble's // rounded BR/BL corners — without it the rectangular hover paint // punches past the curve. No outflow content lives inside the bubble // (option bar, reactions are siblings on the row) so clipping is // safe. overflow: 'hidden', }); // Asymmetric corner per `data-own` — own messages flatten BOTTOM-LEFT // (4px), incoming messages flatten TOP-LEFT. Same pattern as // `StreamBubble.own`/`StreamBubble.others`. globalStyle(`${ChannelRow}[data-bubble="true"][data-own="true"] ${ChannelMessageBody}`, { borderRadius: `${toRem(16)} ${toRem(16)} ${toRem(16)} ${toRem(4)}`, }); globalStyle(`${ChannelRow}[data-bubble="true"][data-own="false"] ${ChannelMessageBody}`, { borderRadius: `${toRem(4)} ${toRem(16)} ${toRem(16)} ${toRem(16)}`, // Peer (not-own) bubble bg for the channel layout — its own var. (The 1-1 // Stream layout's incoming bubble instead binds to color.Surface.Container, // the composer surface.) Covers channels main timeline AND thread drawer // (both pass `headerInBubble`, so `data-bubble="true"` fires). backgroundColor: 'var(--vojo-peer-bubble-bg)', }); // Small gap so the in-bubble header (username + time) doesn't sit flush // against the first line of message text. Matches the Stream layout's // `StreamName` 2px marginBottom. globalStyle(`${ChannelRow}[data-bubble="true"] ${ChannelHeader}[data-in-bubble="true"]`, { marginBottom: toRem(2), }); // Thread-summary footer rendered INSIDE the bubble (rather than as a // separate pill below). Negative L/R margin (matches the bubble's // `paddingLeft/Right: 15px`) stretches the wrapper to the bubble's // inner border edge so the 1px top rule reads as a section divider // spanning the whole bubble. Negative `marginBottom` cancels the // bubble's S200 bottom pad so the footer flushes against the bubble's // rounded bottom edge — bubble + summary read as one card with a // horizontal rule splitting them. // // The footer body keeps no own border/radius — it inherits the bubble's // bottom corners via clipping (`ChannelMessageBody` itself doesn't // `overflow: hidden`, but the rounded bottom of the bubble visually // caps the footer anyway because the divider line never reaches the // curved corner pixel). export const ChannelBubbleThreadSummary = style({ marginTop: config.space.S200, marginLeft: toRem(-15), marginRight: toRem(-15), marginBottom: `calc(-1 * ${config.space.S200})`, borderTop: `1px solid ${color.Surface.ContainerLine}`, }); // Footer button — strip the original ThreadSummaryCard pill chrome // (own bg, radius, padding, max-width) so it reads as a flush bubble // footer. Click target expands to the full footer width. Hover paints // a subtle `SurfaceVariant.Container` shade that contrasts against // the bubble's `Surface.Container` bg, signalling tappable footer // without the pill silhouette returning. globalStyle(`${ChannelBubbleThreadSummary} > button`, { display: 'flex', width: '100%', maxWidth: 'none', borderRadius: 0, backgroundColor: 'transparent', padding: `${config.space.S200} ${toRem(15)}`, }); globalStyle(`${ChannelBubbleThreadSummary} > button:hover`, { backgroundColor: color.SurfaceVariant.Container, }); globalStyle(`${ChannelBubbleThreadSummary} > button:focus-visible`, { // Inset the focus ring slightly so it doesn't punch through the // bubble's rounded bottom corners on the BR/BL when the row is // either own or incoming. outlineOffset: toRem(-2), });