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()
|
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' })
|
||||||
|
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) {
|
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
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -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'
|
||||||
|
}
|
||||||
|
@ -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: {
|
||||||
|