diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index a3c7d4c..d960bd3 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -8,18 +8,33 @@ import { ToolbarItemView } from './ToolbarItemView'; import { ToolbarMeView } from './ToolbarMeView'; import { YouTubePlayerView } from './YouTubePlayerView'; +// The 4 nav rows + backplate are ALWAYS mounted and animate between +// hidden/visible via framer-motion variants. Rapid show/hide toggles +// just retarget the in-flight spring instead of interrupting an +// AnimatePresence enter/exit cycle, so the pre-refactor artifacts +// (icons stuck at opacity 0 or scale 0.8 after spam-clicking the +// toggle) can no longer happen — framer's spring solver picks up from +// whatever the current animated value is. + const containerVariants: Variants = { - hidden: {}, - visible: { transition: { staggerChildren: 0.05 } }, - exit: { transition: { staggerChildren: 0.03, staggerDirection: -1 } } + hidden: { transition: { staggerChildren: 0.03, staggerDirection: -1 } }, + visible: { transition: { staggerChildren: 0.05 } } }; const itemVariants: Variants = { hidden: { opacity: 0, y: 10, scale: 0.8 }, - visible: { opacity: 1, y: 0, scale: 1, transition: { type: 'spring', stiffness: 400, damping: 22 } }, - exit: { opacity: 0, y: 6, scale: 0.85, transition: { duration: 0.1 } } + visible: { opacity: 1, y: 0, scale: 1, transition: { type: 'spring', stiffness: 400, damping: 22 } } }; +const shellVariants: Variants = { + hidden: { opacity: 0, y: 8 }, + visible: { opacity: 1, y: 0 } +}; + +const SHELL_TRANSITION = { type: 'spring' as const, stiffness: 260, damping: 26 }; +const NAV_TRANSITION = { type: 'spring' as const, stiffness: 300, damping: 28 }; +const ME_POPOVER_TRANSITION = { type: 'spring' as const, stiffness: 420, damping: 28 }; + export const ToolbarView: FC<{ isInRoom: boolean }> = props => { const { isInRoom } = props; @@ -35,8 +50,9 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => const { iconState = MessengerIconState.HIDDEN } = useMessenger(); const { openMonitor, showToolbarButton } = useWiredTools(); const isMod = GetSessionDataManager().isModerator; - const hasDesktopUnifiedShell = (isInRoom && isToolbarOpen); - const showDesktopShell = (isToolbarOpen || !isInRoom); + const isVisible = (isToolbarOpen || !isInRoom); + const visibilityVariant = isVisible ? 'visible' : 'hidden'; + const desktopToolbarFrameClasses = isTouchLayout ? '' : 'md:left-1/2 md:right-auto md:h-[52px] md:w-[420px] md:-translate-x-1/2 md:items-center md:px-[6px] md:py-[4px] lg:w-[460px]'; const desktopToolbarOpenClasses = isTouchLayout ? '' : 'md:rounded-none md:border-0 md:bg-transparent md:shadow-none'; const desktopToggleClasses = isTouchLayout ? '' : 'md:mb-0'; @@ -46,6 +62,22 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => const desktopBlockClasses = isTouchLayout ? 'hidden' : 'hidden md:block'; const desktopFlexClasses = isTouchLayout ? 'hidden' : 'hidden md:flex'; + // Compute the wrapper variants. `isInRoom` affects the hidden-state + // offset (the nav slides in from the side when in a room, from the + // bottom otherwise) so the variant object is derived from props. + const leftNavVariants: Variants = { + hidden: { opacity: 0, x: isInRoom ? -10 : 0, y: isInRoom ? 0 : 8, pointerEvents: 'none' }, + visible: { opacity: 1, x: 0, y: 0, pointerEvents: 'auto' } + }; + const rightNavVariants: Variants = { + hidden: { opacity: 0, x: 10, pointerEvents: 'none' }, + visible: { opacity: 1, x: 0, pointerEvents: 'auto' } + }; + const mobileNavVariants: Variants = { + hidden: { opacity: 0, y: 8, pointerEvents: 'none' }, + visible: { opacity: 1, y: 0, pointerEvents: 'auto' } + }; + useMessageEvent(YouTubeRoomSettingsEvent, event => { const enabled = event.getParser().youtubeEnabled; @@ -123,7 +155,7 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => { youtubeEnabled && } { isInRoom && -
+
setIsToolbarOpen(value => !value) } @@ -148,199 +180,211 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
} - - { (isToolbarOpen || !isInRoom) && - <> - { showDesktopShell && + { /* Desktop backplate. Always mounted; opacity-driven. */ } + + + { /* Left nav — desktop. Container variant inheritance staggers items in/out. */ } + + + + { isInRoom + ? VisitDesktop() } className="tb-icon" /> + : CreateLinkEvent('navigator/goto/home') } className="tb-icon" /> } + + + CreateLinkEvent('navigator/toggle') } className="tb-icon" /> + + { GetConfigurationValue('game.center.enabled') && + + CreateLinkEvent('games/toggle') } className="tb-icon" /> + } + + CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" /> + + + CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> + + + CreateLinkEvent('inventory/toggle') } className="tb-icon" /> + { (getFullCount > 0) && + } + + + + { isMeExpanded && + + + } + + + { + setMeExpanded(value => !value); + event.stopPropagation(); + } }> + + + { (getTotalUnseen > 0) && + } + + { (isInRoom && showToolbarButton) && + + + } + { isInRoom && + + CreateLinkEvent('camera/toggle') } className="tb-icon" /> + } + { (isInRoom && youtubeEnabled) && + + + } + { isMod && + + CreateLinkEvent('mod-tools/toggle') } className="tb-icon" /> + } + { isMod && + + CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> + } + + + + { /* Right nav — desktop */ } + + + + CreateLinkEvent('friends/toggle') } className="tb-icon" /> + { (requests.length > 0) && + } + + { ((iconState === MessengerIconState.SHOW) || (iconState === MessengerIconState.UNREAD)) && + + OpenMessengerChat() } /> + } +
+
+ + + + { /* Mobile nav. Two staggered halves split by the Me avatar. */ } + + + + { isInRoom + ? VisitDesktop() } className="tb-icon" /> + : CreateLinkEvent('navigator/goto/home') } className="tb-icon" /> } + + + CreateLinkEvent('navigator/toggle') } className="tb-icon" /> + + { GetConfigurationValue('game.center.enabled') && + + CreateLinkEvent('games/toggle') } className="tb-icon" /> + } + + CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" /> + + + CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> + + + CreateLinkEvent('inventory/toggle') } className="tb-icon" /> + { (getFullCount > 0) && + } + + + + + { isMeExpanded && } - - - - - { isInRoom - ? VisitDesktop() } className="tb-icon" /> - : CreateLinkEvent('navigator/goto/home') } className="tb-icon" /> } - - - CreateLinkEvent('navigator/toggle') } className="tb-icon" /> - - { GetConfigurationValue('game.center.enabled') && - - CreateLinkEvent('games/toggle') } className="tb-icon" /> - } - - CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" /> - - - CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> - - - CreateLinkEvent('inventory/toggle') } className="tb-icon" /> - { (getFullCount > 0) && - } - - - - { isMeExpanded && - - - } - - - { - setMeExpanded(value => !value); event.stopPropagation(); - } }> - - - { (getTotalUnseen > 0) && - } - - { (isInRoom && showToolbarButton) && - - - } - { isInRoom && - - CreateLinkEvent('camera/toggle') } className="tb-icon" /> - } - { (isInRoom && youtubeEnabled) && - - - } - { isMod && - - CreateLinkEvent('mod-tools/toggle') } className="tb-icon" /> - } - { isMod && - - CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> - } - - - - - - - CreateLinkEvent('friends/toggle') } className="tb-icon" /> - { (requests.length > 0) && - } - - { ((iconState === MessengerIconState.SHOW) || (iconState === MessengerIconState.UNREAD)) && - - OpenMessengerChat() } /> - } -
-
- - - - - - - { isInRoom - ? VisitDesktop() } className="tb-icon" /> - : CreateLinkEvent('navigator/goto/home') } className="tb-icon" /> } - - - CreateLinkEvent('navigator/toggle') } className="tb-icon" /> - - { GetConfigurationValue('game.center.enabled') && - - CreateLinkEvent('games/toggle') } className="tb-icon" /> - } - - CreateLinkEvent('catalog/toggle/normal') } className="tb-icon" /> - - - CreateLinkEvent('catalog/toggle/builder') } className="tb-icon" /> - - - CreateLinkEvent('inventory/toggle') } className="tb-icon" /> - { (getFullCount > 0) && - } - - - - - { isMeExpanded && - - - } - - - { - setMeExpanded(value => !value); event.stopPropagation(); - } }> - - - { (getTotalUnseen > 0) && - } - - - { (isInRoom && showToolbarButton) && - - - } - { isInRoom && - - CreateLinkEvent('camera/toggle') } className="tb-icon" /> - } - { (isInRoom && youtubeEnabled) && - - - } - { isMod && - - CreateLinkEvent('mod-tools/toggle') } className="tb-icon" /> - } - { isMod && - - CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> - } - { !isInRoom && - - CreateLinkEvent('friends/toggle') } className="tb-icon" /> - { (requests.length > 0) && - } - } - - - } - + initial={ { opacity: 0, y: 6, scale: 0.97 } } + animate={ { opacity: 1, y: 0, scale: 1 } } + exit={ { opacity: 0, y: 6, scale: 0.97 } } + transition={ ME_POPOVER_TRANSITION } + className="pointer-events-auto absolute bottom-[calc(100%+10px)] left-1/2 z-[70] -translate-x-1/2"> + + } + + + { + setMeExpanded(value => !value); + event.stopPropagation(); + } }> + + + { (getTotalUnseen > 0) && + } + + + { (isInRoom && showToolbarButton) && + + + } + { isInRoom && + + CreateLinkEvent('camera/toggle') } className="tb-icon" /> + } + { (isInRoom && youtubeEnabled) && + + + } + { isMod && + + CreateLinkEvent('mod-tools/toggle') } className="tb-icon" /> + } + { isMod && + + CreateLinkEvent('furni-editor/toggle') } className="tb-icon" /> + } + { !isInRoom && + + CreateLinkEvent('friends/toggle') } className="tb-icon" /> + { (requests.length > 0) && + } + } + + ); };