Add editor preview mode for in-place viewer experience
Adds a Preview button next to Radio Pascal that switches the editor into a viewer-like experience without leaving the page. The preview mode swaps in the viewer's selection manager (hierarchical drill-down), zone system, camera controls (left-click pan, auto-navigate), and interactive item system while hiding editor-only UI (tools, panels, sidebar, grid). Back arrow in the viewer overlay returns to the editor. Also widens the default sidebar from 288px to 432px for better project title visibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
de6bfd8c00
commit
d3e17cac04
@@ -68,6 +68,8 @@ interface ViewerOverlayProps {
|
|||||||
owner?: ProjectOwner | null
|
owner?: ProjectOwner | null
|
||||||
canShowScans?: boolean
|
canShowScans?: boolean
|
||||||
canShowGuides?: boolean
|
canShowGuides?: boolean
|
||||||
|
onBack?: () => void
|
||||||
|
hideCollections?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ViewerOverlay = ({
|
export const ViewerOverlay = ({
|
||||||
@@ -75,6 +77,8 @@ export const ViewerOverlay = ({
|
|||||||
owner,
|
owner,
|
||||||
canShowScans = true,
|
canShowScans = true,
|
||||||
canShowGuides = true,
|
canShowGuides = true,
|
||||||
|
onBack,
|
||||||
|
hideCollections,
|
||||||
}: ViewerOverlayProps) => {
|
}: ViewerOverlayProps) => {
|
||||||
const selection = useViewer((s) => s.selection)
|
const selection = useViewer((s) => s.selection)
|
||||||
const nodes = useScene((s) => s.nodes)
|
const nodes = useScene((s) => s.nodes)
|
||||||
@@ -130,12 +134,21 @@ export const ViewerOverlay = ({
|
|||||||
<div className="pointer-events-auto flex flex-col rounded-2xl border border-border/40 bg-background/95 shadow-lg backdrop-blur-xl transition-colors duration-200 ease-out overflow-hidden min-w-[200px]">
|
<div className="pointer-events-auto flex flex-col rounded-2xl border border-border/40 bg-background/95 shadow-lg backdrop-blur-xl transition-colors duration-200 ease-out overflow-hidden min-w-[200px]">
|
||||||
{/* Project info + back */}
|
{/* Project info + back */}
|
||||||
<div className="flex items-center gap-3 px-3 py-2.5">
|
<div className="flex items-center gap-3 px-3 py-2.5">
|
||||||
|
{onBack ? (
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md hover:bg-white/10 transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md hover:bg-white/10 transition-colors"
|
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md hover:bg-white/10 transition-colors"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4 text-muted-foreground" />
|
<ArrowLeft className="h-4 w-4 text-muted-foreground" />
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-sm font-medium text-foreground truncate">
|
<div className="text-sm font-medium text-foreground truncate">
|
||||||
{projectName || 'Untitled'}
|
{projectName || 'Untitled'}
|
||||||
@@ -248,9 +261,11 @@ export const ViewerOverlay = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Collections Panel - Top Right */}
|
{/* Collections Panel - Top Right */}
|
||||||
|
{!hideCollections && (
|
||||||
<div className="absolute top-4 right-4 z-20 flex flex-col gap-3 dark text-foreground">
|
<div className="absolute top-4 right-4 z-20 flex flex-col gap-3 dark text-foreground">
|
||||||
<CollectionsPanel />
|
<CollectionsPanel />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Controls Panel - Bottom Center */}
|
{/* Controls Panel - Bottom Center */}
|
||||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-20 dark text-foreground">
|
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-20 dark text-foreground">
|
||||||
|
|||||||
@@ -5,14 +5,20 @@ import { useViewer } from '@pascal-app/viewer'
|
|||||||
import { CameraControls, CameraControlsImpl } from '@react-three/drei'
|
import { CameraControls, CameraControlsImpl } from '@react-three/drei'
|
||||||
import { useThree } from '@react-three/fiber'
|
import { useThree } from '@react-three/fiber'
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
import { Vector3 } from 'three'
|
import { Box3, Vector3 } from 'three'
|
||||||
import { EDITOR_LAYER } from '@/lib/constants'
|
import { EDITOR_LAYER } from '@/lib/constants'
|
||||||
|
import useEditor from '@/store/use-editor'
|
||||||
|
|
||||||
const currentTarget = new Vector3()
|
const currentTarget = new Vector3()
|
||||||
|
const tempBox = new Box3()
|
||||||
|
const tempCenter = new Vector3()
|
||||||
|
const tempSize = new Vector3()
|
||||||
|
|
||||||
export const CustomCameraControls = () => {
|
export const CustomCameraControls = () => {
|
||||||
const controls = useRef<CameraControlsImpl>(null!)
|
const controls = useRef<CameraControlsImpl>(null!)
|
||||||
const currentLevelId = useViewer((state) => state.selection.levelId)
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
||||||
|
const selection = useViewer((s) => s.selection)
|
||||||
|
const currentLevelId = selection.levelId
|
||||||
const firstLoad = useRef(true)
|
const firstLoad = useRef(true)
|
||||||
|
|
||||||
const camera = useThree((state) => state.camera)
|
const camera = useThree((state) => state.camera)
|
||||||
@@ -23,6 +29,7 @@ export const CustomCameraControls = () => {
|
|||||||
}, [camera, raycaster])
|
}, [camera, raycaster])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isPreviewMode) return // Preview mode uses auto-navigate instead
|
||||||
let targetY = 0
|
let targetY = 0
|
||||||
if (currentLevelId) {
|
if (currentLevelId) {
|
||||||
const levelMesh = sceneRegistry.nodes.get(currentLevelId)
|
const levelMesh = sceneRegistry.nodes.get(currentLevelId)
|
||||||
@@ -41,7 +48,7 @@ export const CustomCameraControls = () => {
|
|||||||
currentTarget.z,
|
currentTarget.z,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}, [currentLevelId])
|
}, [currentLevelId, isPreviewMode])
|
||||||
|
|
||||||
// Configure mouse buttons based on control mode and camera mode
|
// Configure mouse buttons based on control mode and camera mode
|
||||||
const cameraMode = useViewer((state) => state.cameraMode)
|
const cameraMode = useViewer((state) => state.cameraMode)
|
||||||
@@ -53,12 +60,14 @@ export const CustomCameraControls = () => {
|
|||||||
: CameraControlsImpl.ACTION.DOLLY
|
: CameraControlsImpl.ACTION.DOLLY
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: CameraControlsImpl.ACTION.NONE,
|
left: isPreviewMode
|
||||||
|
? CameraControlsImpl.ACTION.SCREEN_PAN
|
||||||
|
: CameraControlsImpl.ACTION.NONE,
|
||||||
middle: CameraControlsImpl.ACTION.SCREEN_PAN,
|
middle: CameraControlsImpl.ACTION.SCREEN_PAN,
|
||||||
right: CameraControlsImpl.ACTION.ROTATE,
|
right: CameraControlsImpl.ACTION.ROTATE,
|
||||||
wheel: wheelAction,
|
wheel: wheelAction,
|
||||||
}
|
}
|
||||||
}, [cameraMode])
|
}, [cameraMode, isPreviewMode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyState = {
|
const keyState = {
|
||||||
@@ -81,11 +90,15 @@ export const CustomCameraControls = () => {
|
|||||||
? CameraControlsImpl.ACTION.ZOOM
|
? CameraControlsImpl.ACTION.ZOOM
|
||||||
: CameraControlsImpl.ACTION.DOLLY
|
: CameraControlsImpl.ACTION.DOLLY
|
||||||
controls.current.mouseButtons.wheel = wheelAction
|
controls.current.mouseButtons.wheel = wheelAction
|
||||||
controls.current.mouseButtons.left = CameraControlsImpl.ACTION.NONE
|
|
||||||
controls.current.mouseButtons.middle = CameraControlsImpl.ACTION.SCREEN_PAN
|
controls.current.mouseButtons.middle = CameraControlsImpl.ACTION.SCREEN_PAN
|
||||||
controls.current.mouseButtons.right = CameraControlsImpl.ACTION.ROTATE
|
controls.current.mouseButtons.right = CameraControlsImpl.ACTION.ROTATE
|
||||||
if (space) {
|
if (isPreviewMode) {
|
||||||
|
// In preview mode, left-click is always pan (viewer-style)
|
||||||
controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN
|
controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN
|
||||||
|
} else if (space) {
|
||||||
|
controls.current.mouseButtons.left = CameraControlsImpl.ACTION.SCREEN_PAN
|
||||||
|
} else {
|
||||||
|
controls.current.mouseButtons.left = CameraControlsImpl.ACTION.NONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +150,62 @@ export const CustomCameraControls = () => {
|
|||||||
document.removeEventListener('keydown', onKeyDown)
|
document.removeEventListener('keydown', onKeyDown)
|
||||||
document.removeEventListener('keyup', onKeyUp)
|
document.removeEventListener('keyup', onKeyUp)
|
||||||
}
|
}
|
||||||
}, [cameraMode])
|
}, [cameraMode, isPreviewMode])
|
||||||
|
|
||||||
|
// Preview mode: auto-navigate camera to selected node (viewer behavior)
|
||||||
|
const previewTargetNodeId = isPreviewMode
|
||||||
|
? (selection.zoneId ?? selection.levelId ?? selection.buildingId)
|
||||||
|
: null
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPreviewMode || !controls.current) return
|
||||||
|
|
||||||
|
const nodes = useScene.getState().nodes
|
||||||
|
let node = previewTargetNodeId ? nodes[previewTargetNodeId] : null
|
||||||
|
|
||||||
|
if (!previewTargetNodeId) {
|
||||||
|
const site = Object.values(nodes).find((n) => n.type === 'site')
|
||||||
|
node = site || null
|
||||||
|
}
|
||||||
|
if (!node) return
|
||||||
|
|
||||||
|
// Check if node has a saved camera
|
||||||
|
if (node.camera) {
|
||||||
|
const { position, target } = node.camera
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!controls.current) return
|
||||||
|
controls.current.setLookAt(
|
||||||
|
position[0], position[1], position[2],
|
||||||
|
target[0], target[1], target[2],
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previewTargetNodeId) return
|
||||||
|
|
||||||
|
// Calculate camera position from bounding box
|
||||||
|
const object3D = sceneRegistry.nodes.get(previewTargetNodeId)
|
||||||
|
if (!object3D) return
|
||||||
|
|
||||||
|
tempBox.setFromObject(object3D)
|
||||||
|
tempBox.getCenter(tempCenter)
|
||||||
|
tempBox.getSize(tempSize)
|
||||||
|
|
||||||
|
const maxDim = Math.max(tempSize.x, tempSize.y, tempSize.z)
|
||||||
|
const distance = Math.max(maxDim * 2, 15)
|
||||||
|
|
||||||
|
controls.current.setLookAt(
|
||||||
|
tempCenter.x + distance * 0.7,
|
||||||
|
tempCenter.y + distance * 0.5,
|
||||||
|
tempCenter.z + distance * 0.7,
|
||||||
|
tempCenter.x,
|
||||||
|
tempCenter.y,
|
||||||
|
tempCenter.z,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}, [isPreviewMode, previewTargetNodeId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleNodeCapture = ({ nodeId }: CameraControlEvent) => {
|
const handleNodeCapture = ({ nodeId }: CameraControlEvent) => {
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core'
|
import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core'
|
||||||
import { useViewer, Viewer } from '@pascal-app/viewer'
|
import { InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useProjectScene } from '@/features/community/lib/models/hooks'
|
import { useProjectScene } from '@/features/community/lib/models/hooks'
|
||||||
import { useProjectStore } from '@/features/community/lib/projects/store'
|
import { useProjectStore } from '@/features/community/lib/projects/store'
|
||||||
import { useKeyboard } from '@/hooks/use-keyboard'
|
import { useKeyboard } from '@/hooks/use-keyboard'
|
||||||
import { initSFXBus } from '@/lib/sfx-bus'
|
import { initSFXBus } from '@/lib/sfx-bus'
|
||||||
import useEditor from '@/store/use-editor'
|
import useEditor from '@/store/use-editor'
|
||||||
|
import { ViewerOverlay } from '@/app/viewer/[id]/viewer-overlay'
|
||||||
|
import { ViewerZoneSystem } from '@/app/viewer/[id]/viewer-zone-system'
|
||||||
import { FeedbackDialog } from '../feedback-dialog'
|
import { FeedbackDialog } from '../feedback-dialog'
|
||||||
import { PascalRadio } from '../pascal-radio'
|
import { PascalRadio } from '../pascal-radio'
|
||||||
|
import { PreviewButton } from '../preview-button'
|
||||||
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
||||||
import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
|
import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
|
||||||
import { ZoneSystem } from '../systems/zone/zone-system'
|
import { ZoneSystem } from '../systems/zone/zone-system'
|
||||||
@@ -102,6 +105,8 @@ export default function Editor({ projectId }: EditorProps) {
|
|||||||
const isProjectLoading = useProjectStore((state) => state.isLoading)
|
const isProjectLoading = useProjectStore((state) => state.isLoading)
|
||||||
const isSceneLoading = useProjectStore((state) => state.isSceneLoading)
|
const isSceneLoading = useProjectStore((state) => state.isSceneLoading)
|
||||||
const isLoading = isProjectLoading || isSceneLoading
|
const isLoading = isProjectLoading || isSceneLoading
|
||||||
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
||||||
|
const activeProject = useProjectStore((s) => s.activeProject)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
@@ -121,12 +126,23 @@ export default function Editor({ projectId }: EditorProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="w-full h-full dark text-foreground">
|
<div className="w-full h-full dark text-foreground">
|
||||||
{isLoading && <SceneLoader />}
|
{isLoading && <SceneLoader />}
|
||||||
|
|
||||||
|
{isPreviewMode ? (
|
||||||
|
<ViewerOverlay
|
||||||
|
projectName={activeProject?.name}
|
||||||
|
onBack={() => useEditor.getState().setPreviewMode(false)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<ActionMenu />
|
<ActionMenu />
|
||||||
<PanelManager />
|
<PanelManager />
|
||||||
<HelperManager />
|
<HelperManager />
|
||||||
|
|
||||||
{/* Top-right controls */}
|
{/* Top-right controls */}
|
||||||
<div className="pointer-events-none fixed top-4 right-4 z-50 flex items-start gap-2">
|
<div className="pointer-events-none fixed top-4 right-4 z-50 flex items-start gap-2">
|
||||||
|
<div className="pointer-events-auto">
|
||||||
|
<PreviewButton />
|
||||||
|
</div>
|
||||||
<div className="pointer-events-auto">
|
<div className="pointer-events-auto">
|
||||||
<PascalRadio />
|
<PascalRadio />
|
||||||
</div>
|
</div>
|
||||||
@@ -138,23 +154,28 @@ export default function Editor({ projectId }: EditorProps) {
|
|||||||
<SidebarProvider className="fixed z-20">
|
<SidebarProvider className="fixed z-20">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<ErrorBoundary key={projectId} fallback={<EditorSceneCrashFallback />}>
|
<ErrorBoundary key={projectId} fallback={<EditorSceneCrashFallback />}>
|
||||||
<Viewer selectionManager="custom">
|
<Viewer selectionManager={isPreviewMode ? 'default' : 'custom'}>
|
||||||
<SelectionManager />
|
{!isPreviewMode && <SelectionManager />}
|
||||||
<FloatingActionMenu />
|
{!isPreviewMode && <FloatingActionMenu />}
|
||||||
<ExportManager />
|
<ExportManager />
|
||||||
{/* Editor only system to toggle zone visibility */}
|
{/* Swap zone systems: viewer drill-down vs editor layer toggle */}
|
||||||
<ZoneSystem />
|
{isPreviewMode ? <ViewerZoneSystem /> : <ZoneSystem />}
|
||||||
<CeilingSystem />
|
<CeilingSystem />
|
||||||
{/* <Stats /> */}
|
{!isPreviewMode && (
|
||||||
<Grid cellColor="#aaa" sectionColor="#ccc" fadeDistance={500} />
|
<Grid cellColor="#aaa" sectionColor="#ccc" fadeDistance={500} />
|
||||||
<ToolManager />
|
)}
|
||||||
|
{!isPreviewMode && <ToolManager />}
|
||||||
<CustomCameraControls />
|
<CustomCameraControls />
|
||||||
<ThumbnailGenerator projectId={projectId} />
|
<ThumbnailGenerator projectId={projectId} />
|
||||||
<PresetThumbnailGenerator />
|
<PresetThumbnailGenerator />
|
||||||
<SiteEdgeLabels />
|
{!isPreviewMode && <SiteEdgeLabels />}
|
||||||
|
{isPreviewMode && <InteractiveSystem />}
|
||||||
</Viewer>
|
</Viewer>
|
||||||
<ZoneLabelEditorSystem />
|
{!isPreviewMode && <ZoneLabelEditorSystem />}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Eye } from 'lucide-react'
|
||||||
|
import useEditor from '@/store/use-editor'
|
||||||
|
|
||||||
|
export function PreviewButton() {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => useEditor.getState().setPreviewMode(true)}
|
||||||
|
className="flex items-center gap-2 rounded-lg border border-border bg-background/95 shadow-lg backdrop-blur-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-accent/90 transition-colors"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4 shrink-0" />
|
||||||
|
<span className="hidden sm:inline whitespace-nowrap">Preview</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
|
useSidebarStore,
|
||||||
} from "@/components/ui/primitives/sidebar";
|
} from "@/components/ui/primitives/sidebar";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -108,6 +109,11 @@ export function AppSidebar() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
|
// Widen default sidebar (288px → 432px) for better project title visibility
|
||||||
|
const store = useSidebarStore.getState();
|
||||||
|
if (store.width <= 288) {
|
||||||
|
store.setWidth(432);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ type EditorState = {
|
|||||||
// Generic hole editing (works for slabs, ceilings, and any future polygon nodes)
|
// Generic hole editing (works for slabs, ceilings, and any future polygon nodes)
|
||||||
editingHole: { nodeId: string; holeIndex: number } | null
|
editingHole: { nodeId: string; holeIndex: number } | null
|
||||||
setEditingHole: (hole: { nodeId: string; holeIndex: number } | null) => void
|
setEditingHole: (hole: { nodeId: string; holeIndex: number } | null) => void
|
||||||
|
// Preview mode (viewer-like experience inside the editor)
|
||||||
|
isPreviewMode: boolean
|
||||||
|
setPreviewMode: (preview: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useEditor = create<EditorState>()((set, get) => ({
|
const useEditor = create<EditorState>()((set, get) => ({
|
||||||
@@ -219,6 +222,16 @@ const useEditor = create<EditorState>()((set, get) => ({
|
|||||||
setSpaces: (spaces) => set({ spaces }),
|
setSpaces: (spaces) => set({ spaces }),
|
||||||
editingHole: null,
|
editingHole: null,
|
||||||
setEditingHole: (hole) => set({ editingHole: hole }),
|
setEditingHole: (hole) => set({ editingHole: hole }),
|
||||||
|
isPreviewMode: false,
|
||||||
|
setPreviewMode: (preview) => {
|
||||||
|
if (preview) {
|
||||||
|
set({ isPreviewMode: true, mode: 'select', tool: null, catalogCategory: null })
|
||||||
|
// Clear zone/item selection for clean viewer drill-down hierarchy
|
||||||
|
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
||||||
|
} else {
|
||||||
|
set({ isPreviewMode: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export default useEditor
|
export default useEditor
|
||||||
|
|||||||
Reference in New Issue
Block a user