176 lines
5.5 KiB
TypeScript
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',
|
|
});
|