Migrate to new split sidebar from accordion-like panes (#2063)

* Split ModelingSidebar out into own component

* Consolidate all ModelingPane components and config

* Make ModelingSidebar a directory of components and config

* Remove unused components

* Proper pane styling

* Make tooltip configurable to visually appear on hover only

* Remove debug panel from App

* Fix current tests

* Rename to more intuitive names

* Fix useEffect loop bug with showDebugPanel

* Fix snapshot tests

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Rerun CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Merge branch 'main' into franknoirot/sidebar

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Rerun CI

* Maybe some flakiness in the validation initScripts?

* Avoid test flakiness by waiting for more signals that loading is completed

* Don't assert, just wait for the element to be enabled

* Don't let users accidentally click the gap between the pane and the side of the window

* Firm up extrude from command bar test

* Get rid of unused imports

* Add setting to disable blinking cursor (#2065)

* Add support for "current" marker in command bar for boolean settings

* Add a cursorBlinking setting

* Rename setting to blinkingCursor, honor it in the UI

* Fix scroll layout bug in settings modal

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Rerun CI

* CSS tweaks

* Allow settings hotkey within KclEditorPane

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* Rerun CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Rerun CI

* Ensure the KCL code panel is closed for camera movement test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Make sure that the camera position inputs are ready to be read from

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Remove repeat awaits

* Make camera position fields in debug pane update when the pane is initialized

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Undo that CameraControls change because it made other things weird

* retry fixing camera move test

* Fix race condition where cam setting cam position parts were overwriting each other

* Rerun CI

* Rerun CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Frank Noirot
2024-04-15 12:04:17 -04:00
committed by GitHub
parent fdadd059d6
commit 3fdf7bd45e
48 changed files with 927 additions and 706 deletions

View File

@ -16,7 +16,7 @@ export function AstExplorer() {
const [filterKeys, setFilterKeys] = useState<string[]>(['start', 'end'])
return (
<div className="relative" style={{ width: '300px' }}>
<div id="ast-explorer" className="relative">
<div className="">
filter out keys:<div className="w-2 inline-block"></div>
{['start', 'end', 'type'].map((key) => {
@ -45,7 +45,7 @@ export function AstExplorer() {
setHighlightRange([0, 0])
}}
>
<pre className=" text-xs overflow-y-auto" style={{ width: '300px' }}>
<pre className="text-xs">
<DisplayObj
obj={kclManager.ast}
filterKeys={filterKeys}
@ -109,7 +109,7 @@ function DisplayObj({
<pre
ref={ref}
className={`ml-2 border-l border-violet-600 pl-1 ${
hasCursor ? 'bg-violet-100/25' : ''
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`}
onMouseEnter={(e) => {
setHighlightRange([obj?.start || 0, obj.end])

View File

@ -1,57 +0,0 @@
.panel {
@apply relative z-0;
@apply bg-chalkboard-10/70 backdrop-blur-sm;
}
.header::before,
.header::-webkit-details-marker {
display: none;
}
:global(.dark) .panel {
@apply bg-chalkboard-110/50 backdrop-blur-0;
}
.header {
@apply sticky top-0 z-10 cursor-pointer;
@apply flex items-center justify-between gap-2 w-full p-2;
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply bg-chalkboard-10;
}
.header:not(:last-of-type) {
@apply border-b;
}
:global(.dark) .header {
@apply bg-chalkboard-110 border-b-chalkboard-90 text-chalkboard-30;
}
:global(.dark) .header:not(:last-of-type) {
@apply border-b-2;
}
.panel:first-of-type .header {
@apply rounded-t;
}
.panel:last-of-type .header {
@apply rounded-b;
}
.panel[open] .header {
@apply rounded-t rounded-b-none;
}
.panel[open] {
@apply flex-grow max-h-full h-48 my-1 rounded;
}
.panel[open] + .panel[open],
.panel[open]:first-of-type {
@apply mt-0;
}
.panel[open]:last-of-type {
@apply mb-0;
}

View File

@ -1,76 +0,0 @@
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from './ActionIcon'
import styles from './CollapsiblePanel.module.css'
export interface CollapsiblePanelProps
extends React.PropsWithChildren,
React.HTMLAttributes<HTMLDetailsElement> {
title: string
icon?: IconDefinition
open?: boolean
menu?: React.ReactNode
detailsTestId?: string
iconClassNames?: {
bg?: string
icon?: string
}
}
export const PanelHeader = ({
title,
icon,
iconClassNames,
menu,
}: CollapsiblePanelProps) => {
return (
<summary className={styles.header}>
<div className="flex gap-2 items-center flex-1">
<ActionIcon
icon={icon}
className="p-1"
size="sm"
bgClassName={
'dark:!bg-transparent group-open:bg-primary dark:group-open:!bg-primary rounded-sm ' +
(iconClassNames?.bg || '')
}
iconClassName={
'group-open:text-chalkboard-10 ' + (iconClassNames?.icon || '')
}
/>
{title}
</div>
<div className="group-open:opacity-100 opacity-0 group-open:pointer-events-auto pointer-events-none">
{menu}
</div>
</summary>
)
}
export const CollapsiblePanel = ({
title,
icon,
children,
className,
iconClassNames,
menu,
detailsTestId,
...props
}: CollapsiblePanelProps) => {
return (
<details
{...props}
data-testid={detailsTestId}
className={
styles.panel + ' pointer-events-auto group ' + (className || '')
}
>
<PanelHeader
title={title}
icon={icon}
iconClassNames={iconClassNames}
menu={menu}
/>
{children}
</details>
)
}

View File

@ -1,26 +0,0 @@
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { AstExplorer } from './AstExplorer'
import { EngineCommands } from './EngineCommands'
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => {
return (
<CollapsiblePanel
{...props}
className={
'!absolute overflow-auto !h-auto bottom-5 right-5 ' + className
}
// header height, top-5, and bottom-5
style={{ maxHeight: 'calc(100% - 3rem - 1.25rem - 1.25rem)' }}
detailsTestId="debug-panel"
>
<section className="p-4 flex flex-col gap-4">
<EngineCommands />
<CamDebugSettings />
<div style={{ height: '400px' }} className="overflow-y-auto">
<AstExplorer />
</div>
</section>
</CollapsiblePanel>
)
}

View File

@ -1,76 +0,0 @@
import ReactJson from 'react-json-view'
import { useEffect } from 'react'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclProvider'
const ReactJsonTypeHack = ReactJson as any
interface LogPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System>
}
export const Logs = ({ theme = Themes.Light, ...props }: LogPanelProps) => {
const { logs } = useKclContext()
useEffect(() => {
const element = document.querySelector('.console-tile')
if (element) {
element.scrollTop = element.scrollHeight - element.clientHeight
}
}, [logs])
return (
<CollapsiblePanel {...props}>
<div className="relative w-full">
<div className="absolute inset-0 flex flex-col">
<ReactJsonTypeHack
src={logs}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayArrayKey={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
</CollapsiblePanel>
)
}
export const KCLErrors = ({
theme = Themes.Light,
...props
}: LogPanelProps) => {
const { errors } = useKclContext()
useEffect(() => {
const element = document.querySelector('.console-tile')
if (element) {
element.scrollTop = element.scrollHeight - element.clientHeight
}
}, [errors])
return (
<CollapsiblePanel {...props}>
<div className="h-full relative">
<div className="absolute inset-0 flex flex-col">
<ReactJsonTypeHack
src={errors}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayArrayKey={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
</CollapsiblePanel>
)
}

View File

@ -1,70 +0,0 @@
import ReactJson from 'react-json-view'
import { CollapsiblePanel, CollapsiblePanelProps } from './CollapsiblePanel'
import { useMemo } from 'react'
import { ProgramMemory, Path, ExtrudeSurface } from '../lang/wasm'
import { Themes } from '../lib/theme'
import { useKclContext } from 'lang/KclProvider'
interface MemoryPanelProps extends CollapsiblePanelProps {
theme?: Exclude<Themes, Themes.System>
}
export const MemoryPanel = ({
theme = Themes.Light,
...props
}: MemoryPanelProps) => {
const { programMemory } = useKclContext()
const ProcessedMemory = useMemo(
() => processMemory(programMemory),
[programMemory]
)
return (
<CollapsiblePanel {...props}>
<div className="h-full relative">
<div className="absolute inset-0 flex flex-col items-start">
<div
className="overflow-y-auto h-full console-tile w-full"
style={{ marginBottom: 36 }}
>
{/* 36px is the height of PanelHeader */}
<ReactJson
src={ProcessedMemory}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
</div>
</CollapsiblePanel>
)
}
export const processMemory = (programMemory: ProgramMemory) => {
const processedMemory: any = {}
Object.keys(programMemory?.root || {}).forEach((key) => {
const val = programMemory.root[key]
if (typeof val.value !== 'function') {
if (val.type === 'SketchGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else if (val.type === 'ExtrudeGroup') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
})
} else {
processedMemory[key] = val.value
}
} else if (key !== 'log') {
processedMemory[key] = '__function__'
}
})
return processedMemory
}

View File

@ -0,0 +1,27 @@
.panel {
@apply relative z-0 rounded-r max-w-full h-full flex-1;
display: grid;
grid-template-rows: auto 1fr;
@apply bg-chalkboard-10/50 backdrop-blur-sm border border-chalkboard-20;
scroll-margin-block-start: 41px;
}
.header::before,
.header::-webkit-details-marker {
display: none;
}
:global(.dark) .panel {
@apply bg-chalkboard-100/50 backdrop-blur-[3px] border-chalkboard-80;
}
.header {
@apply z-10 relative rounded-tr;
@apply flex h-[41px] items-center justify-between gap-2 px-2;
@apply font-mono text-xs font-bold select-none text-chalkboard-90;
@apply bg-chalkboard-10 border-b border-chalkboard-20;
}
:global(.dark) .header {
@apply bg-chalkboard-90 text-chalkboard-30 border-chalkboard-80;
}

View File

@ -0,0 +1,50 @@
import { useStore } from 'useStore'
import styles from './ModelingPane.module.css'
export interface ModelingPaneProps
extends React.PropsWithChildren,
React.HTMLAttributes<HTMLDivElement> {
title: string
Menu?: React.ReactNode | React.FC
detailsTestId?: string
}
export const ModelingPaneHeader = ({
title,
Menu,
}: Pick<ModelingPaneProps, 'title' | 'Menu'>) => {
return (
<div className={styles.header}>
<div className="flex gap-2 items-center flex-1">{title}</div>
{Menu instanceof Function ? <Menu /> : Menu}
</div>
)
}
export const ModelingPane = ({
title,
children,
className,
Menu,
detailsTestId,
...props
}: ModelingPaneProps) => {
const { buttonDownInStream } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
}))
return (
<section
{...props}
data-testid={detailsTestId}
className={
(buttonDownInStream ? 'pointer-events-none ' : 'pointer-events-auto ') +
styles.panel +
' group ' +
(className || '')
}
>
<ModelingPaneHeader title={title} Menu={Menu} />
<div className="relative w-full">{children}</div>
</section>
)
}

View File

@ -0,0 +1,18 @@
import { AstExplorer } from '../../AstExplorer'
import { EngineCommands } from '../../EngineCommands'
import { CamDebugSettings } from 'clientSideScene/ClientSideSceneComp'
export const DebugPane = () => {
return (
<section
data-testid="debug-panel"
className="absolute inset-0 p-2 box-border overflow-auto"
>
<div className="flex flex-col">
<EngineCommands />
<CamDebugSettings />
<AstExplorer />
</div>
</section>
)
}

View File

@ -1,14 +1,14 @@
import { Menu } from '@headlessui/react'
import { PropsWithChildren } from 'react'
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
import { ActionIcon } from './ActionIcon'
import styles from './CodeMenu.module.css'
import { ActionIcon } from 'components/ActionIcon'
import styles from './KclEditorMenu.module.css'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { editorShortcutMeta } from './TextEditor'
import { editorShortcutMeta } from './KclEditorPane'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { kclManager } from 'lib/singletons'
export const CodeMenu = ({ children }: PropsWithChildren) => {
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
useConvertToVariable()
@ -30,7 +30,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
className="p-1"
size="sm"
bgClassName={
'!bg-transparent hover:!bg-primary/10 hover:dark:!bg-chalkboard-100 ui-active:!bg-primary/10 dark:ui-active:!bg-chalkboard-100 rounded-sm'
'!bg-transparent hover:!bg-primary/10 hover:dark:!bg-chalkboard-100 ui-open:!bg-primary/10 dark:ui-open:!bg-chalkboard-100 rounded-sm'
}
iconClassName={'!text-chalkboard-90 dark:!text-chalkboard-40'}
/>
@ -65,7 +65,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
>
<span>Read the KCL docs</span>
<small>
On GitHub
zoo.dev
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1 align-text-top"
@ -83,7 +83,7 @@ export const CodeMenu = ({ children }: PropsWithChildren) => {
>
<span>KCL samples</span>
<small>
On GitHub
zoo.dev
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1 align-text-top"

View File

@ -3,12 +3,13 @@ import ReactCodeMirror, {
Extension,
ViewUpdate,
SelectionRange,
drawSelection,
} from '@uiw/react-codemirror'
import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes } from 'lib/theme'
import { Themes, getSystemTheme } from 'lib/theme'
import { useEffect, useMemo, useRef } from 'react'
import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections'
@ -37,15 +38,21 @@ import {
bracketMatching,
indentOnInput,
} from '@codemirror/language'
import { CSSRuleObject } from 'tailwindcss/types/config'
import { useModelingContext } from 'hooks/useModelingContext'
import interact from '@replit/codemirror-interact'
import { engineCommandManager, sceneInfra, kclManager } from 'lib/singletons'
import { useKclContext } from 'lang/KclProvider'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { useHotkeys } from 'react-hotkeys-hook'
import { useLspContext } from './LspProvider'
import { isTauri } from 'lib/isTauri'
import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths'
import makeUrlPathRelative from 'lib/makeUrlPathRelative'
import { useLspContext } from 'components/LspProvider'
import { Prec, EditorState } from '@codemirror/state'
import {
closeBrackets,
@ -65,11 +72,14 @@ export const editorShortcutMeta = {
},
}
export const TextEditor = ({
theme,
}: {
theme: Themes.Light | Themes.Dark
}) => {
export const KclEditorPane = () => {
const {
settings: { context },
} = useSettingsAuthContext()
const theme =
context.app.theme.current === Themes.System
? getSystemTheme()
: context.app.theme.current
const { editorView, setEditorView, isShiftDown } = useStore((s) => ({
editorView: s.editorView,
setEditorView: s.setEditorView,
@ -80,6 +90,7 @@ export const TextEditor = ({
const { overallState } = useNetworkStatus()
const isNetworkOkay = overallState === NetworkHealthState.Ok
const { copilotLSP, kclLSP } = useLspContext()
const navigate = useNavigate()
useEffect(() => {
if (typeof window === 'undefined') return
@ -109,6 +120,7 @@ export const TextEditor = ({
const { settings } = useSettingsAuthContext()
const textWrapping = settings.context.textEditor.textWrapping
const cursorBlinking = settings.context.textEditor.blinkingCursor
const { commandBarSend } = useCommandsContext()
const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable()
@ -189,6 +201,9 @@ export const TextEditor = ({
const editorExtensions = useMemo(() => {
const extensions = [
drawSelection({
cursorBlinkRate: cursorBlinking.current ? 1200 : 0,
}),
lineHighlightField,
history(),
closeBrackets(),
@ -208,6 +223,13 @@ export const TextEditor = ({
return false
},
},
{
key: isTauri() ? 'Meta-,' : 'Meta-Shift-,',
run: () => {
navigate(makeUrlPathRelative(paths.SETTINGS))
return false
},
},
{
key: editorShortcutMeta.formatCode.codeMirror,
run: () => {
@ -287,16 +309,14 @@ export const TextEditor = ({
}
return extensions
}, [kclLSP, textWrapping.current, convertCallback])
}, [kclLSP, textWrapping.current, cursorBlinking.current, convertCallback])
return (
<div
id="code-mirror-override"
className="full-height-subtract"
style={{ '--height-subtract': '4.25rem' } as CSSRuleObject}
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
>
<ReactCodeMirror
className="h-full"
value={code}
extensions={editorExtensions}
onChange={onChange}

View File

@ -0,0 +1,53 @@
import ReactJson from 'react-json-view'
import { useKclContext } from 'lang/KclProvider'
import { useResolvedTheme } from 'hooks/useResolvedTheme'
const ReactJsonTypeHack = ReactJson as any
export const LogsPane = () => {
const theme = useResolvedTheme()
const { logs } = useKclContext()
return (
<div className="overflow-hidden">
<div className="absolute inset-0 p-2 flex flex-col overflow-auto">
<ReactJsonTypeHack
src={logs}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayArrayKey={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
)
}
export const KclErrorsPane = () => {
const theme = useResolvedTheme()
const { errors } = useKclContext()
return (
<div className="overflow-hidden">
<div className="absolute inset-0 p-2 flex flex-col overflow-auto">
<ReactJsonTypeHack
src={errors}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayArrayKey={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
)
}

View File

@ -1,6 +1,6 @@
import { processMemory } from './MemoryPanel'
import { enginelessExecutor } from '../lib/testHelpers'
import { initPromise, parse } from '../lang/wasm'
import { processMemory } from './MemoryPane'
import { enginelessExecutor } from '../../../lib/testHelpers'
import { initPromise, parse } from '../../../lang/wasm'
beforeAll(() => initPromise)

View File

@ -0,0 +1,57 @@
import ReactJson from 'react-json-view'
import { useMemo } from 'react'
import { ProgramMemory, Path, ExtrudeSurface } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider'
import { useResolvedTheme } from 'hooks/useResolvedTheme'
export const MemoryPane = () => {
const theme = useResolvedTheme()
const { programMemory } = useKclContext()
const ProcessedMemory = useMemo(
() => processMemory(programMemory),
[programMemory]
)
return (
<div className="h-full relative">
<div className="absolute inset-0 p-2 flex flex-col items-start">
<div className="overflow-auto h-full w-full pb-12">
<ReactJson
src={ProcessedMemory}
collapsed={1}
collapseStringsAfterLength={60}
enableClipboard={false}
displayDataTypes={false}
displayObjectSize={true}
indentWidth={2}
quotesOnKeys={false}
name={false}
theme={theme === 'light' ? 'rjv-default' : 'monokai'}
/>
</div>
</div>
</div>
)
}
export const processMemory = (programMemory: ProgramMemory) => {
const processedMemory: any = {}
Object.keys(programMemory?.root || {}).forEach((key) => {
const val = programMemory.root[key]
if (typeof val.value !== 'function') {
if (val.type === 'SketchGroup') {
processedMemory[key] = val.value.map(({ __geoMeta, ...rest }: Path) => {
return rest
})
} else if (val.type === 'ExtrudeGroup') {
processedMemory[key] = val.value.map(({ ...rest }: ExtrudeSurface) => {
return rest
})
} else {
processedMemory[key] = val.value
}
} else if (key !== 'log') {
processedMemory[key] = '__function__'
}
})
return processedMemory
}

View File

@ -0,0 +1,67 @@
import {
IconDefinition,
faBugSlash,
faCode,
faCodeCommit,
faExclamationCircle,
faSquareRootVariable,
} from '@fortawesome/free-solid-svg-icons'
import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEditorMenu'
import { CustomIconName } from 'components/CustomIcon'
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
import { ReactNode } from 'react'
import type { PaneType } from 'useStore'
import { MemoryPane } from './MemoryPane'
import { KclErrorsPane, LogsPane } from './LoggingPanes'
import { DebugPane } from './DebugPane'
export type Pane = {
id: PaneType
title: string
icon: CustomIconName | IconDefinition
Content: ReactNode | React.FC
Menu?: ReactNode | React.FC
keybinding: string
}
export const topPanes: Pane[] = [
{
id: 'code',
title: 'KCL Code',
icon: faCode,
Content: KclEditorPane,
keybinding: 'shift + c',
Menu: KclEditorMenu,
},
]
export const bottomPanes: Pane[] = [
{
id: 'variables',
title: 'Variables',
icon: faSquareRootVariable,
Content: MemoryPane,
keybinding: 'shift + v',
},
{
id: 'logs',
title: 'Logs',
icon: faCodeCommit,
Content: LogsPane,
keybinding: 'shift + l',
},
{
id: 'kclErrors',
title: 'KCL Errors',
icon: faExclamationCircle,
Content: KclErrorsPane,
keybinding: 'shift + e',
},
{
id: 'debug',
title: 'Debug',
icon: faBugSlash,
Content: DebugPane,
keybinding: 'shift + d',
},
]

View File

@ -0,0 +1,11 @@
.grid {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 1fr 1fr;
row-gap: 0.25rem;
align-items: stretch;
position: relative;
padding-block: 1px;
max-width: 100%;
flex: 1 1 0;
}

View File

@ -0,0 +1,213 @@
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable'
import { useCallback, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { PaneType, useStore } from 'useStore'
import { Tab } from '@headlessui/react'
import { Pane, bottomPanes, topPanes } from './ModelingPanes'
import Tooltip from 'components/Tooltip'
import { ActionIcon } from 'components/ActionIcon'
import styles from './ModelingSidebar.module.css'
import { ModelingPane } from './ModelingPane'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const { buttonDownInStream, openPanes } = useStore((s) => ({
buttonDownInStream: s.buttonDownInStream,
openPanes: s.openPanes,
}))
const { settings } = useSettingsAuthContext()
const {
app: { onboardingStatus },
} = settings.context
const pointerEventsCssClass =
buttonDownInStream || onboardingStatus.current === 'camera'
? 'pointer-events-none '
: 'pointer-events-auto'
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 ') +
'translate-x-1/2 hover:bg-chalkboard-10 hover:dark:bg-chalkboard-110 bg-transparent transition-colors duration-75 transition-ease-out delay-100 ' +
pointerEventsCssClass,
}}
>
<div className={styles.grid + ' flex-1'}>
<ModelingSidebarSection panes={topPanes} />
<ModelingSidebarSection panes={bottomPanes} alignButtons="end" />
</div>
</Resizable>
)
}
interface ModelingSidebarSectionProps {
panes: Pane[]
alignButtons?: 'start' | 'end'
}
function ModelingSidebarSection({
panes,
alignButtons = 'start',
}: 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(
foundOpenPane || ('none' as PaneType | 'none')
)
const togglePane = useCallback(
(newPane: PaneType | 'none') => {
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
const filteredPanes = showDebugPanel.current
? panes
: panes.filter((pane) => pane.id !== 'debug')
useEffect(() => {
if (
!showDebugPanel.current &&
currentPane === 'debug' &&
openPanes.includes('debug')
) {
togglePane('debug')
}
}, [showDebugPanel.current, togglePane, openPanes])
return (
<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
className={
(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 ' +
(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
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 title={pane.title} Menu={pane.Menu}>
{pane.Content instanceof Function ? (
<pane.Content />
) : (
pane.Content
)}
</ModelingPane>
</Tab.Panel>
))}
</Tab.Panels>
</Tab.Group>
)
}
interface ModelingPaneButtonProps {
paneConfig: Pane
currentPane: PaneType | 'none'
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}
>
<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>
)
}

View File

@ -1,6 +1,5 @@
import { Popover, Transition } from '@headlessui/react'
import { ActionButton } from './ActionButton'
import { faHome } from '@fortawesome/free-solid-svg-icons'
import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths'
import { isTauri } from '../lib/isTauri'

View File

@ -116,7 +116,7 @@ export const SettingsAuthProviderBase = ({
},
})
},
toastSuccess: (context, event) => {
toastSuccess: (_, event) => {
const eventParts = event.type.replace(/^set./, '').split('.') as [
keyof typeof settings,
string
@ -211,6 +211,19 @@ export const SettingsAuthProviderBase = ({
)
}, [settingsState.context.app.themeColor.current])
/**
* Update the --cursor-color CSS variable
* based on the setting textEditor.blinkingCursor.current
*/
useEffect(() => {
document.documentElement.style.setProperty(
`--cursor-color`,
settingsState.context.textEditor.blinkingCursor.current
? 'auto'
: 'transparent'
)
}, [settingsState.context.textEditor.blinkingCursor.current])
// Auth machine setup
const [authState, authSend, authActor] = useMachine(authMachine, {
actions: {

View File

@ -94,11 +94,15 @@
position: relative;
}
:is(:hover, :focus-visible, :active) > .tooltip {
:is(:hover, :active) > .tooltip {
opacity: 1;
transition-delay: var(--_delay);
}
:is(:focus-visible) > .tooltip.withFocus {
opacity: 1;
}
:is(:focus, :focus-visible, :focus-within) > .tooltip {
--_delay: 0 !important;
}

View File

@ -15,6 +15,7 @@ interface TooltipProps extends React.PropsWithChildren {
| 'inlineEnd'
className?: string
delay?: number
hoverOnly?: boolean
}
export default function Tooltip({
@ -22,13 +23,16 @@ export default function Tooltip({
position = 'top',
className,
delay = 200,
hoverOnly = false,
}: TooltipProps) {
return (
<div
// @ts-ignore while awaiting merge of this PR for support of "inert" https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
inert="true"
role="tooltip"
className={styles.tooltip + ' ' + styles[position] + ' ' + className}
className={`${styles.tooltip} ${hoverOnly ? '' : styles.withFocus} ${
styles[position]
} ${className}`}
style={{ '--_delay': delay + 'ms' } as React.CSSProperties}
>
{children}