2024-04-15 12:04:17 -04:00
|
|
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
|
|
import { Resizable } from 're-resizable'
|
2024-07-01 15:31:42 -04:00
|
|
|
import { HTMLAttributes, useCallback, useEffect, useState } from 'react'
|
2024-04-15 12:04:17 -04:00
|
|
|
import { useHotkeys } from 'react-hotkeys-hook'
|
2024-04-25 09:56:55 -04:00
|
|
|
import { useStore } from 'useStore'
|
2024-04-15 12:04:17 -04:00
|
|
|
import { Tab } from '@headlessui/react'
|
2024-04-25 09:56:55 -04:00
|
|
|
import {
|
|
|
|
SidebarPane,
|
|
|
|
SidebarType,
|
|
|
|
bottomPanes,
|
|
|
|
topPanes,
|
|
|
|
} from './ModelingPanes'
|
2024-04-15 12:04:17 -04:00
|
|
|
import Tooltip from 'components/Tooltip'
|
|
|
|
import { ActionIcon } from 'components/ActionIcon'
|
|
|
|
import styles from './ModelingSidebar.module.css'
|
|
|
|
import { ModelingPane } from './ModelingPane'
|
2024-04-25 09:56:55 -04:00
|
|
|
import { isTauri } from 'lib/isTauri'
|
2024-04-15 12:04:17 -04:00
|
|
|
|
|
|
|
interface ModelingSidebarProps {
|
|
|
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
|
|
|
}
|
|
|
|
|
|
|
|
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
2024-04-15 21:40:45 -04:00
|
|
|
const { settings } = useSettingsAuthContext()
|
|
|
|
const onboardingStatus = settings.context.app.onboardingStatus
|
|
|
|
const { openPanes, buttonDownInStream } = useStore((s) => ({
|
2024-04-15 12:04:17 -04:00
|
|
|
buttonDownInStream: s.buttonDownInStream,
|
|
|
|
openPanes: s.openPanes,
|
|
|
|
}))
|
|
|
|
const pointerEventsCssClass =
|
2024-04-15 21:40:45 -04:00
|
|
|
buttonDownInStream ||
|
|
|
|
onboardingStatus.current === 'camera' ||
|
|
|
|
openPanes.length === 0
|
2024-04-15 12:04:17 -04:00
|
|
|
? 'pointer-events-none '
|
2024-04-15 21:40:45 -04:00
|
|
|
: 'pointer-events-auto '
|
2024-04-15 12:04:17 -04:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Resizable
|
|
|
|
className={`flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
|
|
|
|
defaultSize={{
|
|
|
|
width: '550px',
|
|
|
|
height: 'auto',
|
|
|
|
}}
|
|
|
|
minWidth={200}
|
|
|
|
maxWidth={800}
|
|
|
|
handleClasses={{
|
|
|
|
right:
|
|
|
|
(openPanes.length === 0 ? 'hidden ' : 'block ') +
|
2024-04-15 21:40:45 -04:00
|
|
|
'translate-x-1/2 hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ',
|
2024-05-16 22:30:47 -04:00
|
|
|
left: 'hidden',
|
|
|
|
top: 'hidden',
|
|
|
|
topLeft: 'hidden',
|
|
|
|
topRight: 'hidden',
|
|
|
|
bottom: 'hidden',
|
|
|
|
bottomLeft: 'hidden',
|
|
|
|
bottomRight: 'hidden',
|
2024-04-15 12:04:17 -04:00
|
|
|
}}
|
|
|
|
>
|
2024-07-01 15:31:42 -04:00
|
|
|
<div id="app-sidebar" className={styles.grid + ' flex-1'}>
|
|
|
|
<ModelingSidebarSection id="sidebar-top" panes={topPanes} />
|
|
|
|
<ModelingSidebarSection
|
|
|
|
id="sidebar-bottom"
|
|
|
|
panes={bottomPanes}
|
|
|
|
alignButtons="end"
|
|
|
|
/>
|
2024-04-15 12:04:17 -04:00
|
|
|
</div>
|
|
|
|
</Resizable>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-07-01 15:31:42 -04:00
|
|
|
interface ModelingSidebarSectionProps extends HTMLAttributes<HTMLDivElement> {
|
2024-04-25 09:56:55 -04:00
|
|
|
panes: SidebarPane[]
|
2024-04-15 12:04:17 -04:00
|
|
|
alignButtons?: 'start' | 'end'
|
|
|
|
}
|
|
|
|
|
|
|
|
function ModelingSidebarSection({
|
|
|
|
panes,
|
|
|
|
alignButtons = 'start',
|
2024-07-01 15:31:42 -04:00
|
|
|
className,
|
|
|
|
...props
|
2024-04-15 12:04:17 -04:00
|
|
|
}: ModelingSidebarSectionProps) {
|
|
|
|
const { settings } = useSettingsAuthContext()
|
|
|
|
const showDebugPanel = settings.context.modeling.showDebugPanel
|
|
|
|
const paneIds = panes.map((pane) => pane.id)
|
|
|
|
const { openPanes, setOpenPanes } = useStore((s) => ({
|
|
|
|
openPanes: s.openPanes,
|
|
|
|
setOpenPanes: s.setOpenPanes,
|
|
|
|
}))
|
|
|
|
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
|
|
|
|
const [currentPane, setCurrentPane] = useState(
|
2024-04-25 09:56:55 -04:00
|
|
|
foundOpenPane || ('none' as SidebarType | 'none')
|
2024-04-15 12:04:17 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
const togglePane = useCallback(
|
2024-04-25 09:56:55 -04:00
|
|
|
(newPane: SidebarType | 'none') => {
|
2024-04-15 12:04:17 -04:00
|
|
|
if (newPane === 'none') {
|
|
|
|
setOpenPanes(openPanes.filter((p) => p !== currentPane))
|
|
|
|
setCurrentPane('none')
|
|
|
|
} else if (newPane === currentPane) {
|
|
|
|
setCurrentPane('none')
|
|
|
|
setOpenPanes(openPanes.filter((p) => p !== newPane))
|
|
|
|
} else {
|
|
|
|
setOpenPanes([...openPanes.filter((p) => p !== currentPane), newPane])
|
|
|
|
setCurrentPane(newPane)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[openPanes, setOpenPanes, currentPane, setCurrentPane]
|
|
|
|
)
|
|
|
|
|
|
|
|
// Filter out the debug panel if it's not supposed to be shown
|
|
|
|
// TODO: abstract out for allowing user to configure which panes to show
|
2024-04-25 09:56:55 -04:00
|
|
|
const filteredPanes = (
|
|
|
|
showDebugPanel.current ? panes : panes.filter((pane) => pane.id !== 'debug')
|
|
|
|
).filter(
|
|
|
|
(pane) =>
|
|
|
|
!pane.hideOnPlatform ||
|
|
|
|
(isTauri()
|
|
|
|
? pane.hideOnPlatform === 'web'
|
|
|
|
: pane.hideOnPlatform === 'desktop')
|
|
|
|
)
|
2024-04-15 12:04:17 -04:00
|
|
|
useEffect(() => {
|
|
|
|
if (
|
|
|
|
!showDebugPanel.current &&
|
|
|
|
currentPane === 'debug' &&
|
|
|
|
openPanes.includes('debug')
|
|
|
|
) {
|
|
|
|
togglePane('debug')
|
|
|
|
}
|
|
|
|
}, [showDebugPanel.current, togglePane, openPanes])
|
|
|
|
|
|
|
|
return (
|
2024-07-01 15:31:42 -04:00
|
|
|
<div className={'group contents ' + className} {...props}>
|
2024-06-18 12:42:47 -04:00
|
|
|
<Tab.Group
|
|
|
|
vertical
|
|
|
|
selectedIndex={
|
|
|
|
currentPane === 'none' ? 0 : paneIds.indexOf(currentPane) + 1
|
2024-04-15 12:04:17 -04:00
|
|
|
}
|
2024-06-18 12:42:47 -04:00
|
|
|
onChange={(index) => {
|
|
|
|
const newPane = index === 0 ? 'none' : paneIds[index - 1]
|
|
|
|
togglePane(newPane)
|
|
|
|
}}
|
2024-04-15 12:04:17 -04:00
|
|
|
>
|
2024-06-18 12:42:47 -04:00
|
|
|
<Tab.List
|
2024-07-01 15:31:42 -04:00
|
|
|
id={`${props.id}-ribbon`}
|
2024-06-18 12:42:47 -04:00
|
|
|
className={
|
|
|
|
'pointer-events-auto ' +
|
|
|
|
(alignButtons === 'start'
|
|
|
|
? 'justify-start self-start'
|
|
|
|
: 'justify-end self-end') +
|
|
|
|
(currentPane === 'none'
|
|
|
|
? ' rounded-r focus-within:!border-primary/50'
|
|
|
|
: ' border-r-0') +
|
|
|
|
' p-2 col-start-1 col-span-1 h-fit w-fit flex flex-col items-start gap-2 ' +
|
|
|
|
'bg-chalkboard-10 border border-solid border-chalkboard-20 dark:bg-chalkboard-90 dark:border-chalkboard-80 group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
|
|
|
|
(openPanes.length === 1 && currentPane === 'none' ? 'pr-0.5' : '')
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<Tab key="none" className="sr-only">
|
|
|
|
No panes open
|
|
|
|
</Tab>
|
|
|
|
{filteredPanes.map((pane) => (
|
|
|
|
<ModelingPaneButton
|
|
|
|
key={pane.id}
|
|
|
|
paneConfig={pane}
|
|
|
|
currentPane={currentPane}
|
|
|
|
togglePane={() => togglePane(pane.id)}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</Tab.List>
|
|
|
|
<Tab.Panels
|
2024-07-01 15:31:42 -04:00
|
|
|
id={`${props.id}-pane`}
|
2024-06-18 12:42:47 -04:00
|
|
|
as="article"
|
|
|
|
className={
|
|
|
|
'col-start-2 col-span-1 ' +
|
|
|
|
(openPanes.length === 1
|
|
|
|
? currentPane !== 'none'
|
|
|
|
? `row-start-1 row-end-3`
|
|
|
|
: `hidden`
|
|
|
|
: ``)
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<Tab.Panel key="none" />
|
|
|
|
{filteredPanes.map((pane) => (
|
|
|
|
<Tab.Panel key={pane.id} className="h-full">
|
|
|
|
<ModelingPane
|
|
|
|
id={`${pane.id}-pane`}
|
|
|
|
title={pane.title}
|
|
|
|
Menu={pane.Menu}
|
|
|
|
>
|
|
|
|
{pane.Content instanceof Function ? (
|
|
|
|
<pane.Content />
|
|
|
|
) : (
|
|
|
|
pane.Content
|
|
|
|
)}
|
|
|
|
</ModelingPane>
|
|
|
|
</Tab.Panel>
|
|
|
|
))}
|
|
|
|
</Tab.Panels>
|
|
|
|
</Tab.Group>
|
|
|
|
</div>
|
2024-04-15 12:04:17 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ModelingPaneButtonProps {
|
2024-04-25 09:56:55 -04:00
|
|
|
paneConfig: SidebarPane
|
|
|
|
currentPane: SidebarType | 'none'
|
2024-04-15 12:04:17 -04:00
|
|
|
togglePane: () => void
|
|
|
|
}
|
|
|
|
|
|
|
|
function ModelingPaneButton({
|
|
|
|
paneConfig,
|
|
|
|
currentPane,
|
|
|
|
togglePane,
|
|
|
|
}: ModelingPaneButtonProps) {
|
|
|
|
useHotkeys(paneConfig.keybinding, togglePane, {
|
|
|
|
scopes: ['modeling'],
|
|
|
|
})
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Tab
|
|
|
|
key={paneConfig.id}
|
|
|
|
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-none"
|
|
|
|
onClick={togglePane}
|
2024-05-23 17:05:54 -07:00
|
|
|
data-testid={paneConfig.title}
|
2024-04-15 12:04:17 -04:00
|
|
|
>
|
|
|
|
<ActionIcon
|
|
|
|
icon={paneConfig.icon}
|
|
|
|
className="p-1"
|
|
|
|
size="sm"
|
|
|
|
iconClassName={
|
|
|
|
paneConfig.id === currentPane
|
|
|
|
? ' !text-chalkboard-10'
|
|
|
|
: '!text-chalkboard-80 dark:!text-chalkboard-30'
|
|
|
|
}
|
|
|
|
bgClassName={
|
|
|
|
'rounded-sm ' +
|
|
|
|
(paneConfig.id === currentPane ? '!bg-primary' : '!bg-transparent')
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
<Tooltip position="right" hoverOnly delay={800}>
|
|
|
|
<span>{paneConfig.title}</span>
|
|
|
|
<br />
|
|
|
|
<span className="text-xs capitalize">{paneConfig.keybinding}</span>
|
|
|
|
</Tooltip>
|
|
|
|
</Tab>
|
|
|
|
)
|
|
|
|
}
|