vojo/src/app/features/room/RoomTimeline.css.ts

98 lines
2.7 KiB
TypeScript

import { globalStyle, keyframes, style } from '@vanilla-extract/css';
import { RecipeVariants, recipe } from '@vanilla-extract/recipes';
import { DefaultReset, color, config, toRem } from 'folds';
export const TimelineFloat = recipe({
base: [
DefaultReset,
{
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
zIndex: 1,
minWidth: 'max-content',
},
],
variants: {
position: {
Top: {
top: config.space.S400,
},
},
},
defaultVariants: {
position: 'Top',
},
});
export type TimelineFloatVariants = RecipeVariants<typeof TimelineFloat>;
// "Jump to latest" FAB. Bottom-right, circular, lavender brand accent.
// `data-hidden` encodes visibility (state inline-styled would clobber the
// `:active` press feedback). Inline `bottom` at the use site offsets for
// the live composer height.
export const JumpToLatestFab = style([
DefaultReset,
{
position: 'absolute',
right: config.space.S400,
width: toRem(44),
height: toRem(44),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: color.Primary.Main,
color: color.Primary.OnMain,
borderRadius: '50%',
border: 'none',
cursor: 'pointer',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.28), 0 2px 4px rgba(0, 0, 0, 0.16)',
transition:
'transform 180ms ease-out, opacity 180ms ease-out, background-color 120ms ease',
zIndex: 5,
transform: 'scale(1)',
opacity: 1,
selectors: {
'&[data-hidden="true"]': {
transform: 'scale(0.6)',
opacity: 0,
pointerEvents: 'none',
},
'&:active': {
transform: 'scale(0.94)',
},
},
'@media': {
'(prefers-reduced-motion: reduce)': {
transition: 'none',
},
},
},
]);
// Mouse-only hover brighten; touch sessions stay flat to avoid the stuck
// `:hover` paint on Android WebView (same precedent as ChatComposer).
globalStyle(`:root[data-input="mouse"] ${JumpToLatestFab}:hover`, {
filter: 'brightness(1.08)',
});
// Bounce animation played when a fresh live message arrives while the
// user is scrolled away from the live edge — signals "there's something
// new below" without dragging in a numeric badge. The slot is keyed by
// a counter at the use site so each new message remounts the span and
// restarts the animation.
const pulseKeyframes = keyframes({
'0%': { transform: 'scale(1)' },
'40%': { transform: 'scale(1.28)' },
'100%': { transform: 'scale(1)' },
});
export const FabIconPulseSlot = style({
display: 'flex',
animation: `${pulseKeyframes} 520ms ease-out`,
'@media': {
'(prefers-reduced-motion: reduce)': {
animation: 'none',
},
},
});