+
Want to create your own 3D project?
diff --git a/apps/editor/app/viewer/[id]/viewer-overlay.tsx b/apps/editor/app/viewer/[id]/viewer-overlay.tsx
index 49c8fd5f..b2c06bb1 100644
--- a/apps/editor/app/viewer/[id]/viewer-overlay.tsx
+++ b/apps/editor/app/viewer/[id]/viewer-overlay.tsx
@@ -11,17 +11,46 @@ import {
import { useViewer } from '@pascal-app/viewer'
import {
ArrowLeft,
- Box,
+ Camera,
ChevronRight,
Diamond,
- Eye,
- EyeOff,
- Image,
Layers,
Layers2,
+ Moon,
+ Sun,
} from 'lucide-react'
import Link from 'next/link'
+import { motion } from 'framer-motion'
+import { cn } from '@/lib/utils'
import type { ProjectOwner } from '@/features/community/lib/projects/types'
+import { ActionButton } from '@/components/ui/action-menu/action-button'
+import { TooltipProvider } from '@/components/ui/primitives/tooltip'
+import { emitter } from '@pascal-app/core'
+
+const levelModeLabels: Record<'stacked' | 'exploded' | 'solo', string> = {
+ stacked: 'Stacked',
+ exploded: 'Exploded',
+ solo: 'Solo',
+}
+
+const wallModeConfig = {
+ up: {
+ icon: (props: any) => (
+

+ ),
+ label: 'Full Height',
+ },
+ cutaway: {
+ icon: (props: any) => (
+

+ ),
+ label: 'Cutaway',
+ },
+ down: {
+ icon: (props: any) =>

,
+ label: 'Low',
+ },
+}
const getNodeName = (node: AnyNode): string => {
if ('name' in node && node.name) return node.name
@@ -53,6 +82,7 @@ export const ViewerOverlay = ({
const cameraMode = useViewer((s) => s.cameraMode)
const levelMode = useViewer((s) => s.levelMode)
const wallMode = useViewer((s) => s.wallMode)
+ const theme = useViewer((s) => s.theme)
const building = selection.buildingId
? (nodes[selection.buildingId] as BuildingNode | undefined)
@@ -95,24 +125,24 @@ export const ViewerOverlay = ({
return (
<>
{/* Unified top-left card */}
-
-
+
+
{/* Project info + back */}
-
+
-
+
-
+
{projectName || 'Untitled'}
{owner?.username && (
@{owner.username}
@@ -122,21 +152,21 @@ export const ViewerOverlay = ({
{/* Breadcrumb — only shown when navigated into a building */}
{building && (
-
-
+
+
{building && (
<>
-
+
@@ -145,10 +175,10 @@ export const ViewerOverlay = ({
{level && (
<>
-
+
@@ -157,9 +187,9 @@ export const ViewerOverlay = ({
{zone && (
<>
-
+
{zone.name}
@@ -168,8 +198,8 @@ export const ViewerOverlay = ({
{selectedNode && zone && (
<>
-
-
+
+
{getNodeName(selectedNode)}
>
@@ -181,161 +211,212 @@ export const ViewerOverlay = ({
{/* Level List (only when building is selected) */}
{building && levels.length > 0 && (
-
-
Levels
- {levels.map((lvl) => (
-
- ))}
+
+
Levels
+
+ {levels.map((lvl) => {
+ const isSelected = lvl.id === selection.levelId;
+ return (
+
+ );
+ })}
+
)}
- {/* Controls Panel - Top Right */}
-
- {/* Visibility Controls */}
- {(canShowScans || canShowGuides) && (
-
-
Visibility
+ {/* Controls Panel - Bottom Center */}
+
+
+
+ {/* Theme Toggle */}
+
+
+
+
+ {/* Scans and Guides Visibility */}
{canShowScans && (
-
+

+
)}
+
{canShowGuides && (
-
+

+
)}
+
+ {(canShowScans || canShowGuides) &&
}
+
+ {/* Camera Mode */}
+
useViewer.getState().setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective')}
+ size="icon"
+ variant="ghost"
+ >
+
+
+
+ {/* Level Mode */}
+
{
+ if (levelMode === 'manual') return useViewer.getState().setLevelMode('stacked')
+ const modes: ('stacked' | 'exploded' | 'solo')[] = ['stacked', 'exploded', 'solo']
+ const nextIndex = (modes.indexOf(levelMode as any) + 1) % modes.length
+ useViewer.getState().setLevelMode(modes[nextIndex] ?? 'stacked')
+ }}
+ size="icon"
+ variant="ghost"
+ >
+ {levelMode === 'solo' && }
+ {levelMode === 'exploded' && }
+ {(levelMode === 'stacked' || levelMode === 'manual') && }
+
+
+ {/* Wall Mode */}
+
{
+ const modes: ('cutaway' | 'up' | 'down')[] = ['cutaway', 'up', 'down']
+ const nextIndex = (modes.indexOf(wallMode as any) + 1) % modes.length
+ useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
+ }}
+ size="icon"
+ variant="ghost"
+ >
+ {(() => {
+ const Icon = wallModeConfig[wallMode as keyof typeof wallModeConfig].icon
+ return
+ })()}
+
+
+
+
+ {/* Camera Actions */}
+
emitter.emit('camera-controls:orbit-ccw')}
+ size="icon"
+ variant="ghost"
+ >
+
+
+
+
emitter.emit('camera-controls:orbit-cw')}
+ size="icon"
+ variant="ghost"
+ >
+
+
+
+
emitter.emit('camera-controls:top-view')}
+ size="icon"
+ variant="ghost"
+ >
+
+
- )}
-
- {/* Camera Mode */}
-
- Camera
-
-
-
- {/* Level Mode */}
-
- Level Mode
-
-
-
-
-
- {/* Wall Mode */}
-
-
Wall Mode
-
-
-
-
+
>
)
diff --git a/apps/editor/components/ui/action-menu/action-button.tsx b/apps/editor/components/ui/action-menu/action-button.tsx
index d9612d55..5ee00acb 100644
--- a/apps/editor/components/ui/action-menu/action-button.tsx
+++ b/apps/editor/components/ui/action-menu/action-button.tsx
@@ -12,11 +12,12 @@ interface ActionButtonProps extends React.ComponentProps
{
shortcut?: string;
isActive?: boolean;
tooltipContent?: React.ReactNode;
+ tooltipSide?: "top" | "right" | "bottom" | "left";
}
export const ActionButton = React.forwardRef(
(
- { className, children, label, shortcut, isActive, tooltipContent, ...props },
+ { className, children, label, shortcut, isActive, tooltipContent, tooltipSide, ...props },
ref
) => {
return (
@@ -47,7 +48,7 @@ export const ActionButton = React.forwardRef
-
+
{tooltipContent || (
{label} {shortcut && `(${shortcut})`}
diff --git a/packages/viewer/src/store/use-viewer.ts b/packages/viewer/src/store/use-viewer.ts
index 21603975..4b504e83 100644
--- a/packages/viewer/src/store/use-viewer.ts
+++ b/packages/viewer/src/store/use-viewer.ts
@@ -89,7 +89,7 @@ const useViewer = create()(
levelMode: "stacked",
setLevelMode: (mode) => set({ levelMode: mode }),
- wallMode: 'cutaway',
+ wallMode: 'up',
setWallMode: (mode) => set({ wallMode: mode }),
showScans: true,