Reunite split sidebar, add ability to register action buttons to it (#3100)
* Rework ribbon to support panes and actions * Restore nice focus-within highlighting * A better export icon * Fix up some issues with tests due to sidebar and tooltip tweaks * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Re-run CI --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@ -3864,10 +3864,7 @@ test.describe('Regression tests', () => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// expand variables section
|
||||
const variablesTabButton = page.getByRole('tab', {
|
||||
name: 'Variables',
|
||||
exact: false,
|
||||
})
|
||||
const variablesTabButton = page.getByTestId('Variables')
|
||||
await variablesTabButton.click()
|
||||
|
||||
// can find sketch001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor)
|
||||
@ -3892,10 +3889,7 @@ test.describe('Regression tests', () => {
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
const variablesTabButton = page.getByRole('tab', {
|
||||
name: 'Variables',
|
||||
exact: false,
|
||||
})
|
||||
const variablesTabButton = page.getByTestId('Variables')
|
||||
await variablesTabButton.click()
|
||||
// expect to see "myVar:5"
|
||||
await expect(
|
||||
@ -7777,7 +7771,7 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const codePane = page.getByRole('textbox').locator('div')
|
||||
const codePaneButton = page.getByRole('tab', { name: 'KCL Code' })
|
||||
const codePaneButton = page.getByTestId('KCL Code')
|
||||
const lineButton = page.getByRole('button', { name: 'Line' })
|
||||
const arcButton = page.getByRole('button', { name: 'Tangential Arc' })
|
||||
const extrudeButton = page.getByRole('button', { name: 'Extrude' })
|
||||
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
@ -58,44 +58,40 @@ async function waitForDefaultPlanesToBeVisible(page: Page) {
|
||||
}
|
||||
|
||||
async function openKclCodePanel(page: Page) {
|
||||
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
|
||||
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
|
||||
const paneLocator = page.getByTestId('KCL Code')
|
||||
const isOpen = (await paneLocator?.getAttribute('aria-pressed')) === 'true'
|
||||
|
||||
if (!isOpen) {
|
||||
await paneLocator.click()
|
||||
await paneLocator.and(page.locator('[aria-selected="true"]')).waitFor()
|
||||
await expect(paneLocator).toHaveAttribute('aria-pressed', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
async function closeKclCodePanel(page: Page) {
|
||||
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
|
||||
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
|
||||
const paneLocator = page.getByTestId('KCL Code')
|
||||
const isOpen = (await paneLocator?.getAttribute('aria-pressed')) === 'true'
|
||||
if (isOpen) {
|
||||
await paneLocator.click()
|
||||
await paneLocator
|
||||
.and(page.locator(':not([aria-selected="true"])'))
|
||||
.waitFor()
|
||||
await expect(paneLocator).not.toHaveAttribute('aria-pressed', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
async function openDebugPanel(page: Page) {
|
||||
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
|
||||
const debugLocator = page.getByTestId('Debug')
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-pressed')) === 'true'
|
||||
|
||||
if (!isOpen) {
|
||||
await debugLocator.click()
|
||||
await debugLocator.and(page.locator('[aria-selected="true"]')).waitFor()
|
||||
await expect(debugLocator).toHaveAttribute('aria-pressed', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
async function closeDebugPanel(page: Page) {
|
||||
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
|
||||
const debugLocator = page.getByTestId('Debug')
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-pressed')) === 'true'
|
||||
if (isOpen) {
|
||||
await debugLocator.click()
|
||||
await debugLocator
|
||||
.and(page.locator(':not([aria-selected="true"])'))
|
||||
.waitFor()
|
||||
await expect(debugLocator).not.toHaveAttribute('aria-pressed', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
@ -471,10 +467,11 @@ export const doExport = async (
|
||||
page: Page
|
||||
): Promise<Paths> => {
|
||||
await page.getByRole('button', { name: APP_NAME }).click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Export', exact: false })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Export', exact: false }).click()
|
||||
const exportMenuButton = page.getByRole('button', {
|
||||
name: 'Export current part',
|
||||
})
|
||||
await expect(exportMenuButton).toBeVisible()
|
||||
await exportMenuButton.click()
|
||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||
|
||||
// Go through export via command bar
|
||||
|
@ -223,6 +223,16 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
floppyDiskArrow: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4 4H4.5L7 4L13 4L13.2071 4L13.3536 4.14645L15.8536 6.64645L16 6.79289V7V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V7.20711L13.5 5.70711V7V7.5L13 7.5L7 7.5L6.5 7.5V7V5L5 5V15H6.5V10V9.5H7H13H13.5V10V10.5C13.1547 10.5 12.8196 10.5438 12.5 10.626V10.5H7.5V15H9.53095C9.57451 15.3493 9.66311 15.6847 9.79076 16H7H4.5H4V15.5V4.5V4ZM7.5 5V6.5L12.5 6.5V5L7.5 5ZM16.3904 14.1877L14.3904 11.6877L13.6096 12.3123L14.9597 14H11V15H14.9597L13.6096 16.6877L14.3904 17.3123L16.3904 14.8123L16.6403 14.5L16.3904 14.1877Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
folder: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
@ -1,5 +1,5 @@
|
||||
.panel {
|
||||
@apply relative z-0 rounded-r max-w-full h-full flex-1;
|
||||
@apply relative z-0 rounded-r max-w-full flex-auto;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
@apply bg-chalkboard-10/50 focus-within:bg-chalkboard-10/90 backdrop-blur-sm border border-chalkboard-20;
|
||||
|
@ -3,10 +3,14 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
|
||||
export interface ModelingPaneProps
|
||||
extends React.PropsWithChildren,
|
||||
React.HTMLAttributes<HTMLDivElement> {
|
||||
icon?: CustomIconName | IconDefinition
|
||||
title: string
|
||||
Menu?: React.ReactNode | React.FC
|
||||
detailsTestId?: string
|
||||
@ -14,13 +18,25 @@ export interface ModelingPaneProps
|
||||
}
|
||||
|
||||
export const ModelingPaneHeader = ({
|
||||
icon,
|
||||
title,
|
||||
Menu,
|
||||
onClose,
|
||||
}: Pick<ModelingPaneProps, 'title' | 'Menu' | 'onClose'>) => {
|
||||
}: Pick<ModelingPaneProps, 'icon' | 'title' | 'Menu' | 'onClose'>) => {
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<div className="flex gap-2 items-center flex-1">{title}</div>
|
||||
<div className="flex gap-2 items-center flex-1">
|
||||
{icon && (
|
||||
<ActionIcon
|
||||
icon={icon}
|
||||
className="p-1"
|
||||
size="sm"
|
||||
iconClassName="!text-chalkboard-80 dark:!text-chalkboard-30"
|
||||
bgClassName="!bg-transparent"
|
||||
/>
|
||||
)}
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
{Menu instanceof Function ? <Menu /> : Menu}
|
||||
<ActionButton
|
||||
Element="button"
|
||||
@ -42,6 +58,7 @@ export const ModelingPaneHeader = ({
|
||||
|
||||
export const ModelingPane = ({
|
||||
title,
|
||||
icon,
|
||||
id,
|
||||
children,
|
||||
className,
|
||||
@ -63,14 +80,19 @@ export const ModelingPane = ({
|
||||
data-testid={detailsTestId}
|
||||
id={id}
|
||||
className={
|
||||
'group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' +
|
||||
'focus-within:border-primary dark:focus-within:border-chalkboard-50 ' +
|
||||
pointerEventsCssClass +
|
||||
styles.panel +
|
||||
' group ' +
|
||||
(className || '')
|
||||
}
|
||||
>
|
||||
<ModelingPaneHeader title={title} Menu={Menu} onClose={onClose} />
|
||||
<ModelingPaneHeader
|
||||
icon={icon}
|
||||
title={title}
|
||||
Menu={Menu}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<div className="relative w-full">{children}</div>
|
||||
</section>
|
||||
)
|
||||
|
@ -35,13 +35,13 @@ export type SidebarPane = {
|
||||
hideOnPlatform?: 'desktop' | 'web'
|
||||
}
|
||||
|
||||
export const topPanes: SidebarPane[] = [
|
||||
export const sidebarPanes: SidebarPane[] = [
|
||||
{
|
||||
id: 'code',
|
||||
title: 'KCL Code',
|
||||
icon: faCode,
|
||||
Content: KclEditorPane,
|
||||
keybinding: 'shift + c',
|
||||
keybinding: 'Shift + C',
|
||||
Menu: KclEditorMenu,
|
||||
},
|
||||
{
|
||||
@ -49,40 +49,37 @@ export const topPanes: SidebarPane[] = [
|
||||
title: 'Project Files',
|
||||
icon: 'folder',
|
||||
Content: FileTreeInner,
|
||||
keybinding: 'shift + f',
|
||||
keybinding: 'Shift + F',
|
||||
Menu: FileTreeMenu,
|
||||
hideOnPlatform: 'web',
|
||||
},
|
||||
]
|
||||
|
||||
export const bottomPanes: SidebarPane[] = [
|
||||
{
|
||||
id: 'variables',
|
||||
title: 'Variables',
|
||||
icon: faSquareRootVariable,
|
||||
Content: MemoryPane,
|
||||
Menu: MemoryPaneMenu,
|
||||
keybinding: 'shift + v',
|
||||
keybinding: 'Shift + V',
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
title: 'Logs',
|
||||
icon: faCodeCommit,
|
||||
Content: LogsPane,
|
||||
keybinding: 'shift + l',
|
||||
keybinding: 'Shift + L',
|
||||
},
|
||||
{
|
||||
id: 'kclErrors',
|
||||
title: 'KCL Errors',
|
||||
icon: faExclamationCircle,
|
||||
Content: KclErrorsPane,
|
||||
keybinding: 'shift + e',
|
||||
keybinding: 'Shift + E',
|
||||
},
|
||||
{
|
||||
id: 'debug',
|
||||
title: 'Debug',
|
||||
icon: faBugSlash,
|
||||
Content: DebugPane,
|
||||
keybinding: 'shift + d',
|
||||
keybinding: 'Shift + D',
|
||||
},
|
||||
]
|
||||
|
@ -1,39 +1,84 @@
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { Resizable } from 're-resizable'
|
||||
import { HTMLAttributes, useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { Tab } from '@headlessui/react'
|
||||
import {
|
||||
SidebarPane,
|
||||
SidebarType,
|
||||
bottomPanes,
|
||||
topPanes,
|
||||
} from './ModelingPanes'
|
||||
import { SidebarType, sidebarPanes } from './ModelingPanes'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import styles from './ModelingSidebar.module.css'
|
||||
import { ModelingPane } from './ModelingPane'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
interface ModelingSidebarProps {
|
||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||
}
|
||||
|
||||
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const onboardingStatus = settings.context.app.onboardingStatus
|
||||
const { context } = useModelingContext()
|
||||
const { send, context } = useModelingContext()
|
||||
const pointerEventsCssClass =
|
||||
context.store?.buttonDownInStream ||
|
||||
onboardingStatus.current === 'camera' ||
|
||||
context.store?.openPanes.length === 0
|
||||
? 'pointer-events-none '
|
||||
: 'pointer-events-auto '
|
||||
const showDebugPanel = settings.context.modeling.showDebugPanel
|
||||
|
||||
const sidebarActions: SidebarAction[] = [
|
||||
{
|
||||
id: 'export',
|
||||
title: 'Export part',
|
||||
icon: 'floppyDiskArrow',
|
||||
iconClassName: '!p-0',
|
||||
keybinding: 'Ctrl + Shift + E',
|
||||
action: () =>
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Export', groupId: 'modeling' },
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
// // 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
|
||||
const filteredPanes = useMemo(
|
||||
() =>
|
||||
(showDebugPanel.current
|
||||
? sidebarPanes
|
||||
: sidebarPanes.filter((pane) => pane.id !== 'debug')
|
||||
).filter(
|
||||
(pane) =>
|
||||
!pane.hideOnPlatform ||
|
||||
(isTauri()
|
||||
? pane.hideOnPlatform === 'web'
|
||||
: pane.hideOnPlatform === 'desktop')
|
||||
),
|
||||
[sidebarPanes, showDebugPanel.current]
|
||||
)
|
||||
|
||||
const togglePane = useCallback(
|
||||
(newPane: SidebarType) => {
|
||||
send({
|
||||
type: 'Set context',
|
||||
data: {
|
||||
openPanes: context.store?.openPanes.includes(newPane)
|
||||
? context.store?.openPanes.filter((pane) => pane !== newPane)
|
||||
: [...context.store?.openPanes, newPane],
|
||||
},
|
||||
})
|
||||
},
|
||||
[context.store?.openPanes, send]
|
||||
)
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
className={`flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
|
||||
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
|
||||
defaultSize={{
|
||||
width: '550px',
|
||||
height: 'auto',
|
||||
@ -54,153 +99,64 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
||||
}}
|
||||
>
|
||||
<div id="app-sidebar" className={styles.grid + ' flex-1'}>
|
||||
<ModelingSidebarSection id="sidebar-top" panes={topPanes} />
|
||||
<ModelingSidebarSection
|
||||
id="sidebar-bottom"
|
||||
panes={bottomPanes}
|
||||
alignButtons="end"
|
||||
/>
|
||||
</div>
|
||||
</Resizable>
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelingSidebarSectionProps extends HTMLAttributes<HTMLDivElement> {
|
||||
panes: SidebarPane[]
|
||||
alignButtons?: 'start' | 'end'
|
||||
}
|
||||
|
||||
function ModelingSidebarSection({
|
||||
panes,
|
||||
alignButtons = 'start',
|
||||
className,
|
||||
...props
|
||||
}: ModelingSidebarSectionProps) {
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const showDebugPanel = settings.context.modeling.showDebugPanel
|
||||
const paneIds = panes.map((pane) => pane.id)
|
||||
const { send, context } = useModelingContext()
|
||||
const foundOpenPane = context.store?.openPanes.find((pane) =>
|
||||
paneIds.includes(pane)
|
||||
)
|
||||
const [currentPane, setCurrentPane] = useState(
|
||||
foundOpenPane || ('none' as SidebarType | 'none')
|
||||
)
|
||||
|
||||
const togglePane = useCallback(
|
||||
(newPane: SidebarType | 'none') => {
|
||||
if (newPane === 'none') {
|
||||
send({
|
||||
type: 'Set context',
|
||||
data: {
|
||||
openPanes: context.store?.openPanes.filter(
|
||||
(p) => p !== currentPane
|
||||
),
|
||||
},
|
||||
})
|
||||
setCurrentPane('none')
|
||||
} else if (newPane === currentPane) {
|
||||
setCurrentPane('none')
|
||||
send({
|
||||
type: 'Set context',
|
||||
data: {
|
||||
openPanes: context.store?.openPanes.filter((p) => p !== newPane),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
send({
|
||||
type: 'Set context',
|
||||
data: {
|
||||
openPanes: [
|
||||
...context.store?.openPanes.filter((p) => p !== currentPane),
|
||||
newPane,
|
||||
],
|
||||
},
|
||||
})
|
||||
setCurrentPane(newPane)
|
||||
}
|
||||
},
|
||||
[context.store?.openPanes, send, 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
|
||||
const filteredPanes = (
|
||||
showDebugPanel.current ? panes : panes.filter((pane) => pane.id !== 'debug')
|
||||
).filter(
|
||||
(pane) =>
|
||||
!pane.hideOnPlatform ||
|
||||
(isTauri()
|
||||
? pane.hideOnPlatform === 'web'
|
||||
: pane.hideOnPlatform === 'desktop')
|
||||
)
|
||||
useEffect(() => {
|
||||
if (
|
||||
!showDebugPanel.current &&
|
||||
currentPane === 'debug' &&
|
||||
context.store?.openPanes.includes('debug')
|
||||
) {
|
||||
togglePane('debug')
|
||||
}
|
||||
}, [showDebugPanel.current, togglePane, context.store?.openPanes])
|
||||
|
||||
return (
|
||||
<div className={'group contents ' + className} {...props}>
|
||||
<Tab.Group
|
||||
vertical
|
||||
selectedIndex={
|
||||
currentPane === 'none' ? 0 : paneIds.indexOf(currentPane) + 1
|
||||
}
|
||||
onChange={(index) => {
|
||||
const newPane = index === 0 ? 'none' : paneIds[index - 1]
|
||||
togglePane(newPane)
|
||||
}}
|
||||
>
|
||||
<Tab.List
|
||||
id={`${props.id}-ribbon`}
|
||||
<ul
|
||||
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 ' +
|
||||
(context.store?.openPanes.length === 1 && currentPane === 'none'
|
||||
? 'pr-0.5'
|
||||
: '')
|
||||
(context.store?.openPanes.length === 0 ? 'rounded-r ' : '') +
|
||||
'relative z-[2] pointer-events-auto p-0 col-start-1 col-span-1 h-fit w-fit flex flex-col ' +
|
||||
'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 '
|
||||
}
|
||||
>
|
||||
<ul
|
||||
id="pane-buttons-section"
|
||||
className={
|
||||
'w-fit p-2 flex flex-col gap-2 ' +
|
||||
(context.store?.openPanes.length >= 1 ? '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)}
|
||||
paneIsOpen={context.store?.openPanes.includes(pane.id)}
|
||||
onClick={() => togglePane(pane.id)}
|
||||
aria-pressed={context.store?.openPanes.includes(pane.id)}
|
||||
/>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels
|
||||
id={`${props.id}-pane`}
|
||||
as="article"
|
||||
</ul>
|
||||
<hr className="w-full border-chalkboard-20 dark:border-chalkboard-80" />
|
||||
<ul id="sidebar-actions" className="w-fit p-2 flex flex-col gap-2">
|
||||
{sidebarActions.map((action) => (
|
||||
<ModelingPaneButton
|
||||
key={action.id}
|
||||
paneConfig={{
|
||||
id: action.id,
|
||||
title: action.title,
|
||||
icon: action.icon,
|
||||
keybinding: action.keybinding,
|
||||
iconClassName: action.iconClassName,
|
||||
iconSize: 'md',
|
||||
}}
|
||||
paneIsOpen={false}
|
||||
onClick={action.action}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</ul>
|
||||
<ul
|
||||
id="pane-section"
|
||||
className={
|
||||
'col-start-2 col-span-1 ' +
|
||||
(context.store?.openPanes.length === 1
|
||||
? currentPane !== 'none'
|
||||
'ml-[-1px] col-start-2 col-span-1 flex flex-col gap-2 ' +
|
||||
(context.store?.openPanes.length >= 1
|
||||
? `row-start-1 row-end-3`
|
||||
: `hidden`
|
||||
: ``)
|
||||
: `hidden`)
|
||||
}
|
||||
>
|
||||
<Tab.Panel key="none" />
|
||||
{filteredPanes.map((pane) => (
|
||||
<Tab.Panel key={pane.id} className="h-full">
|
||||
{filteredPanes
|
||||
.filter((pane) => context?.store.openPanes.includes(pane.id))
|
||||
.map((pane) => (
|
||||
<ModelingPane
|
||||
key={pane.id}
|
||||
icon={pane.icon}
|
||||
id={`${pane.id}-pane`}
|
||||
title={pane.title}
|
||||
Menu={pane.Menu}
|
||||
@ -212,55 +168,76 @@ function ModelingSidebarSection({
|
||||
pane.Content
|
||||
)}
|
||||
</ModelingPane>
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</ul>
|
||||
</div>
|
||||
</Resizable>
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelingPaneButtonProps {
|
||||
paneConfig: SidebarPane
|
||||
currentPane: SidebarType | 'none'
|
||||
togglePane: () => void
|
||||
interface ModelingPaneButtonProps
|
||||
extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
paneConfig: {
|
||||
id: string
|
||||
title: string
|
||||
icon: CustomIconName | IconDefinition
|
||||
keybinding: string
|
||||
iconClassName?: string
|
||||
iconSize?: 'sm' | 'md' | 'lg'
|
||||
}
|
||||
onClick: () => void
|
||||
paneIsOpen: boolean
|
||||
}
|
||||
|
||||
function ModelingPaneButton({
|
||||
paneConfig,
|
||||
currentPane,
|
||||
togglePane,
|
||||
onClick,
|
||||
paneIsOpen,
|
||||
...props
|
||||
}: ModelingPaneButtonProps) {
|
||||
useHotkeys(paneConfig.keybinding, togglePane, {
|
||||
useHotkeys(paneConfig.keybinding, onClick, {
|
||||
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}
|
||||
<button
|
||||
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
|
||||
onClick={onClick}
|
||||
data-testid={paneConfig.title}
|
||||
{...props}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={paneConfig.icon}
|
||||
className="p-1"
|
||||
size="sm"
|
||||
className={'p-1 ' + paneConfig.iconClassName || ''}
|
||||
size={paneConfig.iconSize || 'sm'}
|
||||
iconClassName={
|
||||
paneConfig.id === currentPane
|
||||
paneIsOpen
|
||||
? ' !text-chalkboard-10'
|
||||
: '!text-chalkboard-80 dark:!text-chalkboard-30'
|
||||
}
|
||||
bgClassName={
|
||||
'rounded-sm ' +
|
||||
(paneConfig.id === currentPane ? '!bg-primary' : '!bg-transparent')
|
||||
'rounded-sm ' + (paneIsOpen ? '!bg-primary' : '!bg-transparent')
|
||||
}
|
||||
/>
|
||||
<Tooltip position="right" hoverOnly delay={800}>
|
||||
<span>{paneConfig.title}</span>
|
||||
<br />
|
||||
<span className="text-xs capitalize">{paneConfig.keybinding}</span>
|
||||
<Tooltip
|
||||
position="right"
|
||||
className="!max-w-none flex gap-4 items-center justify-between"
|
||||
hoverOnly
|
||||
delay={800}
|
||||
>
|
||||
<span className="flex-none">{paneConfig.title}</span>
|
||||
<kbd className="hotkey">{paneConfig.keybinding}</kbd>
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export type SidebarAction = {
|
||||
id: string
|
||||
title: string
|
||||
icon: CustomIconName
|
||||
iconClassName?: string // Just until we get rid of FontAwesome icons
|
||||
keybinding: string
|
||||
action: () => void
|
||||
hideOnPlatform?: 'desktop' | 'web'
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
],
|
||||
Export: {
|
||||
description: 'Export the current model.',
|
||||
icon: 'exportFile',
|
||||
icon: 'floppyDiskArrow',
|
||||
needsReview: true,
|
||||
args: {
|
||||
type: {
|
||||
|