import { keyframes, style } from '@vanilla-extract/css'; import { color, config, toRem } from 'folds'; // Dawn voice-note bubble — identical on native and web. The sender avatar sits // OUTSIDE the bubble (bare, no background). Both own and other have a filled // bubble with visible edges; the difference is the FILL TONE: others get a // light subtle fill (SurfaceVariant.ContainerHover #21232b), own gets the // darker fill of the composer / input form (Surface.Container #0d0e11 — the // `${ChatComposer} .${Editor}` override in RoomView.css.ts, NOT the lighter // SurfaceVariant). Same 1px edge on both. See docs/plans/voice_messages.md §5. const fadeIn = keyframes({ from: { opacity: 0, transform: 'translateY(3px)' }, to: { opacity: 1, transform: 'translateY(0)' }, }); export const Row = style({ display: 'flex', alignItems: 'center', gap: config.space.S300, width: '100%', boxSizing: 'border-box', animation: `${fadeIn} 180ms ease`, }); // Own voice note with the avatar shown: the avatar sits to the RIGHT of the // bubble and the bubble packs to the right edge. row-reverse flips the // avatar→bubble order so the JSX order stays unchanged. Applied ONLY where // VoiceContent still draws its avatar — the card previews (pin menu, message // search). The timeline (bubble + channel) and the thread drawer all pass // `hideAvatar`, so this never engages there. export const RowOwn = style({ flexDirection: 'row-reverse', }); // Bare avatar — fixed 40px, no background box of its own (the avatar image / // fallback fills it). Strip any container fill folds might apply. export const AvatarSlot = style({ flexShrink: 0, width: toRem(40), height: toRem(40), backgroundColor: 'transparent', }); export const Bubble = style({ display: 'flex', alignItems: 'center', gap: config.space.S300, flexGrow: 1, // Target width via flex-basis (so the waveform has comfortable room in the // shrink-wrapped 1:1 bubble chat), but `minWidth: 0` lets it shrink below that // on narrow panes/mobile — a hard floor here overflowed the row and got // clipped by the panel's overflow:hidden. Compact on native, a bit wider on // web (≥600px) where there's room. flexBasis: toRem(232), minWidth: 0, maxWidth: toRem(331), '@media': { 'screen and (min-width: 600px)': { flexBasis: toRem(280), maxWidth: toRem(400), }, }, // Fixed height so own and other are pixel-identical regardless of fill. height: toRem(56), boxSizing: 'border-box', padding: `0 ${config.space.S400}`, color: color.SurfaceVariant.OnContainer, borderRadius: config.radii.R400, // Others (default): a light subtle fill with a visible 1px edge — the // "Владислав" look. Own overrides the fill to the darker composer tone below. backgroundColor: color.SurfaceVariant.ContainerHover, border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`, }); // Own messages — the exact composer / input-form fill (Surface.Container, // #0d0e11), darker than the peer fill; same edge. export const BubbleOwn = style({ backgroundColor: color.Surface.Container, }); const playPulse = keyframes({ '0%': { boxShadow: `0 0 0 0 ${color.Primary.Main}` }, '70%': { boxShadow: `0 0 0 ${toRem(6)} rgba(0,0,0,0)` }, '100%': { boxShadow: `0 0 0 0 rgba(0,0,0,0)` }, }); export const PlayButton = style({ flexShrink: 0, width: toRem(40), height: toRem(40), display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: config.radii.Pill, border: 'none', padding: 0, cursor: 'pointer', backgroundColor: color.Primary.Main, color: color.Primary.OnMain, transition: 'transform 120ms ease, background-color 120ms ease', selectors: { '&:hover': { backgroundColor: color.Primary.MainHover, transform: 'scale(1.06)' }, '&:active': { transform: 'scale(0.92)' }, '&:disabled': { cursor: 'default', opacity: 0.6 }, }, }); export const PlayButtonPlaying = style({ animation: `${playPulse} 1.6s ease-out infinite`, }); export const Waveform = style({ position: 'relative', flexGrow: 1, minWidth: 0, display: 'flex', alignItems: 'center', gap: toRem(2), height: toRem(32), cursor: 'pointer', // Guarantee the react-range thumb can never paint past the track edge toward // the time readout (it's a 1px transparent overhang at most, but contain it). overflow: 'hidden', }); const barGrow = keyframes({ from: { transform: 'scaleY(0.25)' }, to: { transform: 'scaleY(1)' }, }); export const WaveBar = style({ flex: '1 1 0', minWidth: toRem(2), minHeight: toRem(3), borderRadius: toRem(2), transformOrigin: 'center', animation: `${barGrow} 220ms ease`, transition: 'background-color 120ms linear, transform 120ms ease', }); export const WaveBarPlayed = style({ backgroundColor: color.Primary.Main }); export const WaveBarRest = style({ backgroundColor: color.SurfaceVariant.ContainerLine }); export const WaveThumb = style({ width: toRem(2), height: '100%', backgroundColor: 'transparent', outline: 'none', }); export const ProgressTrack = style({ position: 'relative', flexGrow: 1, height: toRem(4), borderRadius: config.radii.Pill, backgroundColor: color.SurfaceVariant.ContainerLine, cursor: 'pointer', }); export const ProgressFill = style({ position: 'absolute', left: 0, top: 0, height: '100%', borderRadius: config.radii.Pill, backgroundColor: color.Primary.Main, transition: 'width 120ms linear', }); export const Time = style({ flexShrink: 0, minWidth: toRem(40), textAlign: 'right', fontVariantNumeric: 'tabular-nums', });