From 433eaa2e021a044a3597fa17f0bbafa8ea5a73c9 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 10 Mar 2026 10:56:09 +0100 Subject: [PATCH 1/5] project edit name change --- .../components/ui/sidebar/app-sidebar.tsx | 31 +++---------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/apps/editor/components/ui/sidebar/app-sidebar.tsx b/apps/editor/components/ui/sidebar/app-sidebar.tsx index 4766e6aa..44a9ae7e 100644 --- a/apps/editor/components/ui/sidebar/app-sidebar.tsx +++ b/apps/editor/components/ui/sidebar/app-sidebar.tsx @@ -7,7 +7,6 @@ import { ChevronDown, Clock3, Moon, - Pencil, RotateCcw, Save, Sun, @@ -513,17 +512,6 @@ export function AppSidebar() { } }; - const getPanelTitle = () => { - switch (activePanel) { - case "site": - return "Site"; - case "settings": - return "Settings"; - default: - return ""; - } - }; - return (
@@ -547,15 +535,12 @@ export function AppSidebar() { className="w-full bg-transparent text-foreground outline-none border-b border-primary/50 focus:border-primary rounded-none px-0 py-0 m-0 h-7 font-semibold text-lg" /> ) : ( -
setIsEditingTitle(true)} > -

- {activeProject?.name || "Untitled Project"} -

- -
+ {activeProject?.name || "Untitled Project"} + )}
@@ -569,14 +554,11 @@ export function AppSidebar() { onClick={() => runVersionAction("save")} disabled={isQuickSaveDisabled} className={cn( - "group/save-trigger relative inline-flex h-full min-w-0 items-center border-r border-border/50 px-1.5 text-[10px] transition-colors", + "group/save-trigger relative inline-flex h-full w-16 items-center border-r border-border/50 px-1.5 text-[10px] transition-colors", isQuickSaveDisabled ? "cursor-not-allowed opacity-50" : "hover:bg-black/30", )} - style={{ - width: "clamp(48px, calc(var(--sidebar-width) - 16.5rem), 80px)", - }} > @@ -770,9 +752,6 @@ export function AppSidebar() { )}
- - {getPanelTitle()} - Date: Tue, 10 Mar 2026 12:13:04 +0100 Subject: [PATCH 2/5] project switch --- .../components/ui/sidebar/icon-rail.tsx | 260 +++++++++++++----- 1 file changed, 192 insertions(+), 68 deletions(-) diff --git a/apps/editor/components/ui/sidebar/icon-rail.tsx b/apps/editor/components/ui/sidebar/icon-rail.tsx index 0fd8a443..74bbfa30 100644 --- a/apps/editor/components/ui/sidebar/icon-rail.tsx +++ b/apps/editor/components/ui/sidebar/icon-rail.tsx @@ -1,16 +1,101 @@ "use client"; -import { Moon, Sun } from "lucide-react"; +import { ArrowLeft, Command, FolderOpen, Moon, Search, Sun } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/primitives/tooltip"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/primitives/popover"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/primitives/dialog"; import { cn } from "@/lib/utils"; import { useViewer } from "@pascal-app/viewer"; +import { useProjectStore } from "@/features/community/lib/projects/store"; + +function OpenProjectModal({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const router = useRouter(); + const projects = useProjectStore((s) => s.projects); + const activeProject = useProjectStore((s) => s.activeProject); + const fetchProjects = useProjectStore((s) => s.fetchProjects); + + useEffect(() => { + if (open && projects.length === 0) { + fetchProjects(); + } + }, [open, projects.length, fetchProjects]); + + return ( + + + + Open project + +
+ {projects.length === 0 ? ( +

+ No projects found +

+ ) : ( + projects.map((project) => { + const isActive = project.id === activeProject?.id; + return ( + + ); + }) + )} +
+
+
+ ); +} export type PanelId = "site" | "settings"; @@ -33,89 +118,128 @@ export function IconRail({ const theme = useViewer((state) => state.theme); const setTheme = useViewer((state) => state.setTheme); const [mounted, setMounted] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isOpenProjectOpen, setIsSwitchProjectOpen] = useState(false); useEffect(() => { setMounted(true); }, []); + const handleOpenProject = () => { + setIsMenuOpen(false); + setIsSwitchProjectOpen(true); + }; + return ( -
- {/* Pascal logo - link to the home page */} - - - - Pascal - - - Back to Pascal Editor - + <> +
+ {/* Pascal logo — app menu */} + + + + + + setIsMenuOpen(false)} + > + + Back to community + + +
+ + + - {/* Divider */} -
+ {/* Divider */} +
- {panels.map((panel) => { - const isActive = activePanel === panel.id; - return ( - + {panels.map((panel) => { + const isActive = activePanel === panel.id; + return ( + + + + + {panel.label} + + ); + })} + + {/* Spacer */} +
+ + {/* Theme Toggle */} + {mounted && ( + - {panel.label} + Toggle theme - ); - })} + )} +
- {/* Spacer */} -
- - {/* Theme Toggle */} - {mounted && ( - - - - - Toggle theme - - )} -
+ + ); } From 175040232a529cca3a54b2f43bc1c20461af8d27 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 10 Mar 2026 12:25:30 +0100 Subject: [PATCH 3/5] theme switch minimalist --- .../components/ui/sidebar/app-sidebar.tsx | 58 ------------------- .../components/ui/sidebar/icon-rail.tsx | 12 +++- 2 files changed, 10 insertions(+), 60 deletions(-) diff --git a/apps/editor/components/ui/sidebar/app-sidebar.tsx b/apps/editor/components/ui/sidebar/app-sidebar.tsx index 44a9ae7e..53993315 100644 --- a/apps/editor/components/ui/sidebar/app-sidebar.tsx +++ b/apps/editor/components/ui/sidebar/app-sidebar.tsx @@ -6,12 +6,9 @@ import { ArrowUpCircle, ChevronDown, Clock3, - Moon, RotateCcw, Save, - Sun, } from "lucide-react"; -import { motion } from "framer-motion"; import { useScene } from "@pascal-app/core"; import { @@ -47,7 +44,6 @@ import { } from "@/features/community/lib/models/actions"; import { useProjectStore } from "@/features/community/lib/projects/store"; import { updateProjectName } from "@/features/community/lib/projects/actions"; -import { useViewer } from "@pascal-app/viewer"; import { applySceneGraphToEditor } from "@/features/community/lib/models/hooks"; function formatRelativeTime(value: string): string { @@ -83,9 +79,6 @@ export function AppSidebar() { const setIsVersionPreviewMode = useProjectStore((s) => s.setIsVersionPreviewMode); const setIsSceneLoading = useProjectStore((s) => s.setIsSceneLoading); const setAutosaveStatus = useProjectStore((s) => s.setAutosaveStatus); - const theme = useViewer((state) => state.theme); - const setTheme = useViewer((state) => state.setTheme); - const [mounted, setMounted] = useState(false); const [isEditingTitle, setIsEditingTitle] = useState(false); const [titleValue, setTitleValue] = useState(""); @@ -107,7 +100,6 @@ export function AppSidebar() { const activeProjectId = activeProject?.id ?? null; useEffect(() => { - setMounted(true); // Widen default sidebar (288px → 432px) for better project title visibility const store = useSidebarStore.getState(); if (store.width <= 288) { @@ -700,56 +692,6 @@ export function AppSidebar() { )} - - {mounted && ( - - )}
diff --git a/apps/editor/components/ui/sidebar/icon-rail.tsx b/apps/editor/components/ui/sidebar/icon-rail.tsx index 74bbfa30..b55bea3b 100644 --- a/apps/editor/components/ui/sidebar/icon-rail.tsx +++ b/apps/editor/components/ui/sidebar/icon-rail.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import Image from "next/image"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; +import { motion } from "framer-motion"; import { Tooltip, TooltipContent, @@ -226,11 +227,18 @@ export function IconRail({ Toggle theme From d0f2a1a06f6a4d44c54b205644de32a6583b1326 Mon Sep 17 00:00:00 2001 From: wass08 Date: Tue, 10 Mar 2026 13:35:05 +0100 Subject: [PATCH 4/5] actions command K --- .../components/ui/command-palette/index.tsx | 771 ++++++++++++++++++ .../components/ui/sidebar/app-sidebar.tsx | 4 + .../components/ui/sidebar/icon-rail.tsx | 3 +- apps/editor/package.json | 1 + bun.lock | 5 + 5 files changed, 783 insertions(+), 1 deletion(-) create mode 100644 apps/editor/components/ui/command-palette/index.tsx diff --git a/apps/editor/components/ui/command-palette/index.tsx b/apps/editor/components/ui/command-palette/index.tsx new file mode 100644 index 00000000..efea3c31 --- /dev/null +++ b/apps/editor/components/ui/command-palette/index.tsx @@ -0,0 +1,771 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Command } from "cmdk"; +import { create } from "zustand"; +import { + AppWindow, + ArrowRight, + Building2, + Camera, + ChevronRight, + Copy, + DoorOpen, + Eye, + EyeOff, + FileJson, + Hexagon, + Layers, + Map, + Maximize2, + Minimize2, + Moon, + MousePointer2, + Package, + PencilLine, + Plus, + Redo2, + Search, + Square, + SquareStack, + Sun, + Trash2, + Undo2, + Video, + Box, + Grid3X3, +} from "lucide-react"; +import { Dialog, DialogContent } from "@/components/ui/primitives/dialog"; +import useEditor from "@/store/use-editor"; +import type { StructureTool } from "@/store/use-editor"; +import { useViewer } from "@pascal-app/viewer"; +import { emitter, LevelNode, useScene } from "@pascal-app/core"; +import type { AnyNodeId } from "@pascal-app/core"; +import { useShallow } from "zustand/shallow"; + +// --------------------------------------------------------------------------- +// Open-state store — imported by icon-rail to trigger the palette +// --------------------------------------------------------------------------- +interface CommandPaletteStore { + open: boolean; + setOpen: (open: boolean) => void; +} + +export const useCommandPalette = create((set) => ({ + open: false, + setOpen: (open) => set({ open }), +})); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +function Shortcut({ keys }: { keys: string[] }) { + return ( + + {keys.map((k) => ( + + {k} + + ))} + + ); +} + +function Item({ + icon, + label, + onSelect, + shortcut, + disabled = false, + keywords = [], + badge, + navigate = false, +}: { + icon: React.ReactNode; + label: string; + onSelect: () => void; + shortcut?: string[]; + disabled?: boolean; + keywords?: string[]; + badge?: string; + navigate?: boolean; +}) { + return ( + + + {icon} + + {label} + {badge && ( + + {badge} + + )} + {shortcut && } + {(badge || navigate) && } + + ); +} + +function OptionItem({ + label, + isActive = false, + onSelect, + icon, + disabled = false, +}: { + label: string; + isActive?: boolean; + onSelect: () => void; + icon?: React.ReactNode; + disabled?: boolean; +}) { + return ( + + + {isActive + ?
+ : icon + } + + {label} + + ); +} + +// --------------------------------------------------------------------------- +// Sub-page label map +// --------------------------------------------------------------------------- +const PAGE_LABEL: Record = { + "wall-mode": "Wall Mode", + "level-mode": "Level Mode", + "rename-level": "Rename Level", + "goto-level": "Go to Level", + "camera-view": "Camera Snapshot", + "camera-scope": "", // dynamic — overridden in breadcrumb +}; + +// --------------------------------------------------------------------------- +// Main component +// --------------------------------------------------------------------------- +export function CommandPalette() { + const { open, setOpen } = useCommandPalette(); + const [meta, setMeta] = useState("⌘"); + const [isFullscreen, setIsFullscreen] = useState(false); + const [pages, setPages] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [cameraScope, setCameraScope] = useState<{ nodeId: string; label: string } | null>(null); + + const page = pages[pages.length - 1]; + + const { setPhase, setMode, setTool, setStructureLayer, isPreviewMode, setPreviewMode } = + useEditor(); + + const cameraMode = useViewer((s) => s.cameraMode); + const setCameraMode = useViewer((s) => s.setCameraMode); + const levelMode = useViewer((s) => s.levelMode); + const setLevelMode = useViewer((s) => s.setLevelMode); + const wallMode = useViewer((s) => s.wallMode); + const setWallMode = useViewer((s) => s.setWallMode); + const theme = useViewer((s) => s.theme); + const setTheme = useViewer((s) => s.setTheme); + const selection = useViewer((s) => s.selection); + const exportScene = useViewer((s) => s.exportScene); + + const activeLevelId = selection.levelId; + const activeLevelNode = useScene((s) => activeLevelId ? s.nodes[activeLevelId] : null); + const isLevelZero = + activeLevelNode?.type === "level" && (activeLevelNode as LevelNode).level === 0; + + // Reactive snapshot status for the selected camera scope + const cameraScopeNode = useScene((s) => cameraScope ? s.nodes[cameraScope.nodeId] : null); + const hasScopeSnapshot = !!(cameraScopeNode as any)?.camera; + + const allLevels = useScene( + useShallow((s) => + (Object.values(s.nodes).filter((n) => n.type === "level") as LevelNode[]).sort( + (a, b) => a.level - b.level + ) + ) + ); + + const hasSelection = selection.selectedIds.length > 0; + + // Platform detection + useEffect(() => { + setMeta(/Mac|iPhone|iPad|iPod/.test(navigator.platform) ? "⌘" : "Ctrl"); + }, []); + + // Fullscreen tracking + useEffect(() => { + const handler = () => setIsFullscreen(!!document.fullscreenElement); + document.addEventListener("fullscreenchange", handler); + return () => document.removeEventListener("fullscreenchange", handler); + }, []); + + // Cmd/Ctrl+K global shortcut + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + setOpen(true); + } + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [setOpen]); + + // Reset sub-pages when palette closes + useEffect(() => { + if (!open) { + setPages([]); + setInputValue(""); + setCameraScope(null); + } + }, [open]); + + // --------------------------------------------------------------------------- + // Navigation helpers + // --------------------------------------------------------------------------- + const goBack = () => { + const leavingPage = pages[pages.length - 1]; + if (leavingPage === "camera-scope") setCameraScope(null); + setPages((p) => p.slice(0, -1)); + setInputValue(""); + }; + + const navigateTo = (p: string) => { + // Pre-fill the rename input with the current level name + if (p === "rename-level" && activeLevelId) { + const level = useScene.getState().nodes[activeLevelId] as LevelNode; + setInputValue(level?.name ?? ""); + } else { + setInputValue(""); + } + setPages((prev) => [...prev, p]); + }; + + const navigateToCameraScope = (nodeId: string, label: string) => { + setCameraScope({ nodeId, label }); + setInputValue(""); + setPages((prev) => [...prev, "camera-scope"]); + }; + + // --------------------------------------------------------------------------- + // Action helpers + // --------------------------------------------------------------------------- + const run = (fn: () => void) => { + fn(); + setOpen(false); + }; + + const activateTool = (tool: StructureTool) => { + run(() => { + setPhase("structure"); + setMode("build"); + if (tool === "zone") setStructureLayer("zones"); + setTool(tool); + }); + }; + + const wallModeLabel: Record = { cutaway: "Cutaway", up: "Up", down: "Down" }; + const levelModeLabel: Record = { + stacked: "Stacked", + exploded: "Exploded", + solo: "Solo", + }; + + const deleteSelection = () => { + if (!hasSelection) return; + run(() => { + useScene.getState().deleteNodes(selection.selectedIds as any[]); + }); + }; + + // Level management + const addLevel = () => + run(() => { + const { nodes } = useScene.getState(); + const building = Object.values(nodes).find((n) => n.type === "building"); + if (!building) return; + const newLevel = LevelNode.parse({ + level: building.children.length, + children: [], + parentId: building.id, + }); + useScene.getState().createNode(newLevel, building.id); + useViewer.getState().setSelection({ levelId: newLevel.id }); + }); + + const deleteActiveLevel = () => { + if (!activeLevelId || isLevelZero) return; + run(() => { + useScene.getState().deleteNode(activeLevelId as AnyNodeId); + const { nodes } = useScene.getState(); + const level0 = Object.values(nodes).find( + (n) => n.type === "level" && (n as LevelNode).level === 0 + ); + if (level0) useViewer.getState().setSelection({ levelId: level0.id }); + }); + }; + + const confirmRename = () => { + if (!activeLevelId || !inputValue.trim()) return; + run(() => { + useScene.getState().updateNode(activeLevelId as AnyNodeId, { name: inputValue.trim() } as any); + }); + }; + + // Camera snapshot (scoped to the currently selected camera scope) + const takeSnapshot = () => { + if (!cameraScope) return; + run(() => emitter.emit("camera-controls:capture", { nodeId: cameraScope.nodeId })); + }; + + const viewSnapshot = () => { + if (!cameraScope || !hasScopeSnapshot) return; + run(() => emitter.emit("camera-controls:view", { nodeId: cameraScope.nodeId })); + }; + + const clearSnapshot = () => { + if (!cameraScope || !hasScopeSnapshot) return; + run(() => { + useScene.getState().updateNode(cameraScope.nodeId as AnyNodeId, { camera: undefined } as any); + }); + }; + + // Export helpers + const exportJson = () => + run(() => { + const { nodes, rootNodeIds } = useScene.getState(); + const blob = new Blob([JSON.stringify({ nodes, rootNodeIds }, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = Object.assign(document.createElement("a"), { + href: url, + download: `scene_${new Date().toISOString().split("T")[0]}.json`, + }); + a.click(); + URL.revokeObjectURL(url); + }); + + const copyShareLink = () => + run(() => { + navigator.clipboard.writeText(window.location.href); + }); + + const takeScreenshot = () => + run(() => { + const canvas = document.querySelector("canvas"); + if (!canvas) return; + const a = Object.assign(document.createElement("a"), { + href: canvas.toDataURL("image/png"), + download: `screenshot_${new Date().toISOString().split("T")[0]}.png`, + }); + a.click(); + }); + + const toggleFullscreen = () => + run(() => { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + document.documentElement.requestFullscreen(); + } + }); + + // --------------------------------------------------------------------------- + // Render + // --------------------------------------------------------------------------- + return ( + + + { + if (e.key === "Backspace" && !inputValue && pages.length > 0) { + e.preventDefault(); + goBack(); + } + }} + > + {/* Search bar */} +
+ + {page && ( + + )} + +
+ + + + No commands found. + + + {/* ── Root view ─────────────────────────────────────────────── */} + {!page && ( + <> + {/* Scene / Tools */} + + } label="Wall Tool" onSelect={() => activateTool("wall")} keywords={["draw", "build", "structure"]} /> + } label="Slab Tool" onSelect={() => activateTool("slab")} keywords={["floor", "build"]} /> + } label="Ceiling Tool" onSelect={() => activateTool("ceiling")} keywords={["top", "build"]} /> + } label="Door Tool" onSelect={() => activateTool("door")} keywords={["opening", "entrance"]} /> + } label="Window Tool" onSelect={() => activateTool("window")} keywords={["opening", "glass"]} /> + } label="Item Tool" onSelect={() => activateTool("item")} keywords={["furniture", "object", "asset", "furnish"]} /> + } label="Zone Tool" onSelect={() => activateTool("zone")} keywords={["area", "room", "space"]} /> + } + label="Delete Selection" + onSelect={deleteSelection} + disabled={!hasSelection} + shortcut={["⌫"]} + keywords={["remove", "erase"]} + /> + + + {/* Levels */} + + } + label="Go to Level" + navigate + onSelect={() => navigateTo("goto-level")} + disabled={allLevels.length === 0} + keywords={["level", "floor", "go", "navigate", "switch", "select"]} + /> + } + label="Add Level" + onSelect={addLevel} + keywords={["level", "floor", "add", "create", "new"]} + /> + } + label="Rename Level" + navigate + onSelect={() => navigateTo("rename-level")} + disabled={!activeLevelId} + keywords={["level", "floor", "rename", "name"]} + /> + } + label="Delete Level" + onSelect={deleteActiveLevel} + disabled={!activeLevelId || isLevelZero} + keywords={["level", "floor", "delete", "remove"]} + /> + + + {/* Viewer Controls */} + + } + label="Wall Mode" + badge={wallModeLabel[wallMode]} + onSelect={() => navigateTo("wall-mode")} + keywords={["wall", "cutaway", "up", "down", "view"]} + /> + } + label="Level Mode" + badge={levelModeLabel[levelMode]} + onSelect={() => navigateTo("level-mode")} + keywords={["level", "floor", "exploded", "stacked", "solo"]} + /> + } + label={`Camera: Switch to ${cameraMode === "perspective" ? "Orthographic" : "Perspective"}`} + onSelect={() => + run(() => + setCameraMode( + cameraMode === "perspective" ? "orthographic" : "perspective" + ) + ) + } + keywords={["camera", "ortho", "perspective", "2d", "3d", "view"]} + /> + : } + label={theme === "dark" ? "Switch to Light Theme" : "Switch to Dark Theme"} + onSelect={() => run(() => setTheme(theme === "dark" ? "light" : "dark"))} + keywords={["theme", "dark", "light", "appearance", "color"]} + /> + } + label="Camera Snapshot" + navigate + onSelect={() => navigateTo("camera-view")} + keywords={["camera", "snapshot", "capture", "save", "view", "bookmark"]} + /> + + + {/* View / Mode */} + + : } + label={isPreviewMode ? "Exit Preview" : "Enter Preview"} + onSelect={() => run(() => setPreviewMode(!isPreviewMode))} + keywords={["preview", "view", "read-only", "present"]} + /> + + ) : ( + + ) + } + label={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"} + onSelect={toggleFullscreen} + keywords={["fullscreen", "maximize", "expand", "window"]} + /> + + + {/* History */} + + } + label="Undo" + onSelect={() => run(() => useScene.temporal.getState().undo())} + shortcut={[meta, "Z"]} + keywords={["undo", "revert", "back"]} + /> + } + label="Redo" + onSelect={() => run(() => useScene.temporal.getState().redo())} + shortcut={[meta, "⇧", "Z"]} + keywords={["redo", "forward", "repeat"]} + /> + + + {/* Export / Share */} + + } + label="Export Scene (JSON)" + onSelect={exportJson} + keywords={["export", "download", "json", "save", "data"]} + /> + {exportScene && ( + } + label="Export 3D Model (GLB)" + onSelect={() => run(() => exportScene())} + keywords={["export", "glb", "gltf", "3d", "model", "download"]} + /> + )} + } + label="Copy Share Link" + onSelect={copyShareLink} + keywords={["share", "copy", "url", "link"]} + /> + } + label="Take Screenshot" + onSelect={takeScreenshot} + keywords={["screenshot", "capture", "image", "photo", "png"]} + /> + + + )} + + {/* ── Wall Mode sub-page ────────────────────────────────────── */} + {page === "wall-mode" && ( + + {(["cutaway", "up", "down"] as const).map((mode) => ( + run(() => setWallMode(mode))} + /> + ))} + + )} + + {/* ── Level Mode sub-page ───────────────────────────────────── */} + {page === "level-mode" && ( + + {(["stacked", "exploded", "solo"] as const).map((mode) => ( + run(() => setLevelMode(mode))} + /> + ))} + + )} + + {/* ── Go to Level sub-page ──────────────────────────────────── */} + {page === "goto-level" && ( + + {allLevels.map((level) => ( + + run(() => useViewer.getState().setSelection({ levelId: level.id })) + } + /> + ))} + + )} + + {/* ── Rename Level sub-page ─────────────────────────────────── */} + {page === "rename-level" && ( + + + + + + + {inputValue.trim() ? ( + <>Rename to "{inputValue.trim()}" + ) : ( + Type a new name above… + )} + + + + )} + + {/* ── Camera Snapshot: scope picker ─────────────────────────── */} + {page === "camera-view" && ( + + } + onSelect={() => { + const { rootNodeIds } = useScene.getState(); + const siteId = rootNodeIds[0]; + if (siteId) navigateToCameraScope(siteId, "Site"); + }} + /> + } + onSelect={() => { + const building = Object.values(useScene.getState().nodes).find( + (n) => n.type === "building" + ); + if (building) navigateToCameraScope(building.id, "Building"); + }} + /> + } + disabled={!activeLevelId} + onSelect={() => { + if (activeLevelId) navigateToCameraScope(activeLevelId, "Level"); + }} + /> + } + disabled={!hasSelection} + onSelect={() => { + const firstId = selection.selectedIds[0]; + if (firstId) navigateToCameraScope(firstId, "Selection"); + }} + /> + + )} + + {/* ── Camera Snapshot: actions for selected scope ───────────── */} + {page === "camera-scope" && cameraScope && ( + + } + onSelect={takeSnapshot} + /> + {hasScopeSnapshot && ( + } + onSelect={viewSnapshot} + /> + )} + {hasScopeSnapshot && ( + } + onSelect={clearSnapshot} + /> + )} + + )} + + + {/* Footer hint */} +
+ + navigate + + + select + + {page ? ( + + back + + ) : ( + + close + + )} +
+
+
+
+ ); +} diff --git a/apps/editor/components/ui/sidebar/app-sidebar.tsx b/apps/editor/components/ui/sidebar/app-sidebar.tsx index 53993315..d9dcba7c 100644 --- a/apps/editor/components/ui/sidebar/app-sidebar.tsx +++ b/apps/editor/components/ui/sidebar/app-sidebar.tsx @@ -2,6 +2,7 @@ import { useState, useRef, useEffect, useCallback, useMemo } from "react"; import { IconRail, type PanelId } from "./icon-rail"; +import { CommandPalette } from "@/components/ui/command-palette"; import { ArrowUpCircle, ChevronDown, @@ -505,6 +506,7 @@ export function AppSidebar() { }; return ( + <>
{/* Icon Rail */} @@ -704,5 +706,7 @@ export function AppSidebar() {
+ + ); } diff --git a/apps/editor/components/ui/sidebar/icon-rail.tsx b/apps/editor/components/ui/sidebar/icon-rail.tsx index b55bea3b..2fc845c0 100644 --- a/apps/editor/components/ui/sidebar/icon-rail.tsx +++ b/apps/editor/components/ui/sidebar/icon-rail.tsx @@ -25,6 +25,7 @@ import { import { cn } from "@/lib/utils"; import { useViewer } from "@pascal-app/viewer"; import { useProjectStore } from "@/features/community/lib/projects/store"; +import { useCommandPalette } from "@/components/ui/command-palette"; function OpenProjectModal({ open, @@ -176,7 +177,7 @@ export function IconRail({