Compare commits

..

4 Commits

Author SHA1 Message Date
d52eea3a41 Add Dashboard Development Controls and Update Navigation Styles
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced a new DashboardDevControls component to manage API server connection states and provide visual feedback.
- Enhanced DashboardNavigation with improved layout and styling, including new transition effects for navigation elements.
- Adjusted CSS styles for better spacing and visibility of dashboard components, ensuring a more cohesive user experience.
2026-09-18 02:35:15 +01:00
f3af883bd2 Refine Dashboard Tab Styles and Adjust Margins
- Reduced the left margin for dashboard tab close buttons for improved spacing.
- Removed hover styles for the close button to streamline the design and enhance visual consistency.
2026-09-18 01:47:37 +01:00
5cc74bca18 Enhance Dashboard Styles with New Background and Corner Adjustments
- Added a new background style for the main content layout in light mode under macOS vibrancy for improved visual depth.
- Introduced additional styling for active dashboard tab items, including a new after pseudo-element for better visual distinction.
- Refined corner radius properties for dashboard tab items to enhance alignment and aesthetics.
- Simplified corner fill positioning using the inset property for more consistent layout behavior across components.
2026-09-18 01:42:15 +01:00
65417f75b8 Enhance Dashboard Navigation with New Modal Visibility Handling and Styling
- Introduced state management for modal visibility in the DashboardNavigation component to improve user interaction and layout responsiveness.
- Added new CSS styles for dashboard navigation elements, including transitions and layout adjustments for better visual clarity.
- Refactored the rendering logic to conditionally display navigation elements based on modal visibility, enhancing the overall user experience.
2026-09-18 01:42:07 +01:00
3 changed files with 568 additions and 244 deletions

View File

