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:
@ -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])
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
27
src/components/ModelingSidebar/ModelingPane.module.css
Normal file
27
src/components/ModelingSidebar/ModelingPane.module.css
Normal 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;
|
||||
}
|
50
src/components/ModelingSidebar/ModelingPane.tsx
Normal file
50
src/components/ModelingSidebar/ModelingPane.tsx
Normal 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>
|
||||
)
|
||||
}
|
18
src/components/ModelingSidebar/ModelingPanes/DebugPane.tsx
Normal file
18
src/components/ModelingSidebar/ModelingPanes/DebugPane.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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"
|
@ -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}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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)
|
||||
|
57
src/components/ModelingSidebar/ModelingPanes/MemoryPane.tsx
Normal file
57
src/components/ModelingSidebar/ModelingPanes/MemoryPane.tsx
Normal 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
|
||||
}
|
67
src/components/ModelingSidebar/ModelingPanes/index.ts
Normal file
67
src/components/ModelingSidebar/ModelingPanes/index.ts
Normal 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',
|
||||
},
|
||||
]
|
11
src/components/ModelingSidebar/ModelingSidebar.module.css
Normal file
11
src/components/ModelingSidebar/ModelingSidebar.module.css
Normal 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;
|
||||
}
|
213
src/components/ModelingSidebar/ModelingSidebar.tsx
Normal file
213
src/components/ModelingSidebar/ModelingSidebar.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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'
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}
|
||||
|
Reference in New Issue
Block a user