vojo/src/app/features/bots/BotWidgetMount.css.ts

65 lines
2.6 KiB
TypeScript

import { keyframes, style } from '@vanilla-extract/css';
export const FrameMount = style({
width: '100%',
height: '100%',
});
// Loading bar overlay shown while the widget iframe boots and capability
// handshake completes. Mirrors `SyncIndicator` (sweep + radial fade + glow)
// but pinned to the TOP of the iframe frame (under the BotShell hero) and
// painted in the brand fleet-violet `Primary.Main` (#9580ff) instead of the
// sync-state green. Resolves the «empty page on bot click» complaint:
// before this, the iframe took ~200-800ms to first paint and the user saw
// only the hero on a flat dark surface. Hex is hardcoded for the same
// reason `BotShell.HeroAvatar` hardcodes it: keep the bot accent stable
// across folds palette swaps.
const slide = keyframes({
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(100%)' },
});
// Container clips horizontally so the slide-keyframe wraps invisibly off-
// screen at the edges. Vertically the upper drop-shadow halo (-6px..0) IS
// intentionally clipped — the bar sits flush against the BotShell hero's
// bottom border and a halo glowing UP would visually bleed into the hero
// chrome. Only the lower halo (0..8px) renders, glowing DOWN into the
// iframe — mirror of SyncIndicator's bottom-anchored bar where the halo
// renders UP into the page. Don't bump `top` to surface the upper halo;
// that would float the bar away from the hero edge and break the seam.
export const LoadingBarRoot = style({
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '14px',
pointerEvents: 'none',
// Above the iframe, but no need for the global Z400 — local stacking
// context inside Frame is enough.
zIndex: 2,
overflow: 'hidden',
});
export const LoadingBar = style({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '2px',
background:
'radial-gradient(ellipse 50% 100% at center, #9580ff 0%, rgba(149, 128, 255, 0.45) 35%, rgba(149, 128, 255, 0) 70%)',
filter: 'drop-shadow(0 0 6px rgba(149, 128, 255, 0.75))',
animation: `${slide} 1.6s linear infinite`,
willChange: 'transform',
// 250ms opacity transition matches SyncIndicator's crossfade so the bar
// doesn't pop in/out abruptly when ready flips. animationPlayState is
// toggled inline at the call site so the compositor doesn't keep
// sweeping while the bar is invisible (opacity:0 alone doesn't pause
// CSS animations — verified across browsers).
transition: 'opacity 250ms ease',
'@media': {
'(prefers-reduced-motion: reduce)': {
animation: 'none',
},
},
});