vojo/src/app/components/message/content/VoiceContent.css.ts

176 lines
5.5 KiB
TypeScript

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