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