diff --git a/editor/src/components/canvas/canvas-floating-toolbars.tsx b/editor/src/components/canvas/canvas-floating-toolbars.tsx
index eac19c39c880..87e1e7041f26 100644
--- a/editor/src/components/canvas/canvas-floating-toolbars.tsx
+++ b/editor/src/components/canvas/canvas-floating-toolbars.tsx
@@ -1,11 +1,11 @@
import React from 'react'
import { FlexColumn, FlexRow } from '../../uuiui'
import { CanvasToolbar } from '../editor/canvas-toolbar'
-import { ClosedPanels } from '../editor/closed-panels'
import { Substores, useEditorState } from '../editor/store/store-hook'
import { ErrorOverlayComponent } from './canvas-error-overlay'
import { SafeModeErrorOverlay } from './canvas-wrapper-component'
import { CanvasStrategyPicker } from './controls/select-mode/canvas-strategy-picker'
+import { TestMenu } from '../titlebar/test-menu'
export const CanvasFloatingToolbars = React.memo((props: { style: React.CSSProperties }) => {
const safeMode = useEditorState(
@@ -32,21 +32,20 @@ export const CanvasFloatingToolbars = React.memo((props: { style: React.CSSPrope
position: 'absolute',
top: 0,
alignItems: 'flex-start',
- justifyContent: 'space-between',
+ justifyContent: 'center',
padding: 6,
width: '100%',
height: '100%',
}}
>
-
-
- {/* The error overlays are deliberately the last here so they hide other canvas UI */}
+ {/* The error overlays are deliberately the last here so they hide other canvas UI, except the test menu */}
{safeMode ? : }
+
)
})
diff --git a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx
index c8c59d5527e3..ac434d214eef 100644
--- a/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx
+++ b/editor/src/components/canvas/canvas-strategies/canvas-strategies.tsx
@@ -229,6 +229,7 @@ function getInteractionTargetFromEditorState(editor: EditorState): InteractionTa
case 'textEdit':
case 'comment':
case 'follow':
+ case 'panels':
return targetPaths(editor.selectedViews)
default:
assertNever(editor.mode)
diff --git a/editor/src/components/canvas/controls/panels-mode/panels-mode-hooks.ts b/editor/src/components/canvas/controls/panels-mode/panels-mode-hooks.ts
new file mode 100644
index 000000000000..0f642ea90f76
--- /dev/null
+++ b/editor/src/components/canvas/controls/panels-mode/panels-mode-hooks.ts
@@ -0,0 +1,12 @@
+import type { MouseCallbacks } from '../select-mode/select-mode-hooks'
+import { NO_OP } from '../../../../core/shared/utils'
+
+const noop: MouseCallbacks = {
+ onMouseMove: NO_OP,
+ onMouseDown: NO_OP,
+ onMouseUp: NO_OP,
+}
+
+export function usePanelsModeSelectAndHover(): MouseCallbacks {
+ return noop
+}
diff --git a/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx b/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx
index a226f17ffdcf..fc368de424f8 100644
--- a/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx
+++ b/editor/src/components/canvas/controls/select-mode/select-mode-hooks.tsx
@@ -64,6 +64,7 @@ import { getAllLockedElementPaths } from '../../../../core/shared/element-lockin
import { treatElementAsGroupLike } from '../../canvas-strategies/strategies/group-helpers'
import { useCommentModeSelectAndHover } from '../comment-mode/comment-mode-hooks'
import { useFollowModeSelectAndHover } from '../follow-mode/follow-mode-hooks'
+import { usePanelsModeSelectAndHover } from '../panels-mode/panels-mode-hooks'
const DRAG_START_THRESHOLD = 2
@@ -805,6 +806,7 @@ export function useSelectAndHover(
mode?.type === 'comment' ? mode.comment : null,
)
const followModeCallbacks = useFollowModeSelectAndHover()
+ const panelsModeCallbacks = usePanelsModeSelectAndHover()
if (hasInteractionSession) {
return {
@@ -826,6 +828,8 @@ export function useSelectAndHover(
return commentModeCallbacks
case 'follow':
return followModeCallbacks
+ case 'panels':
+ return panelsModeCallbacks
default:
const _exhaustiveCheck: never = modeType
throw new Error(`Unhandled editor mode ${JSON.stringify(modeType)}`)
diff --git a/editor/src/components/editor/canvas-toolbar-states.tsx b/editor/src/components/editor/canvas-toolbar-states.tsx
index 091326327fbe..01121a624967 100644
--- a/editor/src/components/editor/canvas-toolbar-states.tsx
+++ b/editor/src/components/editor/canvas-toolbar-states.tsx
@@ -27,6 +27,7 @@ type ToolbarMode =
| { primary: 'play' }
| { primary: 'comment' }
| { primary: 'zoom' }
+ | { primary: 'panels' }
export function useToolbarMode(): ToolbarMode {
const editorMode = useEditorState(
@@ -124,6 +125,10 @@ export function useToolbarMode(): ToolbarMode {
}
}
+ if (editorMode.type === 'panels') {
+ return { primary: 'panels' }
+ }
+
// Edit Mode
if (nothingSelected) {
return { primary: 'edit', secondary: 'nothing-selected' }
diff --git a/editor/src/components/editor/canvas-toolbar.tsx b/editor/src/components/editor/canvas-toolbar.tsx
index fbac92139647..801c4ce49268 100644
--- a/editor/src/components/editor/canvas-toolbar.tsx
+++ b/editor/src/components/editor/canvas-toolbar.tsx
@@ -61,6 +61,7 @@ import { insertComponentPickerItem } from '../navigator/navigator-item/component
import { useAtom } from 'jotai'
import { ActiveRemixSceneAtom } from '../canvas/remix/utopia-remix-root-component'
import * as EP from '../../core/shared/element-path'
+import { PanelsPicker } from '../navigator/navigator-item/panels-picker'
export const InsertMenuButtonTestId = 'insert-menu-button'
export const PlayModeButtonTestId = 'canvas-toolbar-play-mode'
@@ -267,6 +268,14 @@ export const CanvasToolbar = React.memo(() => {
}
}, [canvasToolbarMode.primary, dispatch, dispatchSwitchToSelectModeCloseMenus])
+ const togglePanelsButtonClicked = React.useCallback(() => {
+ if (canvasToolbarMode.primary === 'panels') {
+ dispatchSwitchToSelectModeCloseMenus()
+ } else {
+ dispatch([switchEditorMode(EditorModes.panelsMode())])
+ }
+ }, [canvasToolbarMode.primary, dispatch, dispatchSwitchToSelectModeCloseMenus])
+
const currentStrategyState = useEditorState(
Substores.restOfStore,
(store) => store.strategyState,
@@ -423,6 +432,16 @@ export const CanvasToolbar = React.memo(() => {
,
)}
+
+
+
{
: null}
{/* Live Mode */}
{showRemixNavBar ? wrapInSubmenu() : null}
+ {/* Panels Mode */}
+ {canvasToolbarMode.primary === 'panels'
+ ? wrapInSubmenu(
+
+
+
+
+
+
+ ,
+ )
+ : null}
)
})
diff --git a/editor/src/components/editor/closed-panels.tsx b/editor/src/components/editor/closed-panels.tsx
deleted file mode 100644
index a37a6072f3f9..000000000000
--- a/editor/src/components/editor/closed-panels.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-/** @jsxRuntime classic */
-/** @jsx jsx */
-/** @jsxFrag React.Fragment */
-import { jsx } from '@emotion/react'
-import React, { useState } from 'react'
-import type { TooltipProps } from '../../uuiui'
-import { UtopiaStyles, UtopiaTheme, colorTheme } from '../../uuiui'
-import { FlexColumn, Icn, SquareButton, Tooltip as TooltipWithoutSpanFixme } from '../../uuiui'
-import { useDispatch } from './store/dispatch-context'
-import { Substores, useEditorState } from './store/store-hook'
-import { togglePanel } from './actions/action-creators'
-import { stopPropagation } from '../inspector/common/inspector-utils'
-import { setFocus } from '../common/actions'
-import { useAtom } from 'jotai'
-import { GridPanelsStateAtom } from '../canvas/grid-panels-state'
-import type { StoredLayout, StoredPanel } from '../canvas/stored-layout'
-import { assertNever } from '../../core/shared/utils'
-import { TestMenu } from '../titlebar/test-menu'
-import { ToastRenderer } from './editor-component'
-
-function getPanelColumn(side: 'left' | 'right', panels: StoredLayout): Array {
- switch (side) {
- case 'left':
- return [...panels[0].panels, ...panels[1].panels]
- case 'right':
- return [...panels[2].panels, ...panels[3].panels]
- default:
- assertNever(side)
- }
-}
-
-export const ClosedPanels = React.memo((props: { side: 'left' | 'right' }) => {
- const dispatch = useDispatch()
- const [panelState] = useAtom(GridPanelsStateAtom)
- const thisColumn = getPanelColumn(props.side, panelState)
-
- const inspectorInvisible = useEditorState(
- Substores.restOfEditor,
- (store) => {
- const theInspectorInvisible = !store.editor.rightMenu.visible
- const inspectorOnThisSide = thisColumn.some((panel) => panel.name === 'inspector')
- return theInspectorInvisible && inspectorOnThisSide
- },
- 'SettingsPanel inspector.minimized',
- )
-
- const toggleInspectorVisible = React.useCallback(() => {
- dispatch([togglePanel('rightmenu')])
- }, [dispatch])
-
- const navigatorInvisible = useEditorState(
- Substores.restOfEditor,
- (store) => {
- const theNavigatorInvisible = !store.editor.leftMenu.visible
- const navigatorOnThisSide = thisColumn.some((panel) => panel.name === 'navigator')
- return theNavigatorInvisible && navigatorOnThisSide
- },
- 'SettingsPanel navigator.minimised',
- )
-
- const toggleNavigatorVisible = React.useCallback(() => {
- dispatch([togglePanel('leftmenu')])
- }, [dispatch])
-
- const editorInvisible = useEditorState(
- Substores.restOfEditor,
- (store) => {
- const theEditorInvisible = !store.editor.interfaceDesigner.codePaneVisible
- const codeEditorOnThisSide = thisColumn.some((panel) => panel.name === 'code-editor')
- return theEditorInvisible && codeEditorOnThisSide
- },
- 'SettingsPanel navigator.minimised',
- )
-
- const toggleCodeEditorVisible = React.useCallback(
- () => dispatch([togglePanel('codeEditor')]),
- [dispatch],
- )
-
- const focusCanvasOnMouseDown = React.useCallback(
- (event: React.MouseEvent) => {
- stopPropagation(event)
- dispatch([setFocus('canvas')], 'everyone')
- },
- [dispatch],
- )
-
- const [isVisible, setIsVisible] = useState(false)
- const setIsVisibleTrue = React.useCallback(() => {
- setIsVisible(true)
- }, [])
-
- const setIsVisibleFalse = React.useCallback(() => {
- setIsVisible(false)
- }, [])
-
- return (
-
-
- {navigatorInvisible ? (
-
-
-
- ) : null}
- {editorInvisible ? (
-
-
-
- ) : null}
- {inspectorInvisible ? (
-
-
-
- ) : null}
-
- {props.side === 'left' ? : }
-
- )
-})
-ClosedPanels.displayName = 'ClosedPanels'
-
-interface ClosedPanelButtonProps {
- iconType: string
- visible?: boolean
- onClick: (event: React.MouseEvent) => void
-}
-const ClosedPanelButton = React.memo((props: ClosedPanelButtonProps) => {
- const [isHovered, setIsHovered] = useState(false)
- const setIsHoveredTrue = React.useCallback(() => {
- setIsHovered(true)
- }, [])
-
- const setIsHoveredFalse = React.useCallback(() => {
- setIsHovered(false)
- }, [])
-
- return (
-
-
-
- )
-})
-
-const Tooltip = (props: TooltipProps) => {
- return (
-
- {/* TODO why do we need to wrap the children in a span? */}
- {props.children}
-
- )
-}
diff --git a/editor/src/components/editor/editor-modes.ts b/editor/src/components/editor/editor-modes.ts
index 94323918a1b6..8c56b148b70b 100644
--- a/editor/src/components/editor/editor-modes.ts
+++ b/editor/src/components/editor/editor-modes.ts
@@ -165,7 +165,7 @@ export interface CommentMode {
isDragging: IsDragging
}
-export type SelectModeToolbarMode = 'none' | 'pseudo-insert'
+export type SelectModeToolbarMode = 'none' | 'pseudo-insert' | 'panels'
export interface SelectMode {
type: 'select'
@@ -200,6 +200,10 @@ export interface FollowMode {
connectionId: number // the connection ID of the followed player
}
+export interface PanelsMode {
+ type: 'panels'
+}
+
export type Mode =
| InsertMode
| SelectMode
@@ -207,6 +211,7 @@ export type Mode =
| TextEditMode
| CommentMode
| FollowMode
+ | PanelsMode
export type PersistedMode = SelectMode | LiveCanvasMode
export const EditorModes = {
@@ -262,6 +267,11 @@ export const EditorModes = {
connectionId: connectionId,
}
},
+ panelsMode: function (): PanelsMode {
+ return {
+ type: 'panels',
+ }
+ },
}
export function isInsertMode(value: Mode): value is InsertMode {
@@ -295,6 +305,7 @@ export function convertModeToSavedMode(mode: Mode): PersistedMode {
case 'textEdit':
case 'comment':
case 'follow':
+ case 'panels':
return EditorModes.selectMode(null, false, 'none')
}
}
diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts
index 8f5cef751b70..7bae85303aa3 100644
--- a/editor/src/components/editor/store/store-deep-equality-instances.ts
+++ b/editor/src/components/editor/store/store-deep-equality-instances.ts
@@ -530,6 +530,7 @@ import type {
SelectModeToolbarMode,
CommentMode,
FollowMode,
+ PanelsMode,
CommentId,
NewComment,
ExistingComment,
@@ -4018,6 +4019,12 @@ export const FollowModeKeepDeepEquality: KeepDeepEqualityCall = comb
EditorModes.followMode,
)
+export const PanelsModeKeepDeepEquality: KeepDeepEqualityCall = combine1EqualityCall(
+ (mode) => mode.type,
+ StringKeepDeepEquality,
+ EditorModes.panelsMode,
+)
+
export const ModeKeepDeepEquality: KeepDeepEqualityCall = (oldValue, newValue) => {
switch (oldValue.type) {
case 'insert':
@@ -4050,6 +4057,11 @@ export const ModeKeepDeepEquality: KeepDeepEqualityCall = (oldValue, newVa
return FollowModeKeepDeepEquality(newValue, oldValue)
}
break
+ case 'panels':
+ if (newValue.type === oldValue.type) {
+ return PanelsModeKeepDeepEquality(oldValue, newValue)
+ }
+ break
default:
assertNever(oldValue)
}
diff --git a/editor/src/components/navigator/navigator-item/panels-picker.tsx b/editor/src/components/navigator/navigator-item/panels-picker.tsx
new file mode 100644
index 000000000000..7f6af34440bd
--- /dev/null
+++ b/editor/src/components/navigator/navigator-item/panels-picker.tsx
@@ -0,0 +1,87 @@
+/** @jsxRuntime classic */
+/** @jsx jsx */
+import { jsx } from '@emotion/react'
+import React from 'react'
+import { CheckboxInput, colorTheme } from '../../../uuiui'
+import { FlexRow } from 'utopia-api'
+import { Substores, useEditorState } from '../../../components/editor/store/store-hook'
+import { togglePanel } from '../../../components/editor/actions/action-creators'
+import { useDispatch } from '../../../components/editor/store/dispatch-context'
+
+export const PanelsPicker = React.memo(() => {
+ const dispatch = useDispatch()
+
+ const { codeEditorVisible, navigatorVisible, inspectorVisible } = useEditorState(
+ Substores.restOfEditor,
+ (store) => {
+ return {
+ codeEditorVisible: store.editor.interfaceDesigner.codePaneVisible,
+ navigatorVisible: store.editor.leftMenu.visible,
+ inspectorVisible: store.editor.rightMenu.visible,
+ }
+ },
+ 'storedLayoutToResolvedPanels panel visibility',
+ )
+
+ const toggleNavigator = React.useCallback(() => {
+ dispatch([togglePanel('leftmenu')])
+ }, [dispatch])
+
+ const toggleCodeEditor = React.useCallback(() => {
+ dispatch([togglePanel('codeEditor')])
+ }, [dispatch])
+
+ const toggleInspector = React.useCallback(() => {
+ dispatch([togglePanel('rightmenu')])
+ }, [dispatch])
+
+ return (
+
+
+
+
+
+ )
+})
+
+interface CheckboxRowProps {
+ checked: boolean
+ label: string
+ onChange: () => void
+}
+
+const CheckboxRow = React.memo((props: CheckboxRowProps) => {
+ const { checked, label, onChange } = props
+
+ const id = `input-${label}`
+
+ return (
+
+
+
+
+
+
+ )
+})
diff --git a/editor/src/components/titlebar/test-menu.tsx b/editor/src/components/titlebar/test-menu.tsx
index f79b959ab2d0..017c9fb325a1 100644
--- a/editor/src/components/titlebar/test-menu.tsx
+++ b/editor/src/components/titlebar/test-menu.tsx
@@ -93,6 +93,8 @@ export const TestMenu = React.memo(() => {
return (