@ -112,6 +112,79 @@
--webkit-user-select: none; --webkit-user-select: none;
} }
.dashboard-navigation-center {
position: relative;
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
align-items: center;
}
.dashboard-navigation-fade {
opacity: 1;
visibility: visible;
transition:
opacity 0.2s ease 0.2s,
visibility 0s linear 0.2s;
}
.dashboard-navigation-fade-hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
transition:
opacity 0.2s ease 0s,
visibility 0s linear 0.2s;
}
.dashboard-navigation-window-title {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 16px;
padding-right: 16px;
font-size: 14px;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
}
.dashboard-navigation-center-border {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px;
background: var(--color-header-border);
pointer-events: none;
opacity: 1;
/* Wait for outgoing chrome to finish fading before this line appears. */
transition: opacity 0.2s ease 0.2s;
}
.dashboard-navigation-center-border-hidden {
opacity: 0;
/* Stay through the gap, then fade out as incoming chrome fades in. */
transition: opacity 0.2s ease 0.2s;
}
.dashboard-menu-container {
position: relative;
height: 42px;
padding-top: 2px;
}
.dashboard-menu-container:after {
border-bottom: 1px solid var(--color-header-border);
content: '';
position: absolute;
left: 0;
right: -2px;
bottom: 0;
}
.electron-navigation-leading, .electron-navigation-leading,
.electron-navigation-trailing { .electron-navigation-trailing {
align-self: stretch; align-self: stretch;
@ -190,6 +263,17 @@ html.macos-vibrancy .light-mode .ant-layout-sider {
background: rgba(244, 244, 244, 0.82) !important; background: rgba(244, 244, 244, 0.82) !important;
} }
html.macos-vibrancy .light-mode .ant-layout.main-content-layout:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.85) !important;
z-index: -1;
}
html.macos-vibrancy .dark-mode .ant-layout-sider { html.macos-vibrancy .dark-mode .ant-layout-sider {
background: rgba(10, 10, 10, 0.82) !important; background: rgba(10, 10, 10, 0.82) !important;
} }
@ -3413,7 +3497,7 @@ body.objectKanbanColumnResizing * {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 4px; gap: 4px;
padding-left: 12px; padding-left: 14px;
height: 100%; height: 100%;
max-height: 42px; max-height: 42px;
} }
@ -3439,7 +3523,7 @@ body.objectKanbanColumnResizing * {
} }
.dashboard-tab-item-container:first-child:before { .dashboard-tab-item-container:first-child:before {
left: -12px; left: -14px;
} }
.dashboard-tab-item-container:last-child:before { .dashboard-tab-item-container:last-child:before {
@ -3518,7 +3602,19 @@ body.objectKanbanColumnResizing * {
margin-bottom: -1px; margin-bottom: -1px;
padding-bottom: 5px; padding-bottom: 5px;
border-color: transparent; border-color: transparent;
position: relative;
border-radius: 12px 12px 0 0; border-radius: 12px 12px 0 0;
}
.dashboard-tab-item-active:after {
content: '';
position: absolute;
top: 1px;
left: 1px;
right: 1px;
bottom: -1px;
border-radius: 9px 9px 0 0;
z-index: -2;
background: var(--color-layout-background); background: var(--color-layout-background);
} }
@ -3542,7 +3638,7 @@ body.objectKanbanColumnResizing * {
.dashboard-tab-item-corner { .dashboard-tab-item-corner {
--tab-corner-size: 22px; --tab-corner-size: 22px;
--tab-corner-radius: 15.5px; --tab-corner-radius: 16px;
--tab-corner-inset: 1px; --tab-corner-inset: 1px;
--tab-corner-center: calc( --tab-corner-center: calc(
var(--tab-corner-size) - var(--tab-corner-inset) - var(--tab-corner-radius) var(--tab-corner-size) - var(--tab-corner-inset) - var(--tab-corner-radius)
@ -3566,10 +3662,7 @@ body.objectKanbanColumnResizing * {
.dashboard-tab-item-corner-fill { .dashboard-tab-item-corner-fill {
position: absolute; position: absolute;
top: 1px; inset: 0;
left: 2px;
right: 2px;
bottom: -1px;
background: var(--color-layout-background); background: var(--color-layout-background);
-webkit-mask-repeat: no-repeat; -webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat; mask-repeat: no-repeat;
@ -3577,33 +3670,27 @@ body.objectKanbanColumnResizing * {
.dashboard-tab-item-corner-left .dashboard-tab-item-corner-fill { .dashboard-tab-item-corner-left .dashboard-tab-item-corner-fill {
-webkit-mask-image: radial-gradient( -webkit-mask-image: radial-gradient(
circle var(--tab-corner-radius) at var(--tab-corner-center) circle at var(--tab-corner-center) var(--tab-corner-center),
var(--tab-corner-center), transparent calc(var(--tab-corner-radius) - 0.5px),
transparent var(--tab-corner-radius), #000 calc(var(--tab-corner-radius) - 0.5px)
#000 var(--tab-corner-radius)
); );
mask-image: radial-gradient( mask-image: radial-gradient(
circle var(--tab-corner-radius) at var(--tab-corner-center) circle at var(--tab-corner-center) var(--tab-corner-center),
var(--tab-corner-center), transparent calc(var(--tab-corner-radius) - 0.5px),
transparent var(--tab-corner-radius), #000 calc(var(--tab-corner-radius) - 0.5px)
#000 var(--tab-corner-radius)
); );
} }
.dashboard-tab-item-corner-right .dashboard-tab-item-corner-fill { .dashboard-tab-item-corner-right .dashboard-tab-item-corner-fill {
-webkit-mask-image: radial-gradient( -webkit-mask-image: radial-gradient(
circle var(--tab-corner-radius) at circle at calc(100% - var(--tab-corner-center)) var(--tab-corner-center),
calc(var(--tab-corner-radius) + var(--tab-corner-inset)) transparent calc(var(--tab-corner-radius) - 0.5px),
var(--tab-corner-center), #000 calc(var(--tab-corner-radius) - 0.5px)
transparent var(--tab-corner-radius),
#000 var(--tab-corner-radius)
); );
mask-image: radial-gradient( mask-image: radial-gradient(
circle var(--tab-corner-radius) at circle at calc(100% - var(--tab-corner-center)) var(--tab-corner-center),
calc(var(--tab-corner-radius) + var(--tab-corner-inset)) transparent calc(var(--tab-corner-radius) - 0.5px),
var(--tab-corner-center), #000 calc(var(--tab-corner-radius) - 0.5px)
transparent var(--tab-corner-radius),
#000 var(--tab-corner-radius)
); );
} }
@ -3652,7 +3739,7 @@ body.objectKanbanColumnResizing * {
justify-content: center; justify-content: center;
width: 16px; width: 16px;
height: 16px; height: 16px;
margin-left: 2px; margin-left: 1px;
border-radius: 8px; border-radius: 8px;
color: var(--color-text-secondary, inherit); color: var(--color-text-secondary, inherit);
opacity: 0.7; opacity: 0.7;
@ -3666,11 +3753,6 @@ body.objectKanbanColumnResizing * {
inset: -8px -8px -8px -6px; inset: -8px -8px -8px -6px;
} }
.dashboard-tabs-close:hover {
opacity: 1;
background: var(--color-button-background);
}
.dashboard-tabs-add-button { .dashboard-tabs-add-button {
padding-inline: 4px; padding-inline: 4px;
min-width: 20px; min-width: 20px;

View File

@ -0,0 +1,70 @@
import { useContext, useEffect, useState } from 'react'
import { Space, Tag } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import { ApiServerContext } from '../context/ApiServerContext'
import Tooltip from './Tooltip'
import CloudIcon from '../../Icons/CloudIcon'
import DeveloperIcon from '../../Icons/DeveloperIcon'
const DashboardDevControls = () => {
const { connecting, connected } = useContext(ApiServerContext)
const navigate = useNavigate()
const [apiServerState, setApiServerState] = useState('disconnected')
useEffect(() => {
if (connecting == true) {
setApiServerState('connecting')
} else if (connected == true) {
setApiServerState('connected')
} else {
setApiServerState('disconnected')
}
}, [connecting, connected])
if (import.meta.env.MODE !== 'development') return null
return (
<Space>
{apiServerState === 'connected' ? (
<Tooltip content='Connected to api server'>
<Tag
color='success'
style={{ marginRight: 0 }}
icon={<CloudIcon />}
/>
</Tooltip>
) : null}
{apiServerState === 'connecting' ? (
<Tooltip content='Connecting to api server...'>
<Tag
color='warning'
style={{ marginRight: 0 }}
icon={<LoadingOutlined />}
/>
</Tooltip>
) : null}
{apiServerState === 'disconnected' ? (
<Tooltip content='Disconnected from api server'>
<Tag
color='error'
style={{ marginRight: 0 }}
icon={<CloudIcon />}
/>
</Tooltip>
) : null}
<Tooltip content='Developer'>
<Tag
color='yellow'
style={{ marginRight: 0 }}
icon={<DeveloperIcon />}
onClick={() => {
navigate('/dashboard/developer/sessionstorage')
}}
/>
</Tooltip>
</Space>
)
}
export default DashboardDevControls

View File

@ -1,5 +1,6 @@
// DashboardNavigation.js // DashboardNavigation.js
import { useContext, useEffect, useState, useMemo } from 'react' import { useContext, useEffect, useState, useMemo, useCallback, memo } from 'react'
import PropTypes from 'prop-types'
import { import {
Menu, Menu,
Flex, Flex,
@ -11,7 +12,6 @@ import {
Typography, Typography,
Popover Popover
} from 'antd' } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import { AuthContext } from '../context/AuthContext' import { AuthContext } from '../context/AuthContext'
import { SpotlightContext } from '../context/SpotlightContext' import { SpotlightContext } from '../context/SpotlightContext'
import { ApiServerContext } from '../context/ApiServerContext' import { ApiServerContext } from '../context/ApiServerContext'
@ -35,9 +35,9 @@ import CloudIcon from '../../Icons/CloudIcon'
import BellIcon from '../../Icons/BellIcon' import BellIcon from '../../Icons/BellIcon'
import SearchIcon from '../../Icons/SearchIcon' import SearchIcon from '../../Icons/SearchIcon'
import SettingsIcon from '../../Icons/SettingsIcon' import SettingsIcon from '../../Icons/SettingsIcon'
import DeveloperIcon from '../../Icons/DeveloperIcon'
import { ElectronContext } from '../context/ElectronContext' import { ElectronContext } from '../context/ElectronContext'
import DashboardWindowButtons from './DashboardWindowButtons' import DashboardWindowButtons from './DashboardWindowButtons'
import DashboardDevControls from './DashboardDevControls'
import WindowAppMenu from './WindowAppMenu' import WindowAppMenu from './WindowAppMenu'
import WebAppSwitcher from './WebAppSwitcher' import WebAppSwitcher from './WebAppSwitcher'
import DashboardTabs from './DashboardTabs' import DashboardTabs from './DashboardTabs'
@ -57,6 +57,225 @@ import { getEffectiveAppearance } from '../../../database/Settings'
const { Text } = Typography const { Text } = Typography
const NAV_CENTER_FADE_STYLE = {
width: '100%',
minWidth: 0,
display: 'flex',
alignItems: 'center'
}
const NAV_ELECTRON_CENTER_FADE_STYLE = {
flex: 1,
minWidth: 0,
display: 'flex',
alignItems: 'center'
}
const NAV_ELECTRON_TABS_STYLE = { flex: 1, minWidth: 0, display: 'flex' }
const NAV_ELECTRON_DIVIDER_STYLE_WITH_LABELS = {
margin: '3px 1px 0 4px',
height: '14px'
}
const NAV_ELECTRON_DIVIDER_STYLE = {
margin: '3px 1px 0 0',
height: '14px'
}
const NAV_TRAILING_FLEX_STYLE = { marginTop: '-2px', marginRight: '6px' }
const NAV_TRAILING_SPACE_STYLE = { paddingTop: '2px', marginRight: '8px' }
const NAV_SEARCH_BUTTON_STYLE = { marginTop: '4px' }
const NAV_BELL_BUTTON_STYLE = { marginTop: '2px' }
const NAV_BADGE_STYLE = { padding: 0, fontWeight: 600 }
const NAV_UPDATE_TAG_STYLE = { cursor: 'pointer', margin: '2px 0 0 0' }
const NAV_OVERFLOW_BUTTON_STYLE = { marginBottom: '4px' }
const navigationFadeClass = (visible) =>
`dashboard-navigation-fade${visible ? '' : ' dashboard-navigation-fade-hidden'}`
const NavigationFade = memo(function NavigationFade({ visible, children, style }) {
return (
<div
className={navigationFadeClass(visible)}
inert={visible ? undefined : true}
aria-hidden={visible ? undefined : true}
style={style}
>
{children}
</div>
)
})
NavigationFade.propTypes = {
visible: PropTypes.bool,
children: PropTypes.node,
style: PropTypes.object
}
const DashboardNavigationTrailingItems = memo(
function DashboardNavigationTrailingItems({
controlsReady,
isMobile,
isElectron,
unreadCount,
userProfile,
availableUpdate,
userPopoverOpen,
onUserPopoverOpenChange,
onSearch,
onNotifications,
onCheckUpdates
}) {
return (
<Flex gap='small' align='center' style={NAV_TRAILING_FLEX_STYLE}>
{controlsReady ? (
<Space style={NAV_TRAILING_SPACE_STYLE}>
<WebAppSwitcher />
<KeyboardShortcut
shortcut='alt+q'
hint='ALT Q'
onTrigger={onSearch}
>
<Button
icon={<SearchIcon />}
type='text'
style={NAV_SEARCH_BUTTON_STYLE}
onClick={onSearch}
/>
</KeyboardShortcut>
<Badge
count={unreadCount}
size='small'
offset={[-5, 8]}
style={NAV_BADGE_STYLE}
>
<KeyboardShortcut
shortcut='alt+n'
hint='ALT N'
onTrigger={onNotifications}
>
<Button
icon={<BellIcon />}
type='text'
style={NAV_BELL_BUTTON_STYLE}
onClick={onNotifications}
/>
</KeyboardShortcut>
</Badge>
</Space>
) : null}
<DashboardDevControls />
{controlsReady && userProfile ? (
<Space>
<Popover
content={
<UserProfilePopover
onClose={() => onUserPopoverOpenChange(false)}
/>
}
placement='bottomRight'
trigger='hover'
open={userPopoverOpen}
onOpenChange={onUserPopoverOpenChange}
arrow={false}
>
<Tag style={{ marginRight: 0 }} icon={<PersonIcon />}>
{!isMobile && (userProfile?.name || userProfile.username)}
</Tag>
</Popover>
</Space>
) : null}
{controlsReady && isElectron && availableUpdate ? (
<Tag
icon={<CloudIcon />}
style={NAV_UPDATE_TAG_STYLE}
color='cyan'
onClick={onCheckUpdates}
>
Update Available
</Tag>
) : null}
</Flex>
)
}
)
DashboardNavigationTrailingItems.propTypes = {
controlsReady: PropTypes.bool,
isMobile: PropTypes.bool,
isElectron: PropTypes.bool,
unreadCount: PropTypes.number,
userProfile: PropTypes.shape({
name: PropTypes.string,
username: PropTypes.string
}),
availableUpdate: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
userPopoverOpen: PropTypes.bool,
onUserPopoverOpenChange: PropTypes.func,
onSearch: PropTypes.func,
onNotifications: PropTypes.func,
onCheckUpdates: PropTypes.func
}
const DashboardElectronCenterItems = memo(function DashboardElectronCenterItems({
menu,
showNavigationLabels
}) {
return (
<>
<div className='dashboard-menu-container'>{menu}</div>
<Divider
type='vertical'
style={
showNavigationLabels
? NAV_ELECTRON_DIVIDER_STYLE_WITH_LABELS
: NAV_ELECTRON_DIVIDER_STYLE
}
/>
<div style={NAV_ELECTRON_TABS_STYLE}>
<DashboardTabs />
</div>
</>
)
})
DashboardElectronCenterItems.propTypes = {
menu: PropTypes.node,
showNavigationLabels: PropTypes.bool
}
const DashboardNavigationTrailing = memo(function DashboardNavigationTrailing({
showControls,
isOtherApp,
...itemProps
}) {
return (
<>
<div className='electrobun-webkit-app-region-no-drag'>
<NavigationFade visible={showControls}>
<DashboardNavigationTrailingItems {...itemProps} />
</NavigationFade>
</div>
{isOtherApp ? <DashboardWindowButtons /> : null}
</>
)
})
DashboardNavigationTrailing.propTypes = {
showControls: PropTypes.bool,
isOtherApp: PropTypes.bool,
controlsReady: PropTypes.bool,
isMobile: PropTypes.bool,
isElectron: PropTypes.bool,
unreadCount: PropTypes.number,
userProfile: PropTypes.shape({
name: PropTypes.string,
username: PropTypes.string
}),
availableUpdate: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
userPopoverOpen: PropTypes.bool,
onUserPopoverOpenChange: PropTypes.func,
onSearch: PropTypes.func,
onNotifications: PropTypes.func,
onCheckUpdates: PropTypes.func
}
const mapSidebarItemsToMenu = (items, navigate) => const mapSidebarItemsToMenu = (items, navigate) =>
items.map((item) => { items.map((item) => {
if (item?.type === 'divider') { if (item?.type === 'divider') {
@ -83,8 +302,7 @@ const mapSidebarItemsToMenu = (items, navigate) =>
const DashboardNavigation = () => { const DashboardNavigation = () => {
const { userProfile } = useContext(AuthContext) const { userProfile } = useContext(AuthContext)
const { showSpotlight } = useContext(SpotlightContext) const { showSpotlight } = useContext(SpotlightContext)
const { connecting, connected, userSettings, userSettingsLoaded } = const { userSettings, userSettingsLoaded } = useContext(ApiServerContext)
useContext(ApiServerContext)
const { authenticated } = useContext(AuthContext) const { authenticated } = useContext(AuthContext)
const { const {
showNavigationLabels, showNavigationLabels,
@ -94,7 +312,6 @@ const DashboardNavigation = () => {
} = useThemeContext() } = useThemeContext()
const { toggleNotificationCenter, unreadCount } = const { toggleNotificationCenter, unreadCount } =
useContext(NotificationContext) useContext(NotificationContext)
const [apiServerState, setApiServerState] = useState('disconnected')
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const [selectedKey, setSelectedKey] = useState('production') const [selectedKey, setSelectedKey] = useState('production')
@ -209,17 +426,18 @@ const DashboardNavigation = () => {
[includeDevItems, location.pathname, selectedKey, userProfile] [includeDevItems, location.pathname, selectedKey, userProfile]
) )
const headerMenuItems = isMobile const headerMenuItems = useMemo(
? sidebarMenuItems () =>
: showNavigationLabels isMobile
? mainMenuItems ? sidebarMenuItems
: mainMenuItems.map((item) => ({ ...item, label: undefined })) : showNavigationLabels
? mainMenuItems
: mainMenuItems.map((item) => ({ ...item, label: undefined })),
[isMobile, mainMenuItems, showNavigationLabels, sidebarMenuItems]
)
const [userPopoverOpen, setUserPopoverOpen] = useState(false) const [userPopoverOpen, setUserPopoverOpen] = useState(false)
const [anyModalVisible, setAnyModalVisible] = useState(false)
const userPopoverContent = (
<UserProfilePopover onClose={() => setUserPopoverOpen(false)} />
)
useEffect(() => { useEffect(() => {
const pathParts = location.pathname.split('/').filter(Boolean) const pathParts = location.pathname.split('/').filter(Boolean)
@ -229,19 +447,12 @@ const DashboardNavigation = () => {
setSelectedKey(nextItem?.key || sectionKey) setSelectedKey(nextItem?.key || sectionKey)
}, [location.pathname, mainMenuItems]) }, [location.pathname, mainMenuItems])
useEffect(() => { const handleMainMenuClick = useCallback(
if (connecting == true) { ({ key }) => {
setApiServerState('connecting') navigate(getSidebarDefaultPath(key, { includeDev, userProfile }))
} else if (connected == true) { },
setApiServerState('connected') [includeDev, navigate, userProfile]
} else { )
setApiServerState('disconnected')
}
}, [connecting, connected])
const handleMainMenuClick = ({ key }) => {
navigate(getSidebarDefaultPath(key, { includeDev, userProfile }))
}
useEffect(() => { useEffect(() => {
if (!isElectron || !setSidebarViewMenu) return if (!isElectron || !setSidebarViewMenu) return
@ -255,146 +466,110 @@ const DashboardNavigation = () => {
const showDesktopLogo = !isElectron && !isMobile const showDesktopLogo = !isElectron && !isMobile
const showMobileLogo = !isElectron && isMobile const showMobileLogo = !isElectron && isMobile
const showControls = !isElectron || authenticated useEffect(() => {
const isModalWrapVisible = (node) => {
if (!(node instanceof Element)) return false
if (node.getAttribute('aria-hidden') === 'true') return false
const style = window.getComputedStyle(node)
return (
style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.pointerEvents !== 'none'
)
}
const menu = ( const updateModalVisibility = () => {
<Menu const wraps = document.querySelectorAll('.ant-modal-wrap')
mode='horizontal' setAnyModalVisible(Array.from(wraps).some(isModalWrapVisible))
className={`${isElectron ? 'electron-navigation' : null} ${!showNavigationLabels && !isMobile ? 'no-navigation-labels' : ''}${isMobile ? 'mobile-menu' : ''}`} }
items={headerMenuItems}
style={{ let frameId = 0
flexWrap: 'wrap', const scheduleUpdate = () => {
flexGrow: isElectron && !isMobile ? 0 : 1, if (frameId) return
border: 0, frameId = window.requestAnimationFrame(() => {
minWidth: 0 frameId = 0
}} updateModalVisibility()
onClick={isMobile ? undefined : handleMainMenuClick} })
selectedKeys={[isMobile ? selectedSidebarItemKey : selectedKey]} }
overflowedIndicator={
<Button updateModalVisibility()
type='text' const observer = new MutationObserver(scheduleUpdate)
icon={<MenuIcon />} observer.observe(document.body, {
style={{ marginBottom: '4px' }} childList: true,
/> subtree: true,
} attributes: true,
/> attributeFilter: ['style', 'class', 'aria-hidden']
})
return () => {
observer.disconnect()
if (frameId) window.cancelAnimationFrame(frameId)
}
}, [])
const controlsReady = !isElectron || authenticated
const showControls = controlsReady && !anyModalVisible
const handleSearch = useCallback(() => {
showSpotlight()
}, [showSpotlight])
const handleNotifications = useCallback(() => {
toggleNotificationCenter()
}, [toggleNotificationCenter])
const handleCheckUpdates = useCallback(() => {
checkForUpdates()
}, [checkForUpdates])
const overflowedIndicator = useMemo(
() => (
<Button type='text' icon={<MenuIcon />} style={NAV_OVERFLOW_BUTTON_STYLE} />
),
[]
)
const menu = useMemo(
() => (
<Menu
mode='horizontal'
className={`${isElectron ? 'electron-navigation' : null} ${!showNavigationLabels && !isMobile ? 'no-navigation-labels' : ''}${isMobile ? 'mobile-menu' : ''}`}
items={headerMenuItems}
style={{
flexWrap: 'wrap',
flexGrow: isElectron && !isMobile ? 0 : 1,
border: 0,
minWidth: 0
}}
onClick={isMobile ? undefined : handleMainMenuClick}
selectedKeys={[isMobile ? selectedSidebarItemKey : selectedKey]}
overflowedIndicator={overflowedIndicator}
/>
),
[
handleMainMenuClick,
headerMenuItems,
isElectron,
isMobile,
overflowedIndicator,
selectedKey,
selectedSidebarItemKey,
showNavigationLabels
]
) )
const navigationTrailing = ( const navigationTrailing = (
<> <DashboardNavigationTrailing
<div className='electrobun-webkit-app-region-no-drag'> showControls={showControls}
<Flex isOtherApp={isOtherApp}
gap={'small'} controlsReady={controlsReady}
align='center' isMobile={isMobile}
style={{ marginTop: '-2px', marginRight: '6px' }} isElectron={isElectron}
> unreadCount={unreadCount}
{showControls && ( userProfile={userProfile}
<Space style={{ paddingTop: '2px', marginRight: '8px' }}> availableUpdate={availableUpdate}
<WebAppSwitcher /> userPopoverOpen={userPopoverOpen}
<KeyboardShortcut onUserPopoverOpenChange={setUserPopoverOpen}
shortcut='alt+q' onSearch={handleSearch}
hint='ALT Q' onNotifications={handleNotifications}
onTrigger={() => showSpotlight()} onCheckUpdates={handleCheckUpdates}
> />
<Button
icon={<SearchIcon />}
type='text'
style={{ marginTop: '4px' }}
onClick={() => showSpotlight()}
/>
</KeyboardShortcut>
<Badge
count={unreadCount}
size='small'
offset={[-5, 8]}
style={{ padding: 0, fontWeight: 600 }}
>
<KeyboardShortcut
shortcut='alt+n'
hint='ALT N'
onTrigger={() => toggleNotificationCenter()}
>
<Button
icon={<BellIcon />}
type='text'
style={{ marginTop: '2px' }}
onClick={() => toggleNotificationCenter()}
/>
</KeyboardShortcut>
</Badge>
</Space>
)}
{import.meta.env.MODE === 'development' && (
<Space>
{apiServerState === 'connected' ? (
<Tooltip content='Connected to api server'>
<Tag
color='success'
style={{ marginRight: 0 }}
icon={<CloudIcon />}
/>
</Tooltip>
) : null}
{apiServerState === 'connecting' ? (
<Tooltip content='Connecting to api erver...'>
<Tag
color='warning'
style={{ marginRight: 0 }}
icon={<LoadingOutlined />}
/>
</Tooltip>
) : null}
{apiServerState === 'disconnected' ? (
<Tooltip content='Disconnected from api server'>
<Tag
color='error'
style={{ marginRight: 0 }}
icon={<CloudIcon />}
/>
</Tooltip>
) : null}
<Tooltip content='Developer'>
<Tag
color='yellow'
style={{ marginRight: 0 }}
icon={<DeveloperIcon />}
onClick={() => {
navigate('/dashboard/developer/sessionstorage')
}}
/>
</Tooltip>
</Space>
)}
{showControls && userProfile ? (
<Space>
<Popover
content={userPopoverContent}
placement='bottomRight'
trigger='hover'
open={userPopoverOpen}
onOpenChange={setUserPopoverOpen}
arrow={false}
>
<Tag style={{ marginRight: 0 }} icon={<PersonIcon />}>
{!isMobile && (userProfile?.name || userProfile.username)}
</Tag>
</Popover>
</Space>
) : null}
{showControls && isElectron && availableUpdate ? (
<Tag
icon={<CloudIcon />}
style={{ cursor: 'pointer', margin: '2px 0 0 0' }}
color='cyan'
onClick={() => checkForUpdates()}
>
Update Available
</Tag>
) : null}
</Flex>
</div>
{isOtherApp ? <DashboardWindowButtons /> : null}
</>
) )
const navigationLeading = ( const navigationLeading = (
@ -414,7 +589,9 @@ const DashboardNavigation = () => {
/> />
</> </>
) : null} ) : null}
{showControls && isMobile && menu} {isMobile ? (
<NavigationFade visible={showControls}>{menu}</NavigationFade>
) : null}
{showDesktopLogo == true ? ( {showDesktopLogo == true ? (
<FarmControlLogo <FarmControlLogo
style={{ style={{
@ -434,28 +611,38 @@ const DashboardNavigation = () => {
</> </>
) )
const windowTitle = (
<div
className={`dashboard-navigation-window-title ${navigationFadeClass(
!showControls
)}`}
aria-hidden={showControls}
inert={showControls ? true : undefined}
>
<Text type='secondary'>Farm Control</Text>
</div>
)
const navigationCenterBorder = (
<div
className={`dashboard-navigation-center-border${
showControls ? ' dashboard-navigation-center-border-hidden' : ''
}`}
aria-hidden='true'
/>
)
const navigationContents = ( const navigationContents = (
<Flex style={{ width: '100%' }} align='center'> <Flex style={{ width: '100%' }} align='center'>
{navigationLeading} {navigationLeading}
<div style={{ flexGrow: 1, minWidth: 0 }}> <div className='dashboard-navigation-center'>
{showControls && !isMobile ? ( {!isMobile ? (
<Flex align='center' style={{ width: '100%', minWidth: 0 }}> <NavigationFade visible={showControls} style={NAV_CENTER_FADE_STYLE}>
{menu} {menu}
</Flex> </NavigationFade>
) : null} ) : null}
{!showControls && ( {windowTitle}
<Text {navigationCenterBorder}
type='secondary'
style={{
fontSize: '14px',
marginLeft: '8px',
userSelect: 'none',
'--webkit-user-select': 'none'
}}
>
Farm Control
</Text>
)}
</div> </div>
{navigationTrailing} {navigationTrailing}
</Flex> </Flex>
@ -473,39 +660,24 @@ const DashboardNavigation = () => {
className={`electron-navigation-wrapper ${electronDragClass}`} className={`electron-navigation-wrapper ${electronDragClass}`}
style={{ lineHeight: '40px', padding: '0 2px 0 2px' }} style={{ lineHeight: '40px', padding: '0 2px 0 2px' }}
> >
<Flex <Flex align='center' className='electron-navigation-leading'>
align='center'
className='electron-navigation-leading'
style={{ flexGrow: !showControls ? 1 : 0 }}
>
{navigationLeading} {navigationLeading}
{showControls && !isMobile ? menu : null}
{showControls && !isMobile ? (
<Divider
type='vertical'
style={{
margin: showNavigationLabels
? '3px 1px 0 4px'
: '3px 1px 0 0',
height: '14px'
}}
/>
) : null}
{!showControls ? (
<Text
type='secondary'
style={{
fontSize: '14px',
marginLeft: '8px',
userSelect: 'none',
'--webkit-user-select': 'none'
}}
>
Farm Control
</Text>
) : null}
</Flex> </Flex>
{showControls && !isMobile ? <DashboardTabs /> : null} <div className='dashboard-navigation-center'>
{!isMobile ? (
<NavigationFade
visible={showControls}
style={NAV_ELECTRON_CENTER_FADE_STYLE}
>
<DashboardElectronCenterItems
menu={menu}
showNavigationLabels={showNavigationLabels}
/>
</NavigationFade>
) : null}
{windowTitle}
{navigationCenterBorder}
</div>
<Flex align='center' className='electron-navigation-trailing'> <Flex align='center' className='electron-navigation-trailing'>
{navigationTrailing} {navigationTrailing}
</Flex> </Flex>