All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Updated CSS styles for macOS vibrancy modes, adjusting background colors and adding new layout properties for improved visual clarity. - Enhanced dashboard tab components with new corner fill elements and refined dimensions for better alignment and responsiveness. - Modified DashboardNavigation and DashboardTabs components to improve layout handling and user interaction, including adjustments to padding and flex properties. - Introduced transitions for non-active dashboard tab items to enhance user experience during interactions.
327 lines
9.7 KiB
JavaScript
327 lines
9.7 KiB
JavaScript
import PropTypes from 'prop-types'
|
|
import { useCallback, useState } from 'react'
|
|
import { Button, Flex, Typography } from 'antd'
|
|
import classNames from 'classnames'
|
|
import ScrollBox from './ScrollBox'
|
|
import PlusIcon from '../../Icons/PlusIcon'
|
|
import XMarkIcon from '../../Icons/XMarkIcon'
|
|
import HomeIcon from '../../Icons/HomeIcon'
|
|
import { getModelByName } from '../../../database/ObjectModels'
|
|
import { useNavigationTabs } from '../context/NavigationTabsContext'
|
|
import { getDesktopWindowId } from '../../../electrobun-bridge.js'
|
|
import { hasExternalTabDrag, writeTabDragData } from './tabDrag'
|
|
|
|
const { Text } = Typography
|
|
|
|
const DashboardTabChrome = () => (
|
|
<>
|
|
<span className='dashboard-tab-item-outline-container' aria-hidden='true'>
|
|
<span className='dashboard-tab-item-outline' aria-hidden='true' />
|
|
</span>
|
|
<span
|
|
className='dashboard-tab-item-corner dashboard-tab-item-corner-left'
|
|
aria-hidden='true'
|
|
>
|
|
<span className='dashboard-tab-item-corner-fill' />
|
|
<span className='dashboard-tab-item-corner-inner' />
|
|
</span>
|
|
<span
|
|
className='dashboard-tab-item-corner dashboard-tab-item-corner-right'
|
|
aria-hidden='true'
|
|
>
|
|
<span className='dashboard-tab-item-corner-fill' />
|
|
<span className='dashboard-tab-item-corner-inner' />
|
|
</span>
|
|
</>
|
|
)
|
|
|
|
const DashboardTabLabel = ({ tab, onClose }) => (
|
|
<Flex align='center' gap={6} className='dashboard-tabs-label'>
|
|
<Text ellipsis style={{ maxWidth: 240 }}>
|
|
{tab.title || 'Farm Control'}
|
|
</Text>
|
|
<span
|
|
className='dashboard-tabs-close'
|
|
role='button'
|
|
tabIndex={-1}
|
|
onClick={(event) => {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
onClose?.(tab.id)
|
|
}}
|
|
onMouseDown={(event) => event.stopPropagation()}
|
|
>
|
|
<XMarkIcon style={{ fontSize: 10 }} />
|
|
</span>
|
|
</Flex>
|
|
)
|
|
|
|
DashboardTabLabel.propTypes = {
|
|
tab: PropTypes.shape({
|
|
id: PropTypes.string.isRequired,
|
|
title: PropTypes.string
|
|
}).isRequired,
|
|
onClose: PropTypes.func
|
|
}
|
|
|
|
const DashboardTabs = () => {
|
|
const {
|
|
tabs,
|
|
activeTabId,
|
|
selectTab,
|
|
addTab,
|
|
closeTab,
|
|
reorderTabs,
|
|
handleTabDragStart,
|
|
handleTabDragEnd,
|
|
handleExternalTabDrop
|
|
} = useNavigationTabs()
|
|
const [draggedValue, setDraggedValue] = useState(null)
|
|
const [dropTarget, setDropTarget] = useState(null)
|
|
|
|
const clearDragState = useCallback(() => {
|
|
setDraggedValue(null)
|
|
setDropTarget(null)
|
|
}, [])
|
|
|
|
const handleItemDragStart = (event, tabId) => {
|
|
const tab = tabs.find((item) => item.id === tabId)
|
|
if (!tab) return
|
|
|
|
event.stopPropagation()
|
|
const item = event.currentTarget
|
|
if (item) {
|
|
const rect = item.getBoundingClientRect()
|
|
event.dataTransfer.setDragImage(
|
|
item,
|
|
event.clientX - rect.left,
|
|
event.clientY - rect.top
|
|
)
|
|
}
|
|
|
|
setDraggedValue(tabId)
|
|
event.dataTransfer.effectAllowed = 'move'
|
|
writeTabDragData(event, {
|
|
tab,
|
|
sourceWindowId: getDesktopWindowId()
|
|
})
|
|
handleTabDragStart(tabId)
|
|
}
|
|
|
|
const handleItemDragEnd = () => {
|
|
handleTabDragEnd()
|
|
clearDragState()
|
|
}
|
|
|
|
const handleItemDragOver = (event, tab) => {
|
|
const isExternal = hasExternalTabDrag(event)
|
|
if (draggedValue == null && !isExternal) return
|
|
if (draggedValue != null && String(tab.id) === String(draggedValue)) return
|
|
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
event.dataTransfer.dropEffect = 'move'
|
|
|
|
const rect = event.currentTarget.getBoundingClientRect()
|
|
setDropTarget({
|
|
value: tab.id,
|
|
insertBefore: event.clientX < rect.left + rect.width / 2
|
|
})
|
|
}
|
|
|
|
const handleItemDrop = (event, tab) => {
|
|
const isExternal = draggedValue == null && hasExternalTabDrag(event)
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
|
|
const target = dropTarget || {
|
|
value: tab.id,
|
|
insertBefore: true
|
|
}
|
|
|
|
if (isExternal) {
|
|
void handleExternalTabDrop(target.value, target.insertBefore)
|
|
clearDragState()
|
|
return
|
|
}
|
|
|
|
if (draggedValue == null) return
|
|
|
|
if (String(draggedValue) !== String(target.value)) {
|
|
reorderTabs(draggedValue, target.value, target.insertBefore)
|
|
}
|
|
|
|
clearDragState()
|
|
}
|
|
|
|
const handleListDragOver = (event) => {
|
|
const isExternal = hasExternalTabDrag(event)
|
|
if (draggedValue == null && !isExternal) return
|
|
if (
|
|
event.target instanceof Element &&
|
|
event.target.closest('.dashboard-tab-item')
|
|
) {
|
|
return
|
|
}
|
|
|
|
event.preventDefault()
|
|
event.dataTransfer.dropEffect = 'move'
|
|
|
|
const lastTab = tabs[tabs.length - 1]
|
|
if (!lastTab) return
|
|
setDropTarget({
|
|
value: lastTab.id,
|
|
insertBefore: false
|
|
})
|
|
}
|
|
|
|
const handleListDrop = (event) => {
|
|
const isExternal = draggedValue == null && hasExternalTabDrag(event)
|
|
if (draggedValue == null && !isExternal) return
|
|
if (
|
|
event.target instanceof Element &&
|
|
event.target.closest('.dashboard-tab-item')
|
|
) {
|
|
return
|
|
}
|
|
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
|
|
const lastTab = tabs[tabs.length - 1]
|
|
if (!lastTab) {
|
|
clearDragState()
|
|
return
|
|
}
|
|
|
|
if (isExternal) {
|
|
void handleExternalTabDrop(lastTab.id, false)
|
|
clearDragState()
|
|
return
|
|
}
|
|
|
|
if (String(draggedValue) !== String(lastTab.id)) {
|
|
reorderTabs(draggedValue, lastTab.id, false)
|
|
}
|
|
|
|
clearDragState()
|
|
}
|
|
|
|
const handleStripDragOver = (event) => {
|
|
if (!hasExternalTabDrag(event)) return
|
|
event.preventDefault()
|
|
event.dataTransfer.dropEffect = 'move'
|
|
}
|
|
|
|
const handleStripDrop = (event) => {
|
|
if (!hasExternalTabDrag(event)) return
|
|
event.preventDefault()
|
|
const lastTab = tabs[tabs.length - 1]
|
|
void handleExternalTabDrop(lastTab?.id, false)
|
|
}
|
|
|
|
const handleKeyDown = (event, tabIndex) => {
|
|
if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') return
|
|
|
|
event.preventDefault()
|
|
const direction = event.key === 'ArrowRight' ? 1 : -1
|
|
const currentIndex = tabs.findIndex((tab) => tab.id === activeTabId)
|
|
const startIndex = currentIndex === -1 ? tabIndex : currentIndex
|
|
const nextIndex = (startIndex + direction + tabs.length) % tabs.length
|
|
selectTab(tabs[nextIndex].id)
|
|
}
|
|
|
|
const isReordering = draggedValue != null || dropTarget != null
|
|
|
|
return (
|
|
<Flex
|
|
align='flex-start'
|
|
gap={4}
|
|
className={classNames(
|
|
'dashboard-tabs',
|
|
'electrobun-webkit-app-region-drag'
|
|
)}
|
|
style={{ flex: 1, minWidth: 0 }}
|
|
onDragOver={handleStripDragOver}
|
|
onDrop={handleStripDrop}
|
|
>
|
|
<ScrollBox className='dashboard-tabs-wrap' horizontal>
|
|
<Flex align='flex-start' gap={8} className='dashboard-tabs-inner'>
|
|
<div
|
|
className={classNames('dashboard-tabs-list', {
|
|
'dashboard-tabs-reordering': isReordering
|
|
})}
|
|
role='tablist'
|
|
onDragOver={handleListDragOver}
|
|
onDrop={handleListDrop}
|
|
>
|
|
{tabs.map((tab, index) => {
|
|
const model = tab.modelName ? getModelByName(tab.modelName) : null
|
|
const Icon = model?.icon || HomeIcon
|
|
const isSelected = tab.id === activeTabId
|
|
const isDragging =
|
|
draggedValue != null && String(draggedValue) === String(tab.id)
|
|
const isDropTarget =
|
|
dropTarget != null &&
|
|
String(dropTarget.value) === String(tab.id) &&
|
|
String(draggedValue) !== String(tab.id)
|
|
|
|
return (
|
|
<div
|
|
key={tab.id}
|
|
className={classNames('dashboard-tab-item-container', {
|
|
'dashboard-tab-item-container-active': isSelected
|
|
})}
|
|
>
|
|
<button
|
|
type='button'
|
|
role='tab'
|
|
aria-selected={isSelected}
|
|
tabIndex={isSelected ? 0 : -1}
|
|
className={classNames(
|
|
'dashboard-tab-item',
|
|
'electrobun-webkit-app-region-no-drag',
|
|
{
|
|
'dashboard-tab-item-active': isSelected,
|
|
'dashboard-tab-item-dragging': isDragging,
|
|
'dashboard-tab-item-drop-before':
|
|
isDropTarget && dropTarget.insertBefore,
|
|
'dashboard-tab-item-drop-after':
|
|
isDropTarget && !dropTarget.insertBefore
|
|
}
|
|
)}
|
|
draggable
|
|
onDragStart={(event) => handleItemDragStart(event, tab.id)}
|
|
onDragEnd={handleItemDragEnd}
|
|
onClick={() => selectTab(tab.id)}
|
|
onKeyDown={(event) => handleKeyDown(event, index)}
|
|
onDragOver={(event) => handleItemDragOver(event, tab)}
|
|
onDrop={(event) => handleItemDrop(event, tab)}
|
|
>
|
|
<DashboardTabChrome />
|
|
<span className='dashboard-tab-item-icon'>
|
|
<Icon style={{ fontSize: 14 }} color='secondary' />
|
|
</span>
|
|
<DashboardTabLabel tab={tab} onClose={closeTab} />
|
|
</button>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
<div className='dashboard-tabs-add-button-container'>
|
|
<Button
|
|
type='text'
|
|
size='small'
|
|
className='dashboard-tabs-add-button electrobun-webkit-app-region-no-drag'
|
|
onClick={addTab}
|
|
icon={<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />}
|
|
/>
|
|
</div>
|
|
</Flex>
|
|
</ScrollBox>
|
|
</Flex>
|
|
)
|
|
}
|
|
|
|
export default DashboardTabs
|