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