-
-
diff --git a/apps/editor/components/ui/action-menu/phase-switcher.tsx b/apps/editor/components/ui/action-menu/phase-switcher.tsx
deleted file mode 100644
index 3065f26e..00000000
--- a/apps/editor/components/ui/action-menu/phase-switcher.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/primitives/button";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/primitives/tooltip";
-import { Building2, Map, Sofa } from "lucide-react";
-
-import { cn } from "@/lib/utils";
-import useEditor, { Phase } from "@/store/use-editor";
-
-const editorModes: Array<{
- id: Phase;
- icon: typeof Map;
- label: string;
- shortcut: string;
- description: string;
- color: string;
- activeColor: string;
-}> = [
- {
- id: "site",
- icon: Map,
- label: "Site",
- shortcut: "1",
- description: "Edit terrain, place buildings, set boundaries",
- color: "hover:bg-emerald-500/20 hover:text-emerald-400",
- activeColor: "bg-emerald-500/20 text-emerald-400",
- },
- {
- id: "structure",
- icon: Building2,
- label: "Structure",
- shortcut: "2",
- description: "Walls, rooms, doors, windows, ...",
- color: "hover:bg-blue-500/20 hover:text-blue-400",
- activeColor: "bg-blue-500/20 text-blue-400",
- },
- {
- id: "furnish",
- icon: Sofa,
- label: "Furnish",
- shortcut: "3",
- description: "Place furniture, appliances, decorations",
- color: "hover:bg-amber-500/20 hover:text-amber-400",
- activeColor: "bg-amber-500/20 text-amber-400",
- },
-];
-
-export function PhaseSwitcher() {
- const phase = useEditor((state) => state.phase);
- const setPhase = useEditor((state) => state.setPhase);
-
- return (
-
- {editorModes.map((mode) => {
- const Icon = mode.icon;
- const isActive = phase === mode.id;
-
- return (
-
-
- setPhase(mode.id)}
- size="icon"
- variant="ghost"
- >
-
-
-
-
-
- {mode.label} ({mode.shortcut})
-
- {mode.description}
-
-
- );
- })}
-
- );
-}
diff --git a/apps/editor/components/ui/action-menu/structure-tools.tsx b/apps/editor/components/ui/action-menu/structure-tools.tsx
index edaf8f7a..f4101739 100644
--- a/apps/editor/components/ui/action-menu/structure-tools.tsx
+++ b/apps/editor/components/ui/action-menu/structure-tools.tsx
@@ -6,6 +6,7 @@ import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/primit
import { cn } from '@/lib/utils'
import useEditor, { CatalogCategory, StructureTool, Tool } from '@/store/use-editor'
+import { useContextualTools } from '@/hooks/use-contextual-tools'
export type ToolConfig = {
id: StructureTool; iconSrc: string; label: string; catalogCategory?: CatalogCategory }
@@ -28,28 +29,39 @@ export function StructureTools() {
const structureLayer = useEditor((state) => state.structureLayer)
const setTool = useEditor((state) => state.setTool)
const setCatalogCategory = useEditor((state) => state.setCatalogCategory)
+
+ const contextualTools = useContextualTools()
// Filter tools based on structureLayer
const visibleTools = structureLayer === 'zones'
? tools.filter((t) => t.id === 'zone')
: tools.filter((t) => t.id !== 'zone')
+ const hasActiveTool = visibleTools.some((t) =>
+ activeTool === t.id &&
+ (t.catalogCategory ? catalogCategory === t.catalogCategory : true)
+ )
+
return (
-
+
{visibleTools.map((tool, index) => {
// For item tools with catalog category, check both tool and category match
const isActive =
activeTool === tool.id &&
(tool.catalogCategory ? catalogCategory === tool.catalogCategory : true)
+
+ const isContextual = contextualTools.includes(tool.id)
return (
{
if (isActive) {
@@ -58,6 +70,11 @@ export function StructureTools() {
} else {
setTool(tool.id)
setCatalogCategory(tool.catalogCategory ?? null)
+
+ // Automatically switch to build mode if we select a tool
+ if (useEditor.getState().mode !== 'build') {
+ useEditor.getState().setMode('build')
+ }
}
}}
size="icon"
diff --git a/apps/editor/components/ui/action-menu/view-toggles.tsx b/apps/editor/components/ui/action-menu/view-toggles.tsx
index 5b12263a..aa40e294 100644
--- a/apps/editor/components/ui/action-menu/view-toggles.tsx
+++ b/apps/editor/components/ui/action-menu/view-toggles.tsx
@@ -133,8 +133,8 @@ export function ViewToggles() {
className={cn(
'h-8 w-8 text-zinc-400 transition-all p-0',
wallMode !== 'cutaway'
- ? 'bg-emerald-500/20 text-emerald-400'
- : 'hover:bg-zinc-800',
+ ? 'bg-white/10'
+ : 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0 hover:bg-white/5',
)}
onClick={cycleWallMode}
size="icon"
@@ -156,16 +156,16 @@ export function ViewToggles() {
setShowScans(!showScans)}
size="icon"
variant="ghost"
>
-
+
@@ -178,16 +178,16 @@ export function ViewToggles() {
setShowGuides(!showGuides)}
size="icon"
variant="ghost"
>
-
+
diff --git a/apps/editor/components/ui/panels/ceiling-panel.tsx b/apps/editor/components/ui/panels/ceiling-panel.tsx
index 96b1db73..5ab6a8a2 100644
--- a/apps/editor/components/ui/panels/ceiling-panel.tsx
+++ b/apps/editor/components/ui/panels/ceiling-panel.tsx
@@ -119,16 +119,16 @@ export function CeilingPanel() {
return (
{/* Header */}
-
+
-
+
{node.name || `Ceiling (${area.toFixed(1)}m²)`}
@@ -140,7 +140,7 @@ export function CeilingPanel() {
{/* Height */}
-
+
Height
@@ -162,27 +162,27 @@ export function CeilingPanel() {
{/* Quick preset buttons */}
-
+
Presets
handleUpdate({ height: 2.4 })}
>
Low (2.4m)
handleUpdate({ height: 2.5 })}
>
Standard (2.5m)
handleUpdate({ height: 3.0 })}
>
High (3m)
@@ -192,10 +192,10 @@ export function CeilingPanel() {
{/* Area info */}
-
+
Area
-
@@ -203,13 +203,13 @@ export function CeilingPanel() {
{/* Holes */}
-
+
Holes
{editingHole?.nodeId === selectedId ? (
setEditingHole(null)}
>
Done Editing
@@ -217,7 +217,7 @@ export function CeilingPanel() {
) : (
@@ -233,10 +233,10 @@ export function CeilingPanel() {
return (
diff --git a/apps/editor/components/ui/panels/door-panel.tsx b/apps/editor/components/ui/panels/door-panel.tsx
index e92afd51..e2af2f92 100644
--- a/apps/editor/components/ui/panels/door-panel.tsx
+++ b/apps/editor/components/ui/panels/door-panel.tsx
@@ -138,16 +138,16 @@ export function DoorPanel() {
return (
{/* Header */}
-
+
-
+
{node.name || `Door (${node.width}×${node.height}m)`}
@@ -159,7 +159,7 @@ export function DoorPanel() {
{/* Position */}
-
+
Position
@@ -172,7 +172,7 @@ export function DoorPanel() {
@@ -182,7 +182,7 @@ export function DoorPanel() {
{/* Dimensions */}
-
+
Dimensions
@@ -213,7 +213,7 @@ export function DoorPanel() {
{/* Frame */}
-
+
Frame
@@ -246,7 +246,7 @@ export function DoorPanel() {
{/* Content Padding */}
-
+
Content Padding
@@ -279,22 +279,22 @@ export function DoorPanel() {
{/* Swing */}
-
+
Swing
Hinges
-
+
{(['left', 'right'] as const).map((side) => (
handleUpdate({ hingesSide: side })}
- className={`flex-1 rounded border px-2 py-1 text-xs cursor-pointer transition-colors ${
+ className={`flex-1 rounded-md px-2 py-1 text-xs font-medium cursor-pointer transition-all duration-200 ${
node.hingesSide === side
- ? 'border-primary bg-primary text-primary-foreground'
- : 'border-border hover:bg-accent'
+ ? 'bg-white dark:bg-background shadow-sm ring-1 ring-black/5 dark:ring-white/10 text-foreground'
+ : 'text-muted-foreground hover:text-foreground hover:bg-white/50 dark:hover:bg-accent/50'
}`}
>
{side.charAt(0).toUpperCase() + side.slice(1)}
@@ -304,7 +304,7 @@ export function DoorPanel() {
Direction
-
+
{(['inward', 'outward'] as const).map((dir) => (
-
+
Threshold
-
+
Handle
Side
-
+
{(['left', 'right'] as const).map((side) => (
-
+
Hardware
@@ -440,7 +440,7 @@ export function DoorPanel() {
{/* Segments */}
-
+
Leaf segments (top → bottom)
{node.segments.map((seg, i) => {
@@ -451,7 +451,7 @@ export function DoorPanel() {
Segment {i + 1}
-
+
{(['panel', 'glass', 'empty'] as const).map((t) => (
{
const updated = [
...node.segments,
@@ -598,7 +598,7 @@ export function DoorPanel() {
{node.segments.length > 1 && (
{
handleUpdate({ segments: node.segments.slice(0, -1) })
}}
@@ -611,11 +611,11 @@ export function DoorPanel() {
{/* Action Buttons */}
-
+
@@ -623,7 +623,7 @@ export function DoorPanel() {
@@ -631,7 +631,7 @@ export function DoorPanel() {
diff --git a/apps/editor/components/ui/panels/item-panel.tsx b/apps/editor/components/ui/panels/item-panel.tsx
index c3c2d87e..b9173929 100644
--- a/apps/editor/components/ui/panels/item-panel.tsx
+++ b/apps/editor/components/ui/panels/item-panel.tsx
@@ -84,7 +84,7 @@ export function ItemPanel() {
return (
{/* Header */}
-
+
-
+
{node.name || node.asset.name}
@@ -111,7 +111,7 @@ export function ItemPanel() {
{/* Position */}
-
+
Position
@@ -144,7 +144,7 @@ export function ItemPanel() {
{/* Rotation */}
-
+
Rotation
@@ -193,7 +193,7 @@ export function ItemPanel() {
{/* Scale */}
-
+
Scale
-
+
Dimensions
-
+
{(() => {
const [w, h, d] = getScaledDimensions(node)
return `${Math.round(w * 100) / 100}m × ${Math.round(h * 100) / 100}m × ${Math.round(d * 100) / 100}m`
@@ -265,11 +265,11 @@ export function ItemPanel() {
{/* Action Buttons */}
-
+
@@ -277,7 +277,7 @@ export function ItemPanel() {
@@ -285,7 +285,7 @@ export function ItemPanel() {
diff --git a/apps/editor/components/ui/panels/reference-panel.tsx b/apps/editor/components/ui/panels/reference-panel.tsx
index 5bbc644e..2ba02455 100644
--- a/apps/editor/components/ui/panels/reference-panel.tsx
+++ b/apps/editor/components/ui/panels/reference-panel.tsx
@@ -37,19 +37,19 @@ export function ReferencePanel() {
return (
{/* Header */}
-
+
{isScan ? (
) : (
)}
-
+
{node.name || (isScan ? '3D Scan' : 'Guide Image')}
@@ -61,7 +61,7 @@ export function ReferencePanel() {
{/* Position */}
-
+
Position
@@ -83,7 +83,7 @@ export function ReferencePanel() {
{/* Rotation Y */}
-
+
Rotation
@@ -101,7 +101,7 @@ export function ReferencePanel() {
/>
°
handleUpdate({
rotation: [node.rotation[0], node.rotation[1] - Math.PI / 4, node.rotation[2]],
@@ -111,7 +111,7 @@ export function ReferencePanel() {
−45
handleUpdate({
rotation: [node.rotation[0], node.rotation[1] + Math.PI / 4, node.rotation[2]],
@@ -125,7 +125,7 @@ export function ReferencePanel() {
{/* Scale */}
-
+
Scale
-
+
Opacity
- {node.opacity}%
+ {node.opacity}%
{/* Header */}
-
+
-
+
{node.name || 'Gable Roof'}
@@ -60,12 +60,12 @@ export function RoofPanel() {
{/* Length */}
-
+
Length
{
const value = Number.parseFloat(e.target.value)
@@ -83,12 +83,12 @@ export function RoofPanel() {
{/* Height */}
-
+
Height
{
const value = Number.parseFloat(e.target.value)
@@ -107,7 +107,7 @@ export function RoofPanel() {
{/* Slope Widths */}
-
+
Slope Widths
@@ -119,7 +119,7 @@ export function RoofPanel() {
Left
{
const value = Number.parseFloat(e.target.value)
@@ -138,7 +138,7 @@ export function RoofPanel() {
Right
{
const value = Number.parseFloat(e.target.value)
@@ -158,12 +158,12 @@ export function RoofPanel() {
{/* Rotation */}
-
+
Rotation
{
const degrees = Number.parseFloat(e.target.value)
if (!Number.isNaN(degrees)) {
@@ -178,7 +178,7 @@ export function RoofPanel() {
°
{
const newRotation = node.rotation - Math.PI / 2
handleUpdate({ rotation: newRotation })
@@ -188,7 +188,7 @@ export function RoofPanel() {
{
const newRotation = node.rotation + Math.PI / 2
handleUpdate({ rotation: newRotation })
@@ -201,7 +201,7 @@ export function RoofPanel() {
{/* Position */}
-
+
Position
@@ -209,7 +209,7 @@ export function RoofPanel() {
{['X', 'Y', 'Z'][i]}
{
const value = Number.parseFloat(e.target.value)
if (!Number.isNaN(value)) {
diff --git a/apps/editor/components/ui/panels/slab-panel.tsx b/apps/editor/components/ui/panels/slab-panel.tsx
index a78bd41c..987c9fa0 100644
--- a/apps/editor/components/ui/panels/slab-panel.tsx
+++ b/apps/editor/components/ui/panels/slab-panel.tsx
@@ -119,16 +119,16 @@ export function SlabPanel() {
return (
{/* Header */}
-
+
-
+
{node.name || `Slab (${area.toFixed(1)}m²)`}
@@ -140,7 +140,7 @@ export function SlabPanel() {
{/* Elevation */}
-
+
Elevation
@@ -162,34 +162,34 @@ export function SlabPanel() {
{/* Quick preset buttons */}
-
+
Presets
handleUpdate({ elevation: -0.15 })}
>
Sunken (-15cm)
handleUpdate({ elevation: 0 })}
>
Ground (0m)
handleUpdate({ elevation: 0.05 })}
>
Raised (5cm)
handleUpdate({ elevation: 0.15 })}
>
Step (15cm)
@@ -199,10 +199,10 @@ export function SlabPanel() {
{/* Area info */}
-
+
Area
-
@@ -210,13 +210,13 @@ export function SlabPanel() {
{/* Holes */}
-
+
Holes
{editingHole?.nodeId === selectedId ? (
setEditingHole(null)}
>
Done Editing
@@ -224,7 +224,7 @@ export function SlabPanel() {
) : (
@@ -240,10 +240,10 @@ export function SlabPanel() {
return (
diff --git a/apps/editor/components/ui/panels/wall-panel.tsx b/apps/editor/components/ui/panels/wall-panel.tsx
index c51b937f..281c1fa3 100644
--- a/apps/editor/components/ui/panels/wall-panel.tsx
+++ b/apps/editor/components/ui/panels/wall-panel.tsx
@@ -43,16 +43,16 @@ export function WallPanel() {
return (
{/* Header */}
-
+
-
+
{node.name || `Wall (${length.toFixed(2)}m)`}
@@ -64,7 +64,7 @@ export function WallPanel() {
{/* Dimensions */}
-
+
Dimensions
@@ -95,10 +95,10 @@ export function WallPanel() {
{/* Info */}
-
+
Info
-
+
Length: {length.toFixed(2)} m
diff --git a/apps/editor/components/ui/panels/window-panel.tsx b/apps/editor/components/ui/panels/window-panel.tsx
index d957a530..ad65193a 100644
--- a/apps/editor/components/ui/panels/window-panel.tsx
+++ b/apps/editor/components/ui/panels/window-panel.tsx
@@ -127,16 +127,16 @@ export function WindowPanel() {
return (
{/* Header */}
-
+
-
+
{node.name || `Window (${node.width}×${node.height}m)`}
@@ -148,7 +148,7 @@ export function WindowPanel() {
{/* Position */}
-
+
Position
@@ -167,7 +167,7 @@ export function WindowPanel() {
@@ -177,7 +177,7 @@ export function WindowPanel() {
{/* Dimensions */}
-
+
Dimensions
@@ -208,7 +208,7 @@ export function WindowPanel() {
{/* Frame */}
-
+
Frame
@@ -241,7 +241,7 @@ export function WindowPanel() {
{/* Grid */}
-
+
Grid
@@ -343,7 +343,7 @@ export function WindowPanel() {
{/* Sill */}
-
+
Sill
{/* Action Buttons */}
-
+
@@ -395,7 +395,7 @@ export function WindowPanel() {
@@ -403,7 +403,7 @@ export function WindowPanel() {
diff --git a/apps/editor/components/ui/primitives/button.tsx b/apps/editor/components/ui/primitives/button.tsx
index e917d924..b8c41d08 100644
--- a/apps/editor/components/ui/primitives/button.tsx
+++ b/apps/editor/components/ui/primitives/button.tsx
@@ -5,7 +5,7 @@ import type * as React from 'react'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
- "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+ "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-barlow font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
diff --git a/apps/editor/components/ui/primitives/context-menu.tsx b/apps/editor/components/ui/primitives/context-menu.tsx
index b5f079e1..ca2c8082 100644
--- a/apps/editor/components/ui/primitives/context-menu.tsx
+++ b/apps/editor/components/ui/primitives/context-menu.tsx
@@ -45,7 +45,7 @@ function ContextMenuSubTrigger({
return (
(null)
+ const labelRef = useRef(null)
const clamp = useCallback(
(val: number) => {
@@ -131,22 +132,32 @@ export function NumberInput({
)
return (
-
-
-
+ = (max ?? Math.max(10, value)) ? '0.5rem' : '0',
+ borderBottomRightRadius: value >= (max ?? Math.max(10, value)) ? '0.5rem' : '0',
+ borderTopLeftRadius: '0.5rem',
+ borderBottomLeftRadius: '0.5rem',
+ }}
+ />
+
+
{label}
-
+
{isEditing ? (
) : (
- {value.toFixed(precision)}
+
)}
diff --git a/apps/editor/components/ui/primitives/sidebar.tsx b/apps/editor/components/ui/primitives/sidebar.tsx
index 2ba3ea30..d6c8b0d7 100644
--- a/apps/editor/components/ui/primitives/sidebar.tsx
+++ b/apps/editor/components/ui/primitives/sidebar.tsx
@@ -404,7 +404,7 @@ function SidebarGroupLabel({
return (
svg]:size-4 [&>svg]:shrink-0",
+ "flex h-8 shrink-0 items-center rounded-md px-2 font-medium font-barlow text-sidebar-foreground/70 text-xs outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
@@ -419,7 +419,7 @@ function SidebarGroupLabel({
return (
svg]:size-4 [&>svg]:shrink-0",
+ "flex h-8 shrink-0 items-center rounded-md px-2 font-medium font-barlow text-sidebar-foreground/70 text-xs outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
@@ -504,7 +504,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
}
const sidebarMenuButtonVariants = cva(
- "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left font-barlow text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
@@ -735,7 +735,7 @@ function SidebarMenuSubButton({
isActive?: boolean;
}) {
const classes = cn(
- "-translate-x-px flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "-translate-x-px flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-md px-2 font-barlow text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
diff --git a/apps/editor/components/ui/primitives/tooltip.tsx b/apps/editor/components/ui/primitives/tooltip.tsx
index a069880d..1a6863f4 100644
--- a/apps/editor/components/ui/primitives/tooltip.tsx
+++ b/apps/editor/components/ui/primitives/tooltip.tsx
@@ -40,7 +40,7 @@ function TooltipContent({
state.selection.selectedIds.includes(node.id));
const isHovered = useViewer((state) => state.hoveredId === node.id);
const setSelection = useViewer((state) => state.setSelection);
@@ -24,7 +24,7 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
};
const handleDoubleClick = () => {
- setRenameOpen(true);
+ setIsEditing(true);
};
const handleMouseEnter = () => {
@@ -40,33 +40,34 @@ export function CeilingTreeNode({ node, depth }: CeilingTreeNodeProps) {
const defaultName = `Ceiling (${area}m²)`;
return (
- }
+ label={
+ setIsEditing(false)}
+ onStartEditing={() => setIsEditing(true)}
+ defaultName={defaultName}
+ />
+ }
+ depth={depth}
+ hasChildren={node.children.length > 0}
+ expanded={expanded}
+ onToggle={() => setExpanded(!expanded)}
+ onClick={handleClick}
+ onDoubleClick={handleDoubleClick}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ isSelected={isSelected}
+ isHovered={isHovered}
+ isVisible={node.visible !== false}
+ actions={ }
>
- }
- label={node.name || defaultName}
- depth={depth}
- hasChildren={node.children.length > 0}
- expanded={expanded}
- onToggle={() => setExpanded(!expanded)}
- onClick={handleClick}
- onDoubleClick={handleDoubleClick}
- onMouseEnter={handleMouseEnter}
- onMouseLeave={handleMouseLeave}
- isSelected={isSelected}
- isHovered={isHovered}
- isVisible={node.visible !== false}
- actions={ }
- >
- {node.children.map((childId) => (
-
- ))}
-
-
+ {node.children.map((childId) => (
+
+ ))}
+
);
}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/door-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/door-tree-node.tsx
index 5358bd43..d36a8fd8 100644
--- a/apps/editor/components/ui/sidebar/panels/site-panel/door-tree-node.tsx
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/door-tree-node.tsx
@@ -4,7 +4,7 @@ import { DoorNode } from "@pascal-app/core"
import { useViewer } from "@pascal-app/viewer"
import Image from "next/image"
import { useState } from "react"
-import { RenamePopover } from "./rename-popover"
+import { InlineRenameInput } from "./inline-rename-input"
import { TreeNodeWrapper } from "./tree-node"
import { TreeNodeActions } from "./tree-node-actions"
@@ -14,7 +14,7 @@ interface DoorTreeNodeProps {
}
export function DoorTreeNode({ node, depth }: DoorTreeNodeProps) {
- const [renameOpen, setRenameOpen] = useState(false)
+ const [isEditing, setIsEditing] = useState(false)
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id))
const isHovered = useViewer((state) => state.hoveredId === node.id)
const setSelection = useViewer((state) => state.setSelection)
@@ -23,28 +23,29 @@ export function DoorTreeNode({ node, depth }: DoorTreeNodeProps) {
const defaultName = `Door (${node.width}×${node.height}m)`
return (
-
- }
- label={node.name || defaultName}
- depth={depth}
- hasChildren={false}
- expanded={false}
- onToggle={() => {}}
- onClick={() => setSelection({ selectedIds: [node.id] })}
- onDoubleClick={() => setRenameOpen(true)}
- onMouseEnter={() => setHoveredId(node.id)}
- onMouseLeave={() => setHoveredId(null)}
- isSelected={isSelected}
- isHovered={isHovered}
- isVisible={node.visible !== false}
- actions={ }
- />
-
+ }
+ label={
+ setIsEditing(false)}
+ onStartEditing={() => setIsEditing(true)}
+ defaultName={defaultName}
+ />
+ }
+ depth={depth}
+ hasChildren={false}
+ expanded={false}
+ onToggle={() => {}}
+ onClick={() => setSelection({ selectedIds: [node.id] })}
+ onDoubleClick={() => setIsEditing(true)}
+ onMouseEnter={() => setHoveredId(node.id)}
+ onMouseLeave={() => setHoveredId(null)}
+ isSelected={isSelected}
+ isHovered={isHovered}
+ isVisible={node.visible !== false}
+ actions={ }
+ />
)
}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx
index edd234be..cabfcea4 100644
--- a/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/index.tsx
@@ -1,5 +1,6 @@
import {
type AnyNodeId,
+ type AnyNode,
type BuildingNode,
emitter,
LevelNode,
@@ -19,12 +20,12 @@ import {
Plus,
Trash2,
} from "lucide-react";
-import { useState } from "react";
+import { useState, useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
import useEditor from "@/store/use-editor";
import { TreeNode } from "./tree-node";
import { ReferencesDialog } from "./references-dialog";
-import { RenamePopover } from "./rename-popover";
+import { InlineRenameInput } from "./inline-rename-input";
import {
Popover,
PopoverContent,
@@ -307,215 +308,159 @@ function CameraPopover({
);
}
-function SitePhaseView() {
- const nodes = useScene((state) => state.nodes);
- const rootNodeIds = useScene((state) => state.rootNodeIds);
- const updateNode = useScene((state) => state.updateNode);
- const selectedBuildingId = useViewer((state) => state.selection.buildingId);
- const setSelection = useViewer((state) => state.setSelection);
- const [siteCameraOpen, setSiteCameraOpen] = useState(false);
- const [buildingCameraOpen, setBuildingCameraOpen] = useState(null);
- const siteNode = rootNodeIds[0] ? nodes[rootNodeIds[0]] : null;
- const buildings = (siteNode?.type === 'site' ? siteNode.children : [])
- .map((child) => {
- const id = typeof child === 'string' ? child : child.id;
- return nodes[id] as BuildingNode | undefined;
- })
- .filter((node): node is BuildingNode => node?.type === "building");
+function LevelItem({
+ level,
+ selectedLevelId,
+ setSelection,
+ setReferencesLevelId,
+ deleteNode,
+ updateNode,
+}: {
+ level: LevelNode;
+ selectedLevelId: string | null;
+ setSelection: (selection: any) => void;
+ setReferencesLevelId: (id: string | null) => void;
+ deleteNode: (id: AnyNodeId) => void;
+ updateNode: (id: AnyNodeId, updates: Partial) => void;
+}) {
+ const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const itemRef = useRef(null);
+ const isSelected = selectedLevelId === level.id;
+
+ useEffect(() => {
+ if (isSelected && itemRef.current) {
+ itemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+ }, [isSelected]);
return (
-
- {/* Site row */}
- {siteNode && (
-
-
-
- {siteNode.name || "Site"}
-
-
-
+
- {buildings.length === 0 ? (
-
- No buildings yet
-
- ) : (
-
- {buildings.map((building) => (
-
-
setSelection({ buildingId: building.id })}
- >
-
- {building.name || "Building"}
-
-
setBuildingCameraOpen(open ? building.id : null)}
- >
-
- e.stopPropagation()}
- title="Camera snapshot"
- >
-
- {building.camera && (
-
- )}
-
-
- e.stopPropagation()}
- >
-
- {building.camera && (
- {
- e.stopPropagation();
- emitter.emit("camera-controls:view", { nodeId: building.id });
- setBuildingCameraOpen(null);
- }}
- >
-
- View snapshot
-
- )}
- {
- e.stopPropagation();
- emitter.emit("camera-controls:capture", { nodeId: building.id });
- setBuildingCameraOpen(null);
- }}
- >
-
- {building.camera ? "Update snapshot" : "Take snapshot"}
-
- {building.camera && (
- {
- e.stopPropagation();
- updateNode(building.id, { camera: undefined });
- setBuildingCameraOpen(null);
- }}
- >
-
- Clear snapshot
-
- )}
-
-
-
-
- ))}
-
- )}
-
- );
-}
-
-// ============================================================================
-// STRUCTURE/FURNISH PHASE VIEW - Building dropdown + Levels + Content
-// ============================================================================
-
-function BuildingSelector() {
- const nodes = useScene((state) => state.nodes);
- const rootNodeIds = useScene((state) => state.rootNodeIds);
- const selectedBuildingId = useViewer((state) => state.selection.buildingId);
- const setSelection = useViewer((state) => state.setSelection);
-
- // Get site node and its building children
- const siteNode = rootNodeIds[0] ? nodes[rootNodeIds[0]] : null;
- const buildings = (siteNode?.type === 'site' ? siteNode.children : [])
- .map((child) => {
- const id = typeof child === 'string' ? child : child.id;
- return nodes[id] as BuildingNode | undefined;
- })
- .filter((node): node is BuildingNode => node?.type === "building");
-
- const selectedBuilding = selectedBuildingId
- ? (nodes[selectedBuildingId] as BuildingNode)
- : null;
-
- if (buildings.length === 0) return null;
-
- // If only one building, just show it as a header
- if (buildings.length === 1) {
- return (
-
-
-
- {buildings[0]?.name || "Building"}
-
+ >
+
setSelection({ levelId: level.id })}
+ onDoubleClick={() => setIsEditing(true)}
+ >
+
+ setIsEditing(false)}
+ onStartEditing={() => setIsEditing(true)}
+ defaultName={`Level ${level.level}`}
+ />
- );
- }
-
- return (
-
-
-
-
-
-
- {selectedBuilding?.name || "Select Building"}
-
-
-
-
-
-
- {buildings.map((building) => (
- {
- setSelection({ buildingId: building.id });
- // Also select first level if available
- if (building.children.length > 0) {
- setSelection({ levelId: building.children[0] as LevelNode["id"] });
- }
- }}
+ {/* Camera snapshot button */}
+
+
+ e.stopPropagation()}
+ title="Camera snapshot"
+ >
+
+ {level.camera && (
+
+ )}
+
+
+ e.stopPropagation()}
>
-
- {building.name || "Building"}
-
- ))}
-
-
+
+ {level.camera && (
+ {
+ e.stopPropagation();
+ emitter.emit("camera-controls:view", { nodeId: level.id });
+ setCameraPopoverOpen(false);
+ }}
+ >
+
+ View snapshot
+
+ )}
+ {
+ e.stopPropagation();
+ emitter.emit("camera-controls:capture", { nodeId: level.id });
+ setCameraPopoverOpen(false);
+ }}
+ >
+
+ {level.camera ? "Update snapshot" : "Take snapshot"}
+
+ {level.camera && (
+ {
+ e.stopPropagation();
+ updateNode(level.id, { camera: undefined });
+ setCameraPopoverOpen(false);
+ }}
+ >
+
+ Clear snapshot
+
+ )}
+
+
+
+
+
+ e.stopPropagation()}
+ >
+
+
+
+
+ setReferencesLevelId(level.id)}
+ >
+ References
+
+ {level.level !== 0 && (
+ deleteNode(level.id)}
+ >
+
+ Delete
+
+ )}
+
+
+
);
}
@@ -529,7 +474,6 @@ function LevelsSection() {
const setSelection = useViewer((state) => state.setSelection);
const [referencesLevelId, setReferencesLevelId] = useState(null);
- const [cameraPopoverOpen, setCameraPopoverOpen] = useState(null);
const building = selectedBuildingId
? (nodes[selectedBuildingId] as BuildingNode)
@@ -552,9 +496,9 @@ function LevelsSection() {
};
return (
-
+
{/* Header */}
-
+
Levels
@@ -567,123 +511,17 @@ function LevelsSection() {
{/* Level buttons */}
-
+
{levels.map((level) => (
-
-
setSelection({ levelId: level.id })}
- >
-
- {level.name || `Level ${level.level}`}
-
- {/* Camera snapshot button */}
-
setCameraPopoverOpen(open ? level.id : null)}>
-
- e.stopPropagation()}
- title="Camera snapshot"
- >
-
- {level.camera && (
-
- )}
-
-
- e.stopPropagation()}
- >
-
- {level.camera && (
- {
- e.stopPropagation();
- emitter.emit("camera-controls:view", { nodeId: level.id });
- setCameraPopoverOpen(null);
- }}
- >
-
- View snapshot
-
- )}
- {
- e.stopPropagation();
- emitter.emit("camera-controls:capture", { nodeId: level.id });
- setCameraPopoverOpen(null);
- }}
- >
-
- {level.camera ? "Update snapshot" : "Take snapshot"}
-
- {level.camera && (
- {
- e.stopPropagation();
- updateNode(level.id, { camera: undefined });
- setCameraPopoverOpen(null);
- }}
- >
-
- Clear snapshot
-
- )}
-
-
-
-
-
- e.stopPropagation()}
- >
-
-
-
-
- setReferencesLevelId(level.id)}
- >
- References
-
- {level.level !== 0 && (
- deleteNode(level.id)}
- >
-
- Delete
-
- )}
-
-
-
+ level={level}
+ selectedLevelId={selectedLevelId}
+ setSelection={setSelection}
+ setReferencesLevelId={setReferencesLevelId}
+ deleteNode={deleteNode}
+ updateNode={updateNode}
+ />
))}
{levels.length === 0 && (
@@ -709,29 +547,65 @@ function LevelsSection() {
function LayerToggle() {
const structureLayer = useEditor((state) => state.structureLayer);
const setStructureLayer = useEditor((state) => state.setStructureLayer);
+ const phase = useEditor((state) => state.phase);
+ const setPhase = useEditor((state) => state.setPhase);
return (
-
+
@@ -739,7 +613,7 @@ function LayerToggle() {
}
function ZoneItem({ zone }: { zone: ZoneNode }) {
- const [renameOpen, setRenameOpen] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false);
const deleteNode = useScene((state) => state.deleteNode);
const updateNode = useScene((state) => state.updateNode);
@@ -753,6 +627,14 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
const isSelected = selectedZoneId === zone.id;
const isHovered = hoveredId === zone.id;
+ const itemRef = useRef
(null);
+
+ useEffect(() => {
+ if (isSelected && itemRef.current) {
+ itemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+ }, [isSelected]);
+
const area = calculatePolygonArea(zone.polygon).toFixed(1);
const defaultName = `Zone (${area}m²)`;
@@ -763,7 +645,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
};
const handleDoubleClick = () => {
- setRenameOpen(true);
+ setIsEditing(true);
};
const handleDelete = (e: React.MouseEvent) => {
@@ -779,124 +661,127 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
};
return (
- setHoveredId(zone.id)}
+ onMouseLeave={() => setHoveredId(null)}
>
- setHoveredId(zone.id)}
- onMouseLeave={() => setHoveredId(null)}
- >
-
-
- e.stopPropagation()}
- style={{ backgroundColor: zone.color }}
- />
-
-
+
+ e.stopPropagation()}
- >
-
- {PRESET_COLORS.map((color) => (
- handleColorChange(color)}
- style={{ backgroundColor: color }}
- />
- ))}
-
-
-
-
{zone.name || defaultName}
- {/* Camera snapshot button */}
-
-
- e.stopPropagation()}
- title="Camera snapshot"
- >
-
- {zone.camera && (
-
- )}
-
-
-
+
+ e.stopPropagation()}
+ >
+
+ {PRESET_COLORS.map((color) => (
+ handleColorChange(color)}
+ style={{ backgroundColor: color }}
+ />
+ ))}
+
+
+
+
setIsEditing(false)}
+ onStartEditing={() => setIsEditing(true)}
+ defaultName={defaultName}
+ />
+ {/* Camera snapshot button */}
+
+
+ e.stopPropagation()}
+ title="Camera snapshot"
>
-
- {zone.camera && (
-
{
- e.stopPropagation();
- emitter.emit("camera-controls:view", { nodeId: zone.id });
- setCameraPopoverOpen(false);
- }}
- >
-
- View snapshot
-
- )}
+
+ {zone.camera && (
+
+ )}
+
+
+
e.stopPropagation()}
+ >
+
+ {zone.camera && (
{
e.stopPropagation();
- emitter.emit("camera-controls:capture", { nodeId: zone.id });
+ emitter.emit("camera-controls:view", { nodeId: zone.id });
setCameraPopoverOpen(false);
}}
>
- {zone.camera ? "Update snapshot" : "Take snapshot"}
+ View snapshot
- {zone.camera && (
- {
- e.stopPropagation();
- updateNode(zone.id, { camera: undefined });
- setCameraPopoverOpen(false);
- }}
- >
-
- Clear snapshot
-
- )}
-
-
-
-
-
-
-
-
+ )}
+ {
+ e.stopPropagation();
+ emitter.emit("camera-controls:capture", { nodeId: zone.id });
+ setCameraPopoverOpen(false);
+ }}
+ >
+
+ {zone.camera ? "Update snapshot" : "Take snapshot"}
+
+ {zone.camera && (
+ {
+ e.stopPropagation();
+ updateNode(zone.id, { camera: undefined });
+ setCameraPopoverOpen(false);
+ }}
+ >
+
+ Clear snapshot
+
+ )}
+
+
+
+
+
+
+
);
}
@@ -947,7 +832,7 @@ function ContentSection() {
}
return (
-
+
{levelZones.map((zone) => (
))}
@@ -994,7 +879,7 @@ function ContentSection() {
}
return (
-
+
{elementChildren.map((childId) => (
))}
@@ -1002,32 +887,214 @@ function ContentSection() {
);
}
-function StructurePhaseView() {
+function BuildingItem({
+ building,
+ isBuildingActive,
+ buildingCameraOpen,
+ setBuildingCameraOpen,
+}: {
+ building: BuildingNode;
+ isBuildingActive: boolean;
+ buildingCameraOpen: string | null;
+ setBuildingCameraOpen: (id: string | null) => void;
+}) {
+ const setSelection = useViewer((state) => state.setSelection);
const phase = useEditor((state) => state.phase);
+ const setPhase = useEditor((state) => state.setPhase);
+ const updateNode = useScene((state) => state.updateNode);
+ const itemRef = useRef
(null);
+
+ useEffect(() => {
+ if (isBuildingActive && itemRef.current) {
+ itemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+ }, [isBuildingActive]);
return (
-
-
-
- {/* Only show layer toggle in structure phase, furnish is always elements */}
- {phase === "structure" &&
}
-
-
+
+
+
{
+ setSelection({ buildingId: building.id });
+ if (phase === "site") {
+ setPhase("structure");
+ }
+ }}
+ >
+
+ {building.name || "Building"}
+
+
setBuildingCameraOpen(open ? building.id : null)}
+ >
+
+ e.stopPropagation()}
+ title="Camera snapshot"
+ >
+
+ {building.camera && (
+
+ )}
+
+
+ e.stopPropagation()}
+ >
+
+ {building.camera && (
+ {
+ e.stopPropagation();
+ emitter.emit("camera-controls:view", { nodeId: building.id });
+ setBuildingCameraOpen(null);
+ }}
+ >
+
+ View snapshot
+
+ )}
+ {
+ e.stopPropagation();
+ emitter.emit("camera-controls:capture", { nodeId: building.id });
+ setBuildingCameraOpen(null);
+ }}
+ >
+
+ {building.camera ? "Update snapshot" : "Take snapshot"}
+
+ {building.camera && (
+ {
+ e.stopPropagation();
+ updateNode(building.id, { camera: undefined });
+ setBuildingCameraOpen(null);
+ }}
+ >
+
+ Clear snapshot
+
+ )}
+
+
+
+
+ {/* Tools and content for the active building */}
+ {isBuildingActive && (
+
+
+
+
+
+ )}
);
}
-// ============================================================================
-// MAIN SITE PANEL
-// ============================================================================
-
export function SitePanel() {
+ const nodes = useScene((state) => state.nodes);
+ const rootNodeIds = useScene((state) => state.rootNodeIds);
+ const updateNode = useScene((state) => state.updateNode);
+ const selectedBuildingId = useViewer((state) => state.selection.buildingId);
+ const setSelection = useViewer((state) => state.setSelection);
const phase = useEditor((state) => state.phase);
+ const setPhase = useEditor((state) => state.setPhase);
- if (phase === "site") {
- return
;
- }
+ const [siteCameraOpen, setSiteCameraOpen] = useState(false);
+ const [buildingCameraOpen, setBuildingCameraOpen] = useState
(null);
- return ;
+ const siteNode = rootNodeIds[0] ? nodes[rootNodeIds[0]] : null;
+ const buildings = (siteNode?.type === 'site' ? siteNode.children : [])
+ .map((child) => {
+ const id = typeof child === 'string' ? child : child.id;
+ return nodes[id] as BuildingNode | undefined;
+ })
+ .filter((node): node is BuildingNode => node?.type === "building");
+
+ return (
+
+ {/* Site Header */}
+ {siteNode && (
+
setPhase("site")}
+ >
+
+
+
{siteNode.name || "Site"}
+
+
+
+ )}
+
+
+ {/* When phase is site, show property line immediately under site header */}
+ {phase === "site" &&
}
+
+ {/* Buildings List */}
+ {buildings.length === 0 ? (
+
+ No buildings yet
+
+ ) : (
+
+ {buildings.map((building) => {
+ const isBuildingActive = (phase === "structure" || phase === "furnish") && selectedBuildingId === building.id;
+
+ return (
+
+ );
+ })}
+
+ )}
+
+
+ );
}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx
new file mode 100644
index 00000000..78e59afd
--- /dev/null
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx
@@ -0,0 +1,98 @@
+import { useScene, type AnyNode } from "@pascal-app/core";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { Pencil } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+interface InlineRenameInputProps {
+ node: AnyNode;
+ isEditing: boolean;
+ onStopEditing: () => void;
+ defaultName: string;
+ className?: string;
+ onStartEditing?: () => void;
+}
+
+export function InlineRenameInput({
+ node,
+ isEditing,
+ onStopEditing,
+ defaultName,
+ className,
+ onStartEditing,
+}: InlineRenameInputProps) {
+ const updateNode = useScene((s) => s.updateNode);
+ const [value, setValue] = useState(node.name || "");
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (isEditing) {
+ setValue(node.name || "");
+ // Focus and select all text after a short delay
+ setTimeout(() => {
+ if (inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, 0);
+ }
+ }, [isEditing, node.name]);
+
+ const handleSave = useCallback(() => {
+ const trimmed = value.trim();
+ if (trimmed !== node.name) {
+ updateNode(node.id, { name: trimmed || undefined });
+ }
+ onStopEditing();
+ }, [value, node.id, node.name, updateNode, onStopEditing]);
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ handleSave();
+ } else if (e.key === "Escape") {
+ e.preventDefault();
+ onStopEditing();
+ }
+ };
+
+ if (!isEditing) {
+ return (
+
+
+ {node.name || defaultName}
+
+ {onStartEditing && (
+
{
+ e.stopPropagation();
+ onStartEditing();
+ }}
+ >
+
+
+ )}
+
+ );
+ }
+
+ return (
+ setValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ onBlur={handleSave}
+ placeholder={defaultName}
+ className={cn(
+ "flex-1 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-auto text-sm leading-none",
+ className
+ )}
+ onClick={(e) => e.stopPropagation()}
+ onDoubleClick={(e) => e.stopPropagation()}
+ />
+ );
+}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/item-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/item-tree-node.tsx
index c40cc113..8f9c87f5 100644
--- a/apps/editor/components/ui/sidebar/panels/site-panel/item-tree-node.tsx
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/item-tree-node.tsx
@@ -2,7 +2,7 @@ import { type AnyNodeId, ItemNode } from "@pascal-app/core";
import { useViewer } from "@pascal-app/viewer";
import Image from "next/image";
import { useState } from "react";
-import { RenamePopover } from "./rename-popover";
+import { InlineRenameInput } from "./inline-rename-input";
import { TreeNode, TreeNodeWrapper } from "./tree-node";
import { TreeNodeActions } from "./tree-node-actions";
@@ -22,7 +22,7 @@ interface ItemTreeNodeProps {
}
export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
- const [renameOpen, setRenameOpen] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
const [expanded, setExpanded] = useState(true);
const iconSrc = CATEGORY_ICONS[node.asset.category] || "/icons/couch.png";
const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id));
@@ -35,7 +35,7 @@ export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
};
const handleDoubleClick = () => {
- setRenameOpen(true);
+ setIsEditing(true);
};
const handleMouseEnter = () => {
@@ -50,32 +50,33 @@ export function ItemTreeNode({ node, depth }: ItemTreeNodeProps) {
const hasChildren = node.children && node.children.length > 0;
return (
- }
+ label={
+ setIsEditing(false)}
+ onStartEditing={() => setIsEditing(true)}
+ defaultName={defaultName}
+ />
+ }
+ depth={depth}
+ hasChildren={hasChildren}
+ expanded={expanded}
+ onToggle={() => setExpanded(!expanded)}
+ onClick={handleClick}
+ onDoubleClick={handleDoubleClick}
+ onMouseEnter={handleMouseEnter}
+ onMouseLeave={handleMouseLeave}
+ isSelected={isSelected}
+ isHovered={isHovered}
+ isVisible={node.visible !== false}
+ actions={ }
>
- }
- label={node.name || defaultName}
- depth={depth}
- hasChildren={hasChildren}
- expanded={expanded}
- onToggle={() => setExpanded(!expanded)}
- onClick={handleClick}
- onDoubleClick={handleDoubleClick}
- onMouseEnter={handleMouseEnter}
- onMouseLeave={handleMouseLeave}
- isSelected={isSelected}
- isHovered={isHovered}
- isVisible={node.visible !== false}
- actions={ }
- >
- {hasChildren && node.children.map((childId) => (
-
- ))}
-
-
+ {hasChildren && node.children.map((childId) => (
+
+ ))}
+
);
}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/level-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/level-tree-node.tsx
index 43d67bf8..1bab93de 100644
--- a/apps/editor/components/ui/sidebar/panels/site-panel/level-tree-node.tsx
+++ b/apps/editor/components/ui/sidebar/panels/site-panel/level-tree-node.tsx
@@ -2,7 +2,7 @@ import { LevelNode } from "@pascal-app/core";
import { useViewer } from "@pascal-app/viewer";
import { Layers } from "lucide-react";
import { useState } from "react";
-import { RenamePopover } from "./rename-popover";
+import { InlineRenameInput } from "./inline-rename-input";
import { TreeNode, TreeNodeWrapper } from "./tree-node";
import { TreeNodeActions } from "./tree-node-actions";
@@ -13,7 +13,7 @@ interface LevelTreeNodeProps {
export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
const [expanded, setExpanded] = useState(true);
- const [renameOpen, setRenameOpen] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
const isSelected = useViewer((state) => state.selection.levelId === node.id);
const isHovered = useViewer((state) => state.hoveredId === node.id);
const setSelection = useViewer((state) => state.setSelection);
@@ -23,35 +23,36 @@ export function LevelTreeNode({ node, depth }: LevelTreeNodeProps) {
};
const handleDoubleClick = () => {
- setRenameOpen(true);
+ setIsEditing(true);
};
const defaultName = `Level ${node.level}`;
return (
- }
+ label={
+ setIsEditing(false)}
+ onStartEditing={() => setIsEditing(true)}
+ defaultName={defaultName}
+ />
+ }
+ depth={depth}
+ hasChildren={node.children.length > 0}
+ expanded={expanded}
+ onToggle={() => setExpanded(!expanded)}
+ onClick={handleClick}
+ onDoubleClick={handleDoubleClick}
+ isSelected={isSelected}
+ isHovered={isHovered}
+ actions={ }
>
- }
- label={node.name || defaultName}
- depth={depth}
- hasChildren={node.children.length > 0}
- expanded={expanded}
- onToggle={() => setExpanded(!expanded)}
- onClick={handleClick}
- onDoubleClick={handleDoubleClick}
- isSelected={isSelected}
- isHovered={isHovered}
- actions={ }
- >
- {node.children.map((childId) => (
-
- ))}
-
-
+ {node.children.map((childId) => (
+
+ ))}
+
);
}
diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/rename-popover.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/rename-popover.tsx
deleted file mode 100644
index 762a75d6..00000000
--- a/apps/editor/components/ui/sidebar/panels/site-panel/rename-popover.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { useScene, type AnyNode } from "@pascal-app/core";
-import { Check, X } from "lucide-react";
-import { useCallback, useEffect, useRef, useState } from "react";
-import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/primitives/popover";
-
-interface RenamePopoverProps {
- node: AnyNode;
- open: boolean;
- onOpenChange: (open: boolean) => void;
- children: React.ReactNode;
- defaultName: string;
-}
-
-export function RenamePopover({
- node,
- open,
- onOpenChange,
- children,
- defaultName,
-}: RenamePopoverProps) {
- const updateNode = useScene((s) => s.updateNode);
- const [value, setValue] = useState(node.name || "");
- const inputRef = useRef(null);
-
- // Reset value when popover opens
- useEffect(() => {
- if (open) {
- setValue(node.name || "");
- // Focus and select all text after a short delay
- setTimeout(() => {
- inputRef.current?.focus();
- inputRef.current?.select();
- }, 0);
- }
- }, [open, node.name]);
-
- const handleSave = useCallback(() => {
- const trimmed = value.trim();
- // Only update if name actually changed
- if (trimmed !== node.name) {
- updateNode(node.id, { name: trimmed || undefined });
- }
- onOpenChange(false);
- }, [value, node.id, node.name, updateNode, onOpenChange]);
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter") {
- e.preventDefault();
- handleSave();
- } else if (e.key === "Escape") {
- e.preventDefault();
- onOpenChange(false);
- }
- };
-
- return (
-
- {children}
- e.preventDefault()}
- >
-