Refactor Dashboard Navigation and Tabs for Enhanced User Experience
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Restructured the DashboardNavigation component to separate leading and trailing navigation elements, improving layout and clarity. - Updated CSS styles for navigation and dashboard tabs to enhance responsiveness and visual consistency. - Implemented drag-and-drop functionality for dashboard tabs, allowing users to reorder tabs seamlessly. - Introduced new styles for tab items and improved handling of drag states, enhancing the overall interaction experience.
This commit is contained in:
parent
7fef25d911
commit
d6c72c21e8
@ -112,6 +112,18 @@
|
||||
--webkit-user-select: none;
|
||||
}
|
||||
|
||||
.electron-navigation-leading,
|
||||
.electron-navigation-trailing {
|
||||
align-self: stretch;
|
||||
height: 42px;
|
||||
border-bottom: 1px solid var(--color-header-border);
|
||||
}
|
||||
|
||||
.electron-navigation-trailing {
|
||||
flex: 0 1 auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.electron-body * {
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
@ -145,12 +157,20 @@ html.macos-vibrancy .main-layout {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
html.macos-vibrancy .light-mode {
|
||||
--color-layout-background: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
html.macos-vibrancy .dark-mode {
|
||||
--color-layout-background: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
html.macos-vibrancy .light-mode .ant-layout.main-content-layout {
|
||||
background: rgba(255, 255, 255, 0.88) !important;
|
||||
background: var(--color-layout-background) !important;
|
||||
}
|
||||
|
||||
html.macos-vibrancy .dark-mode .ant-layout.main-content-layout {
|
||||
background: rgba(0, 0, 0, 0.88) !important;
|
||||
background: var(--color-layout-background) !important;
|
||||
}
|
||||
|
||||
html.macos-vibrancy .light-mode .ant-layout-sider {
|
||||
@ -367,6 +387,7 @@ code {
|
||||
:root,
|
||||
.light-mode,
|
||||
html:has(.light-mode) {
|
||||
--color-layout-background: #ffffff;
|
||||
--kbd-color: #363636;
|
||||
--kbd-bg: #efefef;
|
||||
--kbd-inset: #e8e8e8;
|
||||
@ -378,6 +399,7 @@ html:has(.light-mode) {
|
||||
|
||||
.dark-mode,
|
||||
html:has(.dark-mode) {
|
||||
--color-layout-background: #000000;
|
||||
--kbd-color: #ececec;
|
||||
--kbd-bg: #3a3a3a;
|
||||
--kbd-inset: #323232;
|
||||
@ -3349,25 +3371,188 @@ body.objectKanbanColumnResizing * {
|
||||
.dashboard-tabs {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.dashboard-tabs .segmented-nav-item-label {
|
||||
padding-inline: 0px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.dashboard-tabs-segmented-wrap {
|
||||
.dashboard-tabs-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dashboard-tabs-segmented-wrap .simplebar-track.simplebar-vertical {
|
||||
.dashboard-tabs-wrap .simplebar-track.simplebar-vertical {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dashboard-tabs-segmented-inner {
|
||||
.dashboard-tabs-inner {
|
||||
width: max-content;
|
||||
height: 100%;
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dashboard-tabs-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
padding-inline: 12px;
|
||||
height: 100%;
|
||||
max-height: 42px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-container {
|
||||
height: 43px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-container-active {
|
||||
border-bottom: none;
|
||||
height: 43px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-container:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: 0;
|
||||
border-bottom: 1px solid var(--color-header-border);
|
||||
}
|
||||
|
||||
.dashboard-tab-item-container-active + .dashboard-tab-item-container:before {
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.dashboard-tabs-list:has(+ .dashboard-tab-item-container-active):before {
|
||||
right: 9.5px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-container-active:before {
|
||||
content: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dashboard-tab-item {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: visible;
|
||||
margin: 5px 0 0 0;
|
||||
padding: 0 8px 0 8px;
|
||||
border: 1px solid var(--color-list-view-tabs-border);
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
|
||||
transition:
|
||||
background 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
line-height: 30px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item:focus-visible {
|
||||
outline: 2px solid
|
||||
color-mix(in srgb, var(--color-primary, #1677ff) 45%, transparent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item:not(.dashboard-tab-item-active):hover {
|
||||
background: var(--color-button-background);
|
||||
border: 1px solid
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--color-button-border) 40%,
|
||||
var(--color-button-background)
|
||||
);
|
||||
}
|
||||
|
||||
.dashboard-tab-item-active {
|
||||
z-index: 2;
|
||||
align-self: stretch;
|
||||
margin-bottom: -1px;
|
||||
padding-bottom: 5px;
|
||||
border-color: transparent;
|
||||
border-radius: 12px 12px 0 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-outline-container {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-outline {
|
||||
position: absolute;
|
||||
inset: 0 0 20px 0;
|
||||
border: 1px solid var(--color-header-border);
|
||||
border-bottom: none;
|
||||
border-radius: 12px 12px 0 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-corner {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-corner-left {
|
||||
left: -21px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-corner-right {
|
||||
right: -21px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-corner-inner {
|
||||
position: absolute;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--color-header-border);
|
||||
border-radius: 15.5px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-corner-left .dashboard-tab-item-corner-inner {
|
||||
bottom: 1px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-corner-right .dashboard-tab-item-corner-inner {
|
||||
bottom: 1px;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-active .dashboard-tab-item-outline,
|
||||
.dashboard-tab-item-active .dashboard-tab-item-corner {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dashboard-tabs-label {
|
||||
@ -3395,7 +3580,36 @@ body.objectKanbanColumnResizing * {
|
||||
.dashboard-tabs-add-button {
|
||||
padding-inline: 4px;
|
||||
min-width: 20px;
|
||||
margin-bottom: 2px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-drop-before::before,
|
||||
.dashboard-tab-item-drop-after::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 2px;
|
||||
border-radius: 1px;
|
||||
background: var(--color-primary, #1677ff);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-drop-before::before {
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
.dashboard-tab-item-drop-after::after {
|
||||
right: -5px;
|
||||
}
|
||||
|
||||
.dashboard-tabs-reordering
|
||||
.dashboard-tab-item:not(.dashboard-tab-item-dragging) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dashboard-tab-panes {
|
||||
|
||||
@ -280,75 +280,8 @@ const DashboardNavigation = () => {
|
||||
/>
|
||||
)
|
||||
|
||||
const navigationContents = (
|
||||
<Flex style={{ width: '100%' }} align='center'>
|
||||
{isMacOSApp ? <DashboardWindowButtons /> : null}
|
||||
{isOtherApp ? (
|
||||
<>
|
||||
<WindowAppMenu />{' '}
|
||||
<Divider
|
||||
type='vertical'
|
||||
style={{
|
||||
height: '14px',
|
||||
margin: showNavigationLabels
|
||||
? '3px 3px 0 1.5px'
|
||||
: '3px 0px 0 1.5px'
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{showControls && isMobile && menu}
|
||||
{showDesktopLogo == true ? (
|
||||
<FarmControlLogo
|
||||
style={{
|
||||
fontSize: '200px',
|
||||
height: '18px',
|
||||
marginRight: '15px'
|
||||
}}
|
||||
/>
|
||||
) : showMobileLogo == true ? (
|
||||
<FarmControlLogoSmall
|
||||
style={{
|
||||
fontSize: '48px',
|
||||
marginRight: isElectron ? '20px' : '25px'
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div style={{ flexGrow: 1, minWidth: 0 }}>
|
||||
{showControls && !isMobile ? (
|
||||
<Flex align='center' style={{ width: '100%', minWidth: 0 }}>
|
||||
{menu}
|
||||
{isElectron ? (
|
||||
<>
|
||||
<Divider
|
||||
type='vertical'
|
||||
style={{
|
||||
margin: showNavigationLabels
|
||||
? '3px 16px 0 4px'
|
||||
: '3px 16px 0 0',
|
||||
height: '14px'
|
||||
}}
|
||||
/>
|
||||
<DashboardTabs />
|
||||
</>
|
||||
) : null}
|
||||
</Flex>
|
||||
) : null}
|
||||
{!showControls && (
|
||||
<Text
|
||||
type='secondary'
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
marginLeft: '8px',
|
||||
userSelect: 'none',
|
||||
'--webkit-user-select': 'none'
|
||||
}}
|
||||
>
|
||||
Farm Control
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
const navigationTrailing = (
|
||||
<>
|
||||
<div className='electrobun-webkit-app-region-no-drag'>
|
||||
<Flex
|
||||
gap={'small'}
|
||||
@ -461,17 +394,117 @@ const DashboardNavigation = () => {
|
||||
</Flex>
|
||||
</div>
|
||||
{isOtherApp ? <DashboardWindowButtons /> : null}
|
||||
</>
|
||||
)
|
||||
|
||||
const navigationLeading = (
|
||||
<>
|
||||
{isMacOSApp ? <DashboardWindowButtons /> : null}
|
||||
{isOtherApp ? (
|
||||
<>
|
||||
<WindowAppMenu />{' '}
|
||||
<Divider
|
||||
type='vertical'
|
||||
style={{
|
||||
height: '14px',
|
||||
margin: showNavigationLabels
|
||||
? '3px 3px 0 1.5px'
|
||||
: '3px 0px 0 1.5px'
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{showControls && isMobile && menu}
|
||||
{showDesktopLogo == true ? (
|
||||
<FarmControlLogo
|
||||
style={{
|
||||
fontSize: '200px',
|
||||
height: '18px',
|
||||
marginRight: '15px'
|
||||
}}
|
||||
/>
|
||||
) : showMobileLogo == true ? (
|
||||
<FarmControlLogoSmall
|
||||
style={{
|
||||
fontSize: '48px',
|
||||
marginRight: isElectron ? '20px' : '25px'
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
|
||||
const navigationContents = (
|
||||
<Flex style={{ width: '100%' }} align='center'>
|
||||
{navigationLeading}
|
||||
<div style={{ flexGrow: 1, minWidth: 0 }}>
|
||||
{showControls && !isMobile ? (
|
||||
<Flex align='center' style={{ width: '100%', minWidth: 0 }}>
|
||||
{menu}
|
||||
</Flex>
|
||||
) : null}
|
||||
{!showControls && (
|
||||
<Text
|
||||
type='secondary'
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
marginLeft: '8px',
|
||||
userSelect: 'none',
|
||||
'--webkit-user-select': 'none'
|
||||
}}
|
||||
>
|
||||
Farm Control
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{navigationTrailing}
|
||||
</Flex>
|
||||
)
|
||||
|
||||
const electronDragClass =
|
||||
isFullScreen || (isMaximized && isOtherApp)
|
||||
? 'electrobun-webkit-app-region-no-drag'
|
||||
: 'electrobun-webkit-app-region-drag'
|
||||
|
||||
return (
|
||||
<>
|
||||
{isElectron ? (
|
||||
<Flex
|
||||
className={`ant-menu-horizontal electron-navigation-wrapper ant-menu-light ${isFullScreen || (isMaximized && isOtherApp) ? 'electrobun-webkit-app-region-no-drag' : 'electrobun-webkit-app-region-drag'}`}
|
||||
className={`electron-navigation-wrapper ${electronDragClass}`}
|
||||
style={{ lineHeight: '40px', padding: '0 2px 0 2px' }}
|
||||
>
|
||||
{navigationContents}
|
||||
<Flex align='center' className='electron-navigation-leading'>
|
||||
{navigationLeading}
|
||||
{showControls && !isMobile ? menu : null}
|
||||
{showControls && !isMobile ? (
|
||||
<Divider
|
||||
type='vertical'
|
||||
style={{
|
||||
margin: showNavigationLabels
|
||||
? '3px 16px 0 4px'
|
||||
: '3px 16px 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>
|
||||
{showControls && !isMobile ? <DashboardTabs /> : null}
|
||||
<Flex align='center' className='electron-navigation-trailing'>
|
||||
{navigationTrailing}
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex vertical>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useMemo } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Button, Flex, Typography } from 'antd'
|
||||
import classNames from 'classnames'
|
||||
import SegmentedNav from './SegmentedNav'
|
||||
import ScrollBox from './ScrollBox'
|
||||
import PlusIcon from '../../Icons/PlusIcon'
|
||||
import XMarkIcon from '../../Icons/XMarkIcon'
|
||||
@ -14,6 +13,26 @@ 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-inner' />
|
||||
</span>
|
||||
<span
|
||||
className='dashboard-tab-item-corner dashboard-tab-item-corner-right'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<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 }}>
|
||||
@ -55,30 +74,31 @@ const DashboardTabs = () => {
|
||||
handleTabDragEnd,
|
||||
handleExternalTabDrop
|
||||
} = useNavigationTabs()
|
||||
const [draggedValue, setDraggedValue] = useState(null)
|
||||
const [dropTarget, setDropTarget] = useState(null)
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
tabs.map((tab) => {
|
||||
const model = tab.modelName ? getModelByName(tab.modelName) : null
|
||||
const Icon = model?.icon || HomeIcon
|
||||
return {
|
||||
value: tab.id,
|
||||
reorderable: true,
|
||||
icon: (
|
||||
<Icon
|
||||
style={{ fontSize: 14, marginTop: '-2px' }}
|
||||
color='secondary'
|
||||
/>
|
||||
),
|
||||
label: <DashboardTabLabel tab={tab} onClose={closeTab} />
|
||||
}
|
||||
}),
|
||||
[closeTab, tabs]
|
||||
)
|
||||
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()
|
||||
@ -86,6 +106,105 @@ const DashboardTabs = () => {
|
||||
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()
|
||||
@ -99,34 +218,105 @@ const DashboardTabs = () => {
|
||||
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='center'
|
||||
align='flex-start'
|
||||
gap={4}
|
||||
className={classNames(
|
||||
'dashboard-tabs',
|
||||
'electrobun-webkit-app-region-no-drag'
|
||||
'electrobun-webkit-app-region-drag'
|
||||
)}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
onDragOver={handleStripDragOver}
|
||||
onDrop={handleStripDrop}
|
||||
>
|
||||
<ScrollBox className='dashboard-tabs-segmented-wrap' horizontal>
|
||||
<Flex align='center' gap={8} className='dashboard-tabs-segmented-inner'>
|
||||
<SegmentedNav
|
||||
className='dashboard-tabs-segmented'
|
||||
value={activeTabId}
|
||||
options={options}
|
||||
onChange={selectTab}
|
||||
onReorder={reorderTabs}
|
||||
onExternalDrop={handleExternalTabDrop}
|
||||
onItemDragStart={handleItemDragStart}
|
||||
onItemDragEnd={handleTabDragEnd}
|
||||
/>
|
||||
<ScrollBox
|
||||
className='dashboard-tabs-wrap'
|
||||
horizontal
|
||||
horizontalBottomPadding={0}
|
||||
>
|
||||
<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, marginTop: '-2px' }}
|
||||
color='secondary'
|
||||
/>
|
||||
</span>
|
||||
<DashboardTabLabel tab={tab} onClose={closeTab} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
type='text'
|
||||
size='small'
|
||||
className='dashboard-tabs-add-button'
|
||||
className='dashboard-tabs-add-button electrobun-webkit-app-region-no-drag'
|
||||
onClick={addTab}
|
||||
icon={<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />}
|
||||
/>
|
||||
|
||||
@ -265,7 +265,10 @@ const SegmentedNav = ({
|
||||
const handleGroupDragOver = useCallback(
|
||||
(event) => {
|
||||
const isExternal = acceptsExternal && hasExternalTabDrag(event)
|
||||
if ((!onReorder && !isExternal) || (draggedValue == null && !isExternal)) {
|
||||
if (
|
||||
(!onReorder && !isExternal) ||
|
||||
(draggedValue == null && !isExternal)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -296,7 +299,10 @@ const SegmentedNav = ({
|
||||
const handleGroupDrop = useCallback(
|
||||
(event) => {
|
||||
const isExternal = acceptsExternal && draggedValue == null
|
||||
if ((!onReorder && !isExternal) || (draggedValue == null && !isExternal)) {
|
||||
if (
|
||||
(!onReorder && !isExternal) ||
|
||||
(draggedValue == null && !isExternal)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -216,6 +216,10 @@ export const ThemeProvider = ({ children }) => {
|
||||
'--color-mobile-footer-item-border',
|
||||
isDarkMode ? '#303030' : '#f0f0f0'
|
||||
)
|
||||
root.style.setProperty(
|
||||
'--color-header-border',
|
||||
isDarkMode ? 'rgba(253,253,253,0.12)' : 'rgba(10,10,10,0.12)'
|
||||
)
|
||||
}, [isDarkMode, primaryColorOverride])
|
||||
|
||||
const themeConfig = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user