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>
This commit is contained in:
Frank Noirot
2024-07-24 22:02:16 -04:00
committed by GitHub
parent 385589ddf9
commit ea0a3ac3ba
26 changed files with 218 additions and 221 deletions

View File

@ -3864,10 +3864,7 @@ test.describe('Regression tests', () => {
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// expand variables section // expand variables section
const variablesTabButton = page.getByRole('tab', { const variablesTabButton = page.getByTestId('Variables')
name: 'Variables',
exact: false,
})
await variablesTabButton.click() await variablesTabButton.click()
// can find sketch001 in the variables summary (pretty-json-container, makes sure we're not looking in the code editor) // 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() await u.waitForAuthSkipAppStart()
const variablesTabButton = page.getByRole('tab', { const variablesTabButton = page.getByTestId('Variables')
name: 'Variables',
exact: false,
})
await variablesTabButton.click() await variablesTabButton.click()
// expect to see "myVar:5" // expect to see "myVar:5"
await expect( await expect(
@ -7777,7 +7771,7 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
await u.closeDebugPanel() await u.closeDebugPanel()
const codePane = page.getByRole('textbox').locator('div') 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 lineButton = page.getByRole('button', { name: 'Line' })
const arcButton = page.getByRole('button', { name: 'Tangential Arc' }) const arcButton = page.getByRole('button', { name: 'Tangential Arc' })
const extrudeButton = page.getByRole('button', { name: 'Extrude' }) const extrudeButton = page.getByRole('button', { name: 'Extrude' })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -58,44 +58,40 @@ async function waitForDefaultPlanesToBeVisible(page: Page) {
} }
async function openKclCodePanel(page: Page) { async function openKclCodePanel(page: Page) {
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false }) const paneLocator = page.getByTestId('KCL Code')
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true' const isOpen = (await paneLocator?.getAttribute('aria-pressed')) === 'true'
if (!isOpen) { if (!isOpen) {
await paneLocator.click() await paneLocator.click()
await paneLocator.and(page.locator('[aria-selected="true"]')).waitFor() await expect(paneLocator).toHaveAttribute('aria-pressed', 'true')
} }
} }
async function closeKclCodePanel(page: Page) { async function closeKclCodePanel(page: Page) {
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false }) const paneLocator = page.getByTestId('KCL Code')
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true' const isOpen = (await paneLocator?.getAttribute('aria-pressed')) === 'true'
if (isOpen) { if (isOpen) {
await paneLocator.click() await paneLocator.click()
await paneLocator await expect(paneLocator).not.toHaveAttribute('aria-pressed', 'true')
.and(page.locator(':not([aria-selected="true"])'))
.waitFor()
} }
} }
async function openDebugPanel(page: Page) { async function openDebugPanel(page: Page) {
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false }) const debugLocator = page.getByTestId('Debug')
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true' const isOpen = (await debugLocator?.getAttribute('aria-pressed')) === 'true'
if (!isOpen) { if (!isOpen) {
await debugLocator.click() await debugLocator.click()
await debugLocator.and(page.locator('[aria-selected="true"]')).waitFor() await expect(debugLocator).toHaveAttribute('aria-pressed', 'true')
} }
} }
async function closeDebugPanel(page: Page) { async function closeDebugPanel(page: Page) {
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false }) const debugLocator = page.getByTestId('Debug')
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true' const isOpen = (await debugLocator?.getAttribute('aria-pressed')) === 'true'
if (isOpen) { if (isOpen) {
await debugLocator.click() await debugLocator.click()
await debugLocator await expect(debugLocator).not.toHaveAttribute('aria-pressed', 'true')
.and(page.locator(':not([aria-selected="true"])'))
.waitFor()
} }
} }
@ -471,10 +467,11 @@ export const doExport = async (
page: Page page: Page
): Promise<Paths> => { ): Promise<Paths> => {
await page.getByRole('button', { name: APP_NAME }).click() await page.getByRole('button', { name: APP_NAME }).click()
await expect( const exportMenuButton = page.getByRole('button', {
page.getByRole('button', { name: 'Export', exact: false }) name: 'Export current part',
).toBeVisible() })
await page.getByRole('button', { name: 'Export', exact: false }).click() await expect(exportMenuButton).toBeVisible()
await exportMenuButton.click()
await expect(page.getByTestId('command-bar')).toBeVisible() await expect(page.getByTestId('command-bar')).toBeVisible()
// Go through export via command bar // Go through export via command bar

View File

@ -223,6 +223,16 @@ const CustomIconMap = {
/> />
</svg> </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: ( folder: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path

View File

@ -1,5 +1,5 @@
.panel { .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; display: grid;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
@apply bg-chalkboard-10/50 focus-within:bg-chalkboard-10/90 backdrop-blur-sm border border-chalkboard-20; @apply bg-chalkboard-10/50 focus-within:bg-chalkboard-10/90 backdrop-blur-sm border border-chalkboard-20;

View File

@ -3,10 +3,14 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import Tooltip from 'components/Tooltip' 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 export interface ModelingPaneProps
extends React.PropsWithChildren, extends React.PropsWithChildren,
React.HTMLAttributes<HTMLDivElement> { React.HTMLAttributes<HTMLDivElement> {
icon?: CustomIconName | IconDefinition
title: string title: string
Menu?: React.ReactNode | React.FC Menu?: React.ReactNode | React.FC
detailsTestId?: string detailsTestId?: string
@ -14,13 +18,25 @@ export interface ModelingPaneProps
} }
export const ModelingPaneHeader = ({ export const ModelingPaneHeader = ({
icon,
title, title,
Menu, Menu,
onClose, onClose,
}: Pick<ModelingPaneProps, 'title' | 'Menu' | 'onClose'>) => { }: Pick<ModelingPaneProps, 'icon' | 'title' | 'Menu' | 'onClose'>) => {
return ( return (
<div className={styles.header}> <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} {Menu instanceof Function ? <Menu /> : Menu}
<ActionButton <ActionButton
Element="button" Element="button"
@ -42,6 +58,7 @@ export const ModelingPaneHeader = ({
export const ModelingPane = ({ export const ModelingPane = ({
title, title,
icon,
id, id,
children, children,
className, className,
@ -63,14 +80,19 @@ export const ModelingPane = ({
data-testid={detailsTestId} data-testid={detailsTestId}
id={id} id={id}
className={ className={
'group-focus-within:border-primary dark:group-focus-within:border-chalkboard-50 ' + 'focus-within:border-primary dark:focus-within:border-chalkboard-50 ' +
pointerEventsCssClass + pointerEventsCssClass +
styles.panel + styles.panel +
' group ' + ' group ' +
(className || '') (className || '')
} }
> >
<ModelingPaneHeader title={title} Menu={Menu} onClose={onClose} /> <ModelingPaneHeader
icon={icon}
title={title}
Menu={Menu}
onClose={onClose}
/>
<div className="relative w-full">{children}</div> <div className="relative w-full">{children}</div>
</section> </section>
) )

View File

@ -35,13 +35,13 @@ export type SidebarPane = {
hideOnPlatform?: 'desktop' | 'web' hideOnPlatform?: 'desktop' | 'web'
} }
export const topPanes: SidebarPane[] = [ export const sidebarPanes: SidebarPane[] = [
{ {
id: 'code', id: 'code',
title: 'KCL Code', title: 'KCL Code',
icon: faCode, icon: faCode,
Content: KclEditorPane, Content: KclEditorPane,
keybinding: 'shift + c', keybinding: 'Shift + C',
Menu: KclEditorMenu, Menu: KclEditorMenu,
}, },
{ {
@ -49,40 +49,37 @@ export const topPanes: SidebarPane[] = [
title: 'Project Files', title: 'Project Files',
icon: 'folder', icon: 'folder',
Content: FileTreeInner, Content: FileTreeInner,
keybinding: 'shift + f', keybinding: 'Shift + F',
Menu: FileTreeMenu, Menu: FileTreeMenu,
hideOnPlatform: 'web', hideOnPlatform: 'web',
}, },
]
export const bottomPanes: SidebarPane[] = [
{ {
id: 'variables', id: 'variables',
title: 'Variables', title: 'Variables',
icon: faSquareRootVariable, icon: faSquareRootVariable,
Content: MemoryPane, Content: MemoryPane,
Menu: MemoryPaneMenu, Menu: MemoryPaneMenu,
keybinding: 'shift + v', keybinding: 'Shift + V',
}, },
{ {
id: 'logs', id: 'logs',
title: 'Logs', title: 'Logs',
icon: faCodeCommit, icon: faCodeCommit,
Content: LogsPane, Content: LogsPane,
keybinding: 'shift + l', keybinding: 'Shift + L',
}, },
{ {
id: 'kclErrors', id: 'kclErrors',
title: 'KCL Errors', title: 'KCL Errors',
icon: faExclamationCircle, icon: faExclamationCircle,
Content: KclErrorsPane, Content: KclErrorsPane,
keybinding: 'shift + e', keybinding: 'Shift + E',
}, },
{ {
id: 'debug', id: 'debug',
title: 'Debug', title: 'Debug',
icon: faBugSlash, icon: faBugSlash,
Content: DebugPane, Content: DebugPane,
keybinding: 'shift + d', keybinding: 'Shift + D',
}, },
] ]

View File

@ -1,39 +1,84 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable' import { Resizable } from 're-resizable'
import { HTMLAttributes, useCallback, useEffect, useState } from 'react' import { useCallback, useMemo } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { Tab } from '@headlessui/react' import { SidebarType, sidebarPanes } from './ModelingPanes'
import {
SidebarPane,
SidebarType,
bottomPanes,
topPanes,
} from './ModelingPanes'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { ActionIcon } from 'components/ActionIcon' import { ActionIcon } from 'components/ActionIcon'
import styles from './ModelingSidebar.module.css' import styles from './ModelingSidebar.module.css'
import { ModelingPane } from './ModelingPane' import { ModelingPane } from './ModelingPane'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { useModelingContext } from 'hooks/useModelingContext' 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 { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40' paneOpacity: '' | 'opacity-20' | 'opacity-40'
} }
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) { export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const { commandBarSend } = useCommandsContext()
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const onboardingStatus = settings.context.app.onboardingStatus const onboardingStatus = settings.context.app.onboardingStatus
const { context } = useModelingContext() const { send, context } = useModelingContext()
const pointerEventsCssClass = const pointerEventsCssClass =
context.store?.buttonDownInStream || context.store?.buttonDownInStream ||
onboardingStatus.current === 'camera' || onboardingStatus.current === 'camera' ||
context.store?.openPanes.length === 0 context.store?.openPanes.length === 0
? 'pointer-events-none ' ? 'pointer-events-none '
: 'pointer-events-auto ' : '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 ( return (
<Resizable <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={{ defaultSize={{
width: '550px', width: '550px',
height: 'auto', height: 'auto',
@ -54,153 +99,64 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
}} }}
> >
<div id="app-sidebar" className={styles.grid + ' flex-1'}> <div id="app-sidebar" className={styles.grid + ' flex-1'}>
<ModelingSidebarSection id="sidebar-top" panes={topPanes} /> <ul
<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`}
className={ className={
'pointer-events-auto ' + (context.store?.openPanes.length === 0 ? 'rounded-r ' : '') +
(alignButtons === 'start' 'relative z-[2] pointer-events-auto p-0 col-start-1 col-span-1 h-fit w-fit flex flex-col ' +
? 'justify-start self-start' '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 '
: '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'
: '')
} }
> >
<Tab key="none" className="sr-only"> <ul
No panes open id="pane-buttons-section"
</Tab> className={
{filteredPanes.map((pane) => ( 'w-fit p-2 flex flex-col gap-2 ' +
<ModelingPaneButton (context.store?.openPanes.length >= 1 ? 'pr-0.5' : '')
key={pane.id} }
paneConfig={pane} >
currentPane={currentPane} {filteredPanes.map((pane) => (
togglePane={() => togglePane(pane.id)} <ModelingPaneButton
/> key={pane.id}
))} paneConfig={pane}
</Tab.List> paneIsOpen={context.store?.openPanes.includes(pane.id)}
<Tab.Panels onClick={() => togglePane(pane.id)}
id={`${props.id}-pane`} aria-pressed={context.store?.openPanes.includes(pane.id)}
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={ className={
'col-start-2 col-span-1 ' + 'ml-[-1px] col-start-2 col-span-1 flex flex-col gap-2 ' +
(context.store?.openPanes.length === 1 (context.store?.openPanes.length >= 1
? currentPane !== 'none' ? `row-start-1 row-end-3`
? `row-start-1 row-end-3` : `hidden`)
: `hidden`
: ``)
} }
> >
<Tab.Panel key="none" /> {filteredPanes
{filteredPanes.map((pane) => ( .filter((pane) => context?.store.openPanes.includes(pane.id))
<Tab.Panel key={pane.id} className="h-full"> .map((pane) => (
<ModelingPane <ModelingPane
key={pane.id}
icon={pane.icon}
id={`${pane.id}-pane`} id={`${pane.id}-pane`}
title={pane.title} title={pane.title}
Menu={pane.Menu} Menu={pane.Menu}
@ -212,55 +168,76 @@ function ModelingSidebarSection({
pane.Content pane.Content
)} )}
</ModelingPane> </ModelingPane>
</Tab.Panel> ))}
))} </ul>
</Tab.Panels> </div>
</Tab.Group> </Resizable>
</div>
) )
} }
interface ModelingPaneButtonProps { interface ModelingPaneButtonProps
paneConfig: SidebarPane extends React.HTMLAttributes<HTMLButtonElement> {
currentPane: SidebarType | 'none' paneConfig: {
togglePane: () => void id: string
title: string
icon: CustomIconName | IconDefinition
keybinding: string
iconClassName?: string
iconSize?: 'sm' | 'md' | 'lg'
}
onClick: () => void
paneIsOpen: boolean
} }
function ModelingPaneButton({ function ModelingPaneButton({
paneConfig, paneConfig,
currentPane, onClick,
togglePane, paneIsOpen,
...props
}: ModelingPaneButtonProps) { }: ModelingPaneButtonProps) {
useHotkeys(paneConfig.keybinding, togglePane, { useHotkeys(paneConfig.keybinding, onClick, {
scopes: ['modeling'], scopes: ['modeling'],
}) })
return ( return (
<Tab <button
key={paneConfig.id} 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"
className="pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent p-0 m-0 rounded-sm !outline-none" onClick={onClick}
onClick={togglePane}
data-testid={paneConfig.title} data-testid={paneConfig.title}
{...props}
> >
<ActionIcon <ActionIcon
icon={paneConfig.icon} icon={paneConfig.icon}
className="p-1" className={'p-1 ' + paneConfig.iconClassName || ''}
size="sm" size={paneConfig.iconSize || 'sm'}
iconClassName={ iconClassName={
paneConfig.id === currentPane paneIsOpen
? ' !text-chalkboard-10' ? ' !text-chalkboard-10'
: '!text-chalkboard-80 dark:!text-chalkboard-30' : '!text-chalkboard-80 dark:!text-chalkboard-30'
} }
bgClassName={ bgClassName={
'rounded-sm ' + 'rounded-sm ' + (paneIsOpen ? '!bg-primary' : '!bg-transparent')
(paneConfig.id === currentPane ? '!bg-primary' : '!bg-transparent')
} }
/> />
<Tooltip position="right" hoverOnly delay={800}> <Tooltip
<span>{paneConfig.title}</span> position="right"
<br /> className="!max-w-none flex gap-4 items-center justify-between"
<span className="text-xs capitalize">{paneConfig.keybinding}</span> hoverOnly
delay={800}
>
<span className="flex-none">{paneConfig.title}</span>
<kbd className="hotkey">{paneConfig.keybinding}</kbd>
</Tooltip> </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'
}

View File

@ -88,7 +88,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
], ],
Export: { Export: {
description: 'Export the current model.', description: 'Export the current model.',
icon: 'exportFile', icon: 'floppyDiskArrow',
needsReview: true, needsReview: true,
args: { args: {
type: { type: {