Rationalize Single Desktop Overflow Menu

This commit is contained in:
Enrico Ros
2025-06-04 16:55:14 -07:00
parent 3a7402b03d
commit 5bff478d06
3 changed files with 140 additions and 126 deletions
+1 -1
View File
@@ -16,7 +16,6 @@ export const Release = {
App: {
versionCode: '2.0.0-open-rc3', // 1.92.0 sequentially...
versionName: 'Big-AGI 2',
releaseNotes: '',
},
// Future compatibility
@@ -65,5 +64,6 @@ export const Release = {
export const BaseProduct = {
ReleaseNotes: '',
SupportForm: (_userId?: string) => 'https://github.com/enricoros/big-agi/issues',
} as const;
+2 -94
View File
@@ -1,35 +1,22 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, Dropdown, IconButton, ListDivider, ListItem, ListItemDecorator, Menu, MenuButton, MenuItem, Typography } from '@mui/joy';
import { Box, IconButton, Typography } from '@mui/joy';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import EngineeringIcon from '@mui/icons-material/Engineering';
import FeedbackIcon from '@mui/icons-material/Feedback';
import HistoryIcon from '@mui/icons-material/History';
import LightbulbOutlinedIcon from '@mui/icons-material/LightbulbOutlined';
import MenuIcon from '@mui/icons-material/Menu';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import NewReleasesIcon from '@mui/icons-material/NewReleases';
import { BuildInfoCard } from '../../../../apps/news/AppNews';
import { blocksRenderHTMLIFrameCss } from '~/modules/blocks/code/code-renderers/RenderCodeHtmlIFrame';
import { BigAgiSquircleIcon } from '~/common/components/icons/big-agi/BigAgiSquircleIcon';
import { Brand } from '~/common/app.config';
import { GoodModal } from '~/common/components/modals/GoodModal';
import { LayoutSidebarRight } from '~/common/components/icons/LayoutSidebarRight';
import { Link } from '~/common/components/Link';
import { Release } from '~/common/app.release';
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
import { checkVisibleNav, NavItemApp } from '~/common/app.nav';
import { navigateToIndex, ROUTE_INDEX } from '~/common/app.routes';
import { useOverlayComponents } from '~/common/layout/overlays/useOverlayComponents';
import { InvertedBar, InvertedBarCornerItem } from '../InvertedBar';
import { PopupPanel } from '../panel/PopupPanel';
import { optimaOpenDrawer, optimaOpenPanel, optimaTogglePanel, useOptimaPanelOpen } from '../useOptima';
import { scratchClipSupported, useScratchClipVisibility } from '../scratchclip/store-scratchclip';
import { useOptimaPortalHasInputs } from '../portals/useOptimaPortalHasInputs';
import { useOptimaPortalOutRef } from '../portals/useOptimaPortalOutRef';
@@ -95,51 +82,12 @@ export function OptimaBar(props: { component: React.ElementType, currentApp?: Na
const appMenuAnchor = React.useRef<HTMLButtonElement>(null);
// external state
/**
* NOTE: shall we fall back to the 'standard' release notes when not available on the tenant?
* - prob not because this could be a per-company deployment, and we don't know the tenant's release notes
*/
const releaseNotesUrl = Release.App.releaseNotes;
const { showPromisedOverlay } = useOverlayComponents();
const hasDrawerContent = useOptimaPortalHasInputs('optima-portal-drawer');
const { panelAsPopup, panelHasContent, panelShownAsPanel, panelShownAsPopup } = useOptimaPanelOpen(props.isMobile, props.currentApp);
const { isVisible: isScratchClipVisible, toggleVisibility: toggleScratchClipVisibility } = useScratchClipVisibility();
// derived state
const navIsShown = checkVisibleNav(props.currentApp);
// Handlers
const handleShowReleaseNotes = React.useCallback(async () => {
if (!releaseNotesUrl) return;
return await showPromisedOverlay('app-recent-changes', { rejectWithValue: false }, ({ onResolve, onUserReject }) =>
<GoodModal
open
onClose={onUserReject}
noTitleBar
themedColor='neutral'
unfilterBackdrop
sx={{ minWidth: { xs: 400, sm: 580, md: 780, lg: 890 } }}
>
<iframe
src={releaseNotesUrl}
style={{ ...blocksRenderHTMLIFrameCss, height: '70svh' }}
title='Release Notes Embed'
loading='lazy' // do not load until visible in the viewport
/>
</GoodModal>,
);
}, [releaseNotesUrl, showPromisedOverlay]);
const handleShowTechnologies = React.useCallback(async () => {
return await showPromisedOverlay<boolean>('app-recent-changes', { rejectWithValue: false }, ({ onUserReject }) =>
<GoodModal open onClose={onUserReject} noTitleBar unfilterBackdrop>
<BuildInfoCard noMargin />
</GoodModal>,
);
}, [showPromisedOverlay]);
// [Desktop] optionally hide the Bar if the current app asks for it
if (props.currentApp?.hideBar && !props.isMobile && !panelHasContent)
return null;
@@ -169,47 +117,7 @@ export function OptimaBar(props: { component: React.ElementType, currentApp?: Na
{/* Pluggable Toolbar Items */}
<CenterItemsPortal currentApp={props.currentApp} />
{/* (PREVIEW) Preview Menu */}
{!props.isMobile && (
<Dropdown>
<MenuButton
aria-label='Quick Tools Menu'
slots={{ root: IconButton }}
slotProps={{ root: { size: 'md' } }}
>
{/*<NotificationsNoneOutlinedIcon />*/}
<LightbulbOutlinedIcon />
{/*<FeedbackOutlinedIcon />*/}
</MenuButton>
<Menu placement='bottom-end' sx={{ minWidth: 220 }}>
<ListItem>
<Typography level='body-xs' sx={{ textTransform: 'uppercase' }}>
{Release.App.versionName}
</Typography>
</ListItem>
{!!releaseNotesUrl && (
<MenuItem onClick={handleShowReleaseNotes}>
<ListItemDecorator><NewReleasesIcon /></ListItemDecorator>
Release Notes
</MenuItem>
)}
<MenuItem onClick={handleShowTechnologies}>
{/*<ListItemDecorator><EventNoteOutlinedIcon /></ListItemDecorator>*/}
<ListItemDecorator><EngineeringIcon /></ListItemDecorator>
Build Info
</MenuItem>
{scratchClipSupported() && <MenuItem onClick={toggleScratchClipVisibility}>
<ListItemDecorator><HistoryIcon /></ListItemDecorator>
{isScratchClipVisible ? 'Hide ' : ''}Clipboard History
</MenuItem>}
</Menu>
</Dropdown>
)}
{/* We used to have the Preview (lightbulb) menu here */}
{/* Panel Open: has content always on Mobile (the app menu) */}
{panelHasContent && (
+137 -31
View File
@@ -2,19 +2,31 @@ import * as React from 'react';
import Router from 'next/router';
import type { SxProps } from '@mui/joy/styles/types';
import { Divider, Dropdown, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip } from '@mui/joy';
import { Divider, Dropdown, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy';
import CodeIcon from '@mui/icons-material/Code';
import HistoryIcon from '@mui/icons-material/History';
import LightbulbOutlinedIcon from '@mui/icons-material/LightbulbOutlined';
import MenuIcon from '@mui/icons-material/Menu';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import { blocksRenderHTMLIFrameCss } from '~/modules/blocks/code/code-renderers/RenderCodeHtmlIFrame';
import { BuildInfoCard } from '../../../../apps/news/AppNews';
import { BaseProduct } from '~/common/app.release';
import { BigAgiSquircleIcon } from '~/common/components/icons/big-agi/BigAgiSquircleIcon';
import { FeatureBadge } from '~/common/components/FeatureBadge';
import { GoodModal } from '~/common/components/modals/GoodModal';
import { PhSquaresFour } from '~/common/components/icons/phosphor/PhSquaresFour';
import { checkDivider, checkVisibileIcon, NavItemApp, navItems } from '~/common/app.nav';
import { themeZIndexDesktopNav } from '~/common/app.theme';
import { useHasLLMs } from '~/common/stores/llms/llms.hooks';
import { useOverlayComponents } from '~/common/layout/overlays/useOverlayComponents';
import { BringTheLove } from './BringTheLove';
import { DesktopNavGroupBox, DesktopNavIcon, navItemClasses } from './DesktopNavIcon';
import { InvertedBar, InvertedBarCornerItem } from '../InvertedBar';
import { optimaOpenModels, optimaOpenPreferences, optimaToggleDrawer, useOptimaDrawerOpen, useOptimaModals } from '../useOptima';
import { scratchClipSupported, useScratchClipVisibility } from '../scratchclip/store-scratchclip';
const desktopNavBarSx: SxProps = {
@@ -34,14 +46,58 @@ const navItemsDividerSx: SxProps = {
export function DesktopNav(props: { component: React.ElementType, currentApp?: NavItemApp }) {
// state
const [releaseNotesShown, setReleaseNotesShown] = React.useState(false);
/**
* NOTE: shall we fall back to the 'standard' release notes when not available on the tenant?
* - prob not because this could be a per-company deployment, and we don't know the tenant's release notes
*/
const releaseNotesUrl = BaseProduct.ReleaseNotes;
// external state
const isDrawerOpen = useOptimaDrawerOpen();
const { showPromisedOverlay } = useOverlayComponents();
const { showModels, showPreferences } = useOptimaModals();
const { isVisible: isScratchClipVisible, toggleVisibility: toggleScratchClipVisibility } = useScratchClipVisibility();
// derived state
const noLLMs = !useHasLLMs();
// handlers
const handleShowReleaseNotes = React.useCallback(async () => {
if (!releaseNotesUrl) return;
setReleaseNotesShown(true);
return await showPromisedOverlay('app-recent-changes', { rejectWithValue: false }, ({ onUserReject }) =>
<GoodModal
open
onClose={onUserReject}
noTitleBar
themedColor='neutral'
unfilterBackdrop
sx={{ minWidth: { xs: 400, sm: 580, md: 780, lg: 890 } }}
>
<iframe
src={releaseNotesUrl}
style={{ ...blocksRenderHTMLIFrameCss, height: '70svh' }}
title='Release Notes Embed'
loading='lazy' // do not load until visible in the viewport
/>
</GoodModal>,
);
}, [releaseNotesUrl, showPromisedOverlay]);
const handleShowTechnologies = React.useCallback(async () => {
return await showPromisedOverlay<boolean>('app-recent-changes', { rejectWithValue: false }, ({ onUserReject }) =>
<GoodModal open onClose={onUserReject} noTitleBar unfilterBackdrop>
<BuildInfoCard noMargin />
</GoodModal>,
);
}, [showPromisedOverlay]);
// show/hide the pane when clicking on the logo
const appUsesDrawer = !props.currentApp?.hideDrawer;
const logoButtonTogglesPane = (appUsesDrawer && !isDrawerOpen) || isDrawerOpen;
@@ -65,7 +121,7 @@ export function DesktopNav(props: { component: React.ElementType, currentApp?: N
}
});
// Application buttons (and group sepearator)
// Application buttons (and group separator)
const components: React.JSX.Element[] = visibleApps.map((app, appIdx) => {
const isActive = app === props.currentApp;
const isDrawerable = isActive && !app.hideDrawer;
@@ -88,35 +144,85 @@ export function DesktopNav(props: { component: React.ElementType, currentApp?: N
);
});
// Overflow dropdown menu
if (overflowApps.length) {
components.push(
<Dropdown key='n-app-overflow'>
<Tooltip disableInteractive enterDelay={600} title='More Apps'>
<MenuButton slots={{ root: DesktopNavIcon }} slotProps={{ root: { className: navItemClasses.typeApp } }}>
<MoreHorizIcon />
</MenuButton>
</Tooltip>
<Menu
variant='outlined'
placement='right-start'
popperOptions={{ modifiers: [{ name: 'offset', options: { offset: [0, -2] } }] }}
sx={{ minWidth: 220 }}
>
{overflowApps.map((app, appIdx) =>
<MenuItem key={'nav-app-extra-' + appIdx} onClick={() => Router.push(app.landingRoute || app.route)}>
<ListItemDecorator>
<app.icon />
</ListItemDecorator>
{app.name + (app.isDev ? ' [DEV]' : '')}
</MenuItem>,
)}
</Menu>
</Dropdown>,
);
}
components.push(
<Dropdown key='nav-quick-menu'>
<Tooltip disableInteractive enterDelay={600} title='Apps & Tools'>
<MenuButton slots={{ root: DesktopNavIcon }} slotProps={{ root: { className: navItemClasses.typeApp } }}>
<PhSquaresFour />
</MenuButton>
</Tooltip>
<Menu
variant="outlined"
placement="right-start"
popperOptions={{ modifiers: [{ name: 'offset', options: { offset: [0, -2] } }] }}
sx={{ minWidth: 260 }}
>
{/* APPS Section */}
{overflowApps.length > 0 && (
<>
<ListItem>
<Typography level="body-xs" sx={{ textTransform: 'uppercase', fontWeight: 600 }}>
Apps
</Typography>
</ListItem>
{overflowApps.map((app, appIdx) =>
<MenuItem key={'nav-app-extra-' + appIdx} onClick={() => Router.push(app.landingRoute || app.route)}>
<ListItemDecorator>
<app.icon />
</ListItemDecorator>
{app.name + (app.isDev ? ' [DEV]' : '')}
</MenuItem>,
)}
<ListDivider />
</>
)}
{/* QUICK TOOLS Section */}
<ListItem>
<Typography level="body-xs" sx={{ textTransform: 'uppercase', fontWeight: 600 }}>
Quick Tools
</Typography>
</ListItem>
<MenuItem disabled={!scratchClipSupported()} onClick={toggleScratchClipVisibility}>
<ListItemDecorator><HistoryIcon /></ListItemDecorator>
{isScratchClipVisible ? 'Hide ' : ''}Clipboard {scratchClipSupported() ? 'History' : '(not supported)'}
</MenuItem>
<ListDivider />
{/* SUPPORT Section */}
<ListItem>
<Typography level="body-xs" sx={{ textTransform: 'uppercase', fontWeight: 600 }}>
Support
</Typography>
</ListItem>
<ListItemButton component="a" href={BaseProduct.SupportForm()} target="_blank">
<ListItemDecorator><LightbulbOutlinedIcon /></ListItemDecorator>
I Have Feedback
</ListItemButton>
{!!releaseNotesUrl && (
<MenuItem onClick={handleShowReleaseNotes}>
<ListItemDecorator>
<FeatureBadge featureKey='nav-quick-menu' active={releaseNotesShown}>
<CodeIcon />
</FeatureBadge>
</ListItemDecorator>
Release Notes
</MenuItem>
)}
<MenuItem onClick={handleShowTechnologies}>
{/*<ListItemDecorator><BuildCircleOutlinedIcon /></ListItemDecorator>*/}
<ListItemDecorator />
Build Info
</MenuItem>
</Menu>
</Dropdown>);
return components;
}, [isDrawerOpen, props.currentApp]);
}, [toggleScratchClipVisibility, isScratchClipVisible, releaseNotesUrl, handleShowReleaseNotes, releaseNotesShown, handleShowTechnologies, props.currentApp, isDrawerOpen]);
// External link items