diff --git a/apps/editor/app/page.tsx b/apps/editor/app/page.tsx index 67924841..80fb1a96 100644 --- a/apps/editor/app/page.tsx +++ b/apps/editor/app/page.tsx @@ -1,14 +1,12 @@ 'use client' -import { - Editor, - ItemsPanel, - type SidebarTab, - ViewerToolbarLeft, - ViewerToolbarRight, -} from '@pascal-app/editor' +import { Editor, ItemsPanel } from '@pascal-app/editor' import { Layers, Package, Settings } from 'lucide-react' import Link from 'next/link' +import { + CommunityViewerToolbarLeft, + CommunityViewerToolbarRight, +} from '@/components/viewer-toolbar' const SIDEBAR_TABS = [ { @@ -59,8 +57,8 @@ export default function Home() { layoutVersion="v2" projectId={PROJECT_ID} sidebarTabs={SIDEBAR_TABS} - viewerToolbarLeft={} - viewerToolbarRight={} + viewerToolbarLeft={} + viewerToolbarRight={} /> ) diff --git a/apps/editor/components/scene-loader.tsx b/apps/editor/components/scene-loader.tsx index 5546c032..b3606f76 100644 --- a/apps/editor/components/scene-loader.tsx +++ b/apps/editor/components/scene-loader.tsx @@ -5,12 +5,11 @@ import { Editor, type SceneGraph, type SidebarTab, - ViewerToolbarLeft, - ViewerToolbarRight, } from '@pascal-app/editor' import Link from 'next/link' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useRef, useState } from 'react' +import { CommunityViewerToolbarLeft, CommunityViewerToolbarRight } from './viewer-toolbar' export interface SceneMeta { id: string @@ -200,8 +199,8 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { onThumbnailCapture={handleThumb} projectId={meta.projectId ?? 'default'} sidebarTabs={SIDEBAR_TABS} - viewerToolbarLeft={} - viewerToolbarRight={} + viewerToolbarLeft={} + viewerToolbarRight={} /> ) diff --git a/apps/editor/components/toolbar-tooltip.tsx b/apps/editor/components/toolbar-tooltip.tsx new file mode 100644 index 00000000..f3554d01 --- /dev/null +++ b/apps/editor/components/toolbar-tooltip.tsx @@ -0,0 +1,49 @@ +'use client' + +import * as TooltipPrimitive from '@radix-ui/react-tooltip' +import type * as React from 'react' +import { cn } from '@/lib/utils' + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return +} + +function Tooltip({ ...props }: React.ComponentProps) { + return ( + + + + ) +} + +function TooltipTrigger({ ...props }: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 6, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } diff --git a/apps/editor/components/viewer-toolbar.tsx b/apps/editor/components/viewer-toolbar.tsx new file mode 100644 index 00000000..e3668ce2 --- /dev/null +++ b/apps/editor/components/viewer-toolbar.tsx @@ -0,0 +1,362 @@ +'use client' + +import { Icon as IconifyIcon } from '@iconify/react' +import { useEditor, useSidebarStore, type ViewMode } from '@pascal-app/editor' +import { useViewer } from '@pascal-app/viewer' +import { + ChevronsLeft, + ChevronsRight, + Columns2, + Eye, + EyeOff, + Footprints, + Grid2X2, + Moon, + Sun, +} from 'lucide-react' +import Image from 'next/image' +import { type ReactNode, useCallback } from 'react' +import { cn } from '@/lib/utils' +import { Tooltip, TooltipContent, TooltipTrigger } from './toolbar-tooltip' + +const TOOLBAR_CONTAINER = + 'inline-flex h-8 items-stretch overflow-hidden rounded-xl border border-border bg-background/90 shadow-2xl backdrop-blur-md' + +const TOOLBAR_BTN = + 'flex w-8 items-center justify-center text-muted-foreground/80 transition-colors hover:bg-white/8 hover:text-foreground/90' + +function ToolbarTooltip({ children, label }: { children: ReactNode; label: string }) { + return ( + + {children} + {label} + + ) +} + +const VIEW_MODES: { id: ViewMode; label: string; icon: React.ReactNode }[] = [ + { + id: '3d', + label: '3D', + icon: ( + + ), + }, + { + id: '2d', + label: '2D', + icon: ( + + ), + }, + { + id: 'split', + label: 'Split', + icon: , + }, +] + +const levelModeOrder = ['stacked', 'exploded', 'solo'] as const +const levelModeLabels: Record = { + manual: 'Stack', + stacked: 'Stack', + exploded: 'Exploded', + solo: 'Solo', +} + +const wallModeOrder = ['cutaway', 'up', 'down'] as const +const wallModeConfig: Record = { + up: { icon: '/icons/room.png', label: 'Full height' }, + cutaway: { icon: '/icons/wallcut.png', label: 'Cutaway' }, + down: { icon: '/icons/walllow.png', label: 'Low' }, +} + +function ViewModeControl() { + const viewMode = useEditor((state) => state.viewMode) + const setViewMode = useEditor((state) => state.setViewMode) + + return ( +
+ {VIEW_MODES.map((mode) => { + const isActive = viewMode === mode.id + return ( + + + + ) + })} +
+ ) +} + +function CollapseSidebarButton() { + const isCollapsed = useSidebarStore((state) => state.isCollapsed) + const setIsCollapsed = useSidebarStore((state) => state.setIsCollapsed) + + const toggle = useCallback(() => { + setIsCollapsed(!isCollapsed) + }, [isCollapsed, setIsCollapsed]) + + return ( +
+ + + +
+ ) +} + +function LevelModeToggle() { + const levelMode = useViewer((state) => state.levelMode) + const setLevelMode = useViewer((state) => state.setLevelMode) + const isDefault = levelMode === 'stacked' || levelMode === 'manual' + + const cycle = () => { + if (levelMode === 'manual') { + setLevelMode('stacked') + return + } + + const index = levelModeOrder.indexOf(levelMode as (typeof levelModeOrder)[number]) + const next = levelModeOrder[(index + 1) % levelModeOrder.length] + if (next) setLevelMode(next) + } + + const label = `Levels: ${levelMode === 'manual' ? 'Manual' : (levelModeLabels[levelMode] ?? 'Stack')}` + + return ( + + + + ) +} + +function WallModeToggle() { + const wallMode = useViewer((state) => state.wallMode) + const setWallMode = useViewer((state) => state.setWallMode) + const config = wallModeConfig[wallMode] ?? wallModeConfig.cutaway! + + const cycle = () => { + const index = wallModeOrder.indexOf(wallMode as (typeof wallModeOrder)[number]) + const next = wallModeOrder[(index + 1) % wallModeOrder.length] + if (next) setWallMode(next) + } + + return ( + + + + ) +} + +function GridVisibilityToggle() { + const showGrid = useViewer((state) => state.showGrid) + const setShowGrid = useViewer((state) => state.setShowGrid) + + return ( + + + + ) +} + +function UnitToggle() { + const unit = useViewer((state) => state.unit) + const setUnit = useViewer((state) => state.setUnit) + + return ( + + + + ) +} + +function ThemeToggle() { + const theme = useViewer((state) => state.theme) + const setTheme = useViewer((state) => state.setTheme) + + return ( + + + + ) +} + +function CameraModeToggle() { + const cameraMode = useViewer((state) => state.cameraMode) + const setCameraMode = useViewer((state) => state.setCameraMode) + + return ( + + + + ) +} + +function WalkthroughButton() { + const isFirstPersonMode = useEditor((state) => state.isFirstPersonMode) + const setFirstPersonMode = useEditor((state) => state.setFirstPersonMode) + + return ( + + + + ) +} + +function PreviewButton() { + return ( + + + + ) +} + +export function CommunityViewerToolbarLeft() { + return ( + <> + + + + ) +} + +export function CommunityViewerToolbarRight() { + return ( +
+ + + +
+ + + +
+ + +
+ ) +} diff --git a/apps/editor/package.json b/apps/editor/package.json index 21e8dad8..f2c00a2e 100644 --- a/apps/editor/package.json +++ b/apps/editor/package.json @@ -11,11 +11,13 @@ "check-types": "next typegen && tsc --noEmit" }, "dependencies": { + "@iconify/react": "^6.0.2", "@number-flow/react": "^0.5.14", "@pascal-app/core": "*", "@pascal-app/editor": "*", "@pascal-app/mcp": "*", "@pascal-app/viewer": "*", + "@radix-ui/react-tooltip": "^1.2.8", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", "@tailwindcss/postcss": "^4.2.1", diff --git a/bun.lock b/bun.lock index 66c38cbf..64ed55c6 100644 --- a/bun.lock +++ b/bun.lock @@ -26,11 +26,13 @@ "name": "editor", "version": "0.1.0", "dependencies": { + "@iconify/react": "^6.0.2", "@number-flow/react": "^0.5.14", "@pascal-app/core": "*", "@pascal-app/editor": "*", "@pascal-app/mcp": "*", "@pascal-app/viewer": "*", + "@radix-ui/react-tooltip": "^1.2.8", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.5.0", "@tailwindcss/postcss": "^4.2.1